<a name="step-20-Intro"></a>
<h1>Introduction</h1>

@dealiiVideoLecture{19,20,21}

This program is devoted to two aspects: the use of mixed finite elements -- in
particular Raviart-Thomas elements -- and using block matrices to define
solvers, preconditioners, and nested versions of those that use the
substructure of the system matrix. The equation we are going to solve is again
the Poisson equation, though with a matrix-valued coefficient:
@f{eqnarray*}{
  -\nabla \cdot K({\mathbf x}) \nabla p &=& f \qquad {\textrm{in}\ } \Omega, \\
  p &=& g \qquad {\textrm{on}\ }\partial\Omega.
@f}
$K({\mathbf x})$ is assumed to be uniformly positive definite, i.e., there is
$\alpha>0$ such that the eigenvalues $\lambda_i({\mathbf x})$ of $K(x)$ satisfy
$\lambda_i({\mathbf x})\ge \alpha$. The use of the symbol $p$ instead of the usual
$u$ for the solution variable will become clear in the next section.

After discussing the equation and the formulation we are going to use to solve
it, this introduction will cover the use of block matrices and vectors, the
definition of solvers and preconditioners, and finally the actual test case we
are going to solve.

We are going to extend this tutorial program in step-21 to
solve not only the mixed Laplace equation, but add another equation that
describes the transport of a mixture of two fluids.

The equations covered here fall into the class of vector-valued problems. A
toplevel overview of this topic can be found in the @ref vector_valued topic.


<h3>The equations</h3>

In the form above, the Poisson equation (i.e., the Laplace equation with a nonzero
right hand side) is generally considered a good model equation
for fluid flow in porous media. Of course, one typically models fluid flow through
the <a href="https://en.wikipedia.org/wiki/Navier%E2%80%93Stokes_equations">Navier-Stokes
equations</a> or, if fluid velocities are slow or the viscosity is large, the
<a href="https://en.wikipedia.org/wiki/Stokes_flow">Stokes
equations</a>
(which we cover in step-22).
In the first of these two models, the forces that act are inertia and
viscous friction, whereas in the second it is only viscous friction -- i.e.,
forces that one fluid particle exerts on a nearby one. This is appropriate
if you have free flow in a large domain, say a pipe, a river, or in the air.
On the other hand, if the fluid is confined in pores, then friction forces
exerted by the pore walls on the fluid become more and more important and
internal viscous friction becomes less and less important. Modeling this
then first leads to the
<a href="https://en.wikipedia.org/wiki/Darcy%27s_law#Brinkman_form_of_Darcy's_law">Brinkman
model</a> if both effects are important, and in the limit of very small pores
to the <a href="https://en.wikipedia.org/wiki/Darcy%27s_law">Darcy equations</a>.
The latter is just a different name for the Poisson or Laplace equation,
connotating it with the area to which one wants to apply it: slow flow
in a porous medium. In essence it says that the velocity is proportional
to the negative pressure gradient that drives the fluid through the
porous medium.

The Darcy equation models this pressure that drives the flow. (Because the
solution variable is a pressure, we here use the name $p$ instead of the
name $u$ more commonly used for the solution of partial differential equations.)
Typical applications of this view of the Laplace equation are then modeling
groundwater flow, or the flow of hydrocarbons in oil reservoirs. In these
applications, $K$ is the permeability tensor, i.e., a measure for how much
resistance the soil or rock matrix asserts on the fluid flow.

In the applications named above, a desirable feature for a numerical
scheme is that it should be locally conservative, i.e., that whatever
flows into a cell also flows out of it (or the difference is equal to
the integral over the source terms over each cell, if the sources are
nonzero). However, as it turns out, the usual discretizations of the
Laplace equation (such as those used in step-3, step-4, or step-6) do
not satisfy this property. But, one can achieve this by choosing a
different formulation of the problem and a particular combination of
finite element spaces.


<h3>Formulation, weak form, and discrete problem</h3>

To this end, one first introduces a second variable, called the velocity,
${\mathbf u}=-K\nabla p$. By its definition, the velocity is a vector in the
negative
direction of the pressure gradient, multiplied by the permeability tensor. If
the permeability tensor is proportional to the unit matrix, this equation is
easy to understand and intuitive: the higher the permeability, the higher the
velocity; and the velocity is proportional to the gradient of the pressure, going from
areas of high pressure to areas of low pressure (thus the negative sign).

With this second variable, one then finds an alternative version of the
Laplace equation, called the <i>mixed formulation</i>:
@f{eqnarray*}{
  K^{-1} {\mathbf u} + \nabla p &=& 0 \qquad {\textrm{in}\ } \Omega, \\
  -{\textrm{div}}\ {\mathbf u} &=& -f \qquad {\textrm{in}\ }\Omega, \\
  p &=& g \qquad {\textrm{on}\ } \partial\Omega.
@f}
Here, we have multiplied the equation defining the velocity ${\mathbf
u}$ by $K^{-1}$ because this makes the set of equations symmetric: one
of the equations has the gradient, the second the negative divergence,
and these two are of course adjoints of each other, resulting in a
symmetric bilinear form and a consequently symmetric system matrix
under the common assumption that $K$ is a symmetric tensor.

The weak formulation of this problem is found by multiplying the two
equations with test functions and integrating some terms by parts:
@f{eqnarray*}{
  A(\{{\mathbf u},p\},\{{\mathbf v},q\}) = F(\{{\mathbf v},q\}),
@f}
where
@f{eqnarray*}{
  A(\{{\mathbf u},p\},\{{\mathbf v},q\})
  &=&
  ({\mathbf v}, K^{-1}{\mathbf u})_\Omega - ({\textrm{div}}\ {\mathbf v}, p)_\Omega
  - (q,{\textrm{div}}\ {\mathbf u})_\Omega
  \\
  F(\{{\mathbf v},q\}) &=& -(g,{\mathbf v}\cdot {\mathbf n})_{\partial\Omega} - (f,q)_\Omega.
@f}
Here, ${\mathbf n}$ is the outward normal vector at the boundary. Note how in this
formulation, Dirichlet boundary values of the original problem are
incorporated in the weak form.

To be well-posed, we have to look for solutions and test functions in the
space $H({\textrm{div}})=\{{\mathbf w}\in L^2(\Omega)^d:\ {\textrm{div}}\ {\mathbf w}\in L^2\}$
for $\mathbf u$,$\mathbf v$, and $L^2$ for $p,q$. It is a well-known fact stated in
almost every book on finite element theory that if one chooses discrete finite
element spaces for the approximation of ${\mathbf u},p$ inappropriately, then the
resulting discrete problem is instable and the discrete solution
will not converge to the exact solution. (Some details on the problem
considered here -- which falls in the class of "saddle-point problems"
-- can be found on the Wikipedia page on the <a
href="https://en.wikipedia.org/wiki/Ladyzhenskaya%E2%80%93Babu%C5%A1ka%E2%80%93Brezzi_condition">Ladyzhenskaya-Babuska-Brezzi
(LBB) condition</a>.)

To overcome this, a number of different finite element pairs for ${\mathbf u},p$
have been developed that lead to a stable discrete problem. One such pair is
to use the Raviart-Thomas spaces $RT(k)$ for the velocity ${\mathbf u}$ and
discontinuous elements of class $DQ(k)$ for the pressure $p$. For details
about these spaces, we refer in particular to the book on mixed finite element
methods by Brezzi and Fortin, but many other books on the theory of finite
elements, for example the classic book by Brenner and Scott, also state the
relevant results. In any case, with appropriate choices of function
spaces, the discrete formulation reads as follows: Find ${\mathbf
u}_h,p_h$ so that
@f{eqnarray*}{
  A(\{{\mathbf u}_h,p_h\},\{{\mathbf v}_h,q_h\}) = F(\{{\mathbf v}_h,q_h\})
  \qquad\qquad \forall {\mathbf v}_h,q_h.
@f}


Before continuing, let us briefly pause and show that the choice of
function spaces above provides us with the desired local conservation
property. In particular, because the pressure space consists of
discontinuous piecewise polynomials, we can choose the test function
$q$ as the function that is equal to one on any given cell $K$ and
zero everywhere else. If we also choose ${\mathbf v}=0$ everywhere
(remember that the weak form above has to hold for <i>all</i> discrete
test functions $q,v$), then putting these choices of test functions
into the weak formulation above implies in particular that
@f{eqnarray*}{
  - (1,{\textrm{div}}\ {\mathbf u}_h)_K
  =
  -(1,f)_K,
@f}
which we can of course write in more explicit form as
@f{eqnarray*}{
  \int_K {\textrm{div}}\ {\mathbf u}_h
 =
  \int_K f.
@f}
Applying the divergence theorem results in the fact that ${\mathbf
u}_h$ has to satisfy, for every choice of cell $K$, the relationship
@f{eqnarray*}{
  \int_{\partial K} {\mathbf u}_h\cdot{\mathbf n}
  =
  \int_K f.
@f}
If you now recall that ${\mathbf u}$ was the velocity, then the
integral on the left is exactly the (discrete) flux across the
boundary of the cell $K$. The statement is then that the flux must be equal
to the integral over the sources within $K$. In particular, if there
are no sources (i.e., $f=0$ in $K$), then the statement is that
<i>total</i> flux is zero, i.e., whatever flows into a cell must flow out
of it through some other part of the cell boundary. This is what we call
<i>local conservation</i> because it holds for every cell.

On the other hand, the usual continuous $Q_k$ elements would not result in
this kind of property when used for the pressure (as, for example, we
do in step-43) because one can not choose a discrete test function
$q_h$ that is one on a cell $K$ and zero everywhere else: It would be
discontinuous and consequently not in the finite element
space. (Strictly speaking, all we can say is that the proof above
would not work for continuous elements. Whether these elements might
still result in local conservation is a different question as one
could think that a different kind of proof might still work; in
reality, however, the property really does not hold.)



<h3>Assembling the linear system</h3>

The deal.II library (of course) implements Raviart-Thomas elements $RT(k)$ of
arbitrary order $k$, as well as discontinuous elements $DG(k)$. If we forget
about their particular properties for a second, we then have to solve a
discrete problem
@f{eqnarray*}{
  A(x_h,w_h) = F(w_h),
@f}
with the bilinear form and right hand side as stated above, and $x_h=\{{\mathbf u}_h,p_h\}$, $w_h=\{{\mathbf v}_h,q_h\}$. Both $x_h$ and $w_h$ are from the space
$X_h=RT(k)\times DQ(k)$, where $RT(k)$ is itself a space of $dim$-dimensional
functions to accommodate for the fact that the flow velocity is vector-valued.
The necessary question then is: how do we do this in a program?

Vector-valued elements have already been discussed in previous tutorial
programs, the first time and in detail in step-8. The main difference there
was that the vector-valued space $V_h$ is uniform in all its components: the
$dim$ components of the displacement vector are all equal and from the same
function space. What we could therefore do was to build $V_h$ as the outer
product of the $dim$ times the usual $Q(1)$ finite element space, and by this
make sure that all our shape functions have only a single non-zero vector
component. Instead of dealing with vector-valued shape functions, all we did
in step-8 was therefore to look at the (scalar) only non-zero component and
use the <code>fe.system_to_component_index(i).first</code> call to figure out
which component this actually is.

This doesn't work with Raviart-Thomas elements: following from their
construction to satisfy certain regularity properties of the space
$H({\textrm{div}})$, the shape functions of $RT(k)$ are usually nonzero in all
their vector components at once. For this reason, were
<code>fe.system_to_component_index(i).first</code> applied to determine the only
nonzero component of shape function $i$, an exception would be generated. What
we really need to do is to get at <em>all</em> vector components of a shape
function. In deal.II diction, we call such finite elements
<em>non-primitive</em>, whereas finite elements that are either scalar or for
which every vector-valued shape function is nonzero only in a single vector
component are called <em>primitive</em>.

So what do we have to do for non-primitive elements? To figure this out, let
us go back in the tutorial programs, almost to the very beginnings. There, we
learned that we use the <code>FEValues</code> class to determine the values and
gradients of shape functions at quadrature points. For example, we would call
<code>fe_values.shape_value(i,q_point)</code> to obtain the value of the
<code>i</code>th shape function on the quadrature point with number
<code>q_point</code>. Later, in step-8 and other tutorial programs, we learned
that this function call also works for vector-valued shape functions (of
primitive finite elements), and that it returned the value of the only
non-zero component of shape function <code>i</code> at quadrature point
<code>q_point</code>.

For non-primitive shape functions, this is clearly not going to work: there is
no single non-zero vector component of shape function <code>i</code>, and the call
to <code>fe_values.shape_value(i,q_point)</code> would consequently not make
much sense. However, deal.II offers a second function call,
<code>fe_values.shape_value_component(i,q_point,comp)</code> that returns the
value of the <code>comp</code>th vector component of shape function  <code>i</code> at
quadrature point <code>q_point</code>, where <code>comp</code> is an index between
zero and the number of vector components of the present finite element; for
example, the element we will use to describe velocities and pressures is going
to have $dim+1$ components. It is worth noting that this function call can
also be used for primitive shape functions: it will simply return zero for all
components except one; for non-primitive shape functions, it will in general
return a non-zero value for more than just one component.

We could now attempt to rewrite the bilinear form above in terms of vector
components. For example, in 2d, the first term could be rewritten like this
(note that $u_0=x_0, u_1=x_1, p=x_2$):
@f{eqnarray*}{
  ({\mathbf u}_h^i, K^{-1}{\mathbf u}_h^j)
  =
  &\left((x_h^i)_0, K^{-1}_{00} (x_h^j)_0\right) +
   \left((x_h^i)_0, K^{-1}_{01} (x_h^j)_1\right) + \\
  &\left((x_h^i)_1, K^{-1}_{10} (x_h^j)_0\right) +
   \left((x_h^i)_1, K^{-1}_{11} (x_h^j)_1\right).
@f}
If we implemented this, we would get code like this:

@code
  for (unsigned int q=0; q<n_q_points; ++q)
    for (unsigned int i=0; i<dofs_per_cell; ++i)
      for (unsigned int j=0; j<dofs_per_cell; ++j)
        local_matrix(i,j) += (k_inverse_values[q][0][0] *
                              fe_values.shape_value_component(i,q,0) *
                              fe_values.shape_value_component(j,q,0)
                              +
                              k_inverse_values[q][0][1] *
                              fe_values.shape_value_component(i,q,0) *
                              fe_values.shape_value_component(j,q,1)
                              +
                              k_inverse_values[q][1][0] *
                              fe_values.shape_value_component(i,q,1) *
                              fe_values.shape_value_component(j,q,0)
                              +
                              k_inverse_values[q][1][1] *
                              fe_values.shape_value_component(i,q,1) *
                              fe_values.shape_value_component(j,q,1)
                             ) *
                             fe_values.JxW(q);
@endcode

This is, at best, tedious, error prone, and not dimension independent. There
are obvious ways to make things dimension independent, but in the end, the
code is simply not pretty. What would be much nicer is if we could simply
extract the ${\mathbf u}$ and $p$ components of a shape function $x_h^i$. In the
program we do that in the following way:

@code
  const FEValuesExtractors::Vector velocities (0);
  const FEValuesExtractors::Scalar pressure (dim);

  ...

  for (unsigned int q=0; q<n_q_points; ++q)
    for (unsigned int i=0; i<dofs_per_cell; ++i)
      for (unsigned int j=0; j<dofs_per_cell; ++j)
        local_matrix(i,j) += (fe_values[velocities].value (i, q) *
                              k_inverse_values[q] *
                              fe_values[velocities].value (j, q)
                              -
                              fe_values[velocities].divergence (i, q) *
                              fe_values[pressure].value (j, q)
                              -
                              fe_values[pressure].value (i, q) *
                              fe_values[velocities].divergence (j, q)) *
                              fe_values.JxW(q);
@endcode

This is, in fact, not only the first term of the bilinear form, but the
whole thing (sans boundary contributions).

What this piece of code does is, given an <code>fe_values</code> object, to extract
the values of the first $dim$ components of shape function <code>i</code> at
quadrature points <code>q</code>, that is the velocity components of that shape
function. Put differently, if we write shape functions $x_h^i$ as the tuple
$\{{\mathbf u}_h^i,p_h^i\}$, then the function returns the velocity part of this
tuple. Note that the velocity is of course a <code>dim</code>-dimensional tensor, and
that the function returns a corresponding object. Similarly, where we
subscript with the pressure extractor, we extract the scalar pressure
component. The whole mechanism is described in more detail in the
@ref vector_valued topic.

In practice, it turns out that we can do a bit better if we evaluate the shape
functions, their gradients and divergences only once per outermost loop, and
store the result, as this saves us a few otherwise repeated computations (it is
possible to save even more repeated operations by calculating all relevant
quantities in advance and then only inserting the results in the actual loop,
see step-22 for a realization of that approach).
The final result then looks like this, working in every space dimension:

@code
  for (const auto &cell : dof_handler.active_cell_iterators())
    {
      fe_values.reinit (cell);
      local_matrix = 0;
      local_rhs = 0;

      right_hand_side.value_list (fe_values.get_quadrature_points(),
                                  rhs_values);
      k_inverse.value_list (fe_values.get_quadrature_points(),
                            k_inverse_values);

      for (unsigned int q=0; q<n_q_points; ++q)
        for (unsigned int i=0; i<dofs_per_cell; ++i)
          {
            const Tensor<1,dim> phi_i_u     = fe_values[velocities].value (i, q);
            const double        div_phi_i_u = fe_values[velocities].divergence (i, q);
            const double        phi_i_p     = fe_values[pressure].value (i, q);

            for (unsigned int j=0; j<dofs_per_cell; ++j)
              {
                const Tensor<1,dim> phi_j_u     = fe_values[velocities].value (j, q);
                const double        div_phi_j_u = fe_values[velocities].divergence (j, q);
                const double        phi_j_p     = fe_values[pressure].value (j, q);

                local_matrix(i,j) += (phi_i_u * k_inverse_values[q] * phi_j_u
                                      - div_phi_i_u * phi_j_p
                                      - phi_i_p * div_phi_j_u) *
                                     fe_values.JxW(q);
              }

            local_rhs(i) += -phi_i_p *
                            rhs_values[q] *
                            fe_values.JxW(q);
          }
@endcode

This very closely resembles the form in which we have originally written down
the bilinear form and right hand side.

There is one final term that we have to take care of: the right hand side
contained the term $(g,{\mathbf v}\cdot {\mathbf n})_{\partial\Omega}$, constituting the
weak enforcement of pressure boundary conditions. We have already seen in
step-7 how to deal with face integrals: essentially exactly the same as with
domain integrals, except that we have to use the FEFaceValues class
instead of <code>FEValues</code>. To compute the boundary term we then simply have
to loop over all boundary faces and integrate there. The mechanism works in
the same way as above, i.e. the extractor classes also work on FEFaceValues objects:

@code
        for (const auto &face : cell->face_iterators())
          if (face->at_boundary())
            {
              fe_face_values.reinit(cell, face);

              pressure_boundary_values.value_list(
                fe_face_values.get_quadrature_points(), boundary_values);

              for (unsigned int q = 0; q < n_face_q_points; ++q)
                for (unsigned int i = 0; i < dofs_per_cell; ++i)
                  local_rhs(i) += -(fe_face_values[velocities].value(i, q) *
                                    fe_face_values.normal_vector(q) *
                                    boundary_values[q] *
                                    fe_face_values.JxW(q));
@endcode

You will find the exact same code as above in the sources for the present
program. We will therefore not comment much on it below.


<h3>Linear solvers and preconditioners</h3>

After assembling the linear system we are faced with the task of solving
it. The problem here is that the matrix possesses two undesirable properties:
- It is <a href="https://en.wikipedia.org/wiki/Definiteness_of_a_matrix">indefinite</a>,
  i.e., it has both positive and negative eigenvalues.
  We don't want to prove this property here, but note that this is true
  for all matrices of the form
  $\left(\begin{array}{cc} M & B \\ B^T & 0 \end{array}\right)$
  such as the one here where $M$ is positive definite.
- The matrix has a zero block at the bottom right (there is no term in
  the bilinear form that couples the pressure $p$ with the
  pressure test function $q$).

At least it is symmetric, but the first issue above still means that
the Conjugate Gradient method is not going to work since it is only
applicable to problems in which the matrix is symmetric and positive definite.
We would have to resort to other iterative solvers instead, such as
MinRes, SymmLQ, or GMRES, that can deal with indefinite systems. However, then
the next problem immediately surfaces: Due to the zero block, there are zeros
on the diagonal and none of the usual, "simple" preconditioners (Jacobi, SSOR)
will work as they require division by diagonal elements.

For the matrix sizes we expect to run with this program, the by far simplest
approach would be to just use a direct solver (in particular, the
SparseDirectUMFPACK class that is bundled with deal.II). step-29 goes this
route and shows that solving <i>any</i> linear system can be done in just
3 or 4 lines of code.

But then, this is a tutorial: We teach how to do things. Consequently,
in the following, we will introduce some techniques that can be used in cases
like these. Namely, we will consider the linear system as not consisting of one
large matrix and vectors, but we will want to decompose matrices
into <i>blocks</i> that correspond to the individual operators that appear in
the system. We note that the resulting solver is not optimal -- there are
much better ways to efficiently compute the system, for example those
explained in the results section of step-22 or the one we use in step-43
for a problem similar to the current one. Here, our goal is simply to
introduce new solution techniques and how they can be implemented in
deal.II.


<h4>Solving using the Schur complement</h4>

In view of the difficulties using standard solvers and preconditioners
mentioned above, let us take another look at the matrix. If we sort our
degrees of freedom so that all velocity come before all pressure variables,
then we can subdivide the linear system $Ax=b$ into the following blocks:
@f{eqnarray*}{
  \left(\begin{array}{cc}
    M & B \\ B^T & 0
  \end{array}\right)
  \left(\begin{array}{cc}
    U \\ P
  \end{array}\right)
  =
  \left(\begin{array}{cc}
    F \\ G
  \end{array}\right),
@f}
where $U,P$ are the values of velocity and pressure degrees of freedom,
respectively, $M$ is the @ref GlossMassMatrix "mass matrix" on the velocity space, $B^T$ corresponds to
the negative divergence operator, and $B$ is its transpose and corresponds
to the gradient.

By block elimination, we can then re-order this system in the following way
(multiply the first row of the system by $B^TM^{-1}$ and then subtract the
second row from it):
@f{eqnarray*}{
  B^TM^{-1}B P &=& B^TM^{-1} F - G, \\
  MU &=& F - BP.
@f}
Here, the matrix $S=B^TM^{-1}B$ (called the
<a href="https://en.wikipedia.org/wiki/Schur_complement">Schur complement</a>
of $A$)
is obviously symmetric and, owing to the positive definiteness of $M$ and the
fact that $B$ has full column rank, $S$ is also positive
definite.

Consequently, if we could compute $S$, we could apply the Conjugate Gradient
method to it. However, computing $S$ is expensive because it requires us
to compute the inverse of the (possibly large) matrix $M$; and $S$ is in fact
also a full matrix because even though $M$ is sparse, its inverse $M^{-1}$
will generally be a dense matrix.
On the other hand, the CG algorithm doesn't require
us to actually have a representation of $S$: It is sufficient to form
matrix-vector products with it. We can do so in steps, using the fact that
matrix products are associative (i.e., we can set parentheses in such a
way that the product is more convenient to compute):
To compute $Sv=(B^TM^{-1}B)v=B^T(M^{-1}(Bv))$, we
<ol>
 <li> compute $w = B v$;
 <li> solve $My = w$ for $y=M^{-1}w$, using the CG method applied to the
  positive definite and symmetric mass matrix $M$;
 <li> compute $z=B^Ty$ to obtain $z=Sv$.
</ol>
Note how we evaluate the expression $B^TM^{-1}Bv$ right to left to
avoid matrix-matrix products; this way, all we have to do is evaluate
matrix-vector products.

In the following, we will then have to come up with ways to represent the
matrix $S$ so that it can be used in a Conjugate Gradient solver,
as well as to define ways in which we can precondition the solution
of the linear system involving $S$, and deal with solving linear systems
with the matrix $M$ (the second step above).

@note The key point in this consideration is to recognize that to implement
an iterative solver such as CG or GMRES, we never actually need the actual
<i>elements</i> of a matrix! All that is required is that we can form
matrix-vector products. The same is true for preconditioners. In deal.II we
encode this requirement by only requiring that matrices and preconditioners
given to solver classes have a <code>vmult()</code> member function that
does the matrix-vector product. How a class chooses to implement this
function is not important to the solver. Consequently, classes can
implement it by, for example, doing a sequence of products and linear
solves as discussed above.


<h4>The LinearOperator framework in deal.II</h4>

deal.II includes support for describing such linear operations in a very
general way. This is done with the LinearOperator class that, like
@ref ConceptMatrixType "the MatrixType concept",
defines a minimal interface for <i>applying</i> a linear operation to a
vector:
@code
    std::function<void(Range &, const Domain &)> vmult;
    std::function<void(Range &, const Domain &)> vmult_add;
    std::function<void(Domain &, const Range &)> Tvmult;
    std::function<void(Domain &, const Range &)> Tvmult_add;
@endcode
The key difference between a LinearOperator and an ordinary matrix is
however that a LinearOperator does not allow any further access to the
underlying object. All you can do with a LinearOperator is to apply its
"action" to a vector! We take the opportunity to introduce the
LinearOperator concept at this point because it is a very useful tool that
allows you to construct complex solvers and preconditioners in a very
intuitive manner.

As a first example let us construct a LinearOperator object that represents
$M^{-1}$. This means that whenever the <code>vmult()</code> function of
this operator is called it has to solve a linear system. This requires us
to specify a solver (and corresponding) preconditioner. Assuming that
<code>M</code> is a reference to the upper left block of the system matrix
we can write:
@code
    const auto op_M = linear_operator(M);

    PreconditionJacobi<SparseMatrix<double>> preconditioner_M;
    preconditioner_M.initialize(M);

    ReductionControl reduction_control_M(2000, 1.0e-18, 1.0e-10);
    SolverCG<Vector<double>>    solver_M(reduction_control_M);

    const auto op_M_inv = inverse_operator(op_M, solver_M, preconditioner_M);
@endcode
Rather than using a SolverControl we use the ReductionControl class here
that stops iterations when either an absolute tolerance is reached (for
which we choose $10^{-18}$) or when the residual is reduced by a certain
factor (here, $10^{-10}$). In contrast the SolverControl class only checks
for absolute tolerances. We have to use ReductionControl in our case to
work around a minor issue: The right hand sides that we  will feed to
<code>op_M_inv</code> are essentially formed by residuals that naturally
decrease vastly in norm as the outer iterations progress. This makes
control by an absolute tolerance very error prone.

We now have a LinearOperator <code>op_M_inv</code> that we can use to
construct more complicated operators such as the Schur complement $S$.
Assuming that <code>B</code> is a reference to the upper right block
constructing a LinearOperator <code>op_S</code> is a matter of two lines:
@code
    const auto op_B = linear_operator(B);
    const auto op_S = transpose_operator(op_B) * op_M_inv * op_B;
@endcode
Here, the multiplication of three LinearOperator objects yields a composite
object <code>op_S</code> whose <code>vmult()</code> function first applies
$B$, then $M^{-1}$ (i.e. solving an equation with $M$), and finally $B^T$
to any given input vector. In that sense <code>op_S.vmult()</code> is
similar to the following code:
@code
    B.vmult (tmp1, src); // multiply with the top right block: B
    solver_M(M, tmp2, tmp1, preconditioner_M); // multiply with M^-1
    B.Tvmult (dst, tmp2); // multiply with the bottom left block: B^T
@endcode
(<code>tmp1</code> and <code>tmp2</code> are two temporary vectors). The
key point behind this approach is the fact that we never actually create an
inner product of matrices. Instead, whenever we have to perform a matrix
vector multiplication with <code>op_S</code> we simply run all individual
<code>vmult</code> operations in above sequence.

@note We could have achieved the same goal of creating a "matrix like"
object by implementing a specialized class <code>SchurComplement</code>
that provides a suitable <code>vmult()</code> function. Skipping over some
details this might have looked like the following:
@code
class SchurComplement
{
  public:

  // ...

  void SchurComplement::vmult (Vector<double>       &dst,
                               const Vector<double> &src) const
  {
    B.vmult (tmp1, src);
    solver_M(M, tmp2, tmp1, preconditioner_M);
    B.Tvmult (dst, tmp2);
  }
};
@endcode
Even though both approaches are exactly equivalent, the LinearOperator
class has a big advantage over this manual approach.
It provides so-called
<i><a href="https://en.wikipedia.org/wiki/Syntactic_sugar">syntactic sugar</a></i>:
Mathematically, we think about $S$ as being the composite matrix
$S=B^TM^{-1}B$ and the LinearOperator class allows you to write this out
more or less verbatim,
@code
const auto op_M_inv = inverse_operator(op_M, solver_M, preconditioner_M);
const auto op_S = transpose_operator(op_B) * op_M_inv * op_B;
@endcode
The manual approach on the other hand obscures this fact.

All that is left for us to do now is to form the right hand sides of the
two equations defining $P$ and $U$, and then solve them with the Schur
complement matrix and the mass matrix, respectively. For example the right
hand side of the first equation reads $B^TM^{-1}F-G$. This could be
implemented as follows:
@code
    Vector<double> schur_rhs (P.size());
    Vector<double> tmp (U.size());
    op_M_inv.vmult (tmp, F);
    transpose_operator(op_B).vmult (schur_rhs, tmp);
    schur_rhs -= G;
@endcode
Again, this is a perfectly valid approach, but the fact that deal.II
requires us to manually resize the final and temporary vector, and that
every operation takes up a new line makes this hard to read. This is the
point where a second class in the linear operator framework can will help
us. Similarly in spirit to LinearOperator, a PackagedOperation stores a
"computation":
@code
    std::function<void(Range &)> apply;
    std::function<void(Range &)> apply_add;
@endcode
The class allows
<a href="https://en.wikipedia.org/wiki/Lazy_evaluation">lazy evaluation</a>
of expressions involving vectors and linear operators. This is done by
storing the computational expression and only performing the computation
when either the object is converted to a vector object, or
PackagedOperation::apply() (or PackagedOperation::apply_add()) is invoked
by hand. Assuming that <code>F</code> and <code>G</code> are the two
vectors of the right hand side we can simply write:
@code
    const auto schur_rhs = transpose_operator(op_B) * op_M_inv * F - G;
@endcode
Here, <code>schur_rhs</code> is a PackagedOperation that <i>records</i> the
computation we specified. It does not create a vector with the actual
result immediately.

With these prerequisites at hand, solving for $P$ and $U$ is a matter of
creating another solver and inverse:
@code
    SolverControl solver_control_S(2000, 1.e-12);
    SolverCG<Vector<double>>    solver_S(solver_control_S);
    PreconditionIdentity preconditioner_S;

    const auto op_S_inv = inverse_operator(op_S, solver_S, preconditioner_S);

    P = op_S_inv * schur_rhs;
    U = op_M_inv * (F - op_B * P);
@endcode

@note The functionality that we developed in this example step by hand is
already readily available in the library. Have a look at
schur_complement(), condense_schur_rhs(), and postprocess_schur_solution().


<h4>A preconditioner for the Schur complement</h4>

One may ask whether it would help if we had a preconditioner for the Schur
complement $S=B^TM^{-1}B$. The general answer, as usual, is: of course. The
problem is only, we don't know anything about this Schur complement matrix. We
do not know its entries, all we know is its action. On the other hand, we have
to realize that our solver is expensive since in each iteration we have to do
one matrix-vector product with the Schur complement, which means that we have
to do invert the mass matrix once in each iteration.

There are different approaches to preconditioning such a matrix. On the one
extreme is to use something that is cheap to apply and therefore has no real
impact on the work done in each iteration. The other extreme is a
preconditioner that is itself very expensive, but in return really brings down
the number of iterations required to solve with $S$.

We will try something along the second approach, as much to improve the
performance of the program as to demonstrate some techniques. To this end, let
us recall that the ideal preconditioner is, of course, $S^{-1}$, but that is
unattainable. However, how about
@f{eqnarray*}{
  \tilde S^{-1} = [B^T ({\textrm{diag}\ }M)^{-1}B]^{-1}
@f}
as a preconditioner? That would mean that every time we have to do one
preconditioning step, we actually have to solve with $\tilde S$. At first,
this looks almost as expensive as solving with $S$ right away. However, note
that in the inner iteration, we do not have to calculate $M^{-1}$, but only
the inverse of its diagonal, which is cheap.

Thankfully, the LinearOperator framework makes this very easy to write out.
We already used a Jacobi preconditioner (<code>preconditioner_M</code>) for
the $M$ matrix earlier. So all that is left to do is to write out how the
approximate Schur complement should look like:
@code
    const auto op_aS =
      transpose_operator(op_B) * linear_operator(preconditioner_M) * op_B;
@endcode
Note how this operator differs in simply doing one Jacobi sweep
(i.e. multiplying with the inverses of the diagonal) instead of multiplying
with the full $M^{-1}$. (This is how a single Jacobi preconditioner step
with $M$ is defined: it is the multiplication with the inverse of the
diagonal of $M$; in other words, the operation $({\textrm{diag}\ }M)^{-1}x$
on a vector $x$ is exactly what PreconditionJacobi does.)

With all this we almost have the preconditioner completed: it should be the
inverse of the approximate Schur complement. We implement this again by
creating a linear operator with inverse_operator() function. This time
however we would like to choose a relatively modest tolerance for the CG
solver (that inverts <code>op_aS</code>). The reasoning is that
<code>op_aS</code> is only coarse approximation to <code>op_S</code>, so we
actually do not need to invert it exactly. This, however creates a subtle
problem: <code>preconditioner_S</code> will be used in the final outer CG
iteration to create an orthogonal basis. But for this to work, it must be
precisely the same linear operation for every invocation. We ensure this by
using an IterationNumberControl that allows us to fix the number of CG
iterations that are performed to a fixed small number (in our case 30):
@code
    IterationNumberControl iteration_number_control_aS(30, 1.e-18);
    SolverCG<Vector<double>>           solver_aS(iteration_number_control_aS);
    PreconditionIdentity preconditioner_aS;
    const auto preconditioner_S =
      inverse_operator(op_aS, solver_aS, preconditioner_aS);
@endcode

That's all!

Obviously, applying this inverse of the approximate Schur complement is a very
expensive preconditioner, almost as expensive as inverting the Schur
complement itself. We can expect it to significantly reduce the number of
outer iterations required for the Schur complement. In fact it does: in a
typical run on 7 times refined meshes using elements of order 0, the number of
outer iterations drops from 592 to 39. On the other hand, we now have to apply
a very expensive preconditioner 25 times. A better measure is therefore simply
the run-time of the program: on a current laptop (as of January 2019), it
drops from 3.57 to 2.05 seconds for this test case. That doesn't seem too
impressive, but the savings become more pronounced on finer meshes and with
elements of higher order. For example, an seven times refined mesh and
using elements of order 2 (which amounts to about 0.4 million degrees of
freedom) yields an improvement of 1134 to 83 outer iterations, at a runtime
of 168 seconds to 40 seconds. Not earth shattering, but significant.


<h3>Definition of the test case</h3>

In this tutorial program, we will solve the Laplace equation in mixed
formulation as stated above. Since we want to monitor convergence of the
solution inside the program, we choose right hand side, boundary conditions,
and the coefficient so that we recover a solution function known to us. In
particular, we choose the pressure solution
@f{eqnarray*}{
  p = -\left(\frac \alpha 2 xy^2 + \beta x - \frac \alpha 6 x^3\right),
@f}
and for the coefficient we choose the unit matrix $K_{ij}=\delta_{ij}$ for
simplicity. Consequently, the exact velocity satisfies
@f{eqnarray*}{
  {\mathbf u} =
  \left(\begin{array}{cc}
    \frac \alpha 2 y^2 + \beta - \frac \alpha 2 x^2 \\
    \alpha xy
  \end{array}\right).
@f}
This solution was chosen since it is exactly divergence free, making it a
realistic test case for incompressible fluid flow. By consequence, the right
hand side equals $f=0$, and as boundary values we have to choose
$g=p|_{\partial\Omega}$.

For the computations in this program, we choose $\alpha=0.3,\beta=1$. You can
find the resulting solution in the <a href="#Results">results section
below</a>, after the commented program.
