<h1>Results</h1>

As in many of the tutorials, the actual output of the program matters less
than how we arrived there. Nonetheless, here it is:
@code
===========================================
Number of active cells: 48
Number of degrees of freedom: 65

Time step 1 at t=0.002
     7 CG iterations.

===========================================
Number of active cells: 60
Number of degrees of freedom: 81


Time step 1 at t=0.002
     7 CG iterations.

===========================================
Number of active cells: 105
Number of degrees of freedom: 136


Time step 1 at t=0.002
     7 CG iterations.

[...]

Time step 249 at t=0.498
     13 CG iterations.
Time step 250 at t=0.5
     14 CG iterations.

===========================================
Number of active cells: 1803
Number of degrees of freedom: 2109
@endcode

Maybe of more interest is a visualization of the solution and the mesh on which
it was computed:

<img src="https://www.dealii.org/images/steps/developer/step-26.movie.gif" alt="Animation of the solution of step 26.">

The movie shows how the two sources switch on and off and how the mesh reacts
to this. It is quite obvious that the mesh as is is probably not the best we
could come up with. We'll get back to this in the next section.


<a name="step-26-extensions"></a>
<h3>Possibilities for extensions</h3>

There are at least two areas where one can improve this program significantly:
adaptive time stepping and a better choice of the mesh.

<h4>Adaptive time stepping</h4>

Having chosen an implicit time stepping scheme, we are not bound by any
CFL-like condition on the time step. Furthermore, because the time scales on
which change happens on a given cell in the heat equation are not bound to the
cells diameter (unlike the case with the wave equation, where we had a fixed
speed of information transport that couples the temporal and spatial scales),
we can choose the time step as we please. Or, better, choose it as we deem
necessary for accuracy.

Looking at the solution, it is clear that the action does not happen uniformly
over time: a lot is changing around the times when we switch on a source, things
become less dramatic once a source is on for a little while, and we enter a
long phase of decline when both sources are off. During these times, we could
surely get away with a larger time step than before without sacrificing too
much accuracy.

The literature has many suggestions on how to choose the time step size
adaptively. Much can be learned, for example, from the way ODE solvers choose
their time steps. One can also be inspired by a posteriori error estimators
that can, ideally, be written in a way that they consist of a temporal and a
spatial contribution to the overall error. If the temporal one is too large,
we should choose a smaller time step. Ideas in this direction can be found,
for example, in the PhD thesis of a former principal developer of deal.II,
Ralf Hartmann, published by the University of Heidelberg, Germany, in 2002
(see @cite Har02).


<h4>Better time stepping methods</h4>

We here use one of the simpler time stepping methods, namely the second order
in time Crank-Nicolson method. This is surely already better than the even
more widely used (and even less accurate) implicit (=backward) Euler method,
but many other, more accurate methods such as BDF or
Runge-Kutta methods are available and should be used as they do not represent
much additional effort. It is not difficult to implement this for the current
program, if one wanted; a more systematic treatment is also given in step-52.

As a general rule, however, one should not be implementing time stepping
methods by hand, as we do here, for problems that do not require
exploiting special properties of the equation and consequently require
specialized time stepping methods. (The heat equation does not fall into
this category, and "standard" time stepping methods are all we need here.)
Rather, one should use one of the available
high-quality libraries for time stepping, for the same reasons as one should
not be implementing finite element methods by hand but use deal.II instead.
Indeed, deal.II has interfaces to two such time stepping library,
[SUNDIALS](https://computing.llnl.gov/projects/sundials) and the
[PETSc TS package](https://petsc.org/release/manualpages/TS/TS/), already available.
In particular, the SUNDIALS::ARKode class would make for a great starting
point for the use of much better (and much more accurate) time steppers,
as would PETScWrappers::TimeStepper;
the methods one would then get also implement the automatic time step
control mentioned above. To see how this works, take a look at
step-86.


<h4>Better refinement criteria</h4>

If you look at the meshes in the movie above, it is clear that they are not
particularly well suited to the task at hand. In fact, they look rather
random.

There are two factors at play. First, there are some islands where cells
have been refined but that are surrounded by non-refined cells (and there
are probably also a few occasional coarsened islands). These are not terrible,
as they most of the time do not affect the approximation quality of the mesh,
but they also don't help because so many of their additional degrees of
freedom are in fact constrained by hanging node constraints. That said,
this is easy to fix: the Triangulation class takes an argument to its
constructor indicating a level of "mesh smoothing". Passing one of many
possible flags, this instructs the triangulation to refine some additional
cells, or not to refine some cells, so that the resulting mesh does not have
these artifacts.

The second problem is more severe: the mesh appears to lag the solution.
The underlying reason is that we only adapt the mesh once every fifth
time step, and only allow for a single refinement in these cases. Whenever a
source switches on, the solution had been very smooth in this area before and
the mesh was consequently rather coarse. This implies that the next time step
when we refine the mesh, we will get one refinement level more in this area,
and five time steps later another level, etc. But this is not enough: first,
we should refine immediately when a source switches on (after all, in the
current context we at least know what the right hand side is), and we should
allow for more than one refinement level. Of course, all of this can be done
using deal.II, it just requires a bit of algorithmic thinking in how to make
this work!


<h4>Positivity preservation</h4>

To increase the accuracy and resolution of your simulation in time, one
typically decreases the time step size $k_n$. If you start playing around
with the time step in this particular example, you will notice that the
solution becomes partly negative, if $k_n$ is below a certain threshold.
This is not what we would expect to happen (in nature).

To get an idea of this behavior mathematically, let us consider a general,
fully discrete problem:
@f{align*}{
  A u^{n} = B u^{n-1}.
@f}
The general form of the $i$th equation then reads:
@f{align*}{
  a_{ii} u^{n}_i &= b_{ii} u^{n-1}_i +
  \sum\limits_{j \in S_i} \left( b_{ij} u^{n-1}_j - a_{ij} u^{n}_j \right),
@f}
where $S_i$ is the set of degrees of freedom that DoF $i$ couples with (i.e.,
for which either the matrix $A$ or matrix $B$ has a nonzero entry at position
$(i,j)$). If all coefficients
fulfill the following conditions:
@f{align*}{
  a_{ii} &> 0, & b_{ii} &\geq 0, & a_{ij} &\leq 0, & b_{ij} &\geq 0,
  &
  \forall j &\in S_i,
@f}
all solutions $u^{n}$ keep their sign from the previous ones $u^{n-1}$, and
consequently from the initial values $u^0$. See e.g.
<a href="http://bookstore.siam.org/cs14/">Kuzmin, H&auml;m&auml;l&auml;inen</a>
for more information on positivity preservation.

Depending on the PDE to solve and the time integration scheme used, one is
able to deduce conditions for the time step $k_n$. For the heat equation with
the Crank-Nicolson scheme,
<a href="https://doi.org/10.2478/cmam-2010-0025">Schatz et. al.</a> have
translated it to the following ones:
@f{align*}{
  (1 - \theta) k a_{ii} &\leq m_{ii},\qquad \forall i,
  &
  \theta k \left| a_{ij} \right| &\geq m_{ij},\qquad j \neq i,
@f}
where $M = m_{ij}$ denotes the @ref GlossMassMatrix "mass matrix" and $A = a_{ij}$ the stiffness
matrix with $a_{ij} \leq 0$ for $j \neq i$, respectively. With
$a_{ij} \leq 0$, we can formulate bounds for the global time step $k$ as
follows:
@f{align*}{
  k_{\text{max}} &= \frac{ 1 }{ 1 - \theta }
  \min\left( \frac{ m_{ii} }{ a_{ii} } \right),~ \forall i,
  &
  k_{\text{min}} &= \frac{ 1 }{ \theta  }
  \max\left( \frac{ m_{ij} }{ \left|a_{ij}\right| } \right),~ j \neq i.
@f}
In other words, the time step is constrained by <i>both a lower
and upper bound</i> in case of a Crank-Nicolson scheme. These bounds should be
considered along with the CFL condition to ensure significance of the performed
simulations.

Being unable to make the time step as small as we want to get more
accuracy without losing the positivity property is annoying. It raises
the question of whether we can at least <i>compute</i> the minimal time step
we can choose  to ensure positivity preservation in this particular tutorial.
Indeed, we can use
the SparseMatrix objects for both mass and stiffness that are created via
the MatrixCreator functions. Iterating through each entry via SparseMatrixIterators
lets us check for diagonal and off-diagonal entries to set a proper time step
dynamically. For quadratic matrices, the diagonal element is stored as the
first member of a row (see SparseMatrix documentation). An exemplary code
snippet on how to grab the entries of interest from the <code>mass_matrix</code>
is shown below.

@code
Assert (mass_matrix.m() == mass_matrix.n(), ExcNotQuadratic());
const unsigned int num_rows = mass_matrix.m();
double mass_matrix_min_diag    = std::numeric_limits<double>::max(),
       mass_matrix_max_offdiag = 0.;

SparseMatrixIterators::Iterator<double,true> row_it (&mass_matrix, 0);

for(unsigned int m = 0; m<num_rows; ++m)
{
  // check the diagonal element
  row_it = mass_matrix.begin(m);
  mass_matrix_min_diag = std::min(row_it->value(), mass_matrix_min_diag);
  ++row_it;

  // check the off-diagonal elements
  for(; row_it != mass_matrix.end(m); ++row_it)
    mass_matrix_max_offdiag = std::max(row_it->value(), mass_matrix_max_offdiag);
}
@endcode

Using the information so computed, we can bound the time step via the formulas
above.
