Skip to content

Commit 2140635

Browse files
Support MatIS and BDDC (#4405)
* Support mat_type="is" * Support mat_type="nest", sub_mat_type="is" * Propagate sub_mat_type and only set sub_mat_type on the diagonal blocks * refine BDDC customization, use callables for gradient and divergence * Simplify coordinates handling of bddcpc driver * tabulate_exterior_derivative as MatIS * FDMPC: Support other variants * bddc: attach constants of the H(grad) space to discrete gradient * add mat_type kawrg to local_to_global_map * Test BDDC for H(curl) and H(div) --------- Co-authored-by: Stefano Zampini <stefano.zampini@gmail.com>
1 parent a9cb1eb commit 2140635

File tree

10 files changed

+652
-240
lines changed

10 files changed

+652
-240
lines changed

firedrake/assemble.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,9 @@ def allocate(self):
364364
else:
365365
test, trial = self._form.arguments()
366366
sparsity = ExplicitMatrixAssembler._make_sparsity(test, trial, self._mat_type, self._sub_mat_type, self.maps_and_regions)
367-
return matrix.Matrix(self._form, self._bcs, self._mat_type, sparsity, ScalarType, options_prefix=self._options_prefix)
367+
return matrix.Matrix(self._form, self._bcs, self._mat_type, sparsity, ScalarType,
368+
sub_mat_type=self._sub_mat_type,
369+
options_prefix=self._options_prefix)
368370
else:
369371
raise NotImplementedError("Only implemented for rank = 2 and diagonal = False")
370372

@@ -1324,12 +1326,12 @@ def _get_mat_type(mat_type, sub_mat_type, arguments):
13241326
for arg in arguments
13251327
for V in arg.function_space()):
13261328
mat_type = "nest"
1327-
if mat_type not in {"matfree", "aij", "baij", "nest", "dense"}:
1329+
if mat_type not in {"matfree", "aij", "baij", "nest", "dense", "is"}:
13281330
raise ValueError(f"Unrecognised matrix type, '{mat_type}'")
13291331
if sub_mat_type is None:
13301332
sub_mat_type = parameters.parameters["default_sub_matrix_type"]
1331-
if sub_mat_type not in {"aij", "baij"}:
1332-
raise ValueError(f"Invalid submatrix type, '{sub_mat_type}' (not 'aij' or 'baij')")
1333+
if sub_mat_type not in {"aij", "baij", "is"}:
1334+
raise ValueError(f"Invalid submatrix type, '{sub_mat_type}' (not 'aij', 'baij', or 'is')")
13331335
return mat_type, sub_mat_type
13341336

13351337

@@ -1373,6 +1375,7 @@ def allocate(self):
13731375
self._sub_mat_type,
13741376
self._make_maps_and_regions())
13751377
return matrix.Matrix(self._form, self._bcs, self._mat_type, sparsity, ScalarType,
1378+
sub_mat_type=self._sub_mat_type,
13761379
options_prefix=self._options_prefix,
13771380
fc_params=self._form_compiler_params)
13781381

@@ -1490,8 +1493,10 @@ def _apply_bc(self, tensor, bc, u=None):
14901493
# Set diagonal entries on bc nodes to 1 if the current
14911494
# block is on the matrix diagonal and its index matches the
14921495
# index of the function space the bc is defined on.
1496+
if op2tensor.handle.getType() == "is":
1497+
# Flag the entire matrix as assembled before indexing the diagonal block
1498+
op2tensor.handle.assemble()
14931499
op2tensor[index, index].set_local_diagonal_entries(bc.nodes, idx=component, diag_val=self.weight)
1494-
14951500
# Handle off-diagonal block involving real function space.
14961501
# "lgmaps" is correctly constructed in _matrix_arg, but
14971502
# is ignored by PyOP2 in this case.
@@ -2067,16 +2072,18 @@ def collect_lgmaps(self):
20672072
row_bcs, col_bcs = self._filter_bcs(i, j)
20682073
# the tensor is already indexed
20692074
rlgmap, clgmap = self._tensor.local_to_global_maps
2070-
rlgmap = self.test_function_space[i].local_to_global_map(row_bcs, rlgmap)
2071-
clgmap = self.trial_function_space[j].local_to_global_map(col_bcs, clgmap)
2075+
mat_type = self._tensor.handle.getType()
2076+
rlgmap = self.test_function_space[i].local_to_global_map(row_bcs, rlgmap, mat_type=mat_type)
2077+
clgmap = self.trial_function_space[j].local_to_global_map(col_bcs, clgmap, mat_type=mat_type)
20722078
return ((rlgmap, clgmap),)
20732079
else:
20742080
lgmaps = []
20752081
for i, j in self.get_indicess():
20762082
row_bcs, col_bcs = self._filter_bcs(i, j)
20772083
rlgmap, clgmap = self._tensor[i, j].local_to_global_maps
2078-
rlgmap = self.test_function_space[i].local_to_global_map(row_bcs, rlgmap)
2079-
clgmap = self.trial_function_space[j].local_to_global_map(col_bcs, clgmap)
2084+
mat_type = self._tensor[i, j].handle.getType()
2085+
rlgmap = self.test_function_space[i].local_to_global_map(row_bcs, rlgmap, mat_type=mat_type)
2086+
clgmap = self.trial_function_space[j].local_to_global_map(col_bcs, clgmap, mat_type=mat_type)
20802087
lgmaps.append((rlgmap, clgmap))
20812088
return tuple(lgmaps)
20822089
else:

firedrake/functionspaceimpl.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -856,10 +856,31 @@ def boundary_nodes(self, sub_domain):
856856
return self._shared_data.boundary_nodes(self, sub_domain)
857857

858858
@PETSc.Log.EventDecorator()
859-
def local_to_global_map(self, bcs, lgmap=None):
859+
def local_to_global_map(self, bcs, lgmap=None, mat_type=None):
860860
r"""Return a map from process local dof numbering to global dof numbering.
861861
862-
If BCs is provided, mask out those dofs which match the BC nodes."""
862+
Parameters
863+
----------
864+
bcs: [firedrake.bcs.BCBase]
865+
If provided, mask out those dofs which match the BC nodes.
866+
lgmap: PETSc.LGMap
867+
The base local-to-global map, which might be partially masked.
868+
mat_type: str
869+
The matrix assembly type. This is required as different matrix types
870+
handle the LGMap differently for MixedFunctionSpace.
871+
872+
Note
873+
----
874+
For a :func:`.VectorFunctionSpace` or :func:`.TensorFunctionSpace` the returned
875+
LGMap will be the scalar one, unless the bcs are imposed on a particular component.
876+
For a :class:`MixedFunctionSpace` the returned LGMap is unblocked,
877+
unless mat_type == "is".
878+
879+
Returns
880+
-------
881+
PETSc.LGMap
882+
A local-to-global map with masked BC dofs.
883+
"""
863884
# Caching these things is too complicated, since it depends
864885
# not just on the bcs, but also the parent space, and anything
865886
# this space has been recursively split out from [e.g. inside
@@ -884,10 +905,16 @@ def local_to_global_map(self, bcs, lgmap=None):
884905
bsize = lgmap.getBlockSize()
885906
assert bsize == self.block_size
886907
else:
887-
# MatBlock case, LGMap is already unrolled.
888-
indices = lgmap.block_indices.copy()
908+
# MatBlock case, the LGMap is implementation dependent
889909
bsize = lgmap.getBlockSize()
890-
unblocked = True
910+
assert bsize == self.block_size
911+
if mat_type == "is":
912+
indices = lgmap.indices.copy()
913+
unblocked = False
914+
else:
915+
# LGMap is already unrolled
916+
indices = lgmap.block_indices.copy()
917+
unblocked = True
891918
nodes = []
892919
for bc in bcs:
893920
if bc.function_space().component is not None:
@@ -997,7 +1024,7 @@ def __hash__(self):
9971024
return hash((self.mesh(), self.dof_dset, self.ufl_element(),
9981025
self.boundary_set))
9991026

1000-
def local_to_global_map(self, bcs, lgmap=None):
1027+
def local_to_global_map(self, bcs, lgmap=None, mat_type=None):
10011028
return lgmap or self.dof_dset.lgmap
10021029

10031030
def collapse(self):
@@ -1204,7 +1231,7 @@ def exterior_facet_node_map(self):
12041231
function space nodes."""
12051232
return op2.MixedMap(s.exterior_facet_node_map() for s in self)
12061233

1207-
def local_to_global_map(self, bcs):
1234+
def local_to_global_map(self, bcs, lgmap=None, mat_type=None):
12081235
r"""Return a map from process local dof numbering to global dof numbering.
12091236
12101237
If BCs is provided, mask out those dofs which match the BC nodes."""
@@ -1452,7 +1479,7 @@ def top_nodes(self):
14521479
":class:`RealFunctionSpace` objects have no bottom nodes."
14531480
return None
14541481

1455-
def local_to_global_map(self, bcs, lgmap=None):
1482+
def local_to_global_map(self, bcs, lgmap=None, mat_type=None):
14561483
assert len(bcs) == 0
14571484
return None
14581485

firedrake/preconditioners/bddc.py

Lines changed: 112 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
from firedrake.petsc import PETSc
55
from firedrake.dmhooks import get_function_space, get_appctx
66
from firedrake.ufl_expr import TestFunction, TrialFunction
7-
from ufl import curl, div, HCurl, HDiv, inner, dx
7+
from firedrake.function import Function
8+
from firedrake.functionspace import FunctionSpace, VectorFunctionSpace, TensorFunctionSpace
9+
from firedrake.preconditioners.fdm import tabulate_exterior_derivative
10+
from firedrake.preconditioners.hiptmair import curl_to_grad
11+
from ufl import H1, H2, inner, dx, JacobianDeterminant
812
from pyop2.utils import as_tuple
13+
import gem
914
import numpy
1015

1116
__all__ = ("BDDCPC",)
@@ -22,17 +27,13 @@ class BDDCPC(PCBase):
2227
- ``'bddc_pc_bddc_dirichlet'`` to set sub-KSPs on subdomain interiors,
2328
- ``'bddc_pc_bddc_coarse'`` to set the coarse solver KSP.
2429
25-
This PC also inspects optional arguments supplied in the application context:
26-
- ``'discrete_gradient'`` for problems in H(curl), this sets the arguments
27-
(a Mat tabulating the gradient of the auxiliary H1 space) and
30+
This PC also inspects optional callbacks supplied in the application context:
31+
- ``'get_discrete_gradient'`` for 3D problems in H(curl), this is a callable that
32+
provide the arguments (a Mat tabulating the gradient of the auxiliary H1 space) and
2833
keyword arguments supplied to ``PETSc.PC.setBDDCDiscreteGradient``.
29-
- ``'divergence_mat'`` for 3D problems in H(div), this sets the Mat with the
30-
assembled bilinear form testing the divergence against an L2 space.
31-
32-
Notes
33-
-----
34-
Currently the Mat type IS is only supported by FDMPC.
35-
34+
- ``'get_divergence_mat'`` for problems in H(div) (resp. 2D H(curl)), this is
35+
provide the arguments (a Mat with the assembled bilinear form testing the divergence
36+
(curl) against an L2 space) and keyword arguments supplied to ``PETSc.PC.setDivergenceMat``.
3637
"""
3738

3839
_prefix = "bddc_"
@@ -44,6 +45,8 @@ def initialize(self, pc):
4445
self.prefix = (pc.getOptionsPrefix() or "") + self._prefix
4546

4647
V = get_function_space(dm)
48+
variant = V.ufl_element().variant()
49+
sobolev_space = V.ufl_element().sobolev_space
4750

4851
# Create new PC object as BDDC type
4952
bddcpc = PETSc.PC().create(comm=pc.comm)
@@ -53,13 +56,17 @@ def initialize(self, pc):
5356
bddcpc.setType(PETSc.PC.Type.BDDC)
5457

5558
opts = PETSc.Options(bddcpc.getOptionsPrefix())
56-
if V.ufl_element().variant() == "fdm" and "pc_bddc_use_local_mat_graph" not in opts:
57-
# Disable computation of disconected components of subdomain interfaces
59+
# Do not use CSR of local matrix to define dofs connectivity unless requested
60+
# Using the CSR only makes sense for H1/H2 problems
61+
is_h1h2 = sobolev_space in [H1, H2]
62+
if "pc_bddc_use_local_mat_graph" not in opts and (not is_h1h2 or not is_lagrange(V.finat_element)):
5863
opts["pc_bddc_use_local_mat_graph"] = False
5964

65+
# Handle boundary dofs
6066
ctx = get_appctx(dm)
61-
bcs = tuple(ctx._problem.bcs)
62-
if V.extruded:
67+
bcs = tuple(ctx._problem.dirichlet_bcs())
68+
mesh = V.mesh().unique()
69+
if mesh.extruded and not mesh.extruded_periodic:
6370
boundary_nodes = numpy.unique(numpy.concatenate(list(map(V.boundary_nodes, ("on_boundary", "top", "bottom")))))
6471
else:
6572
boundary_nodes = V.boundary_nodes("on_boundary")
@@ -69,46 +76,44 @@ def initialize(self, pc):
6976
dir_nodes = numpy.unique(numpy.concatenate([bcdofs(bc, ghost=False) for bc in bcs]))
7077
neu_nodes = numpy.setdiff1d(boundary_nodes, dir_nodes)
7178

72-
V.dof_dset.lgmap.apply(dir_nodes, result=dir_nodes)
79+
dir_nodes = V.dof_dset.lgmap.apply(dir_nodes)
7380
dir_bndr = PETSc.IS().createGeneral(dir_nodes, comm=pc.comm)
7481
bddcpc.setBDDCDirichletBoundaries(dir_bndr)
7582

76-
V.dof_dset.lgmap.apply(neu_nodes, result=neu_nodes)
83+
neu_nodes = V.dof_dset.lgmap.apply(neu_nodes)
7784
neu_bndr = PETSc.IS().createGeneral(neu_nodes, comm=pc.comm)
7885
bddcpc.setBDDCNeumannBoundaries(neu_bndr)
7986

8087
appctx = self.get_appctx(pc)
81-
sobolev_space = V.ufl_element().sobolev_space
88+
degree = max(as_tuple(V.ufl_element().degree()))
89+
90+
# Set coordinates only if corner selection is requested
91+
# There's no API to query from PC
92+
if "pc_bddc_corner_selection" in opts:
93+
W = VectorFunctionSpace(V.mesh(), "Lagrange", degree, variant=variant)
94+
coords = Function(W).interpolate(V.mesh().coordinates)
95+
bddcpc.setCoordinates(coords.dat.data_ro.repeat(V.block_size, axis=0))
8296

8397
tdim = V.mesh().topological_dimension
84-
degree = max(as_tuple(V.ufl_element().degree()))
8598
if tdim >= 2 and V.finat_element.formdegree == tdim-1:
86-
B = appctx.get("divergence_mat", None)
87-
if B is None:
88-
from firedrake.assemble import assemble
89-
d = {HCurl: curl, HDiv: div}[sobolev_space]
90-
Q = V.reconstruct(family="DG", degree=degree-1)
91-
b = inner(d(TrialFunction(V)), TestFunction(Q)) * dx(degree=2*(degree-1))
92-
B = assemble(b, mat_type="matfree")
93-
bddcpc.setBDDCDivergenceMat(B.petscmat)
94-
elif sobolev_space == HCurl:
95-
gradient = appctx.get("discrete_gradient", None)
96-
if gradient is None:
97-
from firedrake.preconditioners.fdm import tabulate_exterior_derivative
98-
from firedrake.preconditioners.hiptmair import curl_to_grad
99-
Q = V.reconstruct(element=curl_to_grad(V.ufl_element()))
100-
gradient = tabulate_exterior_derivative(Q, V)
101-
corners = get_vertex_dofs(Q)
102-
gradient.compose('_elements_corners', corners)
99+
allow_repeated = P.getISAllowRepeated()
100+
get_divergence = appctx.get("get_divergence_mat", get_divergence_mat)
101+
divergence = get_divergence(V, mat_type="is", allow_repeated=allow_repeated)
102+
try:
103+
div_args, div_kwargs = divergence
104+
except ValueError:
105+
div_args = (divergence,)
106+
div_kwargs = dict()
107+
bddcpc.setBDDCDivergenceMat(*div_args, **div_kwargs)
108+
109+
elif tdim >= 3 and V.finat_element.formdegree == 1:
110+
get_gradient = appctx.get("get_discrete_gradient", get_discrete_gradient)
111+
gradient = get_gradient(V)
112+
try:
113+
grad_args, grad_kwargs = gradient
114+
except ValueError:
103115
grad_args = (gradient,)
104-
grad_kwargs = {'order': degree}
105-
else:
106-
try:
107-
grad_args, grad_kwargs = gradient
108-
except ValueError:
109-
grad_args = (gradient,)
110-
grad_kwargs = dict()
111-
116+
grad_kwargs = dict()
112117
bddcpc.setBDDCDiscreteGradient(*grad_args, **grad_kwargs)
113118

114119
bddcpc.setFromOptions()
@@ -127,9 +132,67 @@ def applyTranspose(self, pc, x, y):
127132
self.pc.applyTranspose(x, y)
128133

129134

130-
def get_vertex_dofs(V):
131-
W = V.reconstruct(element=restrict(V.ufl_element(), "vertex"))
135+
def get_restricted_dofs(V, domain):
136+
W = FunctionSpace(V.mesh(), restrict(V.ufl_element(), domain))
132137
indices = get_restriction_indices(V, W)
133-
V.dof_dset.lgmap.apply(indices, result=indices)
134-
vertex_dofs = PETSc.IS().createGeneral(indices, comm=V.comm)
135-
return vertex_dofs
138+
indices = V.dof_dset.lgmap.apply(indices)
139+
return PETSc.IS().createGeneral(indices, comm=V.comm)
140+
141+
142+
def get_divergence_mat(V, mat_type="is", allow_repeated=False):
143+
from firedrake import assemble
144+
degree = max(as_tuple(V.ufl_element().degree()))
145+
Q = TensorFunctionSpace(V.mesh(), "DG", 0, variant=f"integral({degree-1})", shape=V.value_shape[:-1])
146+
B = tabulate_exterior_derivative(V, Q, mat_type=mat_type, allow_repeated=allow_repeated)
147+
148+
Jdet = JacobianDeterminant(V.mesh())
149+
s = assemble(inner(TrialFunction(Q)*(1/Jdet), TestFunction(Q))*dx(degree=0), diagonal=True)
150+
with s.dat.vec as svec:
151+
B.diagonalScale(svec, None)
152+
return (B,), {}
153+
154+
155+
def get_discrete_gradient(V):
156+
from firedrake import Constant
157+
from firedrake.nullspace import VectorSpaceBasis
158+
159+
Q = FunctionSpace(V.mesh(), curl_to_grad(V.ufl_element()))
160+
gradient = tabulate_exterior_derivative(Q, V)
161+
basis = Function(Q)
162+
try:
163+
basis.interpolate(Constant(1))
164+
except NotImplementedError:
165+
basis.project(Constant(1))
166+
nsp = VectorSpaceBasis([basis])
167+
nsp.orthonormalize()
168+
gradient.setNullSpace(nsp.nullspace())
169+
if not is_lagrange(Q.finat_element):
170+
vdofs = get_restricted_dofs(Q, "vertex")
171+
gradient.compose('_elements_corners', vdofs)
172+
173+
degree = max(as_tuple(Q.ufl_element().degree()))
174+
grad_args = (gradient,)
175+
grad_kwargs = {'order': degree}
176+
return grad_args, grad_kwargs
177+
178+
179+
def is_lagrange(finat_element):
180+
"""Returns whether finat_element.dual_basis consists only of point evaluation dofs."""
181+
try:
182+
Q, ps = finat_element.dual_basis
183+
except NotImplementedError:
184+
return False
185+
# Inspect the weight matrix
186+
# Lagrange elements have gem.Delta as the only terminal nodes
187+
children = [Q]
188+
while children:
189+
nodes = []
190+
for c in children:
191+
if isinstance(c, gem.Delta):
192+
pass
193+
elif isinstance(c, gem.gem.Terminal):
194+
return False
195+
else:
196+
nodes.extend(c.children)
197+
children = nodes
198+
return True

0 commit comments

Comments
 (0)