Invertable operators#
A common problem in physics models is solve a matrix equation of the form
for the unknown \(\underline{x}\). Here \(\underline{\underline{A}}\) represents some differential operator subject to boundary conditions. A specific example is the set of Laplacian operators described in Laplacian inversion.
Whilst specific tools are provided to deal with Laplacian and parallel
Helmholtz like equations these do not capture all possible systems and
are typically implemented (at least partially) independently of the
finite difference representation of the forward operators provided by
the rest of BOUT++. To address this a class InvertableOperator
has
been implemented that allows the user to define a generic differential
operator and provides a simple (for the user) method to invert the
operator to find \(\underline{x}\). This class currently relies on
PETSc to provide the inversion functionality and hence is not
available when configuring without PETSc support. It is available in
the namespace bout::inversion
.
There is an example in examples/invertable_operator
that uses the
class to solve a simple Laplacian operator and compares to the
specific Laplacian inversion solvers.
The InvertableOperator
class is templated on the field type of the
operator (essentially defining the domain over which the problem
exists). To define the operator that the InvertableOperator
instances represents one should use the
InvertableOperator::setOperatorFunction
method. This takes a
function of signature std::function<T(const T&)>
. This can be a
std::function
, compatible function pointer, lambda or a
functor. The last of these allows more complicated functions that use
a local context. For example the following code snippet demonstrates a
functor that stores several auxilliary Field3D
variables used in
the operator()
call:
struct myLaplacian {
Field3D D = 1.0, C = 1.0, A = 0.0;
// Drop C term for now
Field3D operator()(const Field3D &input) {
TRACE("myLaplacian::operator()");
Timer timer("invertable_operator_operate");
Field3D result = A * input + D * Delp2(input);
// Ensure boundary points are set appropriately as given by the input field.
result.setBoundaryTo(input);
return result;
};
};
A more complete example is
struct myLaplacian {
Field3D D = 1.0, C = 1.0, A = 0.0;
// Drop C term for now
Field3D operator()(const Field3D &input) {
TRACE("myLaplacian::operator()");
Timer timer("invertable_operator_operate");
Field3D result = A * input + D * Delp2(input);
// Ensure boundary points are set appropriately as given by the input field.
result.setBoundaryTo(input);
return result;
};
};
bout::inversion::InveratbleOperator<Field3D> solver;
myLaplacian laplacianOperator;
laplacianOperator.A = 1.0;
laplacianOperator.B = 2.0;
// Set the function defining the operator
solver.setOperatorFunction(laplacianOperator);
// Perform initial setup
solver.setup();
// Now invert the operator for a given right hand side.
Field3D rhs = 3.0*x;
auto solution = solver.invert(rhs);
The PETSc backend solver is an iterative solver. It can be controlled
through the usual PETSc command line options. Note we define the
options prefix here to be -invertable
, so instead of -ksp_type
one would use -invertable_ksp_type
for example.
By default the solver caches the result to use as the initial guess
for the next call to invert
. There is an overload of invert
that takes a second field, which is used to set the initial guess to
use in that call.
The routine setOperatorFunction
takes the function by value, and
hence subsequent changes to the functor will not be reflected in the
operator without a further call to setOperatorFunction
. For
example:
using bout::inversion;
InvertableOperator<Field3D> solver;
myLaplacian laplacianOperator;
laplacianOperator.A = 1.0;
laplacianOperator.B = 2.0;
// Set the function defining the operator
solver.setOperatorFunction(laplacianOperator);
// Perform initial setup
solver.setup();
// This does not change the operator represented by
// solver yet.
laplacianOperator.B = -1.0;
// This call updates the function used by solver
// and hence the operator is update to reflect the state
// of laplacianOperator.
solver.setOperatorFunction(laplacianOperator);
The class provides a reportTime
method that reports the time spent
in various parts of the class. Note that by including Timer
timer("invertable_operator_operate");
in the function representing
the operator reportTime
will include the time spent actually
applying the operator.
The class provides both apply
and operator()
methods that can
be used to apply the operator to a field. For example the following
should be equivalent to no operation:
// Here result should == input, at least in the main simulation domain
auto result = solver(solver.invert(input));
The class provides a verify
method that checks that applying the
operator to the calculated inverse returns the input field within some
tolerance.
It’s also possible to register a function to use as a preconditioner. By default this is the same as the full operator function.