<h1>Results</h1>

The output of the program looks as follows:
@code
Number of active cells: 1024
Number of degrees of freedom: 1089
36 CG iterations needed to obtain convergence.
Output written to solution.vtk
@endcode

The last line is the output we generated at the bottom of the
`output_results()` function: The program generated the file
<code>solution.vtk</code>, which is in the VTK format that is widely
used by many visualization programs today -- including the two
heavy-weights <a href="https://www.llnl.gov/visit">VisIt</a> and
<a href="https://www.paraview.org">Paraview</a> that are the most
commonly used programs for this purpose today.

Using VisIt, it is not very difficult to generate a picture of the
solution like this:
<table width="60%" align="center">
  <tr>
    <td align="center">
      <img src="https://www.dealii.org/images/steps/developer/step-3.solution-3.png" alt="Visualization of the solution of step-3">
    </td>
  </tr>
</table>
It shows both the solution and the mesh, elevated above the $x$-$y$ plane
based on the value of the solution at each point. Of course the solution
here is not particularly exciting, but that is a result of both what the
Laplace equation represents and the right hand side $f(\mathbf x)=1$ we
have chosen for this program: The Laplace equation describes (among many
other uses) the vertical deformation of a membrane subject to an external
(also vertical) force. In the current example, the membrane's borders
are clamped to a square frame with no vertical variation; a constant
force density will therefore intuitively lead to a membrane that
simply bulges upward -- like the one shown above.

VisIt and Paraview both allow playing with various kinds of visualizations
of the solution. Several video lectures show how to use these programs.
@dealiiVideoLectureSeeAlso{11,32}



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

If you want to play around a little bit with this program, here are a few
suggestions:
</p>

<ul>
  <li>
  Change the geometry and mesh: In the program, we have generated a square
  domain and mesh by using the GridGenerator::hyper_cube()
  function. However, the <code>GridGenerator</code> has a good number of other
  functions as well. Try an L-shaped domain, a ring, or other domains you find
  there.
  </li>

  <li>
  Change the boundary condition: The code uses the Functions::ZeroFunction
  function to generate zero boundary conditions. However, you may want to try
  non-zero constant boundary values using
  <code>Functions::ConstantFunction&lt;2&gt;(1)</code> instead of
  <code>Functions::ZeroFunction&lt;2&gt;()</code> to have unit Dirichlet boundary
  values. More exotic functions are described in the documentation of the
  Functions namespace, and you may pick one to describe your particular boundary
  values.
  </li>

  <li> Modify the type of boundary condition: Presently, what happens
  is that we use Dirichlet boundary values all around, since the
  default is that all boundary parts have boundary indicator zero, and
  then we tell the
  VectorTools::interpolate_boundary_values() function to
  interpolate boundary values to zero on all boundary components with
  indicator zero.  <p> We can change this behavior if we assign parts
  of the boundary different indicators. For example, try this
  immediately after calling GridGenerator::hyper_cube():
  @code
  triangulation.begin_active()->face(0)->set_boundary_id(1);
  @endcode

  What this does is it first asks the triangulation to
  return an iterator that points to the first active cell. Of course,
  this being the coarse mesh for the triangulation of a square, the
  triangulation has only a single cell at this moment, and it is
  active. Next, we ask the cell to return an iterator to its first
  face, and then we ask the face to reset the boundary indicator of
  that face to 1. What then follows is this: When the mesh is refined,
  faces of child cells inherit the boundary indicator of their
  parents, i.e. even on the finest mesh, the faces on one side of the
  square have boundary indicator 1. Later, when we get to
  interpolating boundary conditions, the
  VectorTools::interpolate_boundary_values() call will only produce boundary
  values for those faces that have zero boundary indicator, and leave
  those faces alone that have a different boundary indicator. What
  this then does is to impose Dirichlet boundary conditions on the
  former, and homogeneous Neumann conditions on the latter (i.e. zero
  normal derivative of the solution, unless one adds additional terms
  to the right hand side of the variational equality that deal with
  potentially non-zero Neumann conditions). You will see this if you
  run the program.

  An alternative way to change the boundary indicator is to label
  the boundaries based on the Cartesian coordinates of the face centers.
  For example, we can label all of the cells along the top and
  bottom boundaries with a boundary indicator 1 by checking to
  see if the cell centers' y-coordinates are within a tolerance
  (here `1e-12`) of -1 and 1. Try this immediately after calling
  GridGenerator::hyper_cube(), as before:
  @code
  for (auto &face : triangulation.active_face_iterators())
    if (face->at_boundary())
      if (std::fabs(face->center()(1) - (-1.0)) < 1e-12 ||
          std::fabs(face->center()(1) - (1.0)) < 1e-12)
        face->set_boundary_id(1);
  @endcode
  Although this code is a bit longer than before, it is useful for
  complex geometries, as it does not require knowledge of face labels.

  <li>
  A slight variation of the last point would be to set different boundary
  values as above, but then use a different boundary value function for
  boundary indicator one. In practice, what you have to do is to add a second
  call to <code>interpolate_boundary_values</code> for boundary indicator one:
  @code
  VectorTools::interpolate_boundary_values(dof_handler,
					   types::boundary_id(1),
					   Functions::ConstantFunction<2>(1.),
					   boundary_values);
  @endcode
  If you have this call immediately after the first one to this function, then
  it will interpolate boundary values on faces with boundary indicator 1 to the
  unit value, and merge these interpolated values with those previously
  computed for boundary indicator 0. The result will be that we will get
  discontinuous boundary values, zero on three sides of the square, and one on
  the fourth.

  <li>
  Use triangles: As mentioned in the results section of step-1, for
  historical reasons, almost all tutorial programs for deal.II are
  written using quadrilateral or hexahedral meshes. But deal.II also
  supports triangular and tetrahedral meshes. So a good experiment would
  be to replace the mesh used here by a triangular mesh.

  This is *almost* trivial. First, as discussed in step-1, we may want
  to start with the quadrilateral mesh we are already creating, and
  then convert it into a triangular one. You can do that by replacing
  the first line of `Step3::make_grid()` by the following code:
  @code
  Triangulation<2> triangulation_quad;
  GridGenerator::hyper_cube(triangulation_quad, -1, 1);
  GridGenerator::convert_hypercube_to_simplex_mesh (triangulation_quad,
                                                    triangulation);
  @endcode
  The GridGenerator::convert_hypercube_to_simplex_mesh() replaces each
  quadrilateral by eight triangles with half the diameter of the original
  quadrilateral; as a consequence, the resulting mesh is substantially
  finer and one might expect that the solution is consequently more
  accurate (but also has many more degrees of freedom). That is a question
  you can explore with the techniques discussed in the "Results" section
  of step-4, but that goes beyond what we want to demonstrate here.

  If you run this program, you will run into an error message that
  will look something like this:
  @code
--------------------------------------------------------
An error occurred in line <2633> of file </home/bangerth/p/deal.II/1/dealii/include/deal.II/dofs/dof_accessor.templates.h> in function
    const dealii::FiniteElement<dimension_, space_dimension_>& dealii::DoFCellAccessor<dim, spacedim, lda>::get_fe() const [with int dimension_ = 2; int space_dimension_ = 2; bool level_dof_access = false]
The violated condition was:
    this->reference_cell() == fe.reference_cell()
Additional information:
    The reference-cell type used on this cell (Tri) does not match the
    reference-cell type of the finite element associated with this cell
    (Quad). Did you accidentally use simplex elements on hypercube meshes
    (or the other way around), or are you using a mixed mesh and assigned
    a simplex element to a hypercube cell (or the other way around) via
    the active_fe_index?
  @endcode
  It is worth carefully reading the error message. It doesn't just
  state that there is an error, but also how it may have
  arisen. Specifically, it asks whether we are using a finite element
  for simplex meshes (in 2d simplices are triangles) with a hypercube
  mesh (in 2d hypercubes are quadrilaterals) or the other way around?

  Of course, this is exactly what we are doing, though this may
  perhaps not be clear to you. But if you look up the documentation,
  you will find that the FE_Q element we use in the main class can
  only be used on hypercube meshes; what we *want* to use instead now
  that we are using a simplex mesh is the FE_SimplexP class that is the
  equivalent to FE_Q for simplex cells. (To do this, you will also
  have to add `#include <deal.II/fe/fe_simplex_p.h>` at the top of the file.)

  The last thing you need to change (which at the time of writing is
  unfortunately not prompted by getting an error message) is that when
  we integrate, we need to use a quadrature formula that is
  appropriate for triangles. This is done by changing QGauss by
  QGaussSimplex in the code.

  With all of these steps, you then get the following solution:
  <img src="https://www.dealii.org/images/steps/developer/step-3.solution-triangles.png" alt="Visualization of the solution of step-3 using triangles">

  <li>
  Observe convergence: We will only discuss computing errors in norms in
  step-7, but it is easy to check that computations converge
  already here. For example, we could evaluate the value of the solution in a
  single point and compare the value for different %numbers of global
  refinement (the number of global refinement steps is set in
  <code>LaplaceProblem::make_grid</code> above). To evaluate the
  solution at a point, say at $(\frac 13, \frac 13)$, we could add the
  following code to the <code>LaplaceProblem::output_results</code> function:
  @code
    std::cout << "Solution at (1/3,1/3): "
              << VectorTools::point_value(dof_handler, solution,
                                          Point<2>(1./3, 1./3))
              << std::endl;
  @endcode
  For 1 through 9 global refinement steps, we then get the following sequence
  of point values:
  <table align="center" class="doxtable">
    <tr> <th># of refinements</th> <th>$u_h(\frac 13,\frac13)$</th> </tr>
    <tr> <td>1</td> <td>0.166667</td> </tr>
    <tr> <td>2</td> <td>0.227381</td> </tr>
    <tr> <td>3</td> <td>0.237375</td> </tr>
    <tr> <td>4</td> <td>0.240435</td> </tr>
    <tr> <td>5</td> <td>0.241140</td> </tr>
    <tr> <td>6</td> <td>0.241324</td> </tr>
    <tr> <td>7</td> <td>0.241369</td> </tr>
    <tr> <td>8</td> <td>0.241380</td> </tr>
    <tr> <td>9</td> <td>0.241383</td> </tr>
  </table>
  By noticing that the difference between each two consecutive values reduces
  by about a factor of 4, we can conjecture that the "correct" value may be
  $u(\frac 13, \frac 13)\approx 0.241384$. In fact, if we assumed this to be
  the correct value, we could show that the sequence above indeed shows ${\cal
  O}(h^2)$ convergence &mdash; theoretically, the convergence order should be
  ${\cal O}(h^2 |\log h|)$ but the symmetry of the domain and the mesh may lead
  to the better convergence order observed.

  A slight variant of this would be to repeat the test with quadratic
  elements. All you need to do is to set the polynomial degree of the finite
  element to two in the constructor
  <code>LaplaceProblem::LaplaceProblem</code>.

  <li>Convergence of the mean: A different way to see that the solution
  actually converges (to something &mdash; we can't tell whether it's really
  the correct value!) is to compute the mean of the solution. To this end, add
  the following code to <code>LaplaceProblem::output_results</code>:
  @code
    std::cout << "Mean value: "
              << VectorTools::compute_mean_value (dof_handler,
						  QGauss<2>(fe.degree + 1),
						  solution,
						  0)
              << std::endl;
  @endcode
  The documentation of the function explains what the second and fourth
  parameters mean, while the first and third should be obvious. Doing the same
  study again where we change the number of global refinement steps, we get
  the following result:
  <table align="center" class="doxtable">
    <tr> <th># of refinements</th> <th>$\int_\Omega u_h(x)\; dx$</th> </tr>
    <tr> <td>0</td> <td>0.09375000</td> </tr>
    <tr> <td>1</td> <td>0.12790179</td> </tr>
    <tr> <td>2</td> <td>0.13733440</td> </tr>
    <tr> <td>3</td> <td>0.13976069</td> </tr>
    <tr> <td>4</td> <td>0.14037251</td> </tr>
    <tr> <td>5</td> <td>0.14052586</td> </tr>
    <tr> <td>6</td> <td>0.14056422</td> </tr>
    <tr> <td>7</td> <td>0.14057382</td> </tr>
    <tr> <td>8</td> <td>0.14057622</td> </tr>
  </table>
  Again, the difference between two adjacent values goes down by about a
  factor of four, indicating convergence as ${\cal O}(h^2)$.
</ul>



<h3>Using %HDF5 to output the solution and additional data</h3>

%HDF5 is a commonly used format that can be read by many scripting
languages (e.g. R or Python). It is not difficult to get deal.II to
produce some %HDF5 files that can then be used in external scripts to
postprocess some of the data generated by this program. Here are some
ideas on what is possible.


<h4> Changing the output to .h5</h4>

To fully make use of the automation we first need to introduce a private variable for the number of
global refinement steps <code>unsigned int n_refinement_steps </code>, which will be used for the output filename.
In <code>make_grid()</code> we then replace <code>triangulation.refine_global(5);</code> with
@code
n_refinement_steps = 5;
triangulation.refine_global(n_refinement_steps);
@endcode
The deal.II library has two different %HDF5 bindings, one in the HDF5
namespace (for interfacing to general-purpose data files)
and another one in DataOut (specifically for writing files for the
visualization of solutions).
Although the HDF5 deal.II binding supports both serial and MPI, the %HDF5 DataOut binding
only supports parallel output.
For this reason we need to initialize an MPI
communicator with only one processor. This is done by adding the following code.
@code
int main(int argc, char* argv[])
{
  Utilities::MPI::MPI_InitFinalize mpi_initialization(argc, argv, 1);
  ...
}
@endcode
Next we change the `Step3::output_results()` output routine as
described in the DataOutBase namespace documentation:
@code
const std::string filename_h5 = "solution_" + std::to_string(n_refinement_steps) + ".h5";
DataOutBase::DataOutFilterFlags flags(true, true);
DataOutBase::DataOutFilter data_filter(flags);
data_out.write_filtered_data(data_filter);
data_out.write_hdf5_parallel(data_filter, filename_h5, MPI_COMM_WORLD);
@endcode
The resulting file can then be visualized just like the VTK file that
the original version of the tutorial produces; but, since %HDF5 is a
more general file format, it can also easily be processed in scripting
languages for other purposes.


<h4> Adding the point value and the mean (see extension above) into the .h5 file</h4>

After outputting the solution, the file can be opened again to include
more datasets.  This allows us to keep all the necessary information
of our experiment in a single result file, which can then be read and
processed by some postprocessing script.
(Have a look at HDF5::Group::write_dataset() for further
information on the possible output options.)

To make this happen, we first include the necessary header into our file:
@code
#include <deal.II/base/hdf5.h>
@endcode
Adding the following lines to the end
of our output routine adds the information about the value of the
solution at a particular point, as well as the mean value of the
solution, to our %HDF5 file:
@code
HDF5::File data_file(filename_h5, HDF5::File::FileAccessMode::open, MPI_COMM_WORLD);
Vector<double> point_value(1);
point_value[0] = VectorTools::point_value(dof_handler, solution,
                                          Point<2>(1./3, 1./3));
data_file.write_dataset("point_value", point_value);
Vector<double> mean_value(1);
mean_value[0] = VectorTools::compute_mean_value(dof_handler,
                                                QGauss<2>(fe.degree + 1),
                                                solution, 0);
data_file.write_dataset("mean_value",mean_value);
@endcode



<h3> Using R and ggplot2 to generate plots</h3>
@note Alternatively, one could use the python code in the next subsection.

The data put into %HDF5 files above can then be used from scripting
languages for further postprocessing. In the following, let us show
how this can, in particular, be done with the
<a href="https://en.wikipedia.org/wiki/R_(programming_language)">R
programming language</a>, a widely used language in statistical data
analysis. (Similar things can also be done in Python, for example.)
If you are unfamiliar with R and ggplot2 you could check out the data carpentry course on R
<a href="https://datacarpentry.org/R-ecology-lesson/index.html">here</a>.
Furthermore, since most search engines struggle with searches of the form "R + topic",
we recommend using the specializes service <a
href="http://rseek.org">RSeek </a> instead.

The most prominent difference between R and other languages is that
the assignment operator (`a = 5`) is typically written as
`a <- 5`. As the latter is considered standard we will use it in our examples as well.
To open the `.h5` file in R you have to install the <a href="https://bioconductor.org/packages/release/bioc/html/rhdf5.html">rhdf5</a> package, which is a part of the Bioconductor package.

First we will include all necessary packages and have a look at how the data is structured in our file.
@code{.r}
library(rhdf5)     # library for handling HDF5 files
library(ggplot2)   # main plotting library
library(grDevices) # needed for output to PDF
library(viridis)   # contains good colormaps for sequential data

refinement <- 5
h5f <- H5Fopen(paste("solution_",refinement,".h5",sep=""))
print(h5f)
@endcode
This gives the following output
@code{.unparsed}
HDF5 FILE
   name /
filename

    name       otype  dclass     dim
0 cells       H5I_DATASET INTEGER  x 1024
1 mean_value  H5I_DATASET FLOAT   1
2 nodes       H5I_DATASET FLOAT    x 1089
3 point_value H5I_DATASET FLOAT   1
4 solution    H5I_DATASET FLOAT    x 1089
@endcode
The datasets can be accessed by <code>h5f\$name</code>. The function
<code>dim(h5f\$cells)</code> gives us the dimensions of the matrix
that is used to store our cells.
We can see the following three matrices, as well as the two
additional data points we added.
<ul>
<li> <code>cells</code>: a 4x1024 matrix that stores the  (C++) vertex indices for each cell
<li> <code>nodes</code>: a 2x1089 matrix storing the position values (x,y) for our cell vertices
<li> <code>solution</code>: a 1x1089 matrix storing the values of our solution at each vertex
</ul>
Now we can use this data to generate various plots. Plotting with ggplot2 usually splits into two steps.
At first the data needs to be manipulated and added to a <code>data.frame</code>.
After that, a <code>ggplot</code> object is constructed and manipulated by adding plot elements to it.

<code>nodes</code> and <code>cells</code> contain all the information we need to plot our grid.
The following code wraps all the data into one dataframe for plotting our grid:
@code{.r}
# Counting in R starts at 1 instead of 0, so we need to increment all
# vertex indices by one:
cell_ids <- h5f$cells+1

# Store the x and y positions of each vertex in one big vector in a
# cell by cell fashion (every 4 entries belong to one cell):
cells_x <- h5f$nodes[1,][cell_ids]
cells_y <- h5f$nodes[2,][cell_ids]

# Construct a vector that stores the matching cell by cell grouping
# (1,1,1,1,2,2,2,2,...):
groups <- rep(1:ncol(cell_ids),each=4)

# Finally put everything into one dataframe:
meshdata <- data.frame(x = cells_x, y = cells_y, id = groups)
@endcode

With the finished dataframe we have everything we need to plot our grid:
@code{.r}
pdf (paste("grid_",refinement,".pdf",sep=""),width = 5,height = 5) # Open new PDF file
plt <- ggplot(meshdata,aes(x=x,y=y,group=id))                      # Construction of our plot
                                                                   # object, at first only data

plt <- plt + geom_polygon(fill="white",colour="black")             # Actual plotting of the grid as polygons
plt <- plt + ggtitle(paste("grid at refinement level #",refinement))

print(plt)                                                         # Show the current state of the plot/add it to the pdf
dev.off()                                                          # Close PDF file
@endcode

The contents of this file then look as follows (not very exciting, but
you get the idea):
<table width="60%" align="center">
  <tr>
   <td align="center">
     <img src="https://www.dealii.org/images/steps/developer/step-3.extensions.grid_5.png" alt="Grid after 5 refinement steps of step-3">
   </td>
  </tr>
</table>

We can also visualize the solution itself, and this is going to look
more interesting.
To make a 2D pseudocolor plot of our solution we will use <code>geom_raster</code>.
This function needs a structured grid, i.e. uniform in x and y directions.
Luckily our data at this point is structured in the right way.
The following code plots a pseudocolor representation of our surface into a new PDF:
@code{.r}
pdf (paste("pseudocolor_",refinement,".pdf",sep=""),width = 5,height = 4.2) # Open new PDF file
colordata <- data.frame(x = h5f$nodes[1,],y = h5f$nodes[2,] , solution = h5f$solution[1,])
plt <- ggplot(colordata,aes(x=x,y=y,fill=solution))
plt <- plt + geom_raster(interpolate=TRUE)
plt <- plt + scale_fill_viridis()
plt <- plt + ggtitle(paste("solution at refinement level #",refinement))

print(plt)
dev.off()
H5Fclose(h5f) # Close the HDF5 file
@endcode
This is now going to look as follows:
<table width="60%" align="center">
 <tr>
   <td align="center">
     <img src="https://www.dealii.org/images/steps/developer/step-3.extensions.pseudocolor_5.png" alt="Solution after 5 refinement steps of step-3">
   </td>
 </tr>
</table>

For plotting the convergence curves we need to re-run the C++ code multiple times with different values for <code>n_refinement_steps</code>
starting from 1.
Since every file only contains a single data point we need to loop over them and concatenate the results into a single vector.
@code{.r}
n_ref <- 8   # Maximum refinement level for which results are existing

# First we initiate all vectors with the results of the first level
h5f   <- H5Fopen("solution_1.h5")
dofs  <- dim(h5f$solution)[2]
mean  <- h5f$mean_value
point <- h5f$point_value
H5Fclose(h5f)

for (reflevel in 2:n_ref)
{
   h5f   <- H5Fopen(paste("solution_",reflevel,".h5",sep=""))
   dofs  <- c(dofs,dim(h5f\$solution)[2])
   mean  <- c(mean,h5f\$mean_value)
   point <- c(point,h5f\$point_value)
   H5Fclose(h5f)
}
@endcode
As we are not interested in the values themselves but rather in the error compared to a "exact" solution we will
assume our highest refinement level to be that solution and omit it from the data.
@code{.r}
# Calculate the error w.r.t. our maximum refinement step
mean_error  <- abs(mean[1:n_ref-1]-mean[n_ref])
point_error <- abs(point[1:n_ref-1]-point[n_ref])

# Remove the highest value from our DoF data
dofs     <- dofs[1:n_ref-1]
convdata <- data.frame(dofs = dofs, mean_value= mean_error, point_value = point_error)
@endcode
Now we have all the data available to generate our plots.
It is often useful to plot errors on a log-log scale, which is
accomplished in the following code:
@code
pdf (paste("convergence.pdf",sep=""),width = 5,height = 4.2)
plt <- ggplot(convdata,mapping=aes(x = dofs, y = mean_value))
plt <- plt+geom_line()
plt <- plt+labs(x="#DoFs",y = "mean value error")
plt <- plt+scale_x_log10()+scale_y_log10()
print(plt)

plt <- ggplot(convdata,mapping=aes(x = dofs, y = point_value))
plt <- plt+geom_line()
plt <- plt+labs(x="#DoFs",y = "point value error")
plt <- plt+scale_x_log10()+scale_y_log10()
print(plt)

dev.off()
@endcode
This results in the following plot that shows how the errors in the
mean value and the solution value at the chosen point nicely converge
to zero:
<table style="width:50%" align="center">
  <tr>
    <td><img src="https://www.dealii.org/images/steps/developer/step-3.extensions.convergence_mean.png" alt=""></td>
    <td><img src="https://www.dealii.org/images/steps/developer/step-3.extensions.convergence_point.png" alt=""></td>
  </tr>
</table>

<h3> Using python to generate plots</h3>

In this section we discuss the postprocessing of the data stored in %HDF5 files using the "python" programming language.
The necessary packages to import are
@code{.py}
import numpy as np                        # to work with multidimensional arrays
import h5py                               # to work with %HDF5 files

import pandas as pd                       # for data frames
import matplotlib.pyplot as plt           # plotting
from matplotlib.patches import Polygon

from scipy.interpolate import griddata    # interpolation function
from matplotlib import cm                 # for colormaps

@endcode
The %HDF5 solution file corresponding to `refinement = 5` can be opened as
@code{.py}
refinement = 5
filename = "solution_%d.h5" % refinement
file = h5py.File(filename, "r")
@endcode
The following prints out the tables in the %HDF5 file
@code{.py}
for key, value in file.items():
    print(key, " : ", value)
@endcode
which prints out
@code{.unparsed}
cells  :  <HDF5 dataset "cells": shape (1024, 4), type "<u4">
mean_value  :  <HDF5 dataset "mean_value": shape (1,), type "<f8">
nodes  :  <HDF5 dataset "nodes": shape (1089, 2), type "<f8">
point_value  :  <HDF5 dataset "point_value": shape (1,), type "<f8">
solution  :  <HDF5 dataset "solution": shape (1089, 1), type "<f8">
@endcode
There are $(32+1)\times(32+1) = 1089$ nodes.
The coordinates of these nodes $(x,y)$ are stored in the table `nodes` in the %HDF5 file.
There are a total of $32\times 32 = 1024$ cells. The nodes which make up each cell are
marked in the table `cells` in the %HDF5 file.

Next, we extract the data into multidimensional arrays
@code{.py}
nodes = np.array(file["/nodes"])
cells = np.array(file["/cells"])
solution = np.array(file["/solution"])

x, y = nodes.T
@endcode

The following stores the $x$ and $y$ coordinates of each node of each cell in one flat array.
@code{.py}
cell_x = x[cells.flatten()]
cell_y = y[cells.flatten()]
@endcode
The following tags the cell ids. Each four entries correspond to one cell.
Then we collect the coordinates and ids into a data frame
@code{.py}
n_cells = cells.shape[0]
cell_ids = np.repeat(np.arange(n_cells), 4)
meshdata = pd.DataFrame({"x": cell_x, "y": cell_y, "ids": cell_ids})
@endcode
The data frame looks
@code{.unparsed}
print(meshdata)

      x	      y	      ids
0	0.00000	0.00000	0
1	0.03125	0.00000	0
2	0.03125	0.03125	0
3	0.00000	0.03125	0
4	0.03125	0.00000	1
...	...	...	...
4091	0.93750	1.00000	1022
4092	0.96875	0.96875	1023
4093	1.00000	0.96875	1023
4094	1.00000	1.00000	1023
4095	0.96875	1.00000	1023

4096 rows × 3 columns
@endcode

To plot the mesh, we loop over all cells and connect the first and last node to complete the polygon
@code{.py}
fig, ax = plt.subplots()
ax.set_aspect("equal", "box")
ax.set_title("grid at refinement level #%d" % refinement)

for cell_id, cell in meshdata.groupby(["ids"]):
    cell = pd.concat([cell, cell.head(1)])
    plt.plot(cell["x"], cell["y"], c="k")
@endcode
Alternatively one could use the matplotlib built-in Polygon class
@code{.py}
fig, ax = plt.subplots()
ax.set_aspect("equal", "box")
ax.set_title("grid at refinement level #%d" % refinement)
for cell_id, cell in meshdata.groupby(["ids"]):
    patch = Polygon(cell[["x", "y"]], facecolor="w", edgecolor="k")
    ax.add_patch(patch)
@endcode

To plot the solution, we first create a finer grid and then interpolate the solution values
into the grid and then plot it.
@code{.py}
nx = int(np.sqrt(n_cells)) + 1
nx *= 10
xg = np.linspace(x.min(), x.max(), nx)
yg = np.linspace(y.min(), y.max(), nx)

xgrid, ygrid = np.meshgrid(xg, yg)
solution_grid = griddata((x, y), solution.flatten(), (xgrid, ygrid), method="linear")

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.set_title("solution at refinement level #%d" % refinement)
c = ax.pcolor(xgrid, ygrid, solution_grid, cmap=cm.viridis)
fig.colorbar(c, ax=ax)

plt.show()
@endcode

To check the convergence of `mean_value` and `point_value`
we loop over data of all refinements and store into vectors <code>mean_values</code> and <code>mean_values</code>
@code{.py}
mean_values = np.zeros((8,))
point_values = np.zeros((8,))
dofs = np.zeros((8,))

for refinement in range(1, 9):
    filename = "solution_%d.h5" % refinement
    file = h5py.File(filename, "r")
    mean_values[refinement - 1] = np.array(file["/mean_value"])[0]
    point_values[refinement - 1] = np.array(file["/point_value"])[0]
    dofs[refinement - 1] = np.array(file["/solution"]).shape[0]
@endcode

Following is the plot of <code>mean_values</code> on `log-log` scale
@code{.py}
mean_error = np.abs(mean_values[1:] - mean_values[:-1])
plt.loglog(dofs[:-1], mean_error)
plt.grid()
plt.xlabel("#DoFs")
plt.ylabel("mean value error")
plt.show()
@endcode

Following is the plot of <code>point_values</code> on `log-log` scale
@code{.py}
point_error = np.abs(point_values[1:] - point_values[:-1])
plt.loglog(dofs[:-1], point_error)
plt.grid()
plt.xlabel("#DoFs")
plt.ylabel("point value error")
plt.show()
@endcode

A python package which mimics the `R` ggplot2 (which is based on specifying the grammar of the graphics) is
<a href="https://plotnine.org/">plotnine</a>.
@code{.py}
We need to import the following from the <code>plotnine</code> package
from plotnine import (
    ggplot,
    aes,
    geom_raster,
    geom_polygon,
    geom_line,
    labs,
    scale_x_log10,
    scale_y_log10,
    ggtitle,
)
@endcode
Then plot the mesh <code>meshdata</code> dataframe
@code{.py}
plot = (
    ggplot(meshdata, aes(x="x", y="y", group="ids"))
    + geom_polygon(fill="white", colour="black")
    + ggtitle("grid at refinement level #%d" % refinement)
)

print(plot)
@endcode
Collect the solution into a dataframe
@code{.py}
colordata = pd.DataFrame({"x": x, "y": y, "solution": solution.flatten()})
@endcode
Plot of the solution
@code{.py}
plot = (
    ggplot(colordata, aes(x="x", y="y", fill="solution"))
    + geom_raster(interpolate=True)
    + ggtitle("solution at refinement level #%d" % refinement)
)

print(plot)
@endcode

Collect the convergence data into a dataframe
@code{.py}
convdata = pd.DataFrame(
    {"dofs": dofs[:-1], "mean_value": mean_error, "point_value": point_error}
)

@endcode
Following is the plot of <code>mean_values</code> on `log-log` scale
@code{.py}
plot = (
    ggplot(convdata, mapping=aes(x="dofs", y="mean_value"))
    + geom_line()
    + labs(x="#DoFs", y="mean value error")
    + scale_x_log10()
    + scale_y_log10()
)

plot.save("mean_error.pdf", dpi=60)
print(plot)
@endcode

Following is the plot of <code>point_values</code> on `log-log` scale
@code{.py}
plot = (
    ggplot(convdata, mapping=aes(x="dofs", y="point_value"))
    + geom_line()
    + labs(x="#DoFs", y="point value error")
    + scale_x_log10()
    + scale_y_log10()
)

plot.save("point_error.pdf", dpi=60)
print(plot)
@endcode
