import ufl
from ufl.corealg.dag_traverser import DAGTraverser
from ufl.domain import extract_unique_domain, as_domain
from functools import singledispatch, singledispatchmethod, partial
import firedrake
from firedrake.petsc import PETSc
from firedrake.solving_utils import _SNESContext
from firedrake.dmhooks import (get_transfer_manager, get_appctx, push_appctx, pop_appctx,
get_parent, add_hook)
from . import utils
__all__ = ["coarsen", "refine"]
class ReconstructionError(Exception):
"""Exception raised when reconstructing symbolic information fails."""
pass
class ReconstructIntegrand(DAGTraverser):
"""Reconstruct a :class:`ufl.Expr` with coefficients, arguments and
domain data on the target mesh level."""
def __init__(self, dispatch, coefficient_mapping=None):
super().__init__()
if coefficient_mapping is None:
coefficient_mapping = {}
self.coefficient_mapping = coefficient_mapping
self.dispatch = dispatch
@singledispatchmethod
def process(self, o):
return super().process(o)
@process.register(ufl.BaseForm)
@process.register(ufl.classes.Expr)
def expr(self, o):
return self.reuse_if_untouched(o)
@process.register(ufl.classes.Argument)
def argument(self, o):
V = self.dispatch(o.function_space(), self.dispatch)
return o.reconstruct(V)
@process.register(ufl.classes.Coefficient)
def coefficient(self, o):
return self.dispatch(o, self.dispatch, coefficient_mapping=self.coefficient_mapping)
@process.register(ufl.classes.Cofunction)
def cofunction(self, o):
return self.dispatch(o, self.dispatch, coefficient_mapping=self.coefficient_mapping)
@process.register(ufl.classes.GeometricQuantity)
def geometric_quantity(self, o):
return type(o)(self.dispatch(extract_unique_domain(o), self.dispatch))
@process.register(ufl.classes.Circumradius)
def circumradius(self, o):
mesh = self.dispatch(extract_unique_domain(o), self.dispatch)
return firedrake.Circumradius(mesh)
@process.register(ufl.classes.FacetNormal)
def facet_normal(self, o):
mesh = self.dispatch(extract_unique_domain(o), self.dispatch)
return firedrake.FacetNormal(mesh)
@singledispatch
def _reconstruct(expr, self, coefficient_mapping=None):
"""Fallback case: leave an expression with no registered handler unchanged."""
return expr
[docs]
@singledispatch
def coarsen(expr, self, coefficient_mapping=None):
return _reconstruct(expr, self, coefficient_mapping=coefficient_mapping)
[docs]
@singledispatch
def refine(expr, self, coefficient_mapping=None):
return _reconstruct(expr, self, coefficient_mapping=coefficient_mapping)
def as_ufl_domain(obj):
"""Return the underlying ufl domain"""
if isinstance(obj, _SNESContext):
obj = obj._problem
if isinstance(obj, firedrake.NonlinearVariationalProblem):
obj = obj.u.function_space()
elif isinstance(obj, firedrake.LinearEigenproblem):
obj = obj.output_space
if isinstance(obj, (firedrake.functionspaceimpl.WithGeometryBase,
firedrake.functionspaceimpl.MixedFunctionSpace)):
return obj.mesh()
else:
return as_domain(obj)
def get_relative(dispatch, old, reverse=False):
"""Return old._coarse or old._fine depending on dispatch
if they share the same hierarchy, otherwise None."""
if reverse:
dispatch = coarsen if dispatch is refine else refine
new = None
if dispatch is coarsen:
new = getattr(old, "_coarse", None)
elif dispatch is refine:
new = getattr(old, "_fine", None)
if new is not None:
new_hierarchy, _ = utils.get_level(as_ufl_domain(new))
old_hierarchy, _ = utils.get_level(as_ufl_domain(old))
if new_hierarchy is not old_hierarchy:
new = None
return new
def attach_relative(dispatch, old, new, reverse=False):
"""Set old._coarse or old._fine to new depending on dispatch."""
if reverse:
dispatch = coarsen if dispatch is refine else refine
if dispatch is coarsen:
old._coarse = new
elif dispatch is refine:
old._fine = new
@coarsen.register(ufl.Mesh)
@coarsen.register(ufl.MeshSequence)
def coarsen_mesh(mesh, self, coefficient_mapping=None):
hierarchy, level = utils.get_level(mesh)
if hierarchy is None:
raise ReconstructionError("No mesh hierarchy available")
return hierarchy[level - 1]
@refine.register(ufl.Mesh)
@refine.register(ufl.MeshSequence)
def refine_mesh(mesh, self, coefficient_mapping=None):
hierarchy, level = utils.get_level(mesh)
if hierarchy is None:
raise ReconstructionError("No mesh hierarchy available")
if len(hierarchy) <= level + 1:
raise ReconstructionError("Cannot refine the finest mesh")
return hierarchy[level + 1]
@_reconstruct.register(ufl.BaseForm)
@_reconstruct.register(ufl.classes.Expr)
def reconstruct_expr(expr, self, coefficient_mapping=None):
if expr is None:
return None
mapper = ReconstructIntegrand(self, coefficient_mapping)
return mapper(expr)
@_reconstruct.register(ufl.Form)
def reconstruct_form(form, self, coefficient_mapping=None):
"""Return a coarse or fine mesh version of a form.
:arg form: The :class:`~ufl.classes.Form` to reconstruct.
:kwarg coefficient_mapping: an optional map from coefficients to their
reconstructed equivalents.
This maps over the form and replaces coefficients and arguments
with their coarse/fine mesh equivalents."""
if form is None:
return None
mapper = ReconstructIntegrand(self, coefficient_mapping)
integrals = []
for it in form.integrals():
integrand = mapper(it.integrand())
mesh = it.ufl_domain()
new_mesh = self(mesh, self)
if isinstance(integrand, ufl.classes.Zero):
continue
if it.subdomain_data() is not None:
raise ReconstructionError("Don't know how to reconstruct subdomain data")
# Reconstruct secondary meshes in cross-mesh integrals (e.g. intersect_measures).
integral_type_map = {self(d, self): itype
for d, itype in it.extra_domain_integral_type_map().items()}
new_itg = it.reconstruct(integrand=integrand,
domain=new_mesh,
extra_domain_integral_type_map=integral_type_map)
integrals.append(new_itg)
form = ufl.Form(integrals)
return form
@_reconstruct.register(ufl.FormSum)
def reconstruct_formsum(form, self, coefficient_mapping=None):
return type(form)(*[(self(ci, self, coefficient_mapping=coefficient_mapping),
self(wi, self, coefficient_mapping=coefficient_mapping))
for ci, wi in zip(form.components(), form.weights())])
@_reconstruct.register(firedrake.DirichletBC)
def reconstruct_bc(bc, self, coefficient_mapping=None):
V = self(bc.function_space(), self, coefficient_mapping=coefficient_mapping)
val = self(bc._function_arg, self, coefficient_mapping=coefficient_mapping)
subdomain = bc.sub_domain
return type(bc)(V, val, subdomain)
@_reconstruct.register(firedrake.EquationBC)
def reconstruct_equation_bc(ebc, self, coefficient_mapping=None):
J = self(ebc._J.f, self, coefficient_mapping=coefficient_mapping)
Jp = self(ebc._Jp.f, self, coefficient_mapping=coefficient_mapping)
u = self(ebc._F.u, self, coefficient_mapping=coefficient_mapping)
sub_domain = ebc._F.sub_domain
bcs = [self(bc, self, coefficient_mapping=coefficient_mapping)
for bc in ebc.dirichlet_bcs()]
V = self(ebc._F.function_space(), self, coefficient_mapping=coefficient_mapping)
lhs = self(ebc.eq.lhs, self, coefficient_mapping=coefficient_mapping)
rhs = self(ebc.eq.rhs, self, coefficient_mapping=coefficient_mapping)
return type(ebc)(lhs == rhs, u, sub_domain, V=V, bcs=bcs, J=J, Jp=Jp)
@_reconstruct.register(firedrake.functionspaceimpl.WithGeometryBase)
def reconstruct_function_space(V, self, coefficient_mapping=None):
V_new = get_relative(self, V)
if V_new is not None:
return V_new
# Handle MixedFunctionSpace : V.reconstruct requires MeshSequence.
mesh = V.mesh() if V.index is None else V.parent.mesh()
new_mesh = self(mesh, self)
V_parent = V
while get_relative(self, V_parent, reverse=True) is not None:
V_parent = get_relative(self, V_parent, reverse=True)
name = V_parent.name
if name is not None:
mh, level = utils.get_level(new_mesh)
name = f"{name}_level_{level}"
V_new = V.reconstruct(mesh=new_mesh, name=name)
attach_relative(self, V_new, V, reverse=True)
attach_relative(self, V, V_new)
return V_new
@_reconstruct.register(firedrake.Cofunction)
@_reconstruct.register(firedrake.Function)
def reconstruct_function(expr, self, coefficient_mapping=None):
if coefficient_mapping is None:
coefficient_mapping = {}
new = coefficient_mapping.get(expr)
if new is None:
V = expr.function_space()
Vnew = self(V, self)
name = expr.name()
if name is not None:
try:
name, prev_level = name.split("_level_")
except ValueError:
prev_level = 0
level_inc = 1 if self is refine else -1
level = int(prev_level) + level_inc
name = f"{name}_level_{level}"
new = firedrake.Function(Vnew, name=name)
manager = get_transfer_manager(V.dm)
manager.transfer(expr, new)
coefficient_mapping[expr] = new
return new
@_reconstruct.register(firedrake.NonlinearVariationalProblem)
def reconstruct_nlvp(problem, self, coefficient_mapping=None):
new_problem = get_relative(self, problem)
if new_problem is not None:
return new_problem
def transfer_callback(fine, restriction, rscale, injection, coarse):
cctx = get_appctx(coarse)
cmapping = cctx._coefficient_mapping
if cmapping is None:
return
manager = get_transfer_manager(fine)
for c, mapped in cmapping.items():
manager.transfer(c, mapped)
# Apply bcs
if cctx.pre_apply_bcs:
for bc in cctx._problem.dirichlet_bcs():
bc.apply(cctx._x)
def coarsen_callback(fine, coarse):
if not coarse.getAttr("_coarsen_hook"):
coarse.addCoarsenHook(coarsen_callback, transfer_callback)
coarse.setAttr("_coarsen_hook", True)
dm = problem.u_restrict.function_space().dm
if not dm.getAttr("_coarsen_hook"):
# The hook is persistent and cumulative, but also problem-independent.
# Therefore, we are only adding it once.
dm.addCoarsenHook(coarsen_callback, transfer_callback)
dm.setAttr("_coarsen_hook", True)
if coefficient_mapping is None:
coefficient_mapping = {}
bcs = [self(bc, self, coefficient_mapping=coefficient_mapping) for bc in problem.bcs]
E = self(problem.E, self, coefficient_mapping=coefficient_mapping)
F = self(problem.F, self, coefficient_mapping=coefficient_mapping)
J = self(problem.J, self, coefficient_mapping=coefficient_mapping)
Jp = self(problem.Jp, self, coefficient_mapping=coefficient_mapping)
u = self(problem.u_restrict, self, coefficient_mapping=coefficient_mapping)
new_problem = firedrake.NonlinearVariationalProblem(
F, u, bcs=bcs, J=J, Jp=Jp, objective=E, is_linear=problem.is_linear,
form_compiler_parameters=problem.form_compiler_parameters)
attach_relative(self, problem, new_problem)
return new_problem
@_reconstruct.register(firedrake.LinearEigenproblem)
def reconstruct_eigenproblem(problem, self, coefficient_mapping=None):
new_problem = get_relative(self, problem)
if new_problem is not None:
return new_problem
if coefficient_mapping is None:
coefficient_mapping = {}
bcs = [self(bc, self, coefficient_mapping=coefficient_mapping)
for bc in problem.bcs]
A = self(problem.A, self, coefficient_mapping=coefficient_mapping)
M = self(problem.M, self, coefficient_mapping=coefficient_mapping)
new_problem = firedrake.LinearEigenproblem(A, M, bcs=bcs,
bc_shift=problem.bc_shift, restrict=problem.restrict)
attach_relative(self, problem, new_problem)
return new_problem
@_reconstruct.register(firedrake.VectorSpaceBasis)
def reconstruct_vectorspacebasis(basis, self, coefficient_mapping=None):
# Do not add basis._vecs to the coefficient_mapping,
# as they need to be normalized, and are not meant to be reinjected
coarse_vecs = [self(vec, self) for vec in basis._vecs]
vsb = firedrake.VectorSpaceBasis(coarse_vecs, constant=basis._constant, comm=basis.comm)
vsb.orthonormalize()
return vsb
@_reconstruct.register(firedrake.MixedVectorSpaceBasis)
def reconstruct_mixedvectorspacebasis(mspbasis, self, coefficient_mapping=None):
coarse_V = self(mspbasis._function_space, self, coefficient_mapping=coefficient_mapping)
coarse_bases = []
for basis in mspbasis._bases:
if isinstance(basis, firedrake.VectorSpaceBasis):
coarse_bases.append(self(basis, self, coefficient_mapping=coefficient_mapping))
elif basis.index is not None:
coarse_bases.append(coarse_V.sub(basis.index))
else:
raise RuntimeError("MixedVectorSpaceBasis can only contain vector space bases or indexed function spaces")
return firedrake.MixedVectorSpaceBasis(coarse_V, coarse_bases)
@_reconstruct.register(_SNESContext)
def reconstruct_snescontext(context, self, coefficient_mapping=None):
if coefficient_mapping is None:
coefficient_mapping = {}
new_context = get_relative(self, context)
if new_context is not None:
return new_context
problem = self(context._problem, self, coefficient_mapping=coefficient_mapping)
appctx = context.appctx
new_appctx = {}
for k in sorted(appctx.keys()):
v = appctx[k]
if k != "state":
# Constructor makes this one.
try:
new_appctx[k] = self(v, self, coefficient_mapping=coefficient_mapping)
except ReconstructionError:
# Assume not something that needs reconstruction (e.g. float)
new_appctx[k] = v
# Get options prefix for current level
parent_context = context
while get_relative(self, parent_context, reverse=True) is not None:
parent_context = get_relative(self, parent_context, reverse=True)
parent_prefix = parent_context.options_prefix
opts = PETSc.Options(parent_prefix)
if opts.getString("snes_type", "") == "fas":
solver_prefix = "fas_"
else:
solver_prefix = "mg_"
_, level = utils.get_level(problem.u_restrict.function_space().mesh())
if level == 0:
levels_prefix = f"{solver_prefix}coarse_"
else:
levels_prefix = f"{solver_prefix}levels_"
current_level_prefix = f"{solver_prefix}levels_{level}_"
options_prefix = f"{parent_prefix}{current_level_prefix}"
# Use different mat_type on each level
mat_type = None
pmat_type = None
sub_mat_type = None
sub_pmat_type = None
for prefix in (levels_prefix, current_level_prefix):
mat_type = opts.getString(f"{prefix}mat_type", "") or mat_type
pmat_type = opts.getString(f"{prefix}pmat_type", "") or pmat_type
sub_mat_type = opts.getString(f"{prefix}sub_mat_type", "") or sub_mat_type
sub_pmat_type = opts.getString(f"{prefix}sub_pmat_type", "") or sub_pmat_type
pmat_type = pmat_type or mat_type
sub_pmat_type = sub_pmat_type or sub_mat_type
new_context = context.reconstruct(problem=problem,
mat_type=mat_type,
pmat_type=pmat_type,
sub_mat_type=sub_mat_type,
sub_pmat_type=sub_pmat_type,
appctx=new_appctx,
options_prefix=options_prefix,
)
new_context._coefficient_mapping = coefficient_mapping
attach_relative(self, new_context, context, reverse=True)
attach_relative(self, context, new_context)
solutiondm = context._problem.u_restrict.function_space().dm
parentdm = get_parent(solutiondm)
# Now that we have the reconstructed snescontext, push it to the reconstructed DMs.
# Otherwise they will not have the right transfer manager when they are reconstructed in turn.
for val in coefficient_mapping.values():
if isinstance(val, (firedrake.Function, firedrake.Cofunction)):
V = val.function_space()
newdm = V.dm
# Now attach the hook to the parent DM
if get_appctx(newdm) is None:
push_appctx(newdm, new_context)
if parentdm.getAttr("__setup_hooks__"):
add_hook(parentdm, teardown=partial(pop_appctx, newdm, new_context))
ises = new_context._x.function_space()._ises
new_context._nullspace = self(context._nullspace, self, coefficient_mapping=coefficient_mapping)
new_context.set_nullspace(new_context._nullspace, ises, transpose=False, near=False)
new_context._nullspace_T = self(context._nullspace_T, self, coefficient_mapping=coefficient_mapping)
new_context.set_nullspace(new_context._nullspace_T, ises, transpose=True, near=False)
new_context._near_nullspace = self(context._near_nullspace, self, coefficient_mapping=coefficient_mapping)
new_context.set_nullspace(new_context._near_nullspace, ises, transpose=False, near=True)
return new_context
@_reconstruct.register(firedrake.slate.BlockAssembledVector)
def reconstruct_slate_block_assembled_vector(tensor, self, coefficient_mapping=None):
form = self(tensor.form, self, coefficient_mapping=coefficient_mapping)
block = self(tensor._block, self, coefficient_mapping=coefficient_mapping)
child, = block.children
return type(tensor)(form, child, block._indices)
@_reconstruct.register(firedrake.slate.TensorBase)
def reconstruct_slate_tensor(tensor, self, coefficient_mapping=None):
if tensor.children:
children = (self(c, self, coefficient_mapping=coefficient_mapping)
for c in tensor.children)
return tensor.reconstruct(*children)
form = self(tensor.form, self, coefficient_mapping=coefficient_mapping)
return tensor.reconstruct(form)
class Interpolation(object):
def __init__(self, Vcoarse, Vfine, manager, cbcs=None, fbcs=None):
self.cprimal = firedrake.Function(Vcoarse)
self.fprimal = firedrake.Function(Vfine)
self.cdual = firedrake.Cofunction(Vcoarse.dual())
self.fdual = firedrake.Cofunction(Vfine.dual())
self.cbcs = cbcs or []
self.fbcs = fbcs or []
self.manager = manager
def mult(self, mat, x, y, inc=False):
with self.cprimal.dat.vec_wo as v:
x.copy(v)
self.manager.prolong(self.cprimal, self.fprimal)
for bc in self.fbcs:
bc.zero(self.fprimal)
with self.fprimal.dat.vec_ro as v:
if inc:
y.axpy(1.0, v)
else:
v.copy(y)
def multAdd(self, mat, x, y, w):
if y.handle == w.handle:
self.mult(mat, x, w, inc=True)
else:
self.mult(mat, x, w)
w.axpy(1.0, y)
def multTranspose(self, mat, x, y, inc=False):
with self.fdual.dat.vec_wo as v:
x.copy(v)
self.manager.restrict(self.fdual, self.cdual)
for bc in self.cbcs:
bc.zero(self.cdual)
with self.cdual.dat.vec_ro as v:
if inc:
y.axpy(1.0, v)
else:
v.copy(y)
def multTransposeAdd(self, mat, x, y, w):
if y.handle == w.handle:
self.multTranspose(mat, x, w, inc=True)
else:
self.multTranspose(mat, x, w)
w.axpy(1.0, y)
class Injection(object):
def __init__(self, Vcoarse, Vfine, manager, cbcs=None):
self.cfn = firedrake.Function(Vcoarse)
self.ffn = firedrake.Function(Vfine)
self.cbcs = cbcs or []
self.manager = manager
def mult(self, mat, x, y):
with self.ffn.dat.vec_wo as v:
x.copy(v)
self.manager.inject(self.ffn, self.cfn)
for bc in self.cbcs:
bc.apply(self.cfn)
with self.cfn.dat.vec_ro as v:
v.copy(y)
def multTranspose(self, mat, x, y):
# PETSc's MatRestrict() cannot distinguish an injection matrix from
# an interpolation matrix when the coarse and fine spaces happen to
# have equal size (e.g. a no-op adaptive refinement level), and may
# call MatMultTranspose() instead of MatMult(). Injection is only
# ever used in one direction (fine to coarse), so both must agree.
self.mult(mat, x, y)
def create_interpolation(dmc, dmf):
cctx = get_appctx(dmc)
fctx = get_appctx(dmf)
V_c = cctx._problem.u_restrict.function_space()
V_f = fctx._problem.u_restrict.function_space()
row_size = V_f.dof_dset.layout_vec.getSizes()
col_size = V_c.dof_dset.layout_vec.getSizes()
cbcs = tuple(cctx._problem.dirichlet_bcs())
fbcs = tuple(fctx._problem.dirichlet_bcs())
manager = get_transfer_manager(dmf)
ctx = Interpolation(V_c, V_f, manager, cbcs, fbcs)
mat = PETSc.Mat().create(comm=dmc.comm)
mat.setSizes((row_size, col_size))
mat.setType(mat.Type.PYTHON)
mat.setPythonContext(ctx)
mat.setUp()
if row_size == col_size:
# PETSc cannot determine the coarse space if the dimensions are equal.
# The coarse space is identified by the dimension of rscale, so we provide one.
rscale = mat.createVecRight()
rscale.set(1.0)
else:
rscale = None
return mat, rscale
def create_injection(dmc, dmf):
cctx = get_appctx(dmc)
fctx = get_appctx(dmf)
V_c = cctx._problem.u_restrict.function_space()
V_f = fctx._problem.u_restrict.function_space()
row_size = V_c.dof_dset.layout_vec.getSizes()
col_size = V_f.dof_dset.layout_vec.getSizes()
if (V_c.ufl_element().family() == "Real"
and V_f.ufl_element().family() == "Real"):
assert row_size == col_size
# If the coarse and fine spaces have equal size
# PETSc will apply the transpose of the injection.
# It does not make sense to implement Injection.multTranspose,
# instead we return a concrete identity matrix.
dvec = V_c.dof_dset.layout_vec.duplicate()
dvec.set(1.0)
return PETSc.Mat().createDiagonal(dvec)
manager = get_transfer_manager(dmf)
ctx = Injection(V_c, V_f, manager)
mat = PETSc.Mat().create(comm=dmc.comm)
mat.setSizes((row_size, col_size))
mat.setType(mat.Type.PYTHON)
mat.setPythonContext(ctx)
mat.setUp()
return mat