Skip to content

Commit dbc1c5d

Browse files
authored
Simplex hierarchical elements (#58)
* Simplex Legendre element * unify N2curl dofs * IntegratedLagrange on simplices * DOFs are now moments of grad(v) against grad(bubble) * add bubbles from Beuchler and Schoberl * moments against L2 duals fo bubbles * normalization of 1D bubbles * Add FDM variant * Implement FDMLagrange on simplices * exploit block sparsity in DualSet.to_riesz * optimize FrobeniusIntegralMoment
1 parent e7b2909 commit dbc1c5d

9 files changed

+371
-165
lines changed

FIAT/expansions.py

+133-37
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99

1010
import numpy
1111
import math
12-
from FIAT import reference_element
13-
from FIAT import jacobi
12+
from FIAT import reference_element, jacobi
1413

1514

1615
def morton_index2(p, q=0):
@@ -24,11 +23,22 @@ def morton_index3(p, q=0, r=0):
2423
def jrc(a, b, n):
2524
"""Jacobi recurrence coefficients"""
2625
an = (2*n+1+a+b)*(2*n+2+a+b) / (2*(n+1)*(n+1+a+b))
27-
bn = (a*a-b*b) * (2*n+1+a+b) / (2*(n+1)*(2*n+a+b)*(n+1+a+b))
26+
bn = (a+b)*(a-b)*(2*n+1+a+b) / (2*(n+1)*(n+1+a+b)*(2*n+a+b))
2827
cn = (n+a)*(n+b)*(2*n+2+a+b) / ((n+1)*(n+1+a+b)*(2*n+a+b))
2928
return an, bn, cn
3029

3130

31+
def integrated_jrc(a, b, n):
32+
"""Integrated Jacobi recurrence coefficients"""
33+
if n == 1:
34+
an = (a + b + 2) / 2
35+
bn = (a - 3*b - 2) / 2
36+
cn = 0.0
37+
else:
38+
an, bn, cn = jrc(a-1, b+1, n-1)
39+
return an, bn, cn
40+
41+
3242
def pad_coordinates(ref_pts, embedded_dim):
3343
"""Pad reference coordinates by appending -1.0."""
3444
return tuple(ref_pts) + (-1.0, )*(embedded_dim - len(ref_pts))
@@ -52,10 +62,31 @@ def jacobi_factors(x, y, z, dx, dy, dz):
5262
return fa, fb, fc, dfa, dfb, dfc
5363

5464

55-
def dubiner_recurrence(dim, n, order, ref_pts, jacobian):
56-
"""Dubiner recurrence from (Kirby 2010)"""
65+
def dubiner_recurrence(dim, n, order, ref_pts, Jinv, scale, variant=None):
66+
"""Tabulate a Dubiner expansion set using the recurrence from (Kirby 2010).
67+
68+
:arg dim: The spatial dimension of the simplex.
69+
:arg n: The polynomial degree.
70+
:arg order: The maximum order of differenation.
71+
:arg ref_pts: An ``ndarray`` with the coordinates on the default (-1, 1)^d simplex.
72+
:arg Jinv: The inverse of the Jacobian of the coordinate mapping from the default simplex.
73+
:arg scale: A scale factor that sets the first member of expansion set.
74+
:arg variant: Choose between the default (None) orthogonal basis,
75+
'integral' for integrated Jacobi polynomials,
76+
or 'dual' for the L2-duals of the integrated Jacobi polynomials.
77+
78+
:returns: A tuple with tabulations of the expansion set and its derivatives.
79+
"""
5780
if order > 2:
5881
raise ValueError("Higher order derivatives not supported")
82+
if variant not in [None, "integral", "dual"]:
83+
raise ValueError(f"Invalid variant {variant}")
84+
85+
if variant == "integral":
86+
scale = -scale
87+
if n == 0:
88+
# Always return 1 for n=0 to make regression tests pass
89+
scale = 1.0
5990

6091
num_members = math.comb(n + dim, dim)
6192
results = tuple([None] * num_members for i in range(order+1))
@@ -65,8 +96,8 @@ def dubiner_recurrence(dim, n, order, ref_pts, jacobian):
6596
sym_outer = lambda x, y: outer(x, y) + outer(y, x)
6697

6798
pad_dim = dim + 2
68-
dX = pad_jacobian(jacobian, pad_dim)
69-
phi[0] = sum((ref_pts[i] - ref_pts[i] for i in range(dim)), 1.)
99+
dX = pad_jacobian(Jinv, pad_dim)
100+
phi[0] = sum((ref_pts[i] - ref_pts[i] for i in range(dim)), scale)
70101
if dphi is not None:
71102
dphi[0] = (phi[0] - phi[0]) * dX[0]
72103
if ddphi is not None:
@@ -76,9 +107,10 @@ def dubiner_recurrence(dim, n, order, ref_pts, jacobian):
76107
if dim > 3 or dim < 0:
77108
raise ValueError("Invalid number of spatial dimensions")
78109

110+
beta = 1 if variant == "dual" else 0
111+
coefficients = integrated_jrc if variant == "integral" else jrc
79112
X = pad_coordinates(ref_pts, pad_dim)
80113
idx = (lambda p: p, morton_index2, morton_index3)[dim-1]
81-
82114
for codim in range(dim):
83115
# Extend the basis from codim to codim + 1
84116
fa, fb, fc, dfa, dfb, dfc = jacobi_factors(*X[codim:codim+3], *dX[codim:codim+3])
@@ -87,9 +119,17 @@ def dubiner_recurrence(dim, n, order, ref_pts, jacobian):
87119
# handle i = 1
88120
icur = idx(*sub_index, 0)
89121
inext = idx(*sub_index, 1)
90-
alpha = 2 * sum(sub_index) + len(sub_index)
91-
b = 0.5 * alpha
92-
a = b + 1.0
122+
123+
if variant == "integral":
124+
alpha = 2 * sum(sub_index)
125+
a = b = -0.5
126+
else:
127+
alpha = 2 * sum(sub_index) + len(sub_index)
128+
if variant == "dual":
129+
alpha += 1 + len(sub_index)
130+
a = 0.5 * (alpha + beta) + 1.0
131+
b = 0.5 * (alpha - beta)
132+
93133
factor = a * fa - b * fb
94134
phi[inext] = factor * phi[icur]
95135
if dphi is not None:
@@ -101,7 +141,7 @@ def dubiner_recurrence(dim, n, order, ref_pts, jacobian):
101141
# general i by recurrence
102142
for i in range(1, n - sum(sub_index)):
103143
iprev, icur, inext = icur, inext, idx(*sub_index, i + 1)
104-
a, b, c = jrc(alpha, 0, i)
144+
a, b, c = coefficients(alpha, beta, i)
105145
factor = a * fa - b * fb
106146
phi[inext] = factor * phi[icur] - c * (fc * phi[iprev])
107147
if dphi is None:
@@ -115,11 +155,54 @@ def dubiner_recurrence(dim, n, order, ref_pts, jacobian):
115155
c * (fc * ddphi[iprev] + sym_outer(dphi[iprev], dfc) + phi[iprev] * ddfc))
116156

117157
# normalize
118-
for alpha in reference_element.lattice_iter(0, n+1, codim+1):
119-
icur = idx(*alpha)
120-
scale = math.sqrt(sum(alpha) + 0.5 * len(alpha))
158+
d = codim + 1
159+
shift = 1 if variant == "dual" else 0
160+
for index in reference_element.lattice_iter(0, n+1, d):
161+
icur = idx(*index)
162+
if variant is not None:
163+
p = index[-1] + shift
164+
alpha = 2 * (sum(index[:-1]) + d * shift) - 1
165+
norm2 = 1.0
166+
if p > 0 and p + alpha > 0:
167+
norm2 = (p + alpha) * (2*p + alpha) / p
168+
norm2 *= (2*d+1) / (2*d)
169+
else:
170+
norm2 = (2*sum(index) + d) / d
171+
scale = math.sqrt(norm2)
121172
for result in results:
122173
result[icur] *= scale
174+
175+
# recover facet bubbles
176+
if variant == "integral":
177+
icur = 0
178+
for result in results:
179+
result[icur] *= -1
180+
for inext in range(1, dim+1):
181+
for result in results:
182+
result[icur] -= result[inext]
183+
184+
if dim == 2:
185+
for i in range(2, n+1):
186+
icur = idx(0, i)
187+
iprev = idx(1, i-1)
188+
for result in results:
189+
result[icur] -= result[iprev]
190+
191+
elif dim == 3:
192+
for i in range(2, n+1):
193+
for j in range(0, n+1-i):
194+
icur = idx(0, i, j)
195+
iprev = idx(1, i-1, j)
196+
for result in results:
197+
result[icur] -= result[iprev]
198+
199+
icur = idx(0, 0, i)
200+
iprev0 = idx(1, 0, i-1)
201+
iprev1 = idx(0, 1, i-1)
202+
for result in results:
203+
result[icur] -= result[iprev0]
204+
result[icur] -= result[iprev1]
205+
123206
return results
124207

125208

@@ -141,32 +224,42 @@ def xi_tetrahedron(eta):
141224

142225

143226
class ExpansionSet(object):
144-
def __new__(cls, ref_el, *args, **kwargs):
227+
def __new__(cls, *args, **kwargs):
145228
"""Returns an ExpansionSet instance appopriate for the given
146229
reference element."""
147230
if cls is not ExpansionSet:
148231
return super(ExpansionSet, cls).__new__(cls)
149-
if ref_el.get_shape() == reference_element.POINT:
150-
return PointExpansionSet(ref_el)
151-
elif ref_el.get_shape() == reference_element.LINE:
152-
return LineExpansionSet(ref_el)
153-
elif ref_el.get_shape() == reference_element.TRIANGLE:
154-
return TriangleExpansionSet(ref_el)
155-
elif ref_el.get_shape() == reference_element.TETRAHEDRON:
156-
return TetrahedronExpansionSet(ref_el)
157-
else:
232+
try:
233+
ref_el = args[0]
234+
expansion_set = {
235+
reference_element.POINT: PointExpansionSet,
236+
reference_element.LINE: LineExpansionSet,
237+
reference_element.TRIANGLE: TriangleExpansionSet,
238+
reference_element.TETRAHEDRON: TetrahedronExpansionSet,
239+
}[ref_el.get_shape()]
240+
return expansion_set(*args, **kwargs)
241+
except KeyError:
158242
raise ValueError("Invalid reference element type.")
159243

160-
def __init__(self, ref_el):
244+
def __init__(self, ref_el, scale=None, variant=None):
161245
self.ref_el = ref_el
246+
self.variant = variant
162247
dim = ref_el.get_spatial_dimension()
163248
self.base_ref_el = reference_element.default_simplex(dim)
164249
v1 = ref_el.get_vertices()
165250
v2 = self.base_ref_el.get_vertices()
166251
self.A, self.b = reference_element.make_affine_mapping(v1, v2)
167252
self.mapping = lambda x: numpy.dot(self.A, x) + self.b
168-
self.scale = numpy.sqrt(numpy.linalg.det(self.A))
169253
self._dmats_cache = {}
254+
if scale is None:
255+
scale = math.sqrt(1.0 / self.base_ref_el.volume())
256+
elif isinstance(scale, str):
257+
scale = scale.lower()
258+
if scale == "orthonormal":
259+
scale = math.sqrt(1.0 / ref_el.volume())
260+
elif scale == "l2 piola":
261+
scale = 1.0 / ref_el.volume()
262+
self.scale = scale
170263

171264
def get_num_members(self, n):
172265
D = self.ref_el.get_spatial_dimension()
@@ -184,7 +277,7 @@ def _tabulate(self, n, pts, order=0):
184277
"""A version of tabulate() that also works for a single point.
185278
"""
186279
D = self.ref_el.get_spatial_dimension()
187-
return dubiner_recurrence(D, n, order, self._mapping(pts), self.A)
280+
return dubiner_recurrence(D, n, order, self._mapping(pts), self.A, self.scale, variant=self.variant)
188281

189282
def get_dmats(self, degree):
190283
"""Returns a numpy array with the expansion coefficients dmat[k, j, i]
@@ -266,10 +359,10 @@ def tabulate_jet(self, n, pts, order=1):
266359

267360
class PointExpansionSet(ExpansionSet):
268361
"""Evaluates the point basis on a point reference element."""
269-
def __init__(self, ref_el):
362+
def __init__(self, ref_el, **kwargs):
270363
if ref_el.get_spatial_dimension() != 0:
271364
raise ValueError("Must have a point")
272-
super(PointExpansionSet, self).__init__(ref_el)
365+
super(PointExpansionSet, self).__init__(ref_el, **kwargs)
273366

274367
def tabulate(self, n, pts):
275368
"""Returns a numpy array A[i,j] = phi_i(pts[j]) = 1.0."""
@@ -279,17 +372,20 @@ def tabulate(self, n, pts):
279372

280373
class LineExpansionSet(ExpansionSet):
281374
"""Evaluates the Legendre basis on a line reference element."""
282-
def __init__(self, ref_el):
375+
def __init__(self, ref_el, **kwargs):
283376
if ref_el.get_spatial_dimension() != 1:
284377
raise Exception("Must have a line")
285-
super(LineExpansionSet, self).__init__(ref_el)
378+
super(LineExpansionSet, self).__init__(ref_el, **kwargs)
286379

287380
def _tabulate(self, n, pts, order=0):
288381
"""Returns a tuple of (vals, derivs) such that
289382
vals[i,j] = phi_i(pts[j]), derivs[i,j] = D vals[i,j]."""
383+
if self.variant is not None:
384+
return super(LineExpansionSet, self)._tabulate(n, pts, order=order)
385+
290386
xs = self._mapping(pts).T
291387
results = []
292-
scale = numpy.sqrt(0.5 + numpy.arange(n+1))
388+
scale = self.scale * numpy.sqrt(2 * numpy.arange(n+1) + 1)
293389
for k in range(order+1):
294390
v = numpy.zeros((n + 1, len(xs)), xs.dtype)
295391
if n >= k:
@@ -306,18 +402,18 @@ def _tabulate(self, n, pts, order=0):
306402
class TriangleExpansionSet(ExpansionSet):
307403
"""Evaluates the orthonormal Dubiner basis on a triangular
308404
reference element."""
309-
def __init__(self, ref_el):
405+
def __init__(self, ref_el, **kwargs):
310406
if ref_el.get_spatial_dimension() != 2:
311407
raise Exception("Must have a triangle")
312-
super(TriangleExpansionSet, self).__init__(ref_el)
408+
super(TriangleExpansionSet, self).__init__(ref_el, **kwargs)
313409

314410

315411
class TetrahedronExpansionSet(ExpansionSet):
316412
"""Collapsed orthonormal polynomial expansion on a tetrahedron."""
317-
def __init__(self, ref_el):
413+
def __init__(self, ref_el, **kwargs):
318414
if ref_el.get_spatial_dimension() != 3:
319415
raise Exception("Must be a tetrahedron")
320-
super(TetrahedronExpansionSet, self).__init__(ref_el)
416+
super(TetrahedronExpansionSet, self).__init__(ref_el, **kwargs)
321417

322418

323419
def polynomial_dimension(ref_el, degree):

FIAT/fdm_element.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
import abc
1010
import numpy
1111

12-
from FIAT import finite_element, dual_set, functional, quadrature
13-
from FIAT.reference_element import LINE
14-
from FIAT.orientation_utils import make_entity_permutations_simplex
15-
from FIAT.hierarchical import IntegratedLegendre
12+
from FIAT import dual_set, finite_element, functional, quadrature
1613
from FIAT.barycentric_interpolation import LagrangePolynomialSet
14+
from FIAT.hierarchical import IntegratedLegendre
15+
from FIAT.orientation_utils import make_entity_permutations_simplex
1716
from FIAT.P0 import P0Dual
17+
from FIAT.reference_element import LINE
1818

1919

2020
def sym_eig(A, B):
@@ -166,7 +166,6 @@ def __init__(self, ref_el, degree):
166166
else:
167167
dual = FDMDual(ref_el, degree, bc_order=self._bc_order,
168168
formdegree=self._formdegree, orthogonalize=self._orthogonalize)
169-
170169
if self._formdegree == 0:
171170
poly_set = dual.embedded.poly_set
172171
else:

FIAT/gauss_legendre.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(self, ref_el, degree):
3232
entity_permutations[dim][entity] = perms
3333

3434
# make nodes by getting points
35+
dim = ref_el.get_spatial_dimension()
3536
pts = make_lattice(ref_el.get_vertices(), degree, variant="gl")
3637
nodes = [functional.PointEvaluation(ref_el, x) for x in pts]
3738
entity_ids[dim][0] = list(range(len(nodes)))

0 commit comments

Comments
 (0)