Skip to content

Commit c1d8a15

Browse files
authored
Boundary Quadrature elements (#3973)
* Extract quad degree from Quadrature elements * Projection: support for Boundary Quadrature element * Projector: refactoring
1 parent b295b20 commit c1d8a15

File tree

9 files changed

+97
-48
lines changed

9 files changed

+97
-48
lines changed

firedrake/output/vtk_output.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def is_dg(V):
8181
8282
:arg V: A FunctionSpace.
8383
"""
84-
return V.finat_element.entity_dofs() == V.finat_element.entity_closure_dofs()
84+
return V.finat_element.is_dg()
8585

8686

8787
def is_linear(V):

firedrake/preconditioners/fdm.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -720,10 +720,7 @@ def _element_kernels(self):
720720

721721
@cached_property
722722
def insert_mode(self):
723-
is_dg = {}
724-
for Vsub in self.V:
725-
element = Vsub.finat_element
726-
is_dg[Vsub] = element.entity_dofs() == element.entity_closure_dofs()
723+
is_dg = {Vsub: Vsub.finat_element.is_dg() for Vsub in self.V}
727724

728725
insert_mode = {}
729726
for Vrow, Vcol in product(self.V, self.V):
@@ -1935,9 +1932,7 @@ def assemble_reference_tensor(self, V):
19351932

19361933
degree = max(e.degree() for e in line_elements)
19371934
eta = float(self.appctx.get("eta", degree*(degree+1)))
1938-
element = V.finat_element
1939-
is_dg = element.entity_dofs() == element.entity_closure_dofs()
1940-
1935+
is_dg = V.finat_element.is_dg()
19411936
Afdm = [] # sparse interval mass and stiffness matrices for each direction
19421937
Dfdm = [] # tabulation of normal derivatives at the boundary for each direction
19431938
bdof = [] # indices of point evaluation dofs for each direction

firedrake/projection.py

+17-18
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from firedrake import functionspaceimpl
1313
from firedrake import function
1414
from firedrake.adjoint_utils import annotate_project
15-
from finat import HDivTrace
1615

1716

1817
__all__ = ['project', 'Projector']
@@ -164,24 +163,25 @@ def __init__(
164163
self.form_compiler_parameters = form_compiler_parameters
165164
self.bcs = bcs
166165
self.constant_jacobian = constant_jacobian
167-
try:
168-
element = self.target.function_space().finat_element
169-
is_dg = element.entity_dofs() == element.entity_closure_dofs()
170-
is_variable_layers = self.target.function_space().mesh().variable_layers
171-
except AttributeError:
172-
# Mixed space
173-
is_dg = False
174-
is_variable_layers = True
175-
self.use_slate_for_inverse = (use_slate_for_inverse and is_dg and not is_variable_layers
176-
and (not complex_mode or SLATE_SUPPORTS_COMPLEX))
166+
167+
F = self.target.function_space()
168+
if type(F.ufl_element()) is finat.ufl.MixedElement:
169+
slate_supported = False
170+
needs_trace = False
171+
else:
172+
slate_supported = (F.finat_element.is_dg() and not F.mesh().variable_layers
173+
and (not complex_mode or SLATE_SUPPORTS_COMPLEX))
174+
needs_trace = F.ufl_element().family() in {"HDiv Trace", "Boundary Quadrature"}
175+
176+
self.use_slate_for_inverse = use_slate_for_inverse and slate_supported
177+
self.needs_trace = needs_trace
177178

178179
@cached_property
179180
def A(self):
180-
u = firedrake.TrialFunction(self.target.function_space())
181-
v = firedrake.TestFunction(self.target.function_space())
182181
F = self.target.function_space()
183-
mixed = isinstance(F.ufl_element(), finat.ufl.MixedElement)
184-
if not mixed and isinstance(F.finat_element, HDivTrace):
182+
u = firedrake.TrialFunction(F)
183+
v = firedrake.TestFunction(F)
184+
if self.needs_trace:
185185
if F.extruded:
186186
a = (
187187
firedrake.inner(u, v)*firedrake.ds_t
@@ -247,12 +247,11 @@ class BasicProjector(ProjectorBase):
247247
def rhs_form(self):
248248
v = firedrake.TestFunction(self.target.function_space())
249249
F = self.target.function_space()
250-
mixed = isinstance(F.ufl_element(), finat.ufl.MixedElement)
251-
if not mixed and isinstance(F.finat_element, HDivTrace):
250+
if self.needs_trace:
252251
# Project onto a trace space by supplying the respective form on the facets.
253252
# The measures on the facets differ between extruded and non-extruded mesh.
254253
# FIXME The restrictions of cg onto the facets is also a trace space,
255-
# but we only cover DGT.
254+
# but we only cover DGT and BQ.
256255
if F.extruded:
257256
form = (
258257
firedrake.inner(self.source, v)*firedrake.ds_t

tests/firedrake/regression/test_projection.py

+29
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,32 @@ def run_extr_trace_projection(x, degree=1, family='DGT'):
391391
+ area * (w - ref) * (w - ref) * ds_b
392392
+ area * (w('+') - ref('+')) * (w('+') - ref('+')) * dS_h
393393
+ area * (w('+') - ref('+')) * (w('+') - ref('+')) * dS_v))
394+
395+
396+
def test_project_scalar_boundary_quadrature():
397+
msh = UnitSquareMesh(4, 4)
398+
x = SpatialCoordinate(msh)
399+
f = x[0]*(2-x[0])*x[1]*(2-x[1])
400+
401+
T = FunctionSpace(msh, "BQ", 8)
402+
w = Function(T)
403+
w.project(f, solver_parameters={'mat_type': 'matfree', 'ksp_type': 'preonly', 'pc_type': 'jacobi'})
404+
405+
err = w - f
406+
err2 = inner(err, err)
407+
error = sqrt(assemble(err2 * ds + err2('+') * dS))
408+
assert error < 1E-8
409+
410+
411+
def test_project_vector_boundary_quadrature():
412+
msh = UnitCubeMesh(4, 4, 4)
413+
n = FacetNormal(msh)
414+
415+
T = VectorFunctionSpace(msh, "BQ", 0)
416+
w = Function(T)
417+
w.project(n, solver_parameters={'mat_type': 'matfree', 'ksp_type': 'preonly', 'pc_type': 'jacobi'})
418+
419+
err = w - n
420+
err2 = inner(err, err)
421+
error = sqrt(assemble(err2 * ds))
422+
assert error < 1E-8

tests/firedrake/regression/test_projection_symmetric_tensor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_projection_symmetric_tensor(mesh, degree, family, tdim):
3232
sp = {"mat_type": "matfree",
3333
"ksp_type": "preonly",
3434
"pc_type": "jacobi"}
35-
fcp = {"quadrature_degree": Nq}
35+
fcp = {}
3636
else:
3737
Q = TensorFunctionSpace(mesh, family, degree=degree, shape=shape, symmetry=None)
3838
Qs = TensorFunctionSpace(mesh, family, degree=degree, shape=shape, symmetry=True)
+22-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from firedrake import *
2+
import pytest
23

34

4-
def test_hand_specified_quadrature():
5-
mesh = UnitSquareMesh(5, 5)
5+
@pytest.fixture
6+
def mesh(request):
7+
return UnitSquareMesh(5, 5)
8+
9+
10+
def test_hand_specified_quadrature(mesh):
611
V = FunctionSpace(mesh, 'CG', 2)
712
v = TestFunction(V)
813

@@ -12,3 +17,18 @@ def test_hand_specified_quadrature():
1217
a_q2 = assemble(a, form_compiler_parameters={'quadrature_degree': 2})
1318

1419
assert not np.allclose(a_q0.dat.data, a_q2.dat.data)
20+
21+
22+
@pytest.mark.parametrize("diagonal", [False, True])
23+
@pytest.mark.parametrize("mat_type", ["matfree", "aij"])
24+
@pytest.mark.parametrize("family", ["Quadrature", "Boundary Quadrature"])
25+
def test_quadrature_element(mesh, family, mat_type, diagonal):
26+
V = FunctionSpace(mesh, family, 2)
27+
v = TestFunction(V)
28+
u = TrialFunction(V)
29+
if family == "Boundary Quadrature":
30+
a = inner(u, v) * ds + inner(u('+'), v('+')) * dS
31+
else:
32+
a = inner(u, v) * dx
33+
34+
assemble(a, mat_type=mat_type, diagonal=diagonal)

tsfc/driver.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ def compile_integral(integral_data, form_data, prefix, parameters, interface, *,
9595
interface = firedrake_interface_loopy.KernelBuilder
9696
scalar_type = parameters["scalar_type"]
9797
integral_type = integral_data.integral_type
98-
if integral_type.startswith("interior_facet") and diagonal:
99-
raise NotImplementedError("Sorry, we can't assemble the diagonal of a form for interior facet integrals")
10098
mesh = integral_data.domain
10199
arguments = form_data.preprocessed_form.arguments()
100+
if integral_type.startswith("interior_facet") and diagonal and any(a.function_space().finat_element.is_dg() for a in arguments):
101+
raise NotImplementedError("Sorry, we can't assemble the diagonal of a form for interior facet integrals")
102102
kernel_name = f"{prefix}_{integral_type}_integral"
103103
# Dict mapping domains to index in original_form.ufl_domains()
104104
domain_numbering = form_data.original_form.domain_numbering()

tsfc/fem.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -641,15 +641,13 @@ def fiat_to_ufl(fiat_dict, order):
641641

642642
@translate.register(Argument)
643643
def translate_argument(terminal, mt, ctx):
644-
argument_multiindex = ctx.argument_multiindices[terminal.number()]
645-
sigma = tuple(gem.Index(extent=d) for d in mt.expr.ufl_shape)
646644
element = ctx.create_element(terminal.ufl_element(), restriction=mt.restriction)
647645

648646
def callback(entity_id):
649647
finat_dict = ctx.basis_evaluation(element, mt, entity_id)
650648
# Filter out irrelevant derivatives
651-
filtered_dict = {alpha: table
652-
for alpha, table in finat_dict.items()
649+
filtered_dict = {alpha: finat_dict[alpha]
650+
for alpha in finat_dict
653651
if sum(alpha) == mt.local_derivatives}
654652

655653
# Change from FIAT to UFL arrangement
@@ -664,7 +662,8 @@ def callback(entity_id):
664662
quad_multiindex_permuted = _make_quad_multiindex_permuted(mt, ctx)
665663
mapper = gem.node.MemoizerArg(gem.optimise.filtered_replace_indices)
666664
table = mapper(table, tuple(zip(quad_multiindex, quad_multiindex_permuted)))
667-
return gem.ComponentTensor(gem.Indexed(table, argument_multiindex + sigma), sigma)
665+
argument_multiindex = ctx.argument_multiindices[terminal.number()]
666+
return gem.partial_indexed(table, argument_multiindex)
668667

669668

670669
@translate.register(TSFCConstantMixin)

tsfc/kernel_interface/common.py

+19-12
Original file line numberDiff line numberDiff line change
@@ -295,21 +295,28 @@ def create_context(self):
295295

296296

297297
def set_quad_rule(params, cell, integral_type, functions):
298-
# Check if the integral has a quad degree attached, otherwise use
299-
# the estimated polynomial degree attached by compute_form_data
298+
# Check if the integral has a quad degree or quad element attached,
299+
# otherwise use the estimated polynomial degree attached by compute_form_data
300+
quad_rule = params.get("quadrature_rule", "default")
300301
try:
301302
quadrature_degree = params["quadrature_degree"]
302303
except KeyError:
303-
quadrature_degree = params["estimated_polynomial_degree"]
304-
function_degrees = [f.ufl_function_space().ufl_element().degree()
305-
for f in functions]
306-
if all((asarray(quadrature_degree) > 10 * asarray(degree)).all()
307-
for degree in function_degrees):
308-
logger.warning("Estimated quadrature degree %s more "
309-
"than tenfold greater than any "
310-
"argument/coefficient degree (max %s)",
311-
quadrature_degree, max_degree(function_degrees))
312-
quad_rule = params.get("quadrature_rule", "default")
304+
elements = [f.ufl_function_space().ufl_element() for f in functions]
305+
quad_data = set((e.degree(), e.quadrature_scheme() or "default") for e in elements
306+
if e.family() in {"Quadrature", "Boundary Quadrature"})
307+
if len(quad_data) == 0:
308+
quadrature_degree = params["estimated_polynomial_degree"]
309+
if all((asarray(quadrature_degree) > 10 * asarray(e.degree())).all() for e in elements):
310+
logger.warning("Estimated quadrature degree %s more "
311+
"than tenfold greater than any "
312+
"argument/coefficient degree (max %s)",
313+
quadrature_degree, max_degree([e.degree() for e in elements]))
314+
else:
315+
try:
316+
(quadrature_degree, quad_rule), = quad_data
317+
except ValueError:
318+
raise ValueError("The quadrature rule cannot be inferred from multiple Quadrature elements")
319+
313320
if isinstance(quad_rule, str):
314321
scheme = quad_rule
315322
fiat_cell = as_fiat_cell(cell)

0 commit comments

Comments
 (0)