diff --git a/FIAT/P0.py b/FIAT/P0.py index 1a88ee2d5..01d9a6472 100644 --- a/FIAT/P0.py +++ b/FIAT/P0.py @@ -40,7 +40,7 @@ def __init__(self, ref_el): entity_ids[dim][entity] = [entity] if dim == sd else [] entity_permutations[dim][entity] = perms - super(P0Dual, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) class P0(finite_element.CiarletElement): @@ -49,4 +49,4 @@ def __init__(self, ref_el): dual = P0Dual(ref_el) degree = 0 formdegree = ref_el.get_spatial_dimension() # n-form - super(P0, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) diff --git a/FIAT/Sminus.py b/FIAT/Sminus.py index 6d9d4eaa3..506cd23fd 100644 --- a/FIAT/Sminus.py +++ b/FIAT/Sminus.py @@ -497,7 +497,7 @@ def __init__(self, ref_el, degree): self.basis = {(0, 0): Array(Sminus_list)} else: self.basis = {(0, 0, 0): Array(Sminus_list)} - super(TrimmedSerendipityEdge, self).__init__(ref_el=ref_el, degree=degree, mapping="covariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="covariant piola") class TrimmedSerendipityFace(TrimmedSerendipity): @@ -526,4 +526,4 @@ def __init__(self, ref_el, degree): Sminus_list = EL + FL Sminus_list = [[-a[1], a[0]] for a in Sminus_list] self.basis = {(0, 0): Array(Sminus_list)} - super(TrimmedSerendipityFace, self).__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") diff --git a/FIAT/SminusCurl.py b/FIAT/SminusCurl.py index ce42a194f..3aaab57bf 100644 --- a/FIAT/SminusCurl.py +++ b/FIAT/SminusCurl.py @@ -88,11 +88,11 @@ def __init__(self, ref_el, degree, mapping): entity_closure_ids = make_entity_closure_ids(flat_el, entity_ids) - super(TrimmedSerendipity, self).__init__(ref_el=ref_el, - dual=None, - order=degree, - formdegree=formdegree, - mapping=mapping) + super().__init__(ref_el=ref_el, + dual=None, + order=degree, + formdegree=formdegree, + mapping=mapping) topology = ref_el.get_topology() unflattening_map = compute_unflattening_map(topology) @@ -231,7 +231,7 @@ def __init__(self, ref_el, degree): Sminus_list = EL + FL self.basis = {(0, 0): Array(Sminus_list)} - super(TrimmedSerendipityCurl, self).__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") def e_lambda_1_3d(deg, dx, dy, dz, x_mid, y_mid, z_mid): diff --git a/FIAT/SminusDiv.py b/FIAT/SminusDiv.py index 70527a09b..3be143fa2 100644 --- a/FIAT/SminusDiv.py +++ b/FIAT/SminusDiv.py @@ -78,11 +78,11 @@ def __init__(self, ref_el, degree, mapping): entity_closure_ids = make_entity_closure_ids(flat_el, entity_ids) - super(TrimmedSerendipity, self).__init__(ref_el=ref_el, - dual=None, - order=degree, - formdegree=formdegree, - mapping=mapping) + super().__init__(ref_el=ref_el, + dual=None, + order=degree, + formdegree=formdegree, + mapping=mapping) topology = ref_el.get_topology() unflattening_map = compute_unflattening_map(topology) @@ -198,7 +198,7 @@ def __init__(self, ref_el, degree): IL = () Sminus_list = FL + IL self.basis = {(0, 0, 0): Array(Sminus_list)} - super(TrimmedSerendipityDiv, self).__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") else: # Put all 2 dimensional stuff here. @@ -213,7 +213,7 @@ def __init__(self, ref_el, degree): Sminus_list = EL + FL Sminus_list = [[-a[1], a[0]] for a in Sminus_list] self.basis = {(0, 0): Array(Sminus_list)} - super(TrimmedSerendipityDiv, self).__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") def f_lambda_2_3d(degree, dx, dy, dz, x_mid, y_mid, z_mid): diff --git a/FIAT/__init__.py b/FIAT/__init__.py index 965bf63aa..65afac645 100644 --- a/FIAT/__init__.py +++ b/FIAT/__init__.py @@ -7,9 +7,14 @@ # Import finite element classes from FIAT.finite_element import FiniteElement, CiarletElement # noqa: F401 from FIAT.argyris import Argyris +from FIAT.bernardi_raugel import BernardiRaugel from FIAT.bernstein import Bernstein from FIAT.bell import Bell from FIAT.hct import HsiehCloughTocher +from FIAT.alfeld_sorokina import AlfeldSorokina +from FIAT.arnold_qin import ArnoldQin +from FIAT.guzman_neilan import GuzmanNeilan +from FIAT.christiansen_hu import ChristiansenHu from FIAT.johnson_mercier import JohnsonMercier from FIAT.brezzi_douglas_marini import BrezziDouglasMarini from FIAT.Sminus import TrimmedSerendipityEdge # noqa: F401 @@ -31,8 +36,7 @@ from FIAT.morley import Morley from FIAT.nedelec import Nedelec from FIAT.nedelec_second_kind import NedelecSecondKind -from FIAT.powell_sabin import QuadraticPowellSabin6 -from FIAT.powell_sabin import QuadraticPowellSabin12 +from FIAT.powell_sabin import QuadraticPowellSabin6, QuadraticPowellSabin12 from FIAT.hierarchical import Legendre, IntegratedLegendre from FIAT.P0 import P0 from FIAT.raviart_thomas import RaviartThomas @@ -65,6 +69,7 @@ # List of supported elements and mapping to element classes supported_elements = {"Argyris": Argyris, "Bell": Bell, + "Bernardi-Raugel": BernardiRaugel, "Bernstein": Bernstein, "Brezzi-Douglas-Marini": BrezziDouglasMarini, "Brezzi-Douglas-Fortin-Marini": BrezziDouglasFortinMarini, @@ -79,7 +84,11 @@ "Discontinuous Taylor": DiscontinuousTaylor, "Discontinuous Raviart-Thomas": DiscontinuousRaviartThomas, "Hermite": CubicHermite, - "HsiehCloughTocher": HsiehCloughTocher, + "Hsieh-Clough-Tocher": HsiehCloughTocher, + "Alfeld-Sorokina": AlfeldSorokina, + "Arnold-Qin": ArnoldQin, + "Christiansen-Hu": ChristiansenHu, + "Guzman-Neilan": GuzmanNeilan, "Johnson-Mercier": JohnsonMercier, "Lagrange": Lagrange, "Kong-Mulder-Veldhuizen": KongMulderVeldhuizen, diff --git a/FIAT/alfeld_sorokina.py b/FIAT/alfeld_sorokina.py new file mode 100644 index 000000000..13ef25bce --- /dev/null +++ b/FIAT/alfeld_sorokina.py @@ -0,0 +1,106 @@ +# Copyright (C) 2024 Pablo D. Brubeck +# +# This file is part of FIAT (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Written by Pablo D. Brubeck (brubeck@protonmail.com), 2024 + +from FIAT import finite_element, dual_set, polynomial_set +from FIAT.functional import ComponentPointEvaluation, PointDivergence, IntegralMoment +from FIAT.quadrature_schemes import create_quadrature +from FIAT.quadrature import FacetQuadratureRule +from FIAT.macro import CkPolynomialSet, AlfeldSplit + +import numpy + + +def AlfeldSorokinaSpace(ref_el, degree): + """Return a vector-valued C^0 PolynomialSet on an Alfeld split whose + divergence is also C^0. This works on any simplex and for all + polynomial degrees.""" + ref_complex = AlfeldSplit(ref_el) + sd = ref_complex.get_spatial_dimension() + C0 = CkPolynomialSet(ref_complex, degree, order=0, shape=(sd,), variant="bubble") + expansion_set = C0.get_expansion_set() + num_members = C0.get_num_members() + coeffs = C0.get_coeffs() + + facet_el = ref_complex.construct_subelement(sd-1) + phi = polynomial_set.ONPolynomialSet(facet_el, 0 if sd == 1 else degree-1) + Q = create_quadrature(facet_el, 2 * phi.degree) + qpts, qwts = Q.get_points(), Q.get_weights() + phi_at_qpts = phi.tabulate(qpts)[(0,) * (sd-1)] + weights = numpy.multiply(phi_at_qpts, qwts) + + rows = [] + for facet in ref_complex.get_interior_facets(sd-1): + n = ref_complex.compute_normal(facet) + jumps = expansion_set.tabulate_normal_jumps(degree, qpts, facet, order=1) + div_jump = n[:, None, None] * jumps[1][None, ...] + r = numpy.tensordot(div_jump, weights, axes=(-1, -1)) + rows.append(r.reshape(num_members, -1).T) + + if len(rows) > 0: + dual_mat = numpy.vstack(rows) + _, sig, vt = numpy.linalg.svd(dual_mat, full_matrices=True) + tol = sig[0] * 1E-10 + num_sv = len([s for s in sig if abs(s) > tol]) + coeffs = numpy.tensordot(vt[num_sv:], coeffs, axes=(-1, 0)) + return polynomial_set.PolynomialSet(ref_complex, degree, degree, expansion_set, coeffs) + + +class AlfeldSorokinaDualSet(dual_set.DualSet): + def __init__(self, ref_el, degree, variant=None): + if degree != 2: + raise NotImplementedError("Alfeld-Sorokina only defined for degree = 2") + if variant is None: + variant = "integral" + if variant not in {"integral", "point"}: + raise ValueError(f"Invalid variant {variant}") + + top = ref_el.get_topology() + sd = ref_el.get_spatial_dimension() + entity_ids = {dim: {entity: [] for entity in sorted(top[dim])} for dim in sorted(top)} + + nodes = [] + dims = (0, 1) if variant == "point" else (0,) + for dim in dims: + for entity in sorted(top[dim]): + cur = len(nodes) + pts = ref_el.make_points(dim, entity, degree) + if dim == 0: + pt, = pts + nodes.append(PointDivergence(ref_el, pt)) + nodes.extend(ComponentPointEvaluation(ref_el, k, (sd,), pt) + for pt in pts for k in range(sd)) + entity_ids[dim][entity].extend(range(cur, len(nodes))) + + if variant == "integral": + # Edge moments against quadratic Legendre (mean-free bubble) + dim = 1 + facet = ref_el.construct_subelement(dim) + Q = create_quadrature(facet, degree+dim+1) + f_at_qpts = facet.compute_bubble(Q.get_points()) + f_at_qpts -= numpy.dot(f_at_qpts, Q.get_weights()) / facet.volume() + for entity in sorted(top[dim]): + cur = len(nodes) + Q_mapped = FacetQuadratureRule(ref_el, dim, entity, Q) + detJ = Q_mapped.jacobian_determinant() + phi = f_at_qpts / detJ + nodes.extend(IntegralMoment(ref_el, Q_mapped, phi, comp=k, shp=(sd,)) + for k in range(sd)) + entity_ids[dim][entity].extend(range(cur, len(nodes))) + + super().__init__(nodes, ref_el, entity_ids) + + +class AlfeldSorokina(finite_element.CiarletElement): + """The Alfeld-Sorokina C^0 quadratic macroelement with C^0 divergence. This element + belongs to a Stokes complex, and is paired with Lagrange(ref_el, 1, variant="alfeld").""" + def __init__(self, ref_el, degree=2): + dual = AlfeldSorokinaDualSet(ref_el, degree) + poly_set = AlfeldSorokinaSpace(ref_el, degree) + formdegree = ref_el.get_spatial_dimension() - 1 # (n-1)-form + super().__init__(poly_set, dual, degree, formdegree, + mapping="contravariant piola") diff --git a/FIAT/argyris.py b/FIAT/argyris.py index ba35b61d7..d98dbbe3b 100644 --- a/FIAT/argyris.py +++ b/FIAT/argyris.py @@ -82,7 +82,7 @@ def __init__(self, ref_el, degree, variant, interpolant_deg): entity_ids[2][0] = list(range(cur, len(nodes))) else: raise ValueError("Invalid variant for Argyris") - super(ArgyrisDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class Argyris(finite_element.CiarletElement): @@ -107,4 +107,4 @@ def __init__(self, ref_el, degree=5, variant=None): poly_set = polynomial_set.ONPolynomialSet(ref_el, degree, variant="bubble") dual = ArgyrisDualSet(ref_el, degree, variant, interpolant_deg) - super(Argyris, self).__init__(poly_set, dual, degree) + super().__init__(poly_set, dual, degree) diff --git a/FIAT/arnold_qin.py b/FIAT/arnold_qin.py new file mode 100644 index 000000000..c2a421dd5 --- /dev/null +++ b/FIAT/arnold_qin.py @@ -0,0 +1,71 @@ +# This file is part of FIAT (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Written by Pablo D. Brubeck (brubeck@protonmail.com), 2024 + +from FIAT import finite_element, polynomial_set +from FIAT.bernardi_raugel import BernardiRaugelDualSet +from FIAT.quadrature_schemes import create_quadrature +from FIAT.reference_element import TRIANGLE +from FIAT.macro import CkPolynomialSet +from FIAT.hct import HsiehCloughTocher + +import numpy + + +def ArnoldQinSpace(ref_el, degree, reduced=False): + """Return a basis for the Arnold-Qin space + curl(HCT-red) + P_0 x if reduced = True, and + curl(HCT) + P_0 x if reduced = False.""" + if ref_el.get_shape() != TRIANGLE: + raise ValueError("Arnold-Qin only defined on triangles") + if degree != 2: + raise ValueError("Arnold-Qin only defined for degree = 2") + sd = ref_el.get_spatial_dimension() + HCT = HsiehCloughTocher(ref_el, degree+1, reduced=True) + ref_complex = HCT.get_reference_complex() + Q = create_quadrature(ref_complex, 2 * degree) + qpts, qwts = Q.get_points(), Q.get_weights() + + x = qpts.T + bary = numpy.asarray(ref_el.make_points(sd, 0, sd+1)) + P0x_at_qpts = x[None, :, :] - bary[:, :, None] + + tab = HCT.tabulate(1, qpts) + curl_at_qpts = numpy.stack([tab[(0, 1)], -tab[(1, 0)]], axis=1) + if reduced: + curl_at_qpts = curl_at_qpts[:9] + + C0 = CkPolynomialSet(ref_complex, degree, order=0, scale=1, variant="bubble") + C0_at_qpts = C0.tabulate(qpts)[(0,) * sd] + duals = numpy.multiply(C0_at_qpts, qwts) + M = numpy.dot(duals, C0_at_qpts.T) + duals = numpy.linalg.solve(M, duals) + + # Remove the constant nullspace + ids = [0, 3, 6] + A = numpy.asarray([[1, 1, 1], [1, -1, 0], [0, -1, 1]]) + phis = curl_at_qpts + phis[ids] = numpy.tensordot(A, phis[ids], axes=(-1, 0)) + # Replace the constant nullspace with P_0 x + phis[0] = P0x_at_qpts + coeffs = numpy.tensordot(phis, duals, axes=(-1, -1)) + return polynomial_set.PolynomialSet(ref_complex, degree, degree, + C0.get_expansion_set(), coeffs) + + +class ArnoldQin(finite_element.CiarletElement): + """The Arnold-Qin C^0(Alfeld) quadratic macroelement with divergence in P0. + This element belongs to a Stokes complex, and is paired with unsplit DG0.""" + def __init__(self, ref_el, degree=2, reduced=False): + poly_set = ArnoldQinSpace(ref_el, degree) + if reduced: + subdegree = 1 + mapping = "contravariant piola" + else: + subdegree = degree + mapping = "affine" + dual = BernardiRaugelDualSet(ref_el, degree, subdegree) + formdegree = ref_el.get_spatial_dimension() - 1 # (n-1)-form + super().__init__(poly_set, dual, degree, formdegree, mapping=mapping) diff --git a/FIAT/arnold_winther.py b/FIAT/arnold_winther.py index 5e8bdbbd1..5c12fcc76 100644 --- a/FIAT/arnold_winther.py +++ b/FIAT/arnold_winther.py @@ -25,7 +25,7 @@ class ArnoldWintherNCDual(DualSet): - def __init__(self, cell, degree): + def __init__(self, cell, degree=2): if not degree == 2: raise ValueError("Nonconforming Arnold-Winther elements are" "only defined for degree 2.") @@ -70,24 +70,23 @@ def __init__(self, cell, degree): dof_ids[1][entity_id].append(dof_cur) dof_cur += 1 - super(ArnoldWintherNCDual, self).__init__(dofs, cell, dof_ids) + super().__init__(dofs, cell, dof_ids) class ArnoldWintherNC(CiarletElement): """The definition of the nonconforming Arnold-Winther element. """ - def __init__(self, cell, degree): + def __init__(self, cell, degree=2): assert degree == 2, "Only defined for degree 2" Ps = ONSymTensorPolynomialSet(cell, degree) Ls = ArnoldWintherNCDual(cell, degree) mapping = "double contravariant piola" - super(ArnoldWintherNC, self).__init__(Ps, Ls, degree, - mapping=mapping) + super().__init__(Ps, Ls, degree, mapping=mapping) class ArnoldWintherDual(DualSet): - def __init__(self, cell, degree): + def __init__(self, cell, degree=3): if not degree == 3: raise ValueError("Arnold-Winther elements are" "only defined for degree 3.") @@ -152,15 +151,15 @@ def __init__(self, cell, degree): dof_ids[2][0] += list(range(dof_cur, dof_cur+6)) - super(ArnoldWintherDual, self).__init__(dofs, cell, dof_ids) + super().__init__(dofs, cell, dof_ids) class ArnoldWinther(CiarletElement): """The definition of the conforming Arnold-Winther element. """ - def __init__(self, cell, degree): + def __init__(self, cell, degree=3): assert degree == 3, "Only defined for degree 3" Ps = ONSymTensorPolynomialSet(cell, degree) Ls = ArnoldWintherDual(cell, degree) mapping = "double contravariant piola" - super(ArnoldWinther, self).__init__(Ps, Ls, degree, mapping=mapping) + super().__init__(Ps, Ls, degree, mapping=mapping) diff --git a/FIAT/barycentric_interpolation.py b/FIAT/barycentric_interpolation.py index 7e2f4111d..25862bf2f 100644 --- a/FIAT/barycentric_interpolation.py +++ b/FIAT/barycentric_interpolation.py @@ -73,7 +73,7 @@ def __init__(self, ref_el, pts): self.degree = max(len(wts) for wts in self.weights)-1 self.recurrence_order = self.degree + 1 - super(LagrangeLineExpansionSet, self).__init__(ref_el) + super().__init__(ref_el) self.continuity = None if len(self.x) == sum(len(xk) for xk in self.nodes) else "C0" def get_num_members(self, n): @@ -123,5 +123,4 @@ def __init__(self, ref_el, pts, shape=tuple()): coeffs[cur_idx] = 1.0 cur_bf += 1 - super(LagrangePolynomialSet, self).__init__(ref_el, degree, embedded_degree, - expansion_set, coeffs) + super().__init__(ref_el, degree, embedded_degree, expansion_set, coeffs) diff --git a/FIAT/bell.py b/FIAT/bell.py index 2603d3a77..1d3fe11b3 100644 --- a/FIAT/bell.py +++ b/FIAT/bell.py @@ -53,7 +53,7 @@ def __init__(self, ref_el): from FIAT.jacobi import eval_jacobi rline = ufc_simplex(1) q1d = create_quadrature(rline, 8) - q1dpts = q1d.get_points() + q1dpts = q1d.get_points()[:, 0] leg4_at_qpts = eval_jacobi(0, 0, 4, 2.0*q1dpts - 1) imond = functional.IntegralMomentOfNormalDerivative @@ -64,7 +64,7 @@ def __init__(self, ref_el): entity_ids[2] = {0: []} - super(BellDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class Bell(finite_element.CiarletElement): @@ -73,4 +73,4 @@ class Bell(finite_element.CiarletElement): def __init__(self, ref_el): poly_set = polynomial_set.ONPolynomialSet(ref_el, 5) dual = BellDualSet(ref_el) - super(Bell, self).__init__(poly_set, dual, 5) + super().__init__(poly_set, dual, 5) diff --git a/FIAT/bernardi_raugel.py b/FIAT/bernardi_raugel.py new file mode 100644 index 000000000..68e8720f2 --- /dev/null +++ b/FIAT/bernardi_raugel.py @@ -0,0 +1,102 @@ +# This file is part of FIAT (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Written by Pablo D. Brubeck (brubeck@protonmail.com), 2024 + +# This is not quite Bernardi-Raugel, but it has 2*dim*(dim+1) dofs and includes +# dim**2-1 extra constraint functionals. The first (dim+1)**2 basis functions +# are the reference element bfs, but the extra dim**2-1 are used in the +# transformation theory. + +from FIAT import finite_element, dual_set, polynomial_set, expansions +from FIAT.functional import ComponentPointEvaluation, FrobeniusIntegralMoment +from FIAT.quadrature_schemes import create_quadrature +from FIAT.quadrature import FacetQuadratureRule +import numpy + + +def ExtendedBernardiRaugelSpace(ref_el, subdegree): + r"""Return a basis for the extended Bernardi-Raugel space. + P_k^d + (P_{dim} \ P_{dim-1})^d""" + sd = ref_el.get_spatial_dimension() + if subdegree >= sd: + raise ValueError("The Bernardi-Raugel space is only defined for subdegree < dim") + Pd = polynomial_set.ONPolynomialSet(ref_el, sd, shape=(sd,), scale=1, variant="bubble") + dimPd = expansions.polynomial_dimension(ref_el, sd, continuity="C0") + entity_ids = expansions.polynomial_entity_ids(ref_el, sd, continuity="C0") + ids = [i + j * dimPd + for dim in (*tuple(range(subdegree)), sd-1) + for f in sorted(entity_ids[dim]) + for i in entity_ids[dim][f] + for j in range(sd)] + return Pd.take(ids) + + +class BernardiRaugelDualSet(dual_set.DualSet): + """The Bernardi-Raugel dual set.""" + def __init__(self, ref_complex, degree, subdegree=1, reduced=False): + ref_el = ref_complex.get_parent() or ref_complex + sd = ref_el.get_spatial_dimension() + if subdegree > sd: + raise ValueError("The Bernardi-Raugel dual is only defined for subdegree <= dim") + top = ref_el.get_topology() + entity_ids = {dim: {entity: [] for entity in sorted(top[dim])} for dim in sorted(top)} + + # Point evaluation at lattice points + nodes = [] + for dim in range(subdegree): + for entity in sorted(top[dim]): + cur = len(nodes) + pts = ref_el.make_points(dim, entity, subdegree) + nodes.extend(ComponentPointEvaluation(ref_el, comp, (sd,), pt) + for pt in pts for comp in range(sd)) + entity_ids[dim][entity].extend(range(cur, len(nodes))) + + if subdegree < sd: + # Face moments of normal/tangential components against mean-free bubbles + facet = ref_complex.construct_subcomplex(sd-1) + Q = create_quadrature(facet, 2*degree) + if degree == 1 and facet.is_macrocell(): + P = polynomial_set.ONPolynomialSet(facet, degree, scale=1, variant="bubble") + f_at_qpts = P.tabulate(Q.get_points())[(0,)*(sd-1)][-1] + else: + ref_facet = facet.get_parent() or facet + f_at_qpts = ref_facet.compute_bubble(Q.get_points()) + f_at_qpts -= numpy.dot(f_at_qpts, Q.get_weights()) / facet.volume() + + Qs = {f: FacetQuadratureRule(ref_el, sd-1, f, Q) + for f in sorted(top[sd-1])} + + thats = {f: ref_el.compute_tangents(sd-1, f) + for f in sorted(top[sd-1])} + + R = numpy.array([[0, 1], [-1, 0]]) + ndir = 1 if reduced else sd + for i in range(ndir): + for f, Q_mapped in Qs.items(): + cur = len(nodes) + if i == 0: + udir = numpy.dot(R, *thats[f]) if sd == 2 else numpy.cross(*thats[f]) + else: + udir = thats[f][i-1] + detJ = Q_mapped.jacobian_determinant() + phi_at_qpts = udir[:, None] * f_at_qpts[None, :] / detJ + nodes.append(FrobeniusIntegralMoment(ref_el, Q_mapped, phi_at_qpts)) + entity_ids[sd-1][f].extend(range(cur, len(nodes))) + + super().__init__(nodes, ref_el, entity_ids) + + +class BernardiRaugel(finite_element.CiarletElement): + """The Bernardi-Raugel extended element.""" + def __init__(self, ref_el, degree=None, subdegree=1): + sd = ref_el.get_spatial_dimension() + if degree is None: + degree = sd + if degree != sd: + raise ValueError("Bernardi-Raugel only defined for degree = dim") + poly_set = ExtendedBernardiRaugelSpace(ref_el, subdegree) + dual = BernardiRaugelDualSet(ref_el, degree, subdegree=subdegree) + formdegree = sd - 1 # (n-1)-form + super().__init__(poly_set, dual, degree, formdegree, mapping="contravariant piola") diff --git a/FIAT/bernstein.py b/FIAT/bernstein.py index 81768d12d..096e10f16 100644 --- a/FIAT/bernstein.py +++ b/FIAT/bernstein.py @@ -43,7 +43,7 @@ def __init__(self, ref_el, degree): # Leave nodes unimplemented for now nodes.append(None) - super(BernsteinDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class Bernstein(FiniteElement): @@ -52,7 +52,7 @@ class Bernstein(FiniteElement): def __init__(self, ref_el, degree): dual = BernsteinDualSet(ref_el, degree) k = 0 # 0-form - super(Bernstein, self).__init__(ref_el, dual, degree, k) + super().__init__(ref_el, dual, degree, k) def degree(self): """The degree of the polynomial space.""" diff --git a/FIAT/brezzi_douglas_fortin_marini.py b/FIAT/brezzi_douglas_fortin_marini.py index fb8f81bd8..4361e70f6 100644 --- a/FIAT/brezzi_douglas_fortin_marini.py +++ b/FIAT/brezzi_douglas_fortin_marini.py @@ -58,7 +58,7 @@ def __init__(self, ref_el, degree): entity_ids[sd] = {0: list(range(cur, cur + tangent_count))} cur += tangent_count - super(BDFMDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) def BDFMSpace(ref_el, order): @@ -112,5 +112,5 @@ def __init__(self, ref_el, degree): poly_set = BDFMSpace(ref_el, degree) dual = BDFMDualSet(ref_el, degree - 1) formdegree = ref_el.get_spatial_dimension() - 1 - super(BrezziDouglasFortinMarini, self).__init__(poly_set, dual, degree, formdegree, - mapping="contravariant piola") + super().__init__(poly_set, dual, degree, formdegree, + mapping="contravariant piola") diff --git a/FIAT/brezzi_douglas_marini.py b/FIAT/brezzi_douglas_marini.py index 2c9aeb892..0014b5277 100644 --- a/FIAT/brezzi_douglas_marini.py +++ b/FIAT/brezzi_douglas_marini.py @@ -65,7 +65,7 @@ def __init__(self, ref_el, degree, variant, interpolant_deg): for phi in Ned_at_qpts) entity_ids[sd][0] = list(range(cur, len(nodes))) - super(BDMDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class BrezziDouglasMarini(finite_element.CiarletElement): @@ -99,5 +99,4 @@ def __init__(self, ref_el, degree, variant=None): poly_set = polynomial_set.ONPolynomialSet(ref_el, degree, (sd, )) dual = BDMDualSet(ref_el, degree, variant, interpolant_deg) formdegree = sd - 1 # (n-1)-form - super(BrezziDouglasMarini, self).__init__(poly_set, dual, degree, formdegree, - mapping="contravariant piola") + super().__init__(poly_set, dual, degree, formdegree, mapping="contravariant piola") diff --git a/FIAT/brezzi_douglas_marini_cube.py b/FIAT/brezzi_douglas_marini_cube.py index 7a5ab500c..786a5d309 100644 --- a/FIAT/brezzi_douglas_marini_cube.py +++ b/FIAT/brezzi_douglas_marini_cube.py @@ -76,9 +76,9 @@ def __init__(self, ref_el, degree, mapping): entity_closure_ids = make_entity_closure_ids(flat_el, entity_ids) # Set up FiniteElement - super(BrezziDouglasMariniCube, self).__init__(ref_el=ref_el, dual=None, - order=degree, formdegree=1, - mapping=mapping) + super().__init__(ref_el=ref_el, dual=None, + order=degree, formdegree=1, + mapping=mapping) # Store unflattened entity ID dictionaries topology = ref_el.get_topology() @@ -272,8 +272,7 @@ def __init__(self, ref_el, degree): bdmce_list = construct_bdmce_basis(ref_el, degree) self.basis = {(0, 0): Array(bdmce_list)} - super(BrezziDouglasMariniCubeEdge, self).__init__(ref_el=ref_el, degree=degree, - mapping="covariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="covariant piola") class BrezziDouglasMariniCubeFace(BrezziDouglasMariniCube): @@ -292,8 +291,7 @@ def __init__(self, ref_el, degree): bdmcf_list = [[-a[1], a[0]] for a in bdmce_list] self.basis = {(0, 0): Array(bdmcf_list)} - super(BrezziDouglasMariniCubeFace, self).__init__(ref_el=ref_el, degree=degree, - mapping="contravariant piola") + super().__init__(ref_el=ref_el, degree=degree, mapping="contravariant piola") def construct_bdmce_basis(ref_el, degree): diff --git a/FIAT/bubble.py b/FIAT/bubble.py index 1747d9e5c..0de7b2566 100644 --- a/FIAT/bubble.py +++ b/FIAT/bubble.py @@ -23,18 +23,18 @@ def __init__(self, ref_el, degree, codim): if len(dofs) == 0: raise RuntimeError('Bubble element of degree %d and codimension %d has no dofs' % (degree, codim)) - super(CodimBubble, self).__init__(element, indices=dofs) + super().__init__(element, indices=dofs) class Bubble(CodimBubble): """The bubble finite element: the dofs of the Lagrange FE in the interior of the cell""" def __init__(self, ref_el, degree): - super(Bubble, self).__init__(ref_el, degree, codim=0) + super().__init__(ref_el, degree, codim=0) class FacetBubble(CodimBubble): """The facet bubble finite element: the dofs of the Lagrange FE in the interior of the facets""" def __init__(self, ref_el, degree): - super(FacetBubble, self).__init__(ref_el, degree, codim=1) + super().__init__(ref_el, degree, codim=1) diff --git a/FIAT/check_format_variant.py b/FIAT/check_format_variant.py index b42c01ac6..1431876de 100644 --- a/FIAT/check_format_variant.py +++ b/FIAT/check_format_variant.py @@ -1,6 +1,6 @@ import re -from FIAT.macro import AlfeldSplit, IsoSplit +from FIAT.macro import IsoSplit, AlfeldSplit, WorseyFarinSplit, PowellSabinSplit, PowellSabin12Split # dicts mapping Lagrange variant names to recursivenodes family names supported_cg_variants = { @@ -17,6 +17,14 @@ "gll": "gll", "gl": "gl"} +supported_splits = { + "iso": IsoSplit, + "alfeld": AlfeldSplit, + "worsey-farin": WorseyFarinSplit, + "powell-sabin": PowellSabinSplit, + "powell-sabin(12)": PowellSabin12Split, +} + def check_format_variant(variant, degree): if variant is None: @@ -44,7 +52,7 @@ def parse_lagrange_variant(variant, discontinuous=False, integral=False): variant may be a single option or comma-separated pair indicating the dof type (integral, equispaced, spectral, etc) - and the type of splitting to give a macro-element (Alfeld, iso) + and the type of splitting to give a macro-element (Alfeld, Powell-Sabin, iso) """ if variant is None: variant = "integral" if integral else "equispaced" @@ -66,10 +74,8 @@ def parse_lagrange_variant(variant, discontinuous=False, integral=False): for pre_opt in options: opt = pre_opt.lower() - if opt == "alfeld": - splitting = AlfeldSplit - elif opt == "iso": - splitting = IsoSplit + if opt in supported_splits: + splitting = supported_splits[opt] elif opt.startswith("iso"): match = re.match(r"^iso(?:\((\d+)\))?$", opt) k, = match.groups() diff --git a/FIAT/christiansen_hu.py b/FIAT/christiansen_hu.py new file mode 100644 index 000000000..d9d5b33e0 --- /dev/null +++ b/FIAT/christiansen_hu.py @@ -0,0 +1,78 @@ +# This file is part of FIAT (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Written by Pablo D. Brubeck (brubeck@protonmail.com), 2024 + +# This is not quite Christiansen-Hu, but it has 2*dim*(dim+1) dofs and includes +# dim**2-1 extra constraint functionals. The first (dim+1)**2 basis functions +# are the reference element bfs, but the extra dim**2-1 are used in the +# transformation theory. + +from FIAT import finite_element, polynomial_set +from FIAT.bernardi_raugel import BernardiRaugelDualSet +from FIAT.quadrature_schemes import create_quadrature +from FIAT.macro import CkPolynomialSet, WorseyFarinSplit + +import numpy + + +def ChristiansenHuSpace(ref_el, degree, reduced=False): + """Return a basis for the Christianse-Hu space + set(v in C0 P1(WF)^d : div(v) = 0) + P_0 x if reduced = True, and + this space is agumented with rotated facet bubbles if reduced = False.""" + sd = ref_el.get_spatial_dimension() + ref_complex = WorseyFarinSplit(ref_el) + C0 = CkPolynomialSet(ref_complex, degree, order=0, shape=(sd,), scale=1, variant="bubble") + Q = create_quadrature(ref_complex, degree-1) + tab = C0.tabulate(Q.get_points(), 1) + divC0 = sum(tab[alpha][:, alpha.index(1), :] for alpha in tab if sum(alpha) == 1) + + _, sig, vt = numpy.linalg.svd(divC0.T, full_matrices=True) + tol = sig[0] * 1E-10 + num_sv = len([s for s in sig if abs(s) > tol]) + coeffs = numpy.tensordot(vt[num_sv:], C0.get_coeffs(), axes=(-1, 0)) + + verts = numpy.array(ref_complex.get_vertices()) + WT = verts[-1] + P0x_coeffs = numpy.transpose(verts - WT[None, :]) + coeffs = numpy.concatenate((coeffs, P0x_coeffs[None, ...]), axis=0) + + if not reduced: + # Compute the primal basis via Vandermonde and extract the facet bubbles + dual = BernardiRaugelDualSet(ref_complex, degree, reduced=True) + dualmat = dual.to_riesz(C0) + V = numpy.tensordot(dualmat, coeffs, axes=((1, 2), (1, 2))) + coeffs = numpy.tensordot(numpy.linalg.inv(V.T), coeffs, axes=(-1, 0)) + facet_bubbles = coeffs[-(sd+1):] + + # Rotate the facet bubbles onto the tangent space of the facet + # NOTE they are not aligned with the normal, but they point in the direction + # that connects the split point on the facet with the split point of the cell + WF = verts[sd+1:-1] + top = ref_el.get_topology() + ext = [] + for f in top[sd-1]: + ehat = WF[f] - WT + FB = numpy.dot(ehat, facet_bubbles[f]) + thats = ref_el.compute_tangents(sd-1, f) + for that in thats: + ext.append(that[:, None] * FB[None, :]) + ext_coeffs = numpy.array(ext) + coeffs = numpy.concatenate((coeffs, ext_coeffs), axis=0) + + return polynomial_set.PolynomialSet(ref_complex, degree, degree, + C0.get_expansion_set(), coeffs) + + +class ChristiansenHu(finite_element.CiarletElement): + """The Christiansen-Hu C^0(Worsey-Farin) linear macroelement with divergence in P0. + This element belongs to a Stokes complex, and is paired with unsplit DG0.""" + def __init__(self, ref_el, degree=1): + if degree != 1: + raise ValueError("Christiansen-Hu only defined for degree = 1") + poly_set = ChristiansenHuSpace(ref_el, degree) + ref_complex = poly_set.get_reference_element() + dual = BernardiRaugelDualSet(ref_complex, degree) + formdegree = ref_el.get_spatial_dimension() - 1 # (n-1)-form + super().__init__(poly_set, dual, degree, formdegree, mapping="contravariant piola") diff --git a/FIAT/crouzeix_raviart.py b/FIAT/crouzeix_raviart.py index b8fa3f80e..b809f7a87 100644 --- a/FIAT/crouzeix_raviart.py +++ b/FIAT/crouzeix_raviart.py @@ -46,7 +46,7 @@ def __init__(self, cell, degree): entity_ids[d - 1][i] += [i] # Initialize super-class - super(CrouzeixRaviartDualSet, self).__init__(nodes, cell, entity_ids) + super().__init__(nodes, cell, entity_ids) class CrouzeixRaviart(finite_element.CiarletElement): @@ -67,4 +67,4 @@ def __init__(self, cell, degree): # FiniteElement space = polynomial_set.ONPolynomialSet(cell, 1) dual = CrouzeixRaviartDualSet(cell, 1) - super(CrouzeixRaviart, self).__init__(space, dual, 1) + super().__init__(space, dual, 1) diff --git a/FIAT/discontinuous_lagrange.py b/FIAT/discontinuous_lagrange.py index e086421e5..fd08f8fa3 100644 --- a/FIAT/discontinuous_lagrange.py +++ b/FIAT/discontinuous_lagrange.py @@ -169,7 +169,7 @@ def __init__(self, ref_el, degree, point_variant="equispaced"): entity_permutations[dim][entity] = perms entity_ids[dim][0] = list(range(len(nodes))) - super(BrokenLagrangeDualSet, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) class DiscontinuousLagrangeDualSet(dual_set.DualSet): @@ -196,7 +196,7 @@ def __init__(self, ref_el, degree, point_variant="equispaced"): degree, variant=point_variant) nodes.extend(functional.PointEvaluation(ref_el, x) for x in pts) entity_ids[dim][entity] = list(range(cur, len(nodes))) - super(DiscontinuousLagrangeDualSet, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) class DiscontinuousLagrange(finite_element.CiarletElement): @@ -220,7 +220,7 @@ def __new__(cls, ref_el, degree, variant="equispaced"): if splitting is None and not ref_el.is_macrocell(): # FIXME P0 on the split requires implementing SplitSimplicialComplex.symmetry_group_size() return P0.P0(ref_el) - return super(DiscontinuousLagrange, cls).__new__(cls) + return super().__new__(cls) def __init__(self, ref_el, degree, variant="equispaced"): splitting, point_variant = parse_lagrange_variant(variant, discontinuous=True) @@ -238,4 +238,4 @@ def __init__(self, ref_el, degree, variant="equispaced"): else: poly_set = polynomial_set.ONPolynomialSet(ref_el, degree) formdegree = ref_el.get_spatial_dimension() # n-form - super(DiscontinuousLagrange, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) diff --git a/FIAT/discontinuous_pc.py b/FIAT/discontinuous_pc.py index af02f8861..dbb566906 100644 --- a/FIAT/discontinuous_pc.py +++ b/FIAT/discontinuous_pc.py @@ -39,11 +39,11 @@ def __init__(self, ref_el): dual.entity_permutations = None degree = 0 formdegree = ref_el.get_spatial_dimension() # n-form - super(DPC0, self).__init__(poly_set=poly_set, - dual=dual, - order=degree, - ref_complex=ref_el, - formdegree=formdegree) + super().__init__(poly_set=poly_set, + dual=dual, + order=degree, + ref_complex=ref_el, + formdegree=formdegree) class DPCDualSet(dual_set.DualSet): @@ -94,7 +94,7 @@ def __init__(self, ref_el, flat_el, degree): entity_ids[dim][entity] = [] entity_ids[dim][0] = list(range(len(nodes))) - super(DPCDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class HigherOrderDPC(finite_element.CiarletElement): @@ -105,11 +105,11 @@ def __init__(self, ref_el, degree): poly_set = polynomial_set.ONPolynomialSet(hypercube_simplex_map[flat_el], degree) dual = DPCDualSet(ref_el, flat_el, degree) formdegree = flat_el.get_spatial_dimension() # n-form - super(HigherOrderDPC, self).__init__(poly_set=poly_set, - dual=dual, - order=degree, - ref_complex=ref_el, - formdegree=formdegree) + super().__init__(poly_set=poly_set, + dual=dual, + order=degree, + ref_complex=ref_el, + formdegree=formdegree) def DPC(ref_el, degree): diff --git a/FIAT/discontinuous_raviart_thomas.py b/FIAT/discontinuous_raviart_thomas.py index 12a73e650..8e3444e94 100644 --- a/FIAT/discontinuous_raviart_thomas.py +++ b/FIAT/discontinuous_raviart_thomas.py @@ -49,7 +49,7 @@ def __init__(self, ref_el, degree): # cell dofs entity_ids[sd] = {0: list(range(len(nodes)))} - super(DRTDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class DiscontinuousRaviartThomas(finite_element.CiarletElement): @@ -59,5 +59,4 @@ def __init__(self, ref_el, degree): poly_set = RTSpace(ref_el, degree) dual = DRTDualSet(ref_el, degree) - super(DiscontinuousRaviartThomas, self).__init__(poly_set, dual, degree, - mapping="contravariant piola") + super().__init__(poly_set, dual, degree, mapping="contravariant piola") diff --git a/FIAT/discontinuous_taylor.py b/FIAT/discontinuous_taylor.py index 7c27fd596..1438d1d3b 100644 --- a/FIAT/discontinuous_taylor.py +++ b/FIAT/discontinuous_taylor.py @@ -36,7 +36,7 @@ def __init__(self, ref_el, degree): for d in range(dim + 1)} entity_ids[dim][0] = list(range(len(nodes))) - super(DiscontinuousTaylorDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class HigherOrderDiscontinuousTaylor(finite_element.CiarletElement): @@ -46,7 +46,7 @@ def __init__(self, ref_el, degree): poly_set = polynomial_set.ONPolynomialSet(ref_el, degree) dual = DiscontinuousTaylorDualSet(ref_el, degree) formdegree = ref_el.get_spatial_dimension() # n-form - super(HigherOrderDiscontinuousTaylor, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) def DiscontinuousTaylor(ref_el, degree): diff --git a/FIAT/enriched.py b/FIAT/enriched.py index 97ac5c57c..b57da8401 100644 --- a/FIAT/enriched.py +++ b/FIAT/enriched.py @@ -59,7 +59,7 @@ def __init__(self, *elements): nodes = list(chain.from_iterable(e.dual_basis() for e in elements)) dual = DualSet(nodes, ref_el, entity_ids) - super(EnrichedElement, self).__init__(ref_el, dual, order, formdegree, mapping) + super().__init__(ref_el, dual, order, formdegree, mapping) # required degree (for quadrature) is definitely max self.polydegree = max(e.degree() for e in elements) diff --git a/FIAT/expansions.py b/FIAT/expansions.py index 8ff65f164..ac0eea173 100644 --- a/FIAT/expansions.py +++ b/FIAT/expansions.py @@ -247,7 +247,7 @@ def __new__(cls, *args, **kwargs): """Returns an ExpansionSet instance appropriate for the given reference element.""" if cls is not ExpansionSet: - return super(ExpansionSet, cls).__new__(cls) + return super().__new__(cls) try: ref_el = args[0] expansion_set = { @@ -518,7 +518,7 @@ class PointExpansionSet(ExpansionSet): def __init__(self, ref_el, **kwargs): if ref_el.get_spatial_dimension() != 0: raise ValueError("Must have a point") - super(PointExpansionSet, self).__init__(ref_el, **kwargs) + super().__init__(ref_el, **kwargs) def _tabulate_on_cell(self, n, pts, order=0, cell=0, direction=None): """Returns a dict of tabulations such that @@ -532,13 +532,13 @@ class LineExpansionSet(ExpansionSet): def __init__(self, ref_el, **kwargs): if ref_el.get_spatial_dimension() != 1: raise Exception("Must have a line") - super(LineExpansionSet, self).__init__(ref_el, **kwargs) + super().__init__(ref_el, **kwargs) def _tabulate_on_cell(self, n, pts, order=0, cell=0, direction=None): """Returns a dict of tabulations such that tabulations[alpha][i, j] = D^alpha phi_i(pts[j]).""" if self.variant is not None: - return super(LineExpansionSet, self)._tabulate_on_cell(n, pts, order=order, cell=cell, direction=direction) + return super()._tabulate_on_cell(n, pts, order=order, cell=cell, direction=direction) A, b = self.affine_mappings[cell] Jinv = A[0, 0] if direction is None else numpy.dot(A, direction) @@ -562,7 +562,7 @@ class TriangleExpansionSet(ExpansionSet): def __init__(self, ref_el, **kwargs): if ref_el.get_spatial_dimension() != 2: raise Exception("Must have a triangle") - super(TriangleExpansionSet, self).__init__(ref_el, **kwargs) + super().__init__(ref_el, **kwargs) class TetrahedronExpansionSet(ExpansionSet): @@ -570,7 +570,7 @@ class TetrahedronExpansionSet(ExpansionSet): def __init__(self, ref_el, **kwargs): if ref_el.get_spatial_dimension() != 3: raise Exception("Must be a tetrahedron") - super(TetrahedronExpansionSet, self).__init__(ref_el, **kwargs) + super().__init__(ref_el, **kwargs) def polynomial_dimension(ref_el, n, continuity=None): diff --git a/FIAT/fdm_element.py b/FIAT/fdm_element.py index 5f6408105..3c20c653b 100644 --- a/FIAT/fdm_element.py +++ b/FIAT/fdm_element.py @@ -140,7 +140,7 @@ def __init__(self, ref_el, degree, bc_order=1, formdegree=0, orthogonalize=False entity_permutations = {} entity_permutations[0] = {0: {0: []}, 1: {0: []}} entity_permutations[1] = {0: make_entity_permutations_simplex(1, degree + 1)} - super(FDMDual, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) class FDMFiniteElement(finite_element.CiarletElement): @@ -171,7 +171,7 @@ def __init__(self, ref_el, degree): else: lr = quadrature.GaussLegendreQuadratureLineRule(ref_el, degree+1) poly_set = LagrangePolynomialSet(ref_el, lr.get_points()) - super(FDMFiniteElement, self).__init__(poly_set, dual, degree, self._formdegree) + super().__init__(poly_set, dual, degree, self._formdegree) class FDMLagrange(FDMFiniteElement): diff --git a/FIAT/functional.py b/FIAT/functional.py index a55fdb864..36989e607 100644 --- a/FIAT/functional.py +++ b/FIAT/functional.py @@ -12,13 +12,12 @@ # - a reference element domain # - type information -from collections import OrderedDict from itertools import chain import numpy import sympy -from FIAT import polynomial_set -from FIAT.quadrature import GaussLegendreQuadratureLineRule, QuadratureRule +from FIAT import polynomial_set, jacobi +from FIAT.quadrature import GaussLegendreQuadratureLineRule from FIAT.reference_element import UFCInterval as interval @@ -269,6 +268,16 @@ def __init__(self, ref_el, facet_no, pt): Functional.__init__(self, ref_el, tuple(), {}, dpt_dict, "PointNormalDeriv") +class PointDivergence(Functional): + """Class representing point divergence of vector + functions at a particular point x.""" + + def __init__(self, ref_el, x): + dpt_dict = {x: [(1.0, alpha, (alpha.index(1),)) for alpha in polynomial_set.mis(len(x), 1)]} + + Functional.__init__(self, ref_el, (len(x),), {}, dpt_dict, "PointDiv") + + class IntegralMoment(Functional): """Functional representing integral of the input against some tabulated function f. @@ -313,17 +322,14 @@ def __init__(self, ref_el, facet_no, Q, f_at_qpts): sd = ref_el.get_spatial_dimension() # map points onto facet + transform = ref_el.get_entity_transform(sd-1, facet_no) + points = transform(Q.get_points()) + self.dpts = points + weights = numpy.multiply(f_at_qpts, Q.get_weights()) - fmap = ref_el.get_entity_transform(sd-1, facet_no) - qpts, qwts = Q.get_points(), Q.get_weights() - dpts = [fmap(pt) for pt in qpts] - self.dpts = dpts - - dpt_dict = OrderedDict() - - alphas = [tuple(1 if j == i else 0 for j in range(sd)) for i in range(sd)] - for j, pt in enumerate(dpts): - dpt_dict[tuple(pt)] = [(qwts[j]*n[i]*f_at_qpts[j], alphas[i], tuple()) for i in range(sd)] + alphas = tuple(map(tuple, numpy.eye(sd, dtype=int))) + dpt_dict = {tuple(pt): [(wt*n[i], alphas[i], tuple()) for i in range(sd)] + for pt, wt in zip(points, weights)} Functional.__init__(self, ref_el, tuple(), {}, dpt_dict, "IntegralMomentOfNormalDerivative") @@ -337,20 +343,13 @@ def __init__(self, cell, s, entity, mom_deg, comp_deg, nm=""): shp = (sd,) quadpoints = comp_deg + 1 Q = GaussLegendreQuadratureLineRule(interval(), quadpoints) - legendre = numpy.polynomial.legendre.legval(2*Q.get_points()-1, [0]*mom_deg + [1]) - f_at_qpts = numpy.array([s*legendre[i] for i in range(quadpoints)]) - fmap = cell.get_entity_transform(sd-1, entity) - mappedqpts = [fmap(pt) for pt in Q.get_points()] - mappedQ = QuadratureRule(cell, mappedqpts, Q.get_weights()) - qwts = mappedQ.wts - qpts = mappedQ.pts - - pt_dict = OrderedDict() - - for k in range(len(qpts)): - pt_cur = tuple(qpts[k]) - pt_dict[pt_cur] = [(qwts[k] * f_at_qpts[k, i], (i,)) - for i in range(2)] + x = 2*Q.get_points()[:, 0]-1 + f_at_qpts = jacobi.eval_jacobi(0, 0, mom_deg, x) + transform = cell.get_entity_transform(sd-1, entity) + points = transform(Q.get_points()) + weights = numpy.multiply(f_at_qpts, Q.get_weights()) + pt_dict = {tuple(pt): [(wt*s[i], (i,)) for i in range(sd)] + for pt, wt in zip(points, weights)} super().__init__(cell, shp, pt_dict, {}, nm) @@ -377,32 +376,24 @@ def __init__(self, cell, s1, s2, entity, mom_deg, comp_deg, nm=""): # mom_deg is degree of moment, comp_deg is the total degree of # polynomial you might need to integrate (or something like that) sd = cell.get_spatial_dimension() - shp = (sd, sd) s1s2T = numpy.outer(s1, s2) + shp = s1s2T.shape quadpoints = comp_deg + 1 Q = GaussLegendreQuadratureLineRule(interval(), quadpoints) # The volume squared gets the Jacobian mapping from line interval # and the edge length into the functional. - legendre = numpy.polynomial.legendre.legval(2*Q.get_points()-1, [0]*mom_deg + [1]) * numpy.abs(cell.volume_of_subcomplex(1, entity))**2 - - f_at_qpts = numpy.array([s1s2T*legendre[i] for i in range(quadpoints)]) + x = 2*Q.get_points()[:, 0]-1 + f_at_qpts = jacobi.eval_jacobi(0, 0, mom_deg, x) * numpy.abs(cell.volume_of_subcomplex(1, entity))**2 # Map the quadrature points - fmap = cell.get_entity_transform(sd-1, entity) - mappedqpts = [fmap(pt) for pt in Q.get_points()] - mappedQ = QuadratureRule(cell, mappedqpts, Q.get_weights()) + transform = cell.get_entity_transform(sd-1, entity) + points = transform(Q.get_points()) + weights = numpy.multiply(f_at_qpts, Q.get_weights()) - pt_dict = OrderedDict() - - qpts = mappedQ.pts - qwts = mappedQ.wts - - for k in range(len(qpts)): - pt_cur = tuple(qpts[k]) - pt_dict[pt_cur] = [(qwts[k] * f_at_qpts[k, i, j], (i, j)) - for (i, j) in index_iterator(shp)] + pt_dict = {tuple(pt): [(wt * s1s2T[idx], idx) for idx in index_iterator(shp)] + for pt, wt in zip(points, weights)} super().__init__(cell, shp, pt_dict, {}, nm) @@ -433,15 +424,13 @@ def __init__(self, ref_el, Q, f_at_qpts): sd = ref_el.get_spatial_dimension() - qpts, qwts = Q.get_points(), Q.get_weights() - dpts = qpts - self.dpts = dpts - - dpt_dict = OrderedDict() + points = Q.get_points() + self.dpts = points + weights = numpy.multiply(f_at_qpts, Q.get_weights()) - alphas = [tuple([1 if j == i else 0 for j in range(sd)]) for i in range(sd)] - for j, pt in enumerate(dpts): - dpt_dict[tuple(pt)] = [(qwts[j]*f_at_qpts[j], alphas[i], (i,)) for i in range(sd)] + alphas = tuple(map(tuple, numpy.eye(sd, dtype=int))) + dpt_dict = {tuple(pt): [(wt, alphas[i], (i,)) for i in range(sd)] + for pt, wt in zip(points, weights)} super().__init__(ref_el, tuple(), {}, dpt_dict, "IntegralMomentOfDivergence") @@ -453,24 +442,21 @@ class IntegralMomentOfTensorDivergence(Functional): def __init__(self, ref_el, Q, f_at_qpts): self.f_at_qpts = f_at_qpts self.Q = Q - qpts, qwts = Q.get_points(), Q.get_weights() - nqp = len(qpts) - dpts = qpts - self.dpts = dpts + points = Q.get_points() + self.dpts = points sd = ref_el.get_spatial_dimension() + shp = (sd, sd) assert len(f_at_qpts.shape) == 2 assert f_at_qpts.shape[0] == sd - assert f_at_qpts.shape[1] == nqp + assert f_at_qpts.shape[1] == len(points) + weights = numpy.multiply(f_at_qpts, Q.get_weights()).T - dpt_dict = OrderedDict() + alphas = tuple(map(tuple, numpy.eye(sd, dtype=int))) + dpt_dict = {tuple(pt): [(wt[i], alphas[j], (i, j)) for i, j in index_iterator(shp)] + for pt, wt in zip(points, weights)} - alphas = [tuple([1 if j == i else 0 for j in range(sd)]) for i in range(sd)] - for q, pt in enumerate(dpts): - dpt_dict[tuple(pt)] = [(qwts[q]*f_at_qpts[i, q], alphas[j], (i, j)) for i in range(sd) for j in range(sd)] - - super().__init__(ref_el, tuple(), {}, dpt_dict, - "IntegralMomentOfDivergence") + super().__init__(ref_el, tuple(), {}, dpt_dict, "IntegralMomentOfDivergence") class FrobeniusIntegralMoment(IntegralMoment): @@ -499,11 +485,8 @@ class PointNormalEvaluation(Functional): def __init__(self, ref_el, facet_no, pt): n = ref_el.compute_normal(facet_no) self.n = n - sd = ref_el.get_spatial_dimension() - - pt_dict = {pt: [(n[i], (i,)) for i in range(sd)]} - - shp = (sd,) + shp = n.shape + pt_dict = {pt: [(n[i], (i,)) for i in range(shp[0])]} super().__init__(ref_el, shp, pt_dict, {}, "PointNormalEval") @@ -514,9 +497,8 @@ class PointEdgeTangentEvaluation(Functional): def __init__(self, ref_el, edge_no, pt): t = ref_el.compute_edge_tangent(edge_no) self.t = t - sd = ref_el.get_spatial_dimension() - pt_dict = {pt: [(t[i], (i,)) for i in range(sd)]} - shp = (sd,) + shp = t.shape + pt_dict = {pt: [(t[i], (i,)) for i in range(shp[0])]} super().__init__(ref_el, shp, pt_dict, {}, "PointEdgeTangent") def tostr(self): @@ -539,11 +521,10 @@ def __init__(self, ref_el, Q, P_at_qpts, edge): t = ref_el.compute_edge_tangent(edge) sd = ref_el.get_spatial_dimension() transform = ref_el.get_entity_transform(1, edge) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) - weights = Q.get_weights() - pt_dict = OrderedDict() - for pt, wgt, phi in zip(pts, weights, P_at_qpts): - pt_dict[pt] = [(wgt*phi*t[i], (i, )) for i in range(sd)] + points = transform(Q.get_points()) + weights = numpy.multiply(P_at_qpts, Q.get_weights()) + pt_dict = {tuple(pt): [(wt*t[i], (i,)) for i in range(sd)] + for pt, wt in zip(points, weights)} super().__init__(ref_el, (sd, ), pt_dict, {}, "IntegralMomentOfEdgeTangentEvaluation") @@ -583,9 +564,9 @@ def __init__(self, ref_el, Q, P_at_qpts, facet): n = ref_el.compute_scaled_normal(facet) sd = ref_el.get_spatial_dimension() transform = ref_el.get_entity_transform(sd-1, facet) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) + pts = tuple(map(tuple, transform(Q.get_points()))) weights = Q.get_weights() - pt_dict = OrderedDict() + pt_dict = {} for pt, wgt, phi in zip(pts, weights, P_at_qpts): phixn = [phi[1]*n[2] - phi[2]*n[1], phi[2]*n[0] - phi[0]*n[2], @@ -611,12 +592,11 @@ class MonkIntegralMoment(Functional): def __init__(self, ref_el, Q, P_at_qpts, facet): sd = ref_el.get_spatial_dimension() - weights = Q.get_weights() - pt_dict = OrderedDict() transform = ref_el.get_entity_transform(sd-1, facet) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) - for pt, wgt, phi in zip(pts, weights, P_at_qpts): - pt_dict[pt] = [(wgt*phi[i], (i, )) for i in range(sd)] + pts = transform(Q.get_points()) + weights = Q.get_weights() * P_at_qpts + pt_dict = {tuple(pt): [(wt[i], (i, )) for i in range(sd)] + for pt, wt in zip(pts, weights)} super().__init__(ref_el, (sd, ), pt_dict, {}, "MonkIntegralMoment") @@ -653,11 +633,10 @@ def __init__(self, ref_el, Q, P_at_qpts, facet): n = ref_el.compute_scaled_normal(facet) sd = ref_el.get_spatial_dimension() transform = ref_el.get_entity_transform(sd - 1, facet) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) - weights = Q.get_weights() - pt_dict = OrderedDict() - for pt, wgt, phi in zip(pts, weights, P_at_qpts): - pt_dict[pt] = [(wgt*phi*n[i], (i, )) for i in range(sd)] + pts = transform(Q.get_points()) + weights = Q.get_weights() * P_at_qpts + pt_dict = {tuple(pt): [(wt*n[i], (i, )) for i in range(sd)] + for pt, wt in zip(pts, weights)} super().__init__(ref_el, (sd, ), pt_dict, {}, "IntegralMomentOfScaledNormalEvaluation") @@ -672,15 +651,12 @@ class PointwiseInnerProductEvaluation(Functional): correct weights. """ - def __init__(self, ref_el, v, w, p): - sd = ref_el.get_spatial_dimension() - + def __init__(self, ref_el, v, w, pt): wvT = numpy.outer(w, v) + shp = wvT.shape - pt_dict = {p: [(wvT[i][j], (i, j)) - for i, j in index_iterator((sd, sd))]} + pt_dict = {pt: [(wvT[idx], idx) for idx in index_iterator(shp)]} - shp = (sd, sd) super().__init__(ref_el, shp, pt_dict, {}, "PointwiseInnerProductEval") @@ -696,20 +672,15 @@ class TensorBidirectionalMomentInnerProductEvaluation(Functional): """ def __init__(self, ref_el, v, w, Q, f_at_qpts, comp_deg): - sd = ref_el.get_spatial_dimension() - wvT = numpy.outer(w, v) + shp = wvT.shp - qpts, qwts = Q.get_points(), Q.get_weights() + points = Q.get_points() + weights = numpy.multiply(f_at_qpts, Q.get_weights()) - pt_dict = {} - for k, pt in enumerate(map(tuple(qpts))): - pt_dict[pt] = [] - for i, j in index_iterator((sd, sd)): - pt_dict[pt].append((qwts[k] * wvT[i][j] * f_at_qpts[i, j, k]), - (i, j)) + pt_dict = {tuple(pt): [(wt * wvT[idx], idx) for idx in index_iterator(shp)] + for pt, wt in zip(points, weights)} - shp = (sd, sd) super().__init__(ref_el, shp, pt_dict, {}, "TensorBidirectionalMomentInnerProductEvaluation") @@ -728,12 +699,11 @@ def __init__(self, ref_el, Q, P_at_qpts, facet): n = ref_el.compute_scaled_normal(facet) sd = ref_el.get_spatial_dimension() transform = ref_el.get_entity_transform(sd - 1, facet) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) - weights = Q.get_weights() - pt_dict = OrderedDict() - for pt, wgt, phi in zip(pts, weights, P_at_qpts): - pt_dict[pt] = [(wgt*phi*n[i], (i, )) for i in range(sd)] - super().__init__(ref_el, (sd, ), pt_dict, {}, "IntegralMomentOfScaledNormalEvaluation") + pts = transform(Q.get_points()) + weights = numpy.multiply(P_at_qpts, Q.get_weights()) + pt_dict = {tuple(pt): [(wt*n[i], (i, )) for i in range(sd)] + for pt, wt in zip(pts, weights)} + super().__init__(ref_el, (sd, ), pt_dict, {}, "IntegralMomentOfNormalEvaluation") class IntegralMomentOfTangentialEvaluation(Functional): @@ -752,11 +722,10 @@ def __init__(self, ref_el, Q, P_at_qpts, facet): assert sd == 2 t = ref_el.compute_edge_tangent(facet) transform = ref_el.get_entity_transform(sd - 1, facet) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) - weights = Q.get_weights() - pt_dict = OrderedDict() - for pt, wgt, phi in zip(pts, weights, P_at_qpts): - pt_dict[pt] = [(wgt*phi*t[i], (i, )) for i in range(sd)] + points = transform(Q.get_points()) + weights = numpy.multiply(P_at_qpts, Q.get_weights()) + pt_dict = {tuple(pt): [(wt*t[i], (i, )) for i in range(sd)] + for pt, wt in zip(points, weights)} super().__init__(ref_el, (sd, ), pt_dict, {}, "IntegralMomentOfScaledTangentialEvaluation") @@ -773,11 +742,12 @@ def __init__(self, ref_el, Q, P_at_qpts, facet): # scaling on the normal is ok because edge length then weights # the reference element quadrature appropriately n = ref_el.compute_scaled_normal(facet) + nnT = numpy.outer(n, n)/numpy.linalg.norm(n) + shp = nnT.shape sd = ref_el.get_spatial_dimension() transform = ref_el.get_entity_transform(sd - 1, facet) - pts = tuple(map(lambda p: tuple(transform(p)), Q.get_points())) - weights = Q.get_weights() - pt_dict = OrderedDict() - for pt, wgt, phi in zip(pts, weights, P_at_qpts): - pt_dict[pt] = [(wgt*phi*n[i], (i, )) for i in range(sd)] - super().__init__(ref_el, (sd, ), pt_dict, {}, "IntegralMomentOfScaledNormalEvaluation") + points = transform(Q.get_points()) + weights = numpy.multiply(P_at_qpts, Q.get_weights()) + pt_dict = {tuple(pt): [(wt*nnT[idx], idx) for idx in index_iterator(shp)] + for pt, wt in zip(points, weights)} + super().__init__(ref_el, shp, pt_dict, {}, "IntegralMomentOfNormalNormalEvaluation") diff --git a/FIAT/gauss_legendre.py b/FIAT/gauss_legendre.py index 7962918f6..ff2d9ec82 100644 --- a/FIAT/gauss_legendre.py +++ b/FIAT/gauss_legendre.py @@ -14,4 +14,4 @@ class GaussLegendre(discontinuous_lagrange.DiscontinuousLagrange): """Simplicial discontinuous element with nodes at the (recursive) Gauss-Legendre points.""" def __init__(self, ref_el, degree): - super(GaussLegendre, self).__init__(ref_el, degree, variant="gl") + super().__init__(ref_el, degree, variant="gl") diff --git a/FIAT/gauss_lobatto_legendre.py b/FIAT/gauss_lobatto_legendre.py index e97c7d886..4973a690c 100644 --- a/FIAT/gauss_lobatto_legendre.py +++ b/FIAT/gauss_lobatto_legendre.py @@ -14,4 +14,4 @@ class GaussLobattoLegendre(lagrange.Lagrange): """Simplicial continuous element with nodes at the (recursive) Gauss-Lobatto-Legendre points.""" def __init__(self, ref_el, degree): - super(GaussLobattoLegendre, self).__init__(ref_el, degree, variant="gll", sort_entities=True) + super().__init__(ref_el, degree, variant="gll", sort_entities=True) diff --git a/FIAT/gauss_radau.py b/FIAT/gauss_radau.py index 89191756d..784369b2b 100644 --- a/FIAT/gauss_radau.py +++ b/FIAT/gauss_radau.py @@ -22,7 +22,7 @@ def __init__(self, ref_el, degree, right=True): lr = quadrature.RadauQuadratureLineRule(ref_el, degree+1, right) nodes = [functional.PointEvaluation(ref_el, x) for x in lr.pts] - super(GaussRadauDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class GaussRadau(finite_element.CiarletElement): @@ -33,4 +33,4 @@ def __init__(self, ref_el, degree): poly_set = polynomial_set.ONPolynomialSet(ref_el, degree) dual = GaussRadauDualSet(ref_el, degree) formdegree = ref_el.get_spatial_dimension() # n-form - super(GaussRadau, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) diff --git a/FIAT/guzman_neilan.py b/FIAT/guzman_neilan.py new file mode 100644 index 000000000..ec72be5a6 --- /dev/null +++ b/FIAT/guzman_neilan.py @@ -0,0 +1,147 @@ +# This file is part of FIAT (https://www.fenicsproject.org) +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Written by Pablo D. Brubeck (brubeck@protonmail.com), 2024 + +# This is not quite Guzman-Neilan, but it has 2*dim*(dim+1) dofs and includes +# dim**2-1 extra constraint functionals. The first (dim+1)**2 basis functions +# are the reference element bfs, but the extra dim**2-1 are used in the +# transformation theory. + +from FIAT import finite_element, polynomial_set, expansions +from FIAT.bernardi_raugel import ExtendedBernardiRaugelSpace, BernardiRaugelDualSet, BernardiRaugel +from FIAT.brezzi_douglas_marini import BrezziDouglasMarini +from FIAT.macro import AlfeldSplit +from FIAT.quadrature_schemes import create_quadrature +from itertools import chain + +import numpy +import math + + +def ExtendedGuzmanNeilanSpace(ref_el, subdegree, reduced=False): + """Return a basis for the extended Guzman-Neilan space.""" + sd = ref_el.get_spatial_dimension() + ref_complex = AlfeldSplit(ref_el) + C0 = polynomial_set.ONPolynomialSet(ref_complex, sd, shape=(sd,), scale=1, variant="bubble") + B = take_interior_bubbles(C0) + if sd > 2: + B = modified_bubble_subspace(B) + + if reduced: + BR = BernardiRaugel(ref_el, sd, subdegree=subdegree).get_nodal_basis() + reduced_dim = BR.get_num_members() - (sd-1) * (sd+1) + BR = BR.take(list(range(reduced_dim))) + else: + BR = ExtendedBernardiRaugelSpace(ref_el, subdegree) + GN = constant_div_projection(BR, C0, B) + return GN + + +class GuzmanNeilan(finite_element.CiarletElement): + """The Guzman-Neilan extended element.""" + def __init__(self, ref_el, degree=None, subdegree=1): + sd = ref_el.get_spatial_dimension() + if degree is None: + degree = sd + if degree != sd: + raise ValueError("Guzman-Neilan only defined for degree = dim") + poly_set = ExtendedGuzmanNeilanSpace(ref_el, subdegree) + dual = BernardiRaugelDualSet(ref_el, degree, subdegree=subdegree) + formdegree = sd - 1 # (n-1)-form + super().__init__(poly_set, dual, degree, formdegree, mapping="contravariant piola") + + +def inner(v, u, qwts): + """Compute the L2 inner product from tabulation arrays and quadrature weights""" + return numpy.tensordot(numpy.multiply(v, qwts), u, + axes=(range(1, v.ndim), range(1, u.ndim))) + + +def div(U): + """Compute the divergence from tabulation dict.""" + return sum(U[k][:, k.index(1), :] for k in U if sum(k) == 1) + + +def take_interior_bubbles(C0): + """Take the interior bubbles from a vector-valued C0 PolynomialSet.""" + ref_complex = C0.get_reference_element() + sd = ref_complex.get_spatial_dimension() + dimC0 = C0.get_num_members() // sd + entity_ids = expansions.polynomial_entity_ids(ref_complex, C0.degree, continuity="C0") + ids = [i + j * dimC0 + for dim in range(sd+1) + for f in sorted(ref_complex.get_interior_facets(dim)) + for i in entity_ids[dim][f] + for j in range(sd)] + return C0.take(ids) + + +def modified_bubble_subspace(B): + """Construct the interior bubble space M_k(K^r) from (Guzman and Neilan, 2019).""" + ref_complex = B.get_reference_element() + sd = ref_complex.get_spatial_dimension() + degree = B.degree + rule = create_quadrature(ref_complex, 2*degree) + qpts, qwts = rule.get_points(), rule.get_weights() + + # tabulate the linear hat function associated with the barycenter + hat = B.take([0]) + hat_at_qpts = hat.tabulate(qpts)[(0,)*sd][0, 0] + + # tabulate the BDM facet functions + ref_el = ref_complex.get_parent() + BDM = BrezziDouglasMarini(ref_el, degree-1) + entity_dofs = BDM.entity_dofs() + facet_dofs = list(range(BDM.space_dimension() - len(entity_dofs[sd][0]))) + BDM_facet = BDM.get_nodal_basis().take(facet_dofs) + phis = BDM_facet.tabulate(qpts)[(0,)*sd] + + # tabulate the bubbles = hat ** (degree - k) * BDMk_facet + bubbles = [numpy.eye(sd)[:, :, None] * hat_at_qpts[None, None, :] ** degree] + for k in range(1, degree): + dimPk = math.comb(k + sd-1, sd-1) + idsPk = list(chain.from_iterable(entity_dofs[sd-1][f][:dimPk] + for f in entity_dofs[sd-1])) + bubbles.append(numpy.multiply(phis[idsPk], hat_at_qpts ** (degree-k))) + bubbles = numpy.concatenate(bubbles, axis=0) + + # store the bubbles into a PolynomialSet via L2 projection + v = B.tabulate(qpts)[(0,) * sd] + coeffs = numpy.linalg.solve(inner(v, v, qwts), inner(v, bubbles, qwts)) + coeffs = numpy.tensordot(coeffs, B.get_coeffs(), axes=(0, 0)) + M = polynomial_set.PolynomialSet(ref_complex, degree, degree, + B.get_expansion_set(), coeffs) + return M + + +def constant_div_projection(BR, C0, M): + """Project the BR space into the space of C0 polynomials with constant divergence.""" + ref_complex = C0.get_reference_element() + sd = ref_complex.get_spatial_dimension() + degree = C0.degree + rule = create_quadrature(ref_complex, 2*degree) + qpts, qwts = rule.get_points(), rule.get_weights() + + # Take the test space for the divergence in L2 \ R + Q = polynomial_set.ONPolynomialSet(ref_complex, degree-1) + Q = Q.take(list(range(1, Q.get_num_members()))) + P = Q.tabulate(qpts)[(0,)*sd] + P -= numpy.dot(P, qwts)[:, None] / sum(qwts) + + U = M.tabulate(qpts, 1) + X = BR.tabulate(qpts, 1) + # Invert the divergence + B = inner(P, div(U), qwts) + g = inner(P, div(X), qwts) + w = numpy.linalg.solve(B, g) + + # Add correction to BR bubbles + v = C0.tabulate(qpts)[(0,)*sd] + coeffs = numpy.linalg.solve(inner(v, v, qwts), inner(v, X[(0,)*sd], qwts)) + coeffs = coeffs.T.reshape(BR.get_num_members(), sd, -1) + coeffs -= numpy.tensordot(w, M.get_coeffs(), axes=(0, 0)) + GN = polynomial_set.PolynomialSet(ref_complex, degree, degree, + C0.get_expansion_set(), coeffs) + return GN diff --git a/FIAT/hct.py b/FIAT/hct.py index 7fe0c146a..b09741cdc 100644 --- a/FIAT/hct.py +++ b/FIAT/hct.py @@ -41,20 +41,19 @@ def __init__(self, ref_complex, degree, reduced=False): entity_ids[0][v].extend(range(cur, len(nodes))) k = 2 if reduced else degree - 3 - rline = ufc_simplex(1) - Q = create_quadrature(rline, degree-1+k) + facet = ufc_simplex(1) + Q = create_quadrature(facet, degree-1+k) qpts = Q.get_points() + xref = 2.0 * qpts - 1.0 if reduced: - x, = qpts.T - f_at_qpts = eval_jacobi(0, 0, k, 2.0*x - 1) + f_at_qpts = eval_jacobi(0, 0, k, xref[:, 0]) for e in sorted(top[1]): cur = len(nodes) nodes.append(IntegralMomentOfNormalDerivative(ref_el, e, Q, f_at_qpts)) entity_ids[1][e].extend(range(cur, len(nodes))) else: - x = 2.0*qpts - 1 - phis = eval_jacobi_batch(1, 1, k, x) - dphis = eval_jacobi_deriv_batch(1, 1, k, x) + phis = eval_jacobi_batch(1, 1, k, xref) + dphis = eval_jacobi_deriv_batch(1, 1, k, xref) for e in sorted(top[1]): Q_mapped = FacetQuadratureRule(ref_el, 1, e, Q) scale = 2 / Q_mapped.jacobian_determinant() @@ -73,7 +72,7 @@ def __init__(self, ref_complex, degree, reduced=False): nodes.extend(IntegralMoment(ref_el, Q, phi * scale) for phi in phis) entity_ids[sd][0] = list(range(cur, len(nodes))) - super(HCTDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class HsiehCloughTocher(finite_element.CiarletElement): @@ -85,4 +84,4 @@ def __init__(self, ref_el, degree=3, reduced=False): ref_complex = macro.AlfeldSplit(ref_el) dual = HCTDualSet(ref_complex, degree, reduced=reduced) poly_set = macro.CkPolynomialSet(ref_complex, degree, order=1, vorder=degree-1, variant="bubble") - super(HsiehCloughTocher, self).__init__(poly_set, dual, degree) + super().__init__(poly_set, dual, degree) diff --git a/FIAT/hdiv_trace.py b/FIAT/hdiv_trace.py index 0f9ee66c6..a68509f79 100644 --- a/FIAT/hdiv_trace.py +++ b/FIAT/hdiv_trace.py @@ -25,7 +25,7 @@ class TraceError(Exception): or the gradient of a trace element.""" def __init__(self, msg): - super(TraceError, self).__init__(msg) + super().__init__(msg) self.msg = msg @@ -114,9 +114,9 @@ def __init__(self, ref_el, degree): # Degree of the element deg = max([e.degree() for e in dg_elements.values()]) - super(HDivTrace, self).__init__(ref_el, dual, order=deg, - formdegree=facet_sd, - mapping="affine") + super().__init__(ref_el, dual, order=deg, + formdegree=facet_sd, + mapping="affine") # Set up facet elements self.dg_elements = dg_elements diff --git a/FIAT/hellan_herrmann_johnson.py b/FIAT/hellan_herrmann_johnson.py index d8153152f..8218b00f6 100644 --- a/FIAT/hellan_herrmann_johnson.py +++ b/FIAT/hellan_herrmann_johnson.py @@ -39,7 +39,7 @@ def __init__(self, cell, degree): dofs.extend(_dofs) dof_ids[dim] = _dof_ids - super(HellanHerrmannJohnsonDual, self).__init__(dofs, cell, dof_ids) + super().__init__(dofs, cell, dof_ids) @staticmethod def _generate_edge_dofs(cell, degree, offset): @@ -93,5 +93,4 @@ def __init__(self, cell, degree): # mapping under affine transformation mapping = "double contravariant piola" - super(HellanHerrmannJohnson, self).__init__(Ps, Ls, degree, - mapping=mapping) + super().__init__(Ps, Ls, degree, mapping=mapping) diff --git a/FIAT/hermite.py b/FIAT/hermite.py index e1368fea8..79c83ff97 100644 --- a/FIAT/hermite.py +++ b/FIAT/hermite.py @@ -64,7 +64,7 @@ def __init__(self, ref_el): for facet in top[dim]: entity_ids[dim][facet] = [] - super(CubicHermiteDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class CubicHermite(finite_element.CiarletElement): @@ -75,4 +75,4 @@ def __init__(self, ref_el, deg=3): poly_set = polynomial_set.ONPolynomialSet(ref_el, 3) dual = CubicHermiteDualSet(ref_el) - super(CubicHermite, self).__init__(poly_set, dual, 3) + super().__init__(poly_set, dual, 3) diff --git a/FIAT/hierarchical.py b/FIAT/hierarchical.py index 4120106e8..fcabaf536 100644 --- a/FIAT/hierarchical.py +++ b/FIAT/hierarchical.py @@ -49,7 +49,7 @@ def __init__(self, ref_el, degree, codim=0): entity_ids[dim][entity] = list(range(cur, len(nodes))) entity_permutations[dim][entity] = perms - super(LegendreDual, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) class Legendre(finite_element.CiarletElement): @@ -60,7 +60,7 @@ def __new__(cls, ref_el, degree, variant=None): if splitting is None: # FIXME P0 on the split requires implementing SplitSimplicialComplex.symmetry_group_size() return P0.P0(ref_el) - return super(Legendre, cls).__new__(cls) + return super().__new__(cls) def __init__(self, ref_el, degree, variant=None): splitting, _ = parse_lagrange_variant(variant, integral=True) @@ -69,7 +69,7 @@ def __init__(self, ref_el, degree, variant=None): poly_set = ONPolynomialSet(ref_el, degree) dual = LegendreDual(ref_el, degree) formdegree = ref_el.get_spatial_dimension() # n-form - super(Legendre, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) class IntegratedLegendreDual(dual_set.DualSet): @@ -107,7 +107,7 @@ def __init__(self, ref_el, degree): entity_ids[dim][entity] = list(range(cur, len(nodes))) entity_permutations[dim][entity] = perms - super(IntegratedLegendreDual, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) def make_reference_duals(self, ref_el, degree): Q = create_quadrature(ref_el, 2 * degree) @@ -140,4 +140,4 @@ def __init__(self, ref_el, degree, variant=None): poly_set = ONPolynomialSet(ref_el, degree, variant="bubble") dual = IntegratedLegendreDual(ref_el, degree) formdegree = 0 # 0-form - super(IntegratedLegendre, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) diff --git a/FIAT/johnson_mercier.py b/FIAT/johnson_mercier.py index 7f3d9c3d8..de4683bf4 100644 --- a/FIAT/johnson_mercier.py +++ b/FIAT/johnson_mercier.py @@ -18,6 +18,7 @@ def __init__(self, ref_complex, degree, variant=None): nodes = [] # Face dofs: bidirectional (nn and nt) Legendre moments + R = numpy.array([[0, 1], [-1, 0]]) dim = sd - 1 ref_facet = ref_el.construct_subelement(dim) Qref = create_quadrature(ref_facet, 2*degree) @@ -26,12 +27,11 @@ def __init__(self, ref_complex, degree, variant=None): for facet in sorted(top[dim]): cur = len(nodes) Q = FacetQuadratureRule(ref_el, dim, facet, Qref) - Jdet = Q.jacobian_determinant() - tangents = ref_el.compute_tangents(dim, facet) - normal = ref_el.compute_normal(facet) - normal /= numpy.linalg.norm(normal) - scaled_normal = normal * Jdet - uvecs = (scaled_normal, *tangents) + thats = ref_el.compute_tangents(dim, facet) + nhat = numpy.dot(R, *thats) if sd == 2 else numpy.cross(*thats) + normal = nhat / Q.jacobian_determinant() + + uvecs = (nhat, *thats) comps = [numpy.outer(normal, uvec) for uvec in uvecs] nodes.extend(FrobeniusIntegralMoment(ref_el, Q, comp[:, :, None] * phi[None, None, :]) for phi in phis for comp in comps) @@ -48,7 +48,7 @@ def __init__(self, ref_complex, degree, variant=None): entity_ids[sd][0].extend(range(cur, len(nodes))) - super(JohnsonMercierDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class JohnsonMercier(finite_element.CiarletElement): @@ -59,5 +59,4 @@ def __init__(self, ref_el, degree=1, variant=None): poly_set = macro.HDivSymPolynomialSet(ref_complex, degree) dual = JohnsonMercierDualSet(ref_complex, degree, variant=variant) mapping = "double contravariant piola" - super(JohnsonMercier, self).__init__(poly_set, dual, degree, - mapping=mapping) + super().__init__(poly_set, dual, degree, mapping=mapping) diff --git a/FIAT/lagrange.py b/FIAT/lagrange.py index 51a996154..eead0cfa6 100644 --- a/FIAT/lagrange.py +++ b/FIAT/lagrange.py @@ -50,7 +50,7 @@ def __init__(self, ref_el, degree, point_variant="equispaced", sort_entities=Fal pts_cur = ref_el.make_points(dim, entity, degree, variant=point_variant) nodes.extend(functional.PointEvaluation(ref_el, x) for x in pts_cur) entity_ids[dim][entity] = list(range(cur, len(nodes))) - super(LagrangeDualSet, self).__init__(nodes, ref_el, entity_ids, entity_permutations) + super().__init__(nodes, ref_el, entity_ids, entity_permutations) class Lagrange(finite_element.CiarletElement): @@ -86,4 +86,4 @@ def __init__(self, ref_el, degree, variant="equispaced", sort_entities=False): poly_variant = "bubble" if ref_el.is_macrocell() else None poly_set = polynomial_set.ONPolynomialSet(ref_el, degree, variant=poly_variant) formdegree = 0 # 0-form - super(Lagrange, self).__init__(poly_set, dual, degree, formdegree) + super().__init__(poly_set, dual, degree, formdegree) diff --git a/FIAT/macro.py b/FIAT/macro.py index 486eda57b..f48298c70 100644 --- a/FIAT/macro.py +++ b/FIAT/macro.py @@ -1,4 +1,3 @@ -import copy from itertools import chain, combinations import numpy @@ -74,8 +73,9 @@ def make_topology(sd, num_verts, edges): facet = topology[dim][entity] facet_verts = set(facet) for v in range(min(facet)): - if facet_verts < adjacency[v]: + if (facet_verts < adjacency[v]): entities.append((v, *facet)) + topology[dim+1] = dict(enumerate(sorted(entities))) return topology @@ -88,7 +88,10 @@ class SplitSimplicialComplex(SimplicialComplex): :arg topology: The topology of the simplicial complex. """ def __init__(self, parent, vertices, topology): - self._parent = parent + self._parent_complex = parent + while parent.get_parent(): + parent = parent.get_parent() + self._parent_simplex = parent bary = xy_to_bary(parent.get_vertices(), vertices) parent_top = parent.get_topology() @@ -147,7 +150,7 @@ def __init__(self, parent, vertices, topology): for dim in sorted(child_to_parent)} self._interior_facets = interior_facets - super(SplitSimplicialComplex, self).__init__(parent.shape, vertices, topology) + super().__init__(parent.shape, vertices, topology) def get_child_to_parent(self): """Maps split complex facet tuple to its parent entity tuple.""" @@ -180,53 +183,16 @@ def construct_subelement(self, dimension): :arg dimension: subentity dimension (integer) """ - return self._parent.construct_subelement(dimension) + return self.get_parent().construct_subelement(dimension) def is_macrocell(self): return True def get_parent(self): - return self._parent - - -class AlfeldSplit(SplitSimplicialComplex): - """Splits a simplicial complex by connecting subcell vertices to their - barycenter. - """ - def __init__(self, ref_el): - sd = ref_el.get_spatial_dimension() - top = ref_el.get_topology() - # Keep old facets, respecting the old numbering - new_topology = copy.deepcopy(top) - # Discard the cell interiors - new_topology[sd] = {} - new_verts = list(ref_el.get_vertices()) - - for cell in top[sd]: - # Append the barycenter as the new vertex - new_verts.extend(ref_el.make_points(sd, cell, sd+1)) - new_vert_id = len(new_topology[0]) - new_topology[0][new_vert_id] = (new_vert_id,) + return self._parent_simplex - # Append new facets by adding the barycenter to old facets - for dim in range(1, sd + 1): - cur = len(new_topology[dim]) - for entity, ids in top[dim-1].items(): - if set(ids) < set(top[sd][cell]): - new_topology[dim][cur] = ids + (new_vert_id,) - cur = cur + 1 - - parent = ref_el.get_parent() or ref_el - super(AlfeldSplit, self).__init__(parent, tuple(new_verts), new_topology) - - def construct_subcomplex(self, dimension): - """Constructs the reference subcomplex of the parent cell subentity - specified by subcomplex dimension. - """ - if dimension == self.get_dimension(): - return self - # Alfeld on facets is just the parent subcomplex - return self._parent.construct_subcomplex(dimension) + def get_parent_complex(self): + return self._parent_complex class IsoSplit(SplitSimplicialComplex): @@ -264,11 +230,10 @@ def __init__(self, ref_el, degree=2, variant=None): edges.append(tuple(sorted((v0, v1)))) new_topology = make_topology(sd, len(new_verts), edges) - parent = ref_el.get_parent() or ref_el - super(IsoSplit, self).__init__(parent, tuple(new_verts), new_topology) + super().__init__(ref_el, tuple(new_verts), new_topology) def construct_subcomplex(self, dimension): - """Constructs the reference subcomplex of the parent cell subentity + """Constructs the reference subcomplex of the parent complex specified by subcomplex dimension. """ if dimension == self.get_dimension(): @@ -282,43 +247,75 @@ def construct_subcomplex(self, dimension): class PowellSabinSplit(SplitSimplicialComplex): - """Splits a simplicial complex by connecting barycenters of subentities to - the barycenter of the superentity. + """Splits a simplicial complex by connecting barycenters of subentities + to the barycenter of the superentities of the given dimension or higher. """ - def __init__(self, ref_el): + def __init__(self, ref_el, dimension=1): + self.split_dimension = dimension sd = ref_el.get_spatial_dimension() top = ref_el.get_topology() - inv_top = invert_cell_topology(top) + connectivity = ref_el.get_connectivity() new_verts = list(ref_el.get_vertices()) - edges = [] - offset = {0: 0} - for dim in range(1, sd+1): - offset[dim] = len(new_verts) + dim = dimension - 1 + simplices = {dim: {entity: [top[dim][entity]] for entity in top[dim]}} + + for dim in range(dimension, sd+1): + simplices[dim] = {} for entity in top[dim]: - bary_id = entity + offset[dim] + bary_id = len(new_verts) new_verts.extend(ref_el.make_points(dim, entity, dim+1)) - # Connect subentity barycenter to the entity barycenter - for subdim in range(dim): - edges.extend((inv_top[subdim][subverts] + offset[subdim], bary_id) - for subverts in combinations(top[dim][entity], subdim+1)) - - new_topology = make_topology(sd, len(new_verts), edges) - parent = ref_el.get_parent() or ref_el - super(PowellSabinSplit, self).__init__(parent, tuple(new_verts), new_topology) + # Connect entity barycenter to every subsimplex on the entity + simplices[dim][entity] = [(*s, bary_id) + for child in connectivity[(dim, dim-1)][entity] + for s in simplices[dim-1][child]] + + simplices = list(chain.from_iterable(simplices[sd].values())) + new_topology = {} + new_topology[0] = {i: (i,) for i in range(len(new_verts))} + for dim in range(1, sd): + facets = chain.from_iterable((combinations(s, dim+1) for s in simplices)) + if dim < self.split_dimension: + # Preserve the numbering of the unsplit entities + facets = chain(top[dim].values(), facets) + unique_facets = dict.fromkeys(facets) + new_topology[dim] = dict(enumerate(unique_facets)) + new_topology[sd] = dict(enumerate(simplices)) + + parent = ref_el if dimension == sd else PowellSabinSplit(ref_el, dimension=dimension+1) + super().__init__(parent, tuple(new_verts), new_topology) def construct_subcomplex(self, dimension): - """Constructs the reference subcomplex of the parent cell subentity + """Constructs the reference subcomplex of the parent complex specified by subcomplex dimension. """ if dimension == self.get_dimension(): return self - ref_el = self.construct_subelement(dimension) - if dimension == 0: - return ref_el + parent = self.get_parent_complex() + subcomplex = parent.construct_subcomplex(dimension) + if dimension < self.split_dimension: + return subcomplex else: # Powell-Sabin on facets is Powell-Sabin - return PowellSabinSplit(ref_el) + return PowellSabinSplit(subcomplex, dimension=self.split_dimension) + + +class AlfeldSplit(PowellSabinSplit): + """Splits a simplicial complex by connecting cell vertices to their + barycenter. + """ + def __init__(self, ref_el): + sd = ref_el.get_spatial_dimension() + super().__init__(ref_el, dimension=sd) + + +class WorseyFarinSplit(PowellSabinSplit): + """Splits a simplicial complex by connecting cell and facet vertices to their + barycenter. This reduces to Powell-Sabin on the triangle, and Alfeld on the interval. + """ + def __init__(self, ref_el): + sd = ref_el.get_spatial_dimension() + super().__init__(ref_el, dimension=sd-1) class PowellSabin12Split(SplitSimplicialComplex): @@ -345,8 +342,9 @@ def __init__(self, ref_el): (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 7), (4, 8), (5, 7), (5, 9), (6, 8), (6, 9)] - super(PowellSabin12Split, self).__init__( - ref_el, tuple(new_verts), make_topology(2, len(new_verts), edges)) + parent = PowellSabinSplit(ref_el) + new_topology = make_topology(2, len(new_verts), edges) + super().__init__(parent, tuple(new_verts), new_topology) def construct_subcomplex(self, dimension): """Constructs the reference subcomplex of the parent cell subentity @@ -375,8 +373,7 @@ class MacroQuadratureRule(QuadratureRule): def __init__(self, ref_el, Q_ref, parent_facets=None): parent_dim = Q_ref.ref_el.get_spatial_dimension() if parent_facets is not None: - parent_cell = ref_el.get_parent() - parent_to_children = parent_cell.get_parent_to_children() + parent_to_children = ref_el.get_parent_to_children() facets = [] for parent_entity in parent_facets: children = parent_to_children[parent_dim][parent_entity] @@ -393,7 +390,7 @@ def __init__(self, ref_el, Q_ref, parent_facets=None): wts.extend(Q_cur.wts) pts = tuple(pts) wts = tuple(wts) - super(MacroQuadratureRule, self).__init__(ref_el, pts, wts) + super().__init__(ref_el, pts, wts) class CkPolynomialSet(polynomial_set.PolynomialSet): @@ -452,12 +449,13 @@ def __init__(self, ref_el, degree, order=1, vorder=0, shape=(), **kwargs): num_sv = len([s for s in sig if abs(s) > tol]) coeffs = numpy.dot(vt[num_sv:], coeffs) - if shape != tuple(): + if shape != (): m, n = coeffs.shape - coeffs = coeffs.reshape((m,) + (1,)*len(shape) + (n,)) - coeffs = numpy.tile(coeffs, (1,) + shape + (1,)) + ncomp = numpy.prod(shape) + coeffs = numpy.kron(coeffs, numpy.eye(ncomp)) + coeffs = coeffs.reshape(m*ncomp, *shape, n) - super(CkPolynomialSet, self).__init__(ref_el, degree, degree, expansion_set, coeffs) + super().__init__(ref_el, degree, degree, expansion_set, coeffs) class HDivSymPolynomialSet(polynomial_set.PolynomialSet): @@ -499,9 +497,9 @@ def __init__(self, ref_el, degree, order=0, **kwargs): rows.append(numpy.tensordot(wn, jump, axes=(ax, ax))) if len(rows) > 0: - dual_mat = numpy.row_stack(rows) + dual_mat = numpy.vstack(rows) _, sig, vt = numpy.linalg.svd(dual_mat, full_matrices=True) num_sv = len([s for s in sig if abs(s) > 1.e-10]) coeffs = numpy.tensordot(vt[num_sv:], coeffs, axes=(1, 0)) - super(HDivSymPolynomialSet, self).__init__(ref_el, degree, degree, expansion_set, coeffs) + super().__init__(ref_el, degree, degree, expansion_set, coeffs) diff --git a/FIAT/nedelec.py b/FIAT/nedelec.py index c05133022..c92fa06be 100644 --- a/FIAT/nedelec.py +++ b/FIAT/nedelec.py @@ -171,7 +171,7 @@ def __init__(self, ref_el, degree, variant, interpolant_deg): for d in range(dim) for phi in Phis) entity_ids[dim][0] = list(range(cur, len(nodes))) - super(NedelecDual, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class Nedelec(finite_element.CiarletElement): @@ -205,5 +205,4 @@ def __init__(self, ref_el, degree, variant=None): raise Exception("Not implemented") dual = NedelecDual(ref_el, degree, variant, interpolant_deg) formdegree = 1 # 1-form - super(Nedelec, self).__init__(poly_set, dual, degree, formdegree, - mapping="covariant piola") + super().__init__(poly_set, dual, degree, formdegree, mapping="covariant piola") diff --git a/FIAT/nedelec_second_kind.py b/FIAT/nedelec_second_kind.py index 6e5269721..481dcf64e 100644 --- a/FIAT/nedelec_second_kind.py +++ b/FIAT/nedelec_second_kind.py @@ -52,7 +52,7 @@ def __init__(self, cell, degree, variant, interpolant_deg): # Define degrees of freedom (dofs, ids) = self.generate_degrees_of_freedom(cell, degree, variant, interpolant_deg) # Call init of super-class - super(NedelecSecondKindDual, self).__init__(dofs, cell, ids) + super().__init__(dofs, cell, ids) def generate_degrees_of_freedom(self, cell, degree, variant, interpolant_deg): "Generate dofs and geometry-to-dof maps (ids)." @@ -214,4 +214,4 @@ def __init__(self, cell, degree, variant=None): mapping = "covariant piola" # Call init of super-class - super(NedelecSecondKind, self).__init__(Ps, Ls, degree, formdegree, mapping=mapping) + super().__init__(Ps, Ls, degree, formdegree, mapping=mapping) diff --git a/FIAT/powell_sabin.py b/FIAT/powell_sabin.py index 2b0ea19cb..e9b7c4f3f 100644 --- a/FIAT/powell_sabin.py +++ b/FIAT/powell_sabin.py @@ -37,8 +37,7 @@ def __init__(self, ref_complex, degree=2): nodes.extend(PointDerivative(ref_el, pt, alpha) for alpha in alphas) entity_ids[0][v].extend(range(cur, len(nodes))) - super(QuadraticPowellSabin6DualSet, self).__init__( - nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class QuadraticPowellSabin6(finite_element.CiarletElement): @@ -52,7 +51,7 @@ def __init__(self, ref_el, degree=2): dual = QuadraticPowellSabin6DualSet(ref_complex, degree) poly_set = macro.CkPolynomialSet(ref_complex, degree, order=1) - super(QuadraticPowellSabin6, self).__init__(poly_set, dual, degree) + super().__init__(poly_set, dual, degree) class QuadraticPowellSabin12DualSet(dual_set.DualSet): @@ -90,8 +89,7 @@ def __init__(self, ref_complex, degree=2): nodes.extend(IntegralMomentOfNormalDerivative(ref_el, e, Q, phi) for phi in phis) entity_ids[1][e].extend(range(cur, len(nodes))) - super(QuadraticPowellSabin12DualSet, self).__init__( - nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class QuadraticPowellSabin12(finite_element.CiarletElement): @@ -105,4 +103,4 @@ def __init__(self, ref_el, degree=2): dual = QuadraticPowellSabin12DualSet(ref_complex, degree) poly_set = macro.CkPolynomialSet(ref_complex, degree, order=1) - super(QuadraticPowellSabin12, self).__init__(poly_set, dual, degree) + super().__init__(poly_set, dual, degree) diff --git a/FIAT/quadrature.py b/FIAT/quadrature.py index e8d26aacc..61702c54c 100644 --- a/FIAT/quadrature.py +++ b/FIAT/quadrature.py @@ -21,14 +21,16 @@ def pseudo_determinant(A): def map_quadrature(pts_ref, wts_ref, source_cell, target_cell, jacobian=False): """Map quadrature points and weights defined on source_cell to target_cell. """ + while source_cell.get_parent(): + source_cell = source_cell.get_parent() A, b = reference_element.make_affine_mapping(source_cell.get_vertices(), target_cell.get_vertices()) + if len(pts_ref.shape) != 2: + pts_ref = pts_ref.reshape(-1, A.shape[1]) scale = pseudo_determinant(A) + pts = numpy.dot(pts_ref, A.T) + pts = numpy.add(pts, b, out=pts) wts = scale * wts_ref - if pts_ref.size == 0: - pts = b[None, :] - else: - pts = numpy.dot(pts_ref.reshape((-1, A.shape[1])), A.T) + b[None, :] # return immutable types pts = tuple(map(tuple, pts)) @@ -92,7 +94,7 @@ class GaussLegendreQuadratureLineRule(GaussJacobiQuadratureLineRule): The quadrature rule uses m points for a degree of precision of 2m-1. """ def __init__(self, ref_el, m): - super(GaussLegendreQuadratureLineRule, self).__init__(ref_el, m) + super().__init__(ref_el, m) class RadauQuadratureLineRule(QuadratureRule): diff --git a/FIAT/quadrature_element.py b/FIAT/quadrature_element.py index d71e59c7c..d61eff908 100644 --- a/FIAT/quadrature_element.py +++ b/FIAT/quadrature_element.py @@ -32,7 +32,7 @@ def __init__(self, ref_el, points, weights=None): # Construct the dual set dual = DualSet(nodes, ref_el, entity_dofs) - super(QuadratureElement, self).__init__(ref_el, dual, order=None) + super().__init__(ref_el, dual, order=None) self._points = points # save the quadrature points & weights self._weights = weights diff --git a/FIAT/raviart_thomas.py b/FIAT/raviart_thomas.py index f33ce937f..db0058d12 100644 --- a/FIAT/raviart_thomas.py +++ b/FIAT/raviart_thomas.py @@ -15,7 +15,7 @@ def RTSpace(ref_el, degree): - """Constructs a basis for the the Raviart-Thomas space + """Constructs a basis for the Raviart-Thomas space (P_{degree-1})^d + P_{degree-1} x""" sd = ref_el.get_spatial_dimension() @@ -116,7 +116,7 @@ def __init__(self, ref_el, degree, variant, interpolant_deg): for pt in pts) entity_ids[sd][0] = list(range(cur, len(nodes))) - super(RTDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) class RaviartThomas(finite_element.CiarletElement): @@ -146,5 +146,4 @@ def __init__(self, ref_el, degree, variant=None): poly_set = RTSpace(ref_el, degree) dual = RTDualSet(ref_el, degree, variant, interpolant_deg) formdegree = ref_el.get_spatial_dimension() - 1 # (n-1)-form - super(RaviartThomas, self).__init__(poly_set, dual, degree, formdegree, - mapping="contravariant piola") + super().__init__(poly_set, dual, degree, formdegree, mapping="contravariant piola") diff --git a/FIAT/reference_element.py b/FIAT/reference_element.py index 1e750bbed..f09751578 100644 --- a/FIAT/reference_element.py +++ b/FIAT/reference_element.py @@ -177,12 +177,6 @@ def _key(self): # Default: only type matters return None - def __eq__(self, other): - return type(self) is type(other) and self._key() == other._key() - - def __ne__(self, other): - return not self.__eq__(other) - def __hash__(self): return hash((type(self), self._key())) @@ -268,21 +262,56 @@ def is_simplex(self): def is_macrocell(self): return False + def get_interior_facets(self, dim): + """Return the interior facets this cell is a split and () otherwise.""" + return () + def get_parent(self): """Return the parent cell if this cell is a split and None otherwise.""" return None + def get_parent_complex(self): + """Return the parent complex if this cell is a split and None otherwise.""" + return None + + def is_parent(self, other, strict=False): + """Return whether this cell is the parent of the other cell.""" + parent = other + if strict: + parent = parent.get_parent_complex() + while parent is not None: + if self == parent: + return True + parent = parent.get_parent_complex() + return False + + def __eq__(self, other): + if self is other: + return True + A, B = self.get_vertices(), other.get_vertices() + if not (len(A) == len(B) and numpy.allclose(A, B)): + return False + atop = self.get_topology() + btop = other.get_topology() + for dim in atop: + if set(atop[dim].values()) != set(btop[dim].values()): + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + def __gt__(self, other): - return self.get_parent() == other + return other.is_parent(self, strict=True) def __lt__(self, other): - return self == other.get_parent() + return self.is_parent(other, strict=True) def __ge__(self, other): - return self > other or self == other + return other.is_parent(self, strict=False) def __le__(self, other): - return self < other or self == other + return self.is_parent(other, strict=False) class SimplicialComplex(Cell): @@ -297,7 +326,7 @@ def __init__(self, shape, vertices, topology): for entity in topology[dim]: assert len(topology[dim][entity]) == dim + 1 - super(SimplicialComplex, self).__init__(shape, vertices, topology) + super().__init__(shape, vertices, topology) def compute_normal(self, facet_i, cell=None): """Returns the unit normal vector to facet i of codimension 1.""" @@ -386,7 +415,7 @@ def compute_normalized_tangents(self, dim, i): of dimension dim. Returns a (possibly empty) list. These tangents are normalized to have unit length.""" ts = self.compute_tangents(dim, i) - ts /= numpy.linalg.norm(ts, axis=0)[None, :] + ts /= numpy.linalg.norm(ts, axis=1)[:, None] return ts def compute_edge_tangent(self, edge_i): @@ -539,6 +568,11 @@ def compute_barycentric_coordinates(self, points, entity=None, rescale=False): out = numpy.dot(points, A.T) return numpy.add(out, b, out=out) + def compute_bubble(self, points, entity=None): + """Returns the lowest-order bubble on an entity evaluated at the given + points on the entity.""" + return numpy.prod(self.compute_barycentric_coordinates(points, entity), axis=1) + def distance_to_point_l1(self, points, entity=None, rescale=False): # noqa: D301 """Get the L1 distance (aka 'manhatten', 'taxicab' or rectilinear @@ -784,7 +818,7 @@ class Point(Simplex): def __init__(self): verts = ((),) topology = {0: {0: (0,)}} - super(Point, self).__init__(POINT, verts, topology) + super().__init__(POINT, verts, topology) def construct_subelement(self, dimension): """Constructs the reference element of a cell subentity @@ -804,7 +838,7 @@ def __init__(self): edges = {0: (0, 1)} topology = {0: {0: (0,), 1: (1,)}, 1: edges} - super(DefaultLine, self).__init__(LINE, verts, topology) + super().__init__(LINE, verts, topology) class UFCInterval(UFCSimplex): @@ -815,7 +849,7 @@ def __init__(self): edges = {0: (0, 1)} topology = {0: {0: (0,), 1: (1,)}, 1: edges} - super(UFCInterval, self).__init__(LINE, verts, topology) + super().__init__(LINE, verts, topology) class DefaultTriangle(DefaultSimplex): @@ -830,7 +864,7 @@ def __init__(self): faces = {0: (0, 1, 2)} topology = {0: {0: (0,), 1: (1,), 2: (2,)}, 1: edges, 2: faces} - super(DefaultTriangle, self).__init__(TRIANGLE, verts, topology) + super().__init__(TRIANGLE, verts, topology) class UFCTriangle(UFCSimplex): @@ -843,7 +877,7 @@ def __init__(self): faces = {0: (0, 1, 2)} topology = {0: {0: (0,), 1: (1,), 2: (2,)}, 1: edges, 2: faces} - super(UFCTriangle, self).__init__(TRIANGLE, verts, topology) + super().__init__(TRIANGLE, verts, topology) def compute_normal(self, i): "UFC consistent normal" @@ -863,7 +897,7 @@ def __init__(self): faces = {0: (0, 1, 2)} topology = {0: {0: (0,), 1: (1,), 2: (2,)}, 1: edges, 2: faces} - super(IntrepidTriangle, self).__init__(TRIANGLE, verts, topology) + super().__init__(TRIANGLE, verts, topology) def get_facet_element(self): # I think the UFC interval is equivalent to what the @@ -894,7 +928,7 @@ def __init__(self): 3: (0, 1, 2)} tets = {0: (0, 1, 2, 3)} topology = {0: vs, 1: edges, 2: faces, 3: tets} - super(DefaultTetrahedron, self).__init__(TETRAHEDRON, verts, topology) + super().__init__(TETRAHEDRON, verts, topology) class IntrepidTetrahedron(Simplex): @@ -919,7 +953,7 @@ def __init__(self): 3: (0, 2, 1)} tets = {0: (0, 1, 2, 3)} topology = {0: vs, 1: edges, 2: faces, 3: tets} - super(IntrepidTetrahedron, self).__init__(TETRAHEDRON, verts, topology) + super().__init__(TETRAHEDRON, verts, topology) def get_facet_element(self): return IntrepidTriangle() @@ -947,7 +981,7 @@ def __init__(self): 3: (0, 1, 2)} tets = {0: (0, 1, 2, 3)} topology = {0: vs, 1: edges, 2: faces, 3: tets} - super(UFCTetrahedron, self).__init__(TETRAHEDRON, verts, topology) + super().__init__(TETRAHEDRON, verts, topology) def compute_normal(self, i): "UFC consistent normals." @@ -982,7 +1016,7 @@ def __init__(self, *cells): topology[dim] = dict(enumerate(topology[dim][key] for key in sorted(topology[dim]))) - super(TensorProductCell, self).__init__(TENSORPRODUCT, vertices, topology) + super().__init__(TENSORPRODUCT, vertices, topology) self.cells = tuple(cells) def _key(self): @@ -1181,7 +1215,7 @@ def __init__(self): verts = product.get_vertices() topology = flatten_entities(pt) - super(UFCQuadrilateral, self).__init__(QUADRILATERAL, verts, topology) + super().__init__(QUADRILATERAL, verts, topology) self.product = product self.unflattening_map = compute_unflattening_map(pt) @@ -1284,7 +1318,7 @@ def __init__(self): verts = product.get_vertices() topology = flatten_entities(pt) - super(UFCHexahedron, self).__init__(HEXAHEDRON, verts, topology) + super().__init__(HEXAHEDRON, verts, topology) self.product = product self.unflattening_map = compute_unflattening_map(pt) diff --git a/FIAT/regge.py b/FIAT/regge.py index e6f90e6f6..e9292f663 100644 --- a/FIAT/regge.py +++ b/FIAT/regge.py @@ -42,7 +42,7 @@ def __init__(self, cell, degree): dofs.extend(_dofs) dof_ids[dim] = _dof_ids - super(ReggeDual, self).__init__(dofs, cell, dof_ids) + super().__init__(dofs, cell, dof_ids) @staticmethod def _generate_dofs(cell, entity_dim, degree, offset): @@ -92,4 +92,4 @@ def __init__(self, cell, degree): # mapping under affine transformation mapping = "double covariant piola" - super(Regge, self).__init__(Ps, Ls, degree, mapping=mapping) + super().__init__(Ps, Ls, degree, mapping=mapping) diff --git a/FIAT/restricted.py b/FIAT/restricted.py index 26c82d913..45d73c099 100644 --- a/FIAT/restricted.py +++ b/FIAT/restricted.py @@ -12,24 +12,19 @@ class RestrictedDualSet(DualSet): """Restrict the given DualSet to the specified list of dofs.""" def __init__(self, dual, indices): + indices = list(sorted(indices)) ref_el = dual.get_reference_element() nodes_old = dual.get_nodes() - dof_counter = 0 entity_ids = {} nodes = [] for d, entities in dual.get_entity_ids().items(): entity_ids[d] = {} for entity, dofs in entities.items(): - entity_ids[d][entity] = [] - for dof in dofs: - if dof not in indices: - continue - entity_ids[d][entity].append(dof_counter) - dof_counter += 1 - nodes.append(nodes_old[dof]) - assert dof_counter == len(indices) + entity_ids[d][entity] = [indices.index(dof) + for dof in dofs if dof in indices] + nodes = [nodes_old[i] for i in indices] self._dual = dual - super(RestrictedDualSet, self).__init__(nodes, ref_el, entity_ids) + super().__init__(nodes, ref_el, entity_ids) def get_indices(self, restriction_domain, take_closure=True): """Return the list of dofs with support on a given restriction domain. @@ -74,4 +69,4 @@ def __init__(self, element, indices=None, restriction_domain=None, take_closure= assert all(e_mapping == mapping_new[0] for e_mapping in mapping_new) # Call constructor of CiarletElement - super(RestrictedElement, self).__init__(poly_set, dual, 0, element.get_formdegree(), mapping_new[0]) + super().__init__(poly_set, dual, 0, element.get_formdegree(), mapping_new[0]) diff --git a/FIAT/serendipity.py b/FIAT/serendipity.py index 4f783dd58..94b7930db 100644 --- a/FIAT/serendipity.py +++ b/FIAT/serendipity.py @@ -121,7 +121,7 @@ def __init__(self, ref_el, degree): assert len(s_list) == cur formdegree = 0 - super(Serendipity, self).__init__(ref_el=ref_el, dual=None, order=degree, formdegree=formdegree) + super().__init__(ref_el=ref_el, dual=None, order=degree, formdegree=formdegree) self.basis = {(0,)*dim: Array(s_list)} polynomials, extra_vars = _replace_numbers_with_symbols(Array(s_list)) diff --git a/FIAT/tensor_product.py b/FIAT/tensor_product.py index 90c59c91d..9d2671d2e 100644 --- a/FIAT/tensor_product.py +++ b/FIAT/tensor_product.py @@ -206,7 +206,7 @@ def __init__(self, A, B): dual = dual_set.DualSet(nodes, ref_el, entity_ids) - super(TensorProductElement, self).__init__(ref_el, dual, order, formdegree, mapping) + super().__init__(ref_el, dual, order, formdegree, mapping) # Set up constituent elements self.A = A self.B = B @@ -383,7 +383,7 @@ def __init__(self, element): flat_entity_ids = flatten_entities(entity_ids) dual = DualSet(nodes, ref_el, flat_entity_ids) - super(FlattenedDimensions, self).__init__(ref_el, dual, element.get_order(), element.get_formdegree(), element._mapping) + super().__init__(ref_el, dual, element.get_order(), element.get_formdegree(), element._mapping) self.element = element # Construct unflattening map for passing correct values to tabulate() diff --git a/test/unit/test_fiat.py b/test/unit/test_fiat.py index bed985f5d..5e2cf264d 100644 --- a/test/unit/test_fiat.py +++ b/test/unit/test_fiat.py @@ -42,9 +42,16 @@ from FIAT.tensor_product import TensorProductElement # noqa: F401 from FIAT.tensor_product import FlattenedDimensions # noqa: F401 from FIAT.hdivcurl import Hdiv, Hcurl # noqa: F401 +from FIAT.bernardi_raugel import BernardiRaugel # noqa: F401 from FIAT.argyris import Argyris # noqa: F401 from FIAT.hermite import CubicHermite # noqa: F401 from FIAT.morley import Morley # noqa: F401 +from FIAT.hct import HsiehCloughTocher # noqa: F401 +from FIAT.alfeld_sorokina import AlfeldSorokina # noqa: F401 +from FIAT.arnold_qin import ArnoldQin # noqa: F401 +from FIAT.christiansen_hu import ChristiansenHu # noqa: F401 +from FIAT.guzman_neilan import GuzmanNeilan # noqa: F401 +from FIAT.johnson_mercier import JohnsonMercier # noqa: F401 from FIAT.bubble import Bubble from FIAT.enriched import EnrichedElement # noqa: F401 from FIAT.nodal_enriched import NodalEnrichedElement @@ -298,6 +305,25 @@ def __init__(self, a, b): "CubicHermite(T)", "CubicHermite(S)", "Morley(T)", + "BernardiRaugel(T)", + "BernardiRaugel(S)", + + # Macroelements + "Lagrange(T, 1, 'iso')", + "Lagrange(T, 1, 'alfeld')", + "Lagrange(T, 2, 'alfeld')", + "DiscontinuousLagrange(T, 1, 'alfeld')", + "HsiehCloughTocher(T)", + "JohnsonMercier(T)", + "JohnsonMercier(S)", + "AlfeldSorokina(T)", + "AlfeldSorokina(S)", + "ArnoldQin(T, reduced=False)", + "ArnoldQin(T, reduced=True)", + "ChristiansenHu(T)", + "ChristiansenHu(S)", + "GuzmanNeilan(T)", + "GuzmanNeilan(S)", # MixedElement made of nodal elements should be nodal, but its API # is currently just broken. @@ -351,7 +377,7 @@ def test_nodality(element): # Fetch primal and dual basis poly_set = element.get_nodal_basis() dual_set = element.get_dual_set() - assert poly_set.get_reference_element() == dual_set.get_reference_element() + assert poly_set.get_reference_element() >= dual_set.get_reference_element() # Get coeffs of primal and dual bases w.r.t. expansion set coeffs_poly = poly_set.get_coeffs() diff --git a/test/unit/test_hct.py b/test/unit/test_hct.py index 27b67560d..4dbb96024 100644 --- a/test/unit/test_hct.py +++ b/test/unit/test_hct.py @@ -8,7 +8,9 @@ @pytest.fixture def cell(): - return ufc_simplex(2) + K = ufc_simplex(2) + K.vertices = ((0.0, 0.1), (1.17, -0.09), (0.15, 1.84)) + return K @pytest.mark.parametrize("reduced", (False, True)) diff --git a/test/unit/test_macro.py b/test/unit/test_macro.py index 4a6646716..f1aa683c3 100644 --- a/test/unit/test_macro.py +++ b/test/unit/test_macro.py @@ -2,7 +2,7 @@ import numpy import pytest from FIAT import DiscontinuousLagrange, Lagrange, Legendre, P0 -from FIAT.macro import AlfeldSplit, IsoSplit, CkPolynomialSet +from FIAT.macro import AlfeldSplit, IsoSplit, PowellSabinSplit, CkPolynomialSet from FIAT.quadrature_schemes import create_quadrature from FIAT.reference_element import ufc_simplex from FIAT.expansions import polynomial_entity_ids, polynomial_cell_node_map @@ -10,9 +10,9 @@ from FIAT.barycentric_interpolation import get_lagrange_points -@pytest.fixture(params=("I", "T", "S")) +@pytest.fixture(params=(1, 2, 3), ids=("I", "T", "S")) def cell(request): - dim = {"I": 1, "T": 2, "S": 3}[request.param] + dim = request.param return ufc_simplex(dim) @@ -129,6 +129,21 @@ def test_macro_lagrange(variant, degree, split, cell): assert numpy.allclose(fe.V, V) +def test_powell_sabin(cell): + dim = cell.get_spatial_dimension() + A = AlfeldSplit(cell) + assert A > cell + + PS = PowellSabinSplit(cell, dim) + assert PS == A + + for split_dim in range(1, dim): + PS = PowellSabinSplit(cell, split_dim) + assert PS > A + assert PS > cell + assert len(PS.get_topology()[dim]) == math.factorial(dim+1) // math.factorial(split_dim) + + def make_mass_matrix(fe, order=0): sd = fe.ref_el.get_spatial_dimension() Q = create_quadrature(fe.ref_complex, 2*fe.degree()) diff --git a/test/unit/test_stokes_complex.py b/test/unit/test_stokes_complex.py new file mode 100644 index 000000000..1c20b0e75 --- /dev/null +++ b/test/unit/test_stokes_complex.py @@ -0,0 +1,144 @@ +import pytest +import numpy + +from FIAT import HsiehCloughTocher as HCT +from FIAT import AlfeldSorokina as AS +from FIAT import ArnoldQin as AQ +from FIAT import Lagrange as CG +from FIAT import DiscontinuousLagrange as DG +from FIAT.restricted import RestrictedElement +from FIAT.reference_element import ufc_simplex + +from FIAT.macro import CkPolynomialSet +from FIAT.alfeld_sorokina import AlfeldSorokinaSpace +from FIAT.arnold_qin import ArnoldQinSpace +from FIAT.christiansen_hu import ChristiansenHuSpace +from FIAT.guzman_neilan import ExtendedGuzmanNeilanSpace + + +T = ufc_simplex(2) +S = ufc_simplex(3) + + +@pytest.mark.parametrize("cell", (T, S)) +@pytest.mark.parametrize("degree", (2, 3, 4)) +def test_AlfeldSorokinaSpace(cell, degree): + # Test that the divergence of the Alfeld-Sorokina space is spanned by a C0 basis + V = AlfeldSorokinaSpace(cell, degree) + A = V.get_reference_element() + top = A.get_topology() + sd = A.get_spatial_dimension() + + pts = [] + for dim in top: + for entity in top[dim]: + pts.extend(A.make_points(dim, entity, degree)) + + V_tab = V.tabulate(pts, 1) + V_div = sum(V_tab[alpha][:, alpha.index(1), :] + for alpha in V_tab if sum(alpha) == 1) + + C0 = CkPolynomialSet(A, degree-1, order=0, variant="bubble") + C0_tab = C0.tabulate(pts)[(0,)*sd] + _, residual, *_ = numpy.linalg.lstsq(C0_tab.T, V_div.T) + assert numpy.allclose(residual, 0) + + +@pytest.mark.parametrize("cell", (T,)) +@pytest.mark.parametrize("reduced", (False, True)) +def test_hct_stokes_complex(cell, reduced): + # Test that we have the lowest-order discrete Stokes complex, verifying + # that the range of the exterior derivative of each space is contained by + # the next space in the sequence + H2 = HCT(T, reduced=reduced) + A = H2.get_reference_complex() + if reduced: + # HCT-red(3) -curl-> AQ-red(2) -div-> DG(0) + H2 = RestrictedElement(H2, restriction_domain="vertex") + H1 = RestrictedElement(AQ(T, reduced=reduced), indices=list(range(9))) + L2 = DG(cell, 0) + else: + # HCT(3) -curl-> AS(2) -div-> CG(1, Alfeld) + H1 = AS(cell) + L2 = CG(A, 1) + + pts = [] + top = A.get_topology() + for dim in top: + for entity in top[dim]: + pts.extend(A.make_points(dim, entity, 4)) + + H2tab = H2.tabulate(1, pts) + H1tab = H1.tabulate(1, pts) + L2tab = L2.tabulate(0, pts) + + L2val = L2tab[(0, 0)] + H1val = H1tab[(0, 0)] + H1div = sum(H1tab[alpha][:, alpha.index(1), :] for alpha in H1tab if sum(alpha) == 1) + H2curl = numpy.stack([H2tab[(0, 1)], -H2tab[(1, 0)]], axis=1) + + H2dim = H2.space_dimension() + H1dim = H1.space_dimension() + _, residual, *_ = numpy.linalg.lstsq(H1val.reshape(H1dim, -1).T, H2curl.reshape(H2dim, -1).T) + assert numpy.allclose(residual, 0) + + _, residual, *_ = numpy.linalg.lstsq(L2val.T, H1div.T) + assert numpy.allclose(residual, 0) + + +@pytest.mark.parametrize("cell", (T, S)) +@pytest.mark.parametrize("family", ("AQ", "CH", "GN")) +def test_minimal_stokes_space(cell, family): + # Test that the C0 Stokes space is spanned by a C0 basis + # Also test that its divergence is constant + sd = cell.get_spatial_dimension() + if family == "GN": + degree = 1 + space = ExtendedGuzmanNeilanSpace + elif family == "CH": + degree = 1 + space = ChristiansenHuSpace + elif family == "AQ": + if sd != 2: + return + degree = 2 + space = ArnoldQinSpace + + W = space(cell, degree) + V = space(cell, degree, reduced=True) + Wdim = W.get_num_members() + Vdim = V.get_num_members() + K = W.get_reference_element() + sd = K.get_spatial_dimension() + top = K.get_topology() + + pts = [] + for dim in top: + for entity in top[dim]: + pts.extend(K.make_points(dim, entity, degree)) + + C0 = CkPolynomialSet(K, sd, order=0, variant="bubble") + C0_tab = C0.tabulate(pts) + Wtab = W.tabulate(pts, 1) + Vtab = V.tabulate(pts, 1) + z = (0,)*sd + for tab in (Vtab, Wtab): + # Test that the space is full rank + _, sig, _ = numpy.linalg.svd(tab[z].reshape(-1, sd*len(pts)).T, full_matrices=True) + assert all(sig > 1E-10) + + # Test that the space is C0 + for k in range(sd): + _, residual, *_ = numpy.linalg.lstsq(C0_tab[z].T, tab[z][:, k, :].T) + assert numpy.allclose(residual, 0) + + # Test that divergence is in P0 + div = sum(tab[alpha][:, alpha.index(1), :] + for alpha in tab if sum(alpha) == 1)[:Vdim] + assert numpy.allclose(div, div[:, 0][:, None]) + + # Test that the full space includes the reduced space + assert Wdim > Vdim + _, residual, *_ = numpy.linalg.lstsq(Wtab[z].reshape(Wdim, -1).T, + Vtab[z].reshape(Vdim, -1).T) + assert numpy.allclose(residual, 0)