from firedrake.preconditioners.base import SNESBase
from firedrake.function import Function
from firedrake.cofunction import Cofunction
from firedrake.ufl_expr import Argument, TestFunction
from firedrake.petsc import PETSc
from ufl import replace
from firedrake.dmhooks import get_function_space, get_appctx as get_dm_appctx
__all__ = ("AuxiliaryOperatorSNES",)
[docs]
class AuxiliaryOperatorSNES(SNESBase):
"""
Solve a residual form :math:`F(u) = 0` using a nonlinear Richardson
iteration preconditioned with an auxiliary form :math:`G`.
This may be used to create nonlinear preconditioners for
iterative methods such as Anderson acceleration or NGMRES.
The `k`-th nonlinearly preconditioned Richardson iteration is:
.. math ::
G(u_{k+1}; u_{k}) = G(u_{k}; u_{k}) - F(u_{k})
where :math:`u_{k}` is the current guess and :math:`u_{k+1}` is the
next guess to be computed. A solution :math:`u_{*}` of :math:`F(u_{*}) = 0`
is a fixed point of the Richardson iteration.
Options for the inner solve for :math:`G(u_{k+1}; u_{k})` are specified
using the ``"aux_"`` prefix.
The following solver parameters will specify the above Richardson
iteration, assuming that the user has defined a class
``UserAuxiliarySNES`` which inherits from
:class:`~.AuxiliaryOperatorSNES` and implements the
:meth:`~.AuxiliaryOperatorSNES.form` method. In this example, the
inner solve uses a Newton method with a relative tolerance of 1e-4.
.. code-block:: python3
solver_parameters = {
"snes_rtol": 1e-8,
"snes_type": "python",
"snes_python_type": f"{__name__}.UserAuxiliarySNES",
"aux": {
"snes_rtol": 1e-4,
"snes_type": "newtonls",
...
}
}
More details, including how to use this class as a nonlinear
preconditioner with methods like Anderson acceleration, can
be found in the manual page on :doc:`preconditioning`.
Notes
-----
If the auxiliary form is linear, i.e. :math:`G(u)=Au`, with
:math:`A\\approx\\nabla F` approximating the Jacobian of the
outer residual, then the iteration is an inexact Newton method:
.. math ::
u_{k+1} = u_{k} - A^{-1}F(u_{k}).
"""
_prefix = "aux_"
[docs]
@PETSc.Log.EventDecorator()
def initialize(self, snes):
from firedrake.variational_solver import ( # circular import at file level
NonlinearVariationalSolver, NonlinearVariationalProblem)
ctx = get_dm_appctx(snes.dm)
parent_prefix = snes.getOptionsPrefix() or ""
prefix = parent_prefix + self._prefix
V = get_function_space(snes.getDM()).collapse()
# buffers for current and next iterates
u = Function(V)
u_k = Function(V)
self.u = u
self.u_k = u_k
# auxiliary form G(k+1)
test = TestFunction(V)
G, bcs = self.form(snes, u_k, u, test)
b = Cofunction(V.dual())
# This is the form we will solve:
# G(u_{k+1}; u_{k}) - b = 0
# and we will assemble G(u_{k}; u_{k}) - F(u_{k}) into b.
Gb = G - b
self.b = b
# a buffer for intermediate values when assembling b = Gk - Fk
self._b_wrk = Cofunction(V.dual())
problem = NonlinearVariationalProblem(
Gb, u, bcs=bcs, form_compiler_parameters=ctx.fcp
)
self.solver = NonlinearVariationalSolver(
problem,
nullspace=ctx._nullspace,
transpose_nullspace=ctx._nullspace_T,
near_nullspace=ctx._near_nullspace,
appctx=ctx.appctx,
options_prefix=prefix,
pre_apply_bcs=ctx.pre_apply_bcs,
)
# indent monitor outputs
outer_snes = snes
inner_snes = self.solver.snes
inner_snes.incrementTabLevel(1, parent=outer_snes)
inner_snes.ksp.incrementTabLevel(1, parent=outer_snes)
inner_snes.ksp.pc.incrementTabLevel(1, parent=outer_snes)
[docs]
def update(self, snes):
pass
[docs]
@PETSc.Log.EventDecorator()
def step(self, snes, x, f, y):
# x = u_{k} is state at current iteration
with self.u_k.dat.vec_wo as vec:
x.copy(vec)
# initial guess u_{k+1} = u_{k}
with self.u.dat.vec_wo as vec:
x.copy(vec)
# b = F(u_{k})
with self.b.dat.vec as vec:
f.copy(vec)
# At this point we have:
# u = x = u_{k}, and b = F(u_{k}),
# so the solver's assembler computes:
# b_wrk = G(u) - b = G(u_{k}) - F(u_{k})
# which is exactly the forcing we need.
self.solver._ctx._assemble_residual(tensor=self._b_wrk)
# we assign b = G(u_{k}) - F(u_{k}) so now the
# form in the solver has the correct forcing to
# calculate u_{k+1} using G(u) - b = 0
self.b.assign(self._b_wrk)
self.solver.solve()
# y = d = u_{k+1} - u_{k}
with self.u.dat.vec_ro as vec:
vec.copy(y)
y.aypx(-1, x)
[docs]
def view(self, snes, viewer=None):
super().view(snes, viewer)
if hasattr(self, "solver"):
viewer.printfASCII("SNES to apply a preconditioned Richardson iteration.\n")
self.solver.snes.view(viewer)