6060#
6161# We begin by defining a basis of the polynomial space spanned by the
6262# 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
6466# contains $1$, $y$, $y^2$, $x$, $xy$, $xy^2$, $x^2$, and $x^2y$, which
6567# are the first 8 polynomials in the degree 2 set of polynomials on a
6668# quadrilateral. We create an $8 \times 9$ matrix (number of dofs by
146148# We now create the element. Using the Basix UFL interface, we can wrap
147149# this element so that it can be used with FFCx/DOLFINx.
148150
149- tnt_degree1 = basix .ufl .custom_element (
151+ tnt_degree2 = basix .ufl .custom_element (
150152 basix .CellType .quadrilateral ,
151153 [],
152154 wcoeffs ,
168170
169171
170172def create_tnt_quad (degree ):
171- assert degree > 1
173+ assert degree > 0
172174 # 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
175177
176178 wcoeffs = np .zeros ((ndofs , npoly ))
177179
178180 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
182184 dof_n += 1
183185
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
186188 dof_n += 1
187189
190+ if degree > 1 :
191+ wcoeffs [dof_n , 2 * degree + 1 ] = 1
192+
188193 # Interpolation
189194 geometry = basix .geometry (basix .CellType .quadrilateral )
190195 topology = basix .topology (basix .CellType .quadrilateral )
@@ -197,31 +202,44 @@ def create_tnt_quad(degree):
197202 M [0 ].append (np .array ([[[[1.0 ]]]]))
198203
199204 # 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 )
215225
216226 # Interior
217- if degree == 1 :
227+ if degree < 3 :
218228 x [2 ].append (np .zeros ([0 , 2 ]))
219229 M [2 ].append (np .zeros ([0 , 1 , 0 , 1 ]))
220230 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
224236 )
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 ))
225243 face_ndofs = poly .shape [0 ]
226244 x [2 ].append (pts )
227245 mat = np .zeros ((face_ndofs , 1 , len (pts ), 1 ))
@@ -239,8 +257,8 @@ def create_tnt_quad(degree):
239257 basix .MapType .identity ,
240258 basix .SobolevSpace .H1 ,
241259 False ,
260+ max (degree - 1 , 1 ),
242261 degree ,
243- degree + 1 ,
244262 dtype = default_real_type ,
245263 )
246264
@@ -296,12 +314,12 @@ def poisson_error(V: fem.FunctionSpace):
296314tnt_degrees = []
297315tnt_errors = []
298316
299- V = fem .functionspace (msh , tnt_degree1 )
317+ V = fem .functionspace (msh , tnt_degree2 )
300318tnt_degrees .append (2 )
301319tnt_ndofs .append (V .dofmap .index_map .size_global )
302320tnt_errors .append (poisson_error (V ))
303321print (f"TNT degree 2 error: { tnt_errors [- 1 ]} " )
304- for degree in range (2 , 9 ):
322+ for degree in range (1 , 9 ):
305323 V = fem .functionspace (msh , create_tnt_quad (degree ))
306324 tnt_degrees .append (degree + 1 )
307325 tnt_ndofs .append (V .dofmap .index_map .size_global )
@@ -317,6 +335,17 @@ def poisson_error(V: fem.FunctionSpace):
317335 q_ndofs .append (V .dofmap .index_map .size_global )
318336 q_errors .append (poisson_error (V ))
319337 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+
320349# -
321350
322351# We now plot the data that we have obtained. First we plot the error
@@ -326,27 +355,30 @@ def poisson_error(V: fem.FunctionSpace):
326355if MPI .COMM_WORLD .rank == 0 : # Only plot on one rank
327356 plt .plot (q_degrees , q_errors , "bo-" )
328357 plt .plot (tnt_degrees , tnt_errors , "gs-" )
358+ plt .plot (s_degrees , s_errors , "rD-" )
329359 plt .yscale ("log" )
330360 plt .xlabel ("Polynomial degree" )
331361 plt .ylabel ("Error" )
332- plt .legend (["Q" , "TNT" ])
362+ plt .legend (["Q" , "TNT" , "S" ])
333363 plt .savefig ("demo_tnt-elements_degrees_vs_error.png" )
334364 plt .clf ()
335365
336366# 
337367#
338368# 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
340370# the following diagram, where we plot the error against the square root
341371# of the number of DOFs (providing a measure of cell size in 2D)
372+ # S serendipity element perform even better here.
342373
343374if MPI .COMM_WORLD .rank == 0 : # Only plot on one rank
344375 plt .plot (np .sqrt (q_ndofs ), q_errors , "bo-" )
345376 plt .plot (np .sqrt (tnt_ndofs ), tnt_errors , "gs-" )
377+ plt .plot (np .sqrt (s_ndofs ), s_errors , "rD-" )
346378 plt .yscale ("log" )
347379 plt .xlabel ("Square root of number of DOFs" )
348380 plt .ylabel ("Error" )
349- plt .legend (["Q" , "TNT" ])
381+ plt .legend (["Q" , "TNT" , "S" ])
350382 plt .savefig ("demo_tnt-elements_ndofs_vs_error.png" )
351383 plt .clf ()
352384
0 commit comments