60
60
#
61
61
# We begin by defining a basis of the polynomial space spanned by the
62
62
# TNT element, which is defined in terms of the orthogonal Legendre
63
- # polynomials on the cell. For a degree 1 element, the polynomial set
63
+ # polynomials on the cell. For a degree 2 element (here, we use the
64
+ # superdegree rather than the conventional subdegree to allign with
65
+ # the definition of other elements), the polynomial set
64
66
# contains $1$, $y$, $y^2$, $x$, $xy$, $xy^2$, $x^2$, and $x^2y$, which
65
67
# are the first 8 polynomials in the degree 2 set of polynomials on a
66
68
# quadrilateral. We create an $8 \times 9$ matrix (number of dofs by
146
148
# We now create the element. Using the Basix UFL interface, we can wrap
147
149
# this element so that it can be used with FFCx/DOLFINx.
148
150
149
- tnt_degree1 = basix .ufl .custom_element (
151
+ tnt_degree2 = basix .ufl .custom_element (
150
152
basix .CellType .quadrilateral ,
151
153
[],
152
154
wcoeffs ,
168
170
169
171
170
172
def create_tnt_quad (degree ):
171
- assert degree > 1
173
+ assert degree > 0
172
174
# Polyset
173
- ndofs = ( degree + 1 ) ** 2 + 4
174
- npoly = (degree + 2 ) ** 2
175
+ ndofs = 4 * degree + max ( degree - 2 , 0 ) ** 2
176
+ npoly = (degree + 1 ) ** 2
175
177
176
178
wcoeffs = np .zeros ((ndofs , npoly ))
177
179
178
180
dof_n = 0
179
- for i in range (degree + 1 ):
180
- for j in range (degree + 1 ):
181
- wcoeffs [dof_n , i * (degree + 2 ) + j ] = 1
181
+ for i in range (degree ):
182
+ for j in range (degree ):
183
+ wcoeffs [dof_n , i * (degree + 1 ) + j ] = 1
182
184
dof_n += 1
183
185
184
- for i , j in [(degree + 1 , 1 ), (degree + 1 , 0 ), (1 , degree + 1 ), ( 0 , degree + 1 )]:
185
- wcoeffs [dof_n , i * (degree + 2 ) + j ] = 1
186
+ for i , j in [(degree , 1 ), (degree , 0 ), (0 , degree )]:
187
+ wcoeffs [dof_n , i * (degree + 1 ) + j ] = 1
186
188
dof_n += 1
187
189
190
+ if degree > 1 :
191
+ wcoeffs [dof_n , 2 * degree + 1 ] = 1
192
+
188
193
# Interpolation
189
194
geometry = basix .geometry (basix .CellType .quadrilateral )
190
195
topology = basix .topology (basix .CellType .quadrilateral )
@@ -197,31 +202,44 @@ def create_tnt_quad(degree):
197
202
M [0 ].append (np .array ([[[[1.0 ]]]]))
198
203
199
204
# Edges
200
- pts , wts = basix .make_quadrature (basix .CellType .interval , 2 * degree )
201
- poly = basix .tabulate_polynomials (
202
- basix .PolynomialType .legendre , basix .CellType .interval , degree - 1 , pts
203
- )
204
- edge_ndofs = poly .shape [0 ]
205
- for e in topology [1 ]:
206
- v0 = geometry [e [0 ]]
207
- v1 = geometry [e [1 ]]
208
- edge_pts = np .array ([v0 + p * (v1 - v0 ) for p in pts ])
209
- x [1 ].append (edge_pts )
210
-
211
- mat = np .zeros ((edge_ndofs , 1 , len (pts ), 1 ))
212
- for i in range (edge_ndofs ):
213
- mat [i , 0 , :, 0 ] = wts [:] * poly [i , :]
214
- M [1 ].append (mat )
205
+ if degree < 2 :
206
+ for _ in topology [1 ]:
207
+ x [1 ].append (np .zeros ([0 , 2 ]))
208
+ M [1 ].append (np .zeros ([0 , 1 , 0 , 1 ]))
209
+ else :
210
+ pts , wts = basix .make_quadrature (basix .CellType .interval , 2 * degree - 2 )
211
+ poly = basix .tabulate_polynomials (
212
+ basix .PolynomialType .legendre , basix .CellType .interval , degree - 2 , pts
213
+ )
214
+ edge_ndofs = poly .shape [0 ]
215
+ for e in topology [1 ]:
216
+ v0 = geometry [e [0 ]]
217
+ v1 = geometry [e [1 ]]
218
+ edge_pts = np .array ([v0 + p * (v1 - v0 ) for p in pts ])
219
+ x [1 ].append (edge_pts )
220
+
221
+ mat = np .zeros ((edge_ndofs , 1 , len (pts ), 1 ))
222
+ for i in range (edge_ndofs ):
223
+ mat [i , 0 , :, 0 ] = wts [:] * poly [i , :]
224
+ M [1 ].append (mat )
215
225
216
226
# Interior
217
- if degree == 1 :
227
+ if degree < 3 :
218
228
x [2 ].append (np .zeros ([0 , 2 ]))
219
229
M [2 ].append (np .zeros ([0 , 1 , 0 , 1 ]))
220
230
else :
221
- pts , wts = basix .make_quadrature (basix .CellType .quadrilateral , 2 * degree - 1 )
222
- poly = basix .tabulate_polynomials (
223
- basix .PolynomialType .legendre , basix .CellType .quadrilateral , degree - 2 , pts
231
+ pts , wts = basix .make_quadrature (basix .CellType .quadrilateral , 2 * degree - 2 )
232
+ u = pts [:, 0 ]
233
+ v = pts [:, 1 ]
234
+ pol_set = basix .polynomials .tabulate_polynomial_set (
235
+ basix .CellType .quadrilateral , basix .PolynomialType .legendre , degree - 3 , 2 , pts
224
236
)
237
+ # this assumes the conventional [0 to 1][0 to 1] domain of the reference element,
238
+ # and takes the Laplacian of (1-u)*u*(1-v)*v*pol_set[0],
239
+ # cf https://github.com/mscroggs/symfem/blob/main/symfem/elements/tnt.py
240
+ poly = (pol_set [5 ]+ pol_set [3 ])* (u - 1 )* u * (v - 1 )* v + \
241
+ 2 * (pol_set [2 ]* (u - 1 )* u * (2 * v - 1 )+ pol_set [1 ]* (v - 1 )* v * (2 * u - 1 )+ \
242
+ pol_set [0 ]* ((u - 1 )* u + (v - 1 )* v ))
225
243
face_ndofs = poly .shape [0 ]
226
244
x [2 ].append (pts )
227
245
mat = np .zeros ((face_ndofs , 1 , len (pts ), 1 ))
@@ -239,8 +257,8 @@ def create_tnt_quad(degree):
239
257
basix .MapType .identity ,
240
258
basix .SobolevSpace .H1 ,
241
259
False ,
260
+ max (degree - 1 , 1 ),
242
261
degree ,
243
- degree + 1 ,
244
262
dtype = default_real_type ,
245
263
)
246
264
@@ -296,12 +314,12 @@ def poisson_error(V: fem.FunctionSpace):
296
314
tnt_degrees = []
297
315
tnt_errors = []
298
316
299
- V = fem .functionspace (msh , tnt_degree1 )
317
+ V = fem .functionspace (msh , tnt_degree2 )
300
318
tnt_degrees .append (2 )
301
319
tnt_ndofs .append (V .dofmap .index_map .size_global )
302
320
tnt_errors .append (poisson_error (V ))
303
321
print (f"TNT degree 2 error: { tnt_errors [- 1 ]} " )
304
- for degree in range (2 , 9 ):
322
+ for degree in range (1 , 9 ):
305
323
V = fem .functionspace (msh , create_tnt_quad (degree ))
306
324
tnt_degrees .append (degree + 1 )
307
325
tnt_ndofs .append (V .dofmap .index_map .size_global )
@@ -317,6 +335,17 @@ def poisson_error(V: fem.FunctionSpace):
317
335
q_ndofs .append (V .dofmap .index_map .size_global )
318
336
q_errors .append (poisson_error (V ))
319
337
print (f"Q degree { degree } error: { q_errors [- 1 ]} " )
338
+
339
+ s_ndofs = []
340
+ s_degrees = []
341
+ s_errors = []
342
+ for degree in range (1 , 9 ):
343
+ V = fem .functionspace (msh , ("S" , degree ))
344
+ s_degrees .append (degree )
345
+ s_ndofs .append (V .dofmap .index_map .size_global )
346
+ s_errors .append (poisson_error (V ))
347
+ print (f"S degree { degree } error: { s_errors [- 1 ]} " )
348
+
320
349
# -
321
350
322
351
# We now plot the data that we have obtained. First we plot the error
@@ -326,27 +355,30 @@ def poisson_error(V: fem.FunctionSpace):
326
355
if MPI .COMM_WORLD .rank == 0 : # Only plot on one rank
327
356
plt .plot (q_degrees , q_errors , "bo-" )
328
357
plt .plot (tnt_degrees , tnt_errors , "gs-" )
358
+ plt .plot (s_degrees , s_errors , "rD-" )
329
359
plt .yscale ("log" )
330
360
plt .xlabel ("Polynomial degree" )
331
361
plt .ylabel ("Error" )
332
- plt .legend (["Q" , "TNT" ])
362
+ plt .legend (["Q" , "TNT" , "S" ])
333
363
plt .savefig ("demo_tnt-elements_degrees_vs_error.png" )
334
364
plt .clf ()
335
365
336
366
# 
337
367
#
338
368
# A key advantage of TNT elements is that for a given degree, they span
339
- # a smaller polynomial space than Q elements. This can be observed in
369
+ # a smaller polynomial space than Q tensorproduct elements. This can be observed in
340
370
# the following diagram, where we plot the error against the square root
341
371
# of the number of DOFs (providing a measure of cell size in 2D)
372
+ # S serendipity element perform even better here.
342
373
343
374
if MPI .COMM_WORLD .rank == 0 : # Only plot on one rank
344
375
plt .plot (np .sqrt (q_ndofs ), q_errors , "bo-" )
345
376
plt .plot (np .sqrt (tnt_ndofs ), tnt_errors , "gs-" )
377
+ plt .plot (np .sqrt (s_ndofs ), s_errors , "rD-" )
346
378
plt .yscale ("log" )
347
379
plt .xlabel ("Square root of number of DOFs" )
348
380
plt .ylabel ("Error" )
349
- plt .legend (["Q" , "TNT" ])
381
+ plt .legend (["Q" , "TNT" , "S" ])
350
382
plt .savefig ("demo_tnt-elements_ndofs_vs_error.png" )
351
383
plt .clf ()
352
384
0 commit comments