-
Notifications
You must be signed in to change notification settings - Fork 7
C2 (macro)elements #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
C2 (macro)elements #212
Changes from all commits
b7bf542
e7e8b51
7cb2607
4f41966
7d63ce1
d396550
cd5879f
a5c4cc3
903e8de
0f2d2d0
e248b3f
631b680
0d524a9
ec23f0c
71661b2
d62f91d
b6f1919
ea46922
f28b62c
cb9f4ff
14e0c9b
f7d1b6a
78c6ecd
8d1a14d
7c970e5
00b1dfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # Copyright (C) 2026 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), 2026 | ||
|
|
||
| from FIAT.functional import (PointEvaluation, PointDerivative, | ||
| IntegralMoment, IntegralMomentOfDerivative) | ||
| from FIAT import finite_element, dual_set, macro, polynomial_set | ||
| from FIAT.polynomial_set import mis | ||
| from FIAT.check_format_variant import parse_quadrature_scheme | ||
| from FIAT.reference_element import TRIANGLE, ufc_simplex | ||
| from FIAT.quadrature import FacetQuadratureRule | ||
| from FIAT.jacobi import eval_jacobi_batch, eval_jacobi_deriv_batch | ||
|
|
||
|
|
||
| class C2DualSet(dual_set.DualSet): | ||
| """A DualSet for triangular C2 elements. | ||
|
|
||
| By default, this imposes C4 continuity at vertices for non-macroelements, | ||
| and the minimal C2 continuity for macroelements. | ||
| """ | ||
| def __init__(self, ref_complex, degree, vorder=None, reduced=False, quad_scheme=None): | ||
| if vorder is None: | ||
| vorder = 2 if ref_complex.is_macrocell() else 4 | ||
|
|
||
| if degree < 2*vorder+1: | ||
| raise ValueError(f"{type(self).__name__} only defined for degree >= {2*vorder+1}") | ||
|
|
||
| ref_el = ref_complex.get_parent() or ref_complex | ||
| if ref_el.get_shape() != TRIANGLE: | ||
| raise ValueError(f"{type(self).__name__} only defined on triangles") | ||
|
|
||
| top = ref_el.get_topology() | ||
| verts = ref_el.get_vertices() | ||
| sd = ref_el.get_spatial_dimension() | ||
| entity_ids = {dim: {entity: [] for entity in sorted(top[dim])} for dim in sorted(top)} | ||
|
|
||
| # vorder jet at vertices | ||
| nodes = [] | ||
| for v in sorted(top[0]): | ||
| pt = verts[v] | ||
| cur = len(nodes) | ||
| nodes.append(PointEvaluation(ref_el, pt)) | ||
| nodes.extend(PointDerivative(ref_el, pt, alpha) for i in range(1, vorder+1) for alpha in mis(sd, i)) | ||
| entity_ids[0][v].extend(range(cur, len(nodes))) | ||
|
|
||
| k = degree - 2*vorder | ||
| facet = ufc_simplex(1) | ||
| Q_ref = parse_quadrature_scheme(facet, degree-2+k, quad_scheme) | ||
| x = facet.compute_barycentric_coordinates(Q_ref.get_points()) | ||
| xref = x[:, [1]] - x[:, [0]] | ||
|
|
||
| if reduced: | ||
| raise NotImplementedError | ||
| else: | ||
| # Integral moments of normal derivatives against Jacobi polynomials along edges | ||
| phis = eval_jacobi_batch(vorder, vorder, k, xref) | ||
| dphis = 2*eval_jacobi_deriv_batch(vorder, vorder, k, xref, order=1) | ||
| ddphis = 4*eval_jacobi_deriv_batch(vorder, vorder, k, xref, order=2) | ||
| for e in sorted(top[1]): | ||
| Q = FacetQuadratureRule(ref_el, 1, e, Q_ref, avg=True) | ||
| n = ref_el.compute_normal(e) | ||
| cur = len(nodes) | ||
| nodes.extend(IntegralMoment(ref_el, Q, ddphi) for ddphi in ddphis[2:]) | ||
| nodes.extend(IntegralMomentOfDerivative(ref_el, Q, dphi, n) for dphi in dphis[1:]) | ||
| nodes.extend(IntegralMomentOfDerivative(ref_el, Q, phi, n, n) for phi in phis) | ||
| entity_ids[1][e].extend(range(cur, len(nodes))) | ||
|
|
||
| # Interior moments against a basis for Pq | ||
| q = degree - 3 * (vorder // 2 + 1) | ||
| if q >= 0: | ||
| Q = parse_quadrature_scheme(ref_complex, degree + q, quad_scheme) | ||
| Pq = polynomial_set.ONPolynomialSet(ref_el, q, scale=1) | ||
| phis = Pq.tabulate(Q.get_points())[(0,) * sd] | ||
| phis *= 1/ref_el.volume() | ||
| cur = len(nodes) | ||
| nodes.extend(IntegralMoment(ref_el, Q, phi) for phi in phis) | ||
| entity_ids[sd][0].extend(range(cur, len(nodes))) | ||
|
|
||
| super().__init__(nodes, ref_el, entity_ids) | ||
|
|
||
|
|
||
| class BrambleZlamalC2(finite_element.CiarletElement): | ||
| """The Bramble-Zlamal C2 element.""" | ||
| def __init__(self, ref_el, degree=9, reduced=False, quad_scheme=None): | ||
| poly_set = polynomial_set.ONPolynomialSet(ref_el, degree) | ||
| dual = C2DualSet(ref_el, degree, reduced=reduced, quad_scheme=quad_scheme) | ||
| super().__init__(poly_set, dual, degree, formdegree=0) | ||
|
|
||
|
|
||
| def AlfeldC2Space(ref_el, degree): | ||
| """Construct the generalization of the quintic C2 spline on the double Alfeld split.""" | ||
| # Construct the double Alfeld split by splitting twice | ||
| ref_complex = macro.AlfeldSplit(macro.AlfeldSplit(ref_el)) | ||
| # C3 on major split facets, C2 elsewhere | ||
| order = {} | ||
| order[1] = dict.fromkeys(ref_complex.get_interior_facets(1), 2) | ||
| order[1].update(dict.fromkeys(range(3, 6), degree-2)) | ||
| # C4 at minor split barycenters, C3 at major split barycenter | ||
| order[0] = dict.fromkeys(ref_complex.get_interior_facets(0), degree-1) | ||
| order[0][3] = degree-2 | ||
| return macro.CkPolynomialSet(ref_complex, degree, order=order, variant="bubble") | ||
|
|
||
|
|
||
| class AlfeldC2(finite_element.CiarletElement): | ||
| """The Alfeld C^2 macroelement on a double barycentric split. | ||
| See Section 7.5 of Lai & Schumacher for the quintic C^2 spline. | ||
| """ | ||
| def __init__(self, ref_el, degree=5, reduced=False, quad_scheme=None): | ||
| poly_set = AlfeldC2Space(ref_el, degree) | ||
| ref_complex = poly_set.get_reference_element() | ||
| dual = C2DualSet(ref_complex, degree, reduced=reduced, quad_scheme=quad_scheme) | ||
| super().__init__(poly_set, dual, degree, formdegree=0) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,66 @@ | ||
| import numpy | ||
| from math import comb | ||
| from itertools import chain | ||
|
|
||
| import FIAT | ||
|
|
||
| from gem import Literal, ListTensor | ||
| from gem import Literal, ListTensor, Zero | ||
|
|
||
| from finat.citations import cite | ||
| from finat.fiat_elements import ScalarFiatElement | ||
| from finat.physically_mapped import identity, PhysicallyMappedElement | ||
|
|
||
|
|
||
| def _jet_transform(J, order): | ||
| """Basis transformation for derivative evaluation.""" | ||
| if order == 0: | ||
| return identity(1) | ||
| sd = J.shape[0] | ||
| shape = (sd,)*order | ||
|
|
||
| # Mapping from multiindices to linearly-independent (flattened) components | ||
| mapping = {} | ||
| alphas = [] | ||
| for indices in numpy.ndindex(shape): | ||
| alpha = [0] * sd | ||
| for i in indices: | ||
| alpha[i] += 1 | ||
| alpha = tuple(alpha) | ||
| if alpha not in alphas: | ||
| alphas.append(alpha) | ||
| mapping[indices] = alphas.index(alpha) | ||
| # Inverse mapping | ||
| imapping = {v: k for k, v in mapping.items()} | ||
|
|
||
| # Get the transformation for a covariant tensor. | ||
| # We take the outer product, as each index maps with the Jacobian. | ||
| Jnp = numpy.asarray([[J[i, j] for j in range(sd)] for i in range(sd)]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate the generality here. However, the math + numpy is a bit unclear. For example, how does this go from the 2x2 jet to 3x3 Hessian map (often called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess if we write out the maths in a paper this will all become clearer to me? |
||
| Jprod = Jnp | ||
| for i in range(1, order): | ||
| Jprod = Jprod[..., None, None] * Jnp | ||
|
|
||
|
pbrubeck marked this conversation as resolved.
|
||
| # Deal with symmetries by contracting along linearly-dependent components. | ||
| B = numpy.full((len(alphas), len(alphas)), Zero(), dtype=object) | ||
| for i, ii in imapping.items(): | ||
| for jj, j in mapping.items(): | ||
| B[i, j] += Jprod[tuple(chain.from_iterable(zip(jj, ii)))] | ||
| return B | ||
|
|
||
|
|
||
| def _vertex_transform(V, vorder, fiat_cell, coordinate_mapping): | ||
| """Basis transformation for evaluation, gradient, and hessian at vertices.""" | ||
| """Basis transformation for jet at vertices.""" | ||
| sd = fiat_cell.get_spatial_dimension() | ||
| top = fiat_cell.get_topology() | ||
| bary, = fiat_cell.make_points(sd, 0, sd+1) | ||
| J = coordinate_mapping.jacobian_at(bary) | ||
|
|
||
| gdofs = sd | ||
| G = [[J[j, i] for j in range(sd)] for i in range(sd)] | ||
|
|
||
| if vorder < 2: | ||
| hdofs = 0 | ||
| H = [[]] | ||
| else: | ||
| hdofs = (sd*(sd+1))//2 | ||
| indices = [(i, j) for i in range(sd) for j in range(i, sd)] | ||
| H = numpy.zeros((hdofs, hdofs), dtype=object) | ||
| for p, (i, j) in enumerate(indices): | ||
| for q, (m, n) in enumerate(indices): | ||
| H[p, q] = J[m, i] * J[n, j] + J[m, j] * J[n, i] | ||
| H[:, [i == j for i, j in indices]] *= 0.5 | ||
|
|
||
| jet = [_jet_transform(J, k) for k in range(vorder+1)] | ||
| s = 0 | ||
| for v in sorted(top[0]): | ||
| s += 1 | ||
| V[s:s+gdofs, s:s+gdofs] = G | ||
| s += gdofs | ||
| V[s:s+hdofs, s:s+hdofs] = H | ||
| s += hdofs | ||
| for B in jet: | ||
| ndofs = len(B) | ||
| V[s:s+ndofs, s:s+ndofs] = B | ||
| s += ndofs | ||
| return V | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| from finat.fiat_elements import ScalarFiatElement | ||
| from finat.physically_mapped import identity, PhysicallyMappedElement | ||
| from finat.citations import cite | ||
| from finat.argyris import (_jet_transform, _vertex_transform, | ||
| _normal_tangential_transform) | ||
| from gem import ListTensor | ||
|
|
||
| import FIAT | ||
| import numpy | ||
| from math import comb | ||
|
|
||
|
|
||
| class C2Element(PhysicallyMappedElement): | ||
|
|
||
| def basis_transformation(self, coordinate_mapping): | ||
|
rckirby marked this conversation as resolved.
|
||
| top = self.cell.topology | ||
| sd = self.cell.get_spatial_dimension() | ||
| entity_ids = self._element.entity_dofs() | ||
|
|
||
| # Detect the maximum derivative order of the vertex dofs | ||
| nodes = self._element.dual_basis() | ||
| vorder = max(nodes[i].max_deriv_order for i in entity_ids[0][0]) | ||
|
|
||
| V = identity(self.space_dimension()) | ||
| _vertex_transform(V, vorder, self.cell, coordinate_mapping) | ||
|
|
||
| bary, = self.cell.make_points(sd, 0, sd+1) | ||
| J = coordinate_mapping.jacobian_at(bary) | ||
| detJ = coordinate_mapping.detJ_at(bary) | ||
| Thetainv = _jet_transform(J, 2) | ||
|
|
||
| ns = coordinate_mapping.physical_normals() | ||
| ts = coordinate_mapping.physical_tangents() | ||
| lens = coordinate_mapping.physical_edge_lengths() | ||
| nhats = coordinate_mapping.reference_normals() | ||
| thats = coordinate_mapping.normalized_reference_edge_tangents() | ||
|
|
||
| n0 = self.degree - 2*vorder - 1 | ||
| n1 = n0 + 1 | ||
| for e in top[1]: | ||
| v0, v1 = top[1][e] | ||
| vid0 = entity_ids[0][v0] | ||
| vid1 = entity_ids[0][v1] | ||
| eids = entity_ids[1][e] | ||
| emoments = (eids[:n0], eids[n0:n0+n1], eids[n0+n1:]) | ||
|
|
||
| G = numpy.array([[u[e, j] for j in range(sd)] for u in (ns, ts)]) | ||
| Ghat = numpy.array([[u[e, j] for j in range(sd)] for u in (nhats, thats)]) | ||
| Gamma = _jet_transform(G, 2) | ||
| Gammainvhat = _jet_transform(Ghat.T, 2) | ||
|
|
||
| B2 = (Gammainvhat @ Thetainv) @ Gamma | ||
| beta = B2[0, 1:] @ G / lens[e] | ||
|
|
||
| Bnn, Bnt, Jt = _normal_tangential_transform(self.cell, J, detJ, e) | ||
| if self.avg: | ||
| Bnn = Bnn * lens[e] | ||
|
|
||
| # first derivative moments | ||
| for k, s1 in enumerate(emoments[1], start=1): | ||
| # Derivative of Jacobi polynomial at the endpoints | ||
| dP1 = comb(k + vorder, k-1) * (2*vorder+k+1) | ||
| dP0 = (-1)**k * dP1 | ||
|
|
||
| V[s1, s1] = Bnn | ||
| V[s1, vid0[0]] = dP0 * Bnt | ||
| V[s1, vid1[0]] = dP1 * Bnt | ||
| if k > 1: | ||
| s0 = emoments[0][k-2] | ||
| V[s1, s0] = -1 * Bnt | ||
|
|
||
| # second derivative moments | ||
| for k, s2 in enumerate(emoments[2]): | ||
| # Jacobi polynomial at the endpoints | ||
| P1 = comb(k + vorder, k) | ||
| P0 = -(-1)**k * P1 | ||
|
|
||
| V[s2, s2] = B2[0, 0] | ||
| V[s2, vid0[1:sd+1]] = P0 * beta | ||
| V[s2, vid1[1:sd+1]] = P1 * beta | ||
| if k > 0: | ||
| s1 = emoments[1][k-1] | ||
| V[s2, s1] = -2 * Bnt * V[s1, s1] | ||
| V[s2, vid0[0]] = -1 * Bnt * V[s1, vid0[0]] | ||
| V[s2, vid1[0]] = -1 * Bnt * V[s1, vid1[0]] | ||
| if k > 1: | ||
| s0 = emoments[0][k-2] | ||
| V[s2, s0] = -1 * Bnt * V[s1, s0] | ||
|
|
||
| # Now let's fix the scaling. | ||
| h = coordinate_mapping.cell_size() | ||
| for v in top[0]: | ||
| vids = entity_ids[0][v] | ||
| scale = 1 / h[v] | ||
| F = scale | ||
| iend = 1 | ||
| # scale the jet at vertices | ||
| for k in range(1, vorder+1): | ||
| istart = iend | ||
| iend = istart + comb(k+sd-1, sd-1) | ||
| V[:, vids[istart:iend]] *= F | ||
| F *= scale | ||
|
|
||
| for e in top[1]: | ||
| eids = entity_ids[1][e] | ||
| emoments = (eids[:n0], eids[n0:n0+n1], eids[n0+n1:]) | ||
| he = (1/len(top[1][e])) * sum(h[v] for v in top[1][e]) | ||
| # scale first and second derivative moments | ||
| V[:, emoments[1]] *= 1 / he | ||
| V[:, emoments[2]] *= 1 / (he * he) | ||
|
|
||
| return ListTensor(V.T) | ||
|
|
||
|
|
||
| class BrambleZlamalC2(C2Element, ScalarFiatElement): | ||
| def __init__(self, cell, degree=9, avg=True): | ||
| cite("BrambleZlamal1970") | ||
| self.avg = avg | ||
| super().__init__(FIAT.BrambleZlamalC2(cell, degree)) | ||
|
|
||
|
|
||
| class AlfeldC2(C2Element, ScalarFiatElement): | ||
| def __init__(self, cell, degree=5, avg=True): | ||
| cite("Alfeld1984") | ||
| self.avg = avg | ||
| super().__init__(FIAT.AlfeldC2(cell, degree)) | ||
Uh oh!
There was an error while loading. Please reload this page.