Skip to content

Commit a660140

Browse files
pbrubeckrckirby
andauthored
Add Johnson-Mercier elements (#67)
* HDivSymPolynomialSet add the Johnson-Mercier macro element --------- Co-authored-by: Rob Kirby <robert.c.kirby@gmail.com>
1 parent fa86ed3 commit a660140

File tree

5 files changed

+145
-19
lines changed

5 files changed

+145
-19
lines changed

FIAT/__init__.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
# Import finite element classes
88
from FIAT.finite_element import FiniteElement, CiarletElement # noqa: F401
9-
from FIAT.argyris import Argyris
10-
from FIAT.hct import HsiehCloughTocher
9+
from FIAT.argyris import Argyris, QuinticArgyris
1110
from FIAT.bernstein import Bernstein
1211
from FIAT.bell import Bell
13-
from FIAT.argyris import QuinticArgyris
12+
from FIAT.hct import HsiehCloughTocher
13+
from FIAT.johnson_mercier import JohnsonMercier
1414
from FIAT.brezzi_douglas_marini import BrezziDouglasMarini
1515
from FIAT.Sminus import TrimmedSerendipityEdge # noqa: F401
1616
from FIAT.Sminus import TrimmedSerendipityFace # noqa: F401
@@ -62,7 +62,6 @@
6262

6363
# List of supported elements and mapping to element classes
6464
supported_elements = {"Argyris": Argyris,
65-
"HsiehCloughTocher": HsiehCloughTocher,
6665
"Bell": Bell,
6766
"Bernstein": Bernstein,
6867
"Brezzi-Douglas-Marini": BrezziDouglasMarini,
@@ -78,6 +77,8 @@
7877
"Discontinuous Taylor": DiscontinuousTaylor,
7978
"Discontinuous Raviart-Thomas": DiscontinuousRaviartThomas,
8079
"Hermite": CubicHermite,
80+
"HsiehCloughTocher": HsiehCloughTocher,
81+
"Johnson-Mercier": JohnsonMercier,
8182
"Lagrange": Lagrange,
8283
"Kong-Mulder-Veldhuizen": KongMulderVeldhuizen,
8384
"Gauss-Lobatto-Legendre": GaussLobattoLegendre,

FIAT/expansions.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ def dubiner_recurrence(dim, n, order, ref_pts, Jinv, scale, variant=None):
8484

8585
if variant == "bubble":
8686
scale = -scale
87-
if n == 0:
88-
# Always return 1 for n=0 to make regression tests pass
89-
scale = 1.0
9087

9188
num_members = math.comb(n + dim, dim)
9289
results = tuple([None] * num_members for i in range(order+1))
@@ -286,18 +283,24 @@ def __init__(self, ref_el, scale=None, variant=None):
286283
base_verts) for cell in top[sd]]
287284
if scale is None:
288285
scale = math.sqrt(1.0 / self.base_ref_el.volume())
289-
elif isinstance(scale, str):
290-
scale = scale.lower()
291-
if scale == "orthonormal":
292-
scale = math.sqrt(1.0 / ref_el.volume())
293-
elif scale == "l2 piola":
294-
scale = 1.0 / ref_el.volume()
295286
self.scale = scale
296287
self.continuity = "C0" if variant == "bubble" else None
297288
self.recurrence_order = 2
298289
self._dmats_cache = {}
299290
self._cell_node_map_cache = {}
300291

292+
def get_scale(self, cell=0):
293+
scale = self.scale
294+
if isinstance(scale, str):
295+
sd = self.ref_el.get_spatial_dimension()
296+
vol = self.ref_el.volume_of_subcomplex(sd, cell)
297+
scale = scale.lower()
298+
if scale == "orthonormal":
299+
scale = math.sqrt(1.0 / vol)
300+
elif scale == "l2 piola":
301+
scale = 1.0 / vol
302+
return scale
303+
301304
def get_num_members(self, n):
302305
return polynomial_dimension(self.ref_el, n, self.continuity)
303306

@@ -317,8 +320,11 @@ def _tabulate_on_cell(self, n, pts, order=0, cell=0, direction=None):
317320
ref_pts = apply_mapping(A, b, numpy.transpose(pts))
318321
Jinv = A if direction is None else numpy.dot(A, direction)[:, None]
319322
sd = self.ref_el.get_spatial_dimension()
323+
324+
# Always return 1 for n=0 to make regression tests pass
325+
scale = 1.0 if n == 0 and len(self.affine_mappings) == 1 else self.get_scale(cell=cell)
320326
phi = dubiner_recurrence(sd, n, lorder, ref_pts, Jinv,
321-
self.scale, variant=self.variant)
327+
scale, variant=self.variant)
322328
if self.continuity == "C0":
323329
phi = C0_basis(sd, n, phi)
324330

@@ -494,7 +500,7 @@ def _tabulate_on_cell(self, n, pts, order=0, cell=0, direction=None):
494500
Jinv = A[0, 0] if direction is None else numpy.dot(A, direction)
495501
xs = apply_mapping(A, b, numpy.transpose(pts)).T
496502
results = {}
497-
scale = self.scale * numpy.sqrt(2 * numpy.arange(n+1) + 1)
503+
scale = self.get_scale(cell=cell) * numpy.sqrt(2 * numpy.arange(n+1) + 1)
498504
for k in range(order+1):
499505
v = numpy.zeros((n + 1, len(xs)), "d")
500506
if n >= k:

FIAT/functional.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -457,18 +457,17 @@ def __init__(self, ref_el, Q, f_at_qpts):
457457
nqp = len(qpts)
458458
dpts = qpts
459459
self.dpts = dpts
460+
sd = ref_el.get_spatial_dimension()
460461

461462
assert len(f_at_qpts.shape) == 2
462-
assert f_at_qpts.shape[0] == 2
463+
assert f_at_qpts.shape[0] == sd
463464
assert f_at_qpts.shape[1] == nqp
464465

465-
sd = ref_el.get_spatial_dimension()
466-
467466
dpt_dict = OrderedDict()
468467

469468
alphas = [tuple([1 if j == i else 0 for j in range(sd)]) for i in range(sd)]
470469
for q, pt in enumerate(dpts):
471-
dpt_dict[tuple(pt)] = [(qwts[q]*f_at_qpts[i, q], alphas[j], (i, j)) for i in range(2) for j in range(2)]
470+
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)]
472471

473472
super().__init__(ref_el, tuple(), {}, dpt_dict,
474473
"IntegralMomentOfDivergence")

FIAT/johnson_mercier.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from FIAT import finite_element, dual_set, macro, polynomial_set
2+
from FIAT.functional import IntegralMoment, FrobeniusIntegralMoment
3+
from FIAT.quadrature import FacetQuadratureRule
4+
from FIAT.quadrature_schemes import create_quadrature
5+
import numpy
6+
7+
8+
class JohnsonMercierDualSet(dual_set.DualSet):
9+
def __init__(self, ref_complex, degree, variant=None):
10+
if degree != 1:
11+
raise ValueError("Johnson-Mercier only defined for degree=1")
12+
if variant is not None:
13+
raise ValueError(f"Johnson-Mercier does not have the {variant} variant")
14+
ref_el = ref_complex.get_parent()
15+
top = ref_el.get_topology()
16+
sd = ref_el.get_spatial_dimension()
17+
entity_ids = {dim: {entity: [] for entity in sorted(top[dim])} for dim in sorted(top)}
18+
nodes = []
19+
20+
# Face dofs: bidirectional (nn and nt) Legendre moments
21+
dim = sd - 1
22+
ref_facet = ref_el.construct_subelement(dim)
23+
Qref = create_quadrature(ref_facet, 2*degree)
24+
P = polynomial_set.ONPolynomialSet(ref_facet, degree)
25+
phis = P.tabulate(Qref.get_points())[(0,) * dim]
26+
for facet in sorted(top[dim]):
27+
cur = len(nodes)
28+
Q = FacetQuadratureRule(ref_el, dim, facet, Qref)
29+
Jdet = Q.jacobian_determinant()
30+
tangents = ref_el.compute_tangents(dim, facet)
31+
normal = ref_el.compute_normal(facet)
32+
normal /= numpy.linalg.norm(normal)
33+
scaled_normal = normal * Jdet
34+
uvecs = (scaled_normal, *tangents)
35+
comps = [numpy.outer(normal, uvec) for uvec in uvecs]
36+
nodes.extend(FrobeniusIntegralMoment(ref_el, Q, comp[:, :, None] * phi[None, None, :])
37+
for phi in phis for comp in comps)
38+
entity_ids[dim][facet].extend(range(cur, len(nodes)))
39+
40+
cur = len(nodes)
41+
if variant is None:
42+
# Interior dofs: moments for each independent component
43+
Q = create_quadrature(ref_complex, 2*degree-1)
44+
P = polynomial_set.ONPolynomialSet(ref_el, degree-1)
45+
phis = P.tabulate(Q.get_points())[(0,) * sd]
46+
nodes.extend(IntegralMoment(ref_el, Q, phi, comp=(i, j))
47+
for j in range(sd) for i in range(j+1) for phi in phis)
48+
49+
entity_ids[sd][0].extend(range(cur, len(nodes)))
50+
51+
super(JohnsonMercierDualSet, self).__init__(nodes, ref_el, entity_ids)
52+
53+
54+
class JohnsonMercier(finite_element.CiarletElement):
55+
"""The Johnson-Mercier finite element."""
56+
57+
def __init__(self, ref_el, degree=1, variant=None):
58+
ref_complex = macro.AlfeldSplit(ref_el)
59+
poly_set = macro.HDivSymPolynomialSet(ref_complex, degree)
60+
dual = JohnsonMercierDualSet(ref_complex, degree, variant=variant)
61+
mapping = "double contravariant piola"
62+
super(JohnsonMercier, self).__init__(poly_set, dual, degree,
63+
mapping=mapping)

test/unit/test_johnson_mercier.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
import numpy
3+
4+
from FIAT import JohnsonMercier, Nedelec
5+
from FIAT.reference_element import ufc_simplex
6+
from FIAT.quadrature_schemes import create_quadrature
7+
8+
9+
@pytest.fixture(params=("T-ref", "T-phys", "S-ref", "S-phys"))
10+
def cell(request):
11+
cell, deform = request.param.split("-")
12+
dim = {"T": 2, "S": 3}[cell]
13+
K = ufc_simplex(dim)
14+
if deform == "phys":
15+
if dim == 2:
16+
K.vertices = ((0.0, 0.0), (2.0, 0.1), (0.0, 1.0))
17+
else:
18+
K.vertices = ((0, 0, 0), (1., 0.1, -0.37),
19+
(0.01, 0.987, -.23), (-0.1, -0.2, 1.38))
20+
return K
21+
22+
23+
def test_johnson_mercier_divergence_rigid_body_motions(cell):
24+
# test that the divergence of interior JM basis functions is orthogonal to
25+
# the rigid-body motions
26+
degree = 1
27+
variant = None
28+
sd = cell.get_spatial_dimension()
29+
JM = JohnsonMercier(cell, degree, variant=variant)
30+
31+
ref_complex = JM.get_reference_complex()
32+
Q = create_quadrature(ref_complex, 2*(degree)-1)
33+
qpts, qwts = Q.get_points(), Q.get_weights()
34+
35+
tab = JM.tabulate(1, qpts)
36+
div = sum(tab[alpha][:, alpha.index(1), :, :] for alpha in tab if sum(alpha) == 1)
37+
38+
# construct rigid body motions
39+
N1 = Nedelec(cell, 1)
40+
N1_at_qpts = N1.tabulate(0, qpts)
41+
rbms = N1_at_qpts[(0,)*sd]
42+
ells = rbms * qwts[None, None, :]
43+
44+
edofs = JM.entity_dofs()
45+
idofs = edofs[sd][0]
46+
L = numpy.tensordot(div, ells, axes=((1, 2), (1, 2)))
47+
assert numpy.allclose(L[idofs], 0)
48+
49+
if variant == "divergence":
50+
edofs = JM.entity_dofs()
51+
cdofs = []
52+
for entity in edofs[sd-1]:
53+
cdofs.extend(edofs[sd-1][entity][:sd])
54+
D = L[cdofs]
55+
M = numpy.tensordot(rbms, ells, axes=((1, 2), (1, 2)))
56+
X = numpy.linalg.solve(M, D.T)
57+
assert numpy.allclose(numpy.tensordot(X, rbms, axes=(0, 0)), div[cdofs])

0 commit comments

Comments
 (0)