Peano
Loading...
Searching...
No Matches
Tutorial 1: Matrix-free Continuous Galerkin single-level solver on a regular grid
This documentation is autogenerated from ./tutorials/mghype/examples/01_matrix-free-cg.ipynb. Once you clone the Peano repository, please work against this notebook or study Peano's generic description how to generate web docu from Jupyter notebooks.

This tutorial runs through the basic steps how to construct a geometric single level Jacobi solver for

\( -\Delta u = f \)

While noone would use such a numerical scheme as a real solver for any problem of relevant size, this type of solver often is a building block of proper multigrid algorithms.

Prepare the environment

MGHyPE codes need to know where to find the MGHyPE and Peano Python sources. For this, we can either add the Python sources to our environment, or export the path variable prior to invoking our script/kicking off our Jupyter notebook, or we set the path hard-coded in our script. To be honest, this last approach is likely the least elegant one, as it hard-codes the pathes, but it works:

import sys, os
sys.path.insert(0, os.path.abspath("../../../python/"))
sys.path.insert(0, os.path.abspath("../../../src/"))

Once we have ensured that all pathes are correct, we import the packages of interest:

import peano4.api, mghype

An MGHyPE project

Every MGHyPE project starts with an MGHyPE project object:

project = mghype.matrixfree.api.Project( project_name = "Tutorial01",
namespace = [ "tutorials", "mghype", "examples" ],
executable = "tutorial01",
)

This creates the domain-specific project. We immediately set the domain properties:

cube_size = 1.0
dimensions = 2
project.set_global_simulation_parameters(
dimensions = dimensions,
offset = [ 0.0 for _ in range(dimensions) ],
domain_size = [ cube_size for _ in range(dimensions) ],
plot_after_each_mesh_sweep = False,
max_iterations = 20
)

We parse the system setup next, so all the compiler settings and stuff is known to MGHyPE/Peano. As we develop code, we switch to asserts mode, which means a lot of checks are injected into the code base. Once we are sure everything works, we toggle this switch to Release:

project.set_Peano4_installation( "../../..", peano4.api.output.CompileMode.Asserts )

Add a solver

Once we have a project, we can start to construct our solver. One of the first things that we will need is the element assembly matrices. MGHyPE always assumes that the discretisation yields a system

\( Ax = Mb \)

with a system matrix \( A\) and a right-hand side matrix \( M\) . If we employ the scheme as an actual solver, this \( M\) is the mass matrix corresponding to an integral

\( \int (f, \varphi ) \, dx \)

Furthermore, both \( A\) and \( M\) are always handed over to the solver as normalised matrices (over the unit cube) plus some h-scaling. There's no need to create these matrices manually. We can use one of MGHyPE's factory mechanisms:

poisson_equation_matrices = mghype.api.matrixgenerators.DLinear(
dimensions = dimensions,
unknowns_per_node = 1
)
system_matrix, system_matrix_scaling = poisson_equation_matrices.get_cell_system_matrix_for_laplacian()
mass_matrix, mass_matrix_scaling = poisson_equation_matrices.get_cell_mass_matrix()
Simple matrix generator for continuous d-linear finite elements.
Definition DLinear.py:10

We briefly inspect the results (something we would likely not do for a "real" simulation):

print( system_matrix )
print( system_matrix_scaling)
print( mass_matrix )
print( mass_matrix_scaling )

The first matrix is the standard (2d) element matrix for the Poisson equation. The scaling array indicates that this one has to be scaled with \( h^0 \) before we continue. More details can be found in the documentation of these routines on the webpage. When we study the mass matrix, there are no big surprises either.

Next we create the solver using these matrices and add it to the project:

solver_name = "CollocatedPoisson"
mesh_size = 0.1
solver = mghype.matrixfree.solvers.api.CollocatedDLinearDiscretisationWithPointJacobi(solver_name,
1, # unknowns per vertex, i.e. scalar equation
dimensions,
mesh_size,
mesh_size,
local_assembly_matrix = system_matrix,
local_assembly_matrix_scaling=system_matrix_scaling,
mass_matrix = mass_matrix,
mass_matrix_scaling = mass_matrix_scaling,
solver_tolerance=0.01
)
project.add_solver(solver)

Create and build project

We have now set up the solver. Hence, we can lower it to C++:

peano4_project = project.generate_Peano4_project(verbose=True)
peano4_project.generate()

We build the project now:

!make -j8

So far, this is a dysfunctional solver, as we haven't set any boundary conditions, and no right-hand side. There are multiple ways to do this in MGHyPE. We demonstrate the low-level version here: Every solver is mapped onto an AbstractCollocatedPoisson class and a subclass called CollocatedPoisson. The abstract class is regenerated every time we run Python and basically stores all the data from the Python within the C++ code. It represents a translation from Python into the compiled world. The subclass can be used by us to tailor the code to our actual needs.

Exercise

Open the C++ file of the solver and ensure that it realises a proper solver, i.e. something alike

#include <cmath>
tutorials::mghype::examples::CollocatedPoisson::CollocatedPoisson() {
}
tutorials::mghype::examples::CollocatedPoisson::~CollocatedPoisson() {
}
void tutorials::mghype::examples::CollocatedPoisson::initVertex(
) {
value = 0.0;
rhs = 1.0;
for (int d=0; d<Dimensions; d++) {
rhs *= std::sin( tarch::la::PI * x(d) );
}
}
void tutorials::mghype::examples::CollocatedPoisson::setBoundaryConditions(
) {
value = 0.0;
}
constexpr double PI
Definition Scalar.h:12
Simple vector class.
Definition Vector.h:159

Running the experiment

We next run the experiment but ensure that the output is piped into a file, as we'll later on use this dump to run some analysis:

!rm *.peano-patch-file
!./tutorial01 > tutorial01.out
!cat tutorial01.out

Nothing controversial here. The residual goes down albeit slowly, and we therefore never reach our numerical termination criterion. Instead, we quit once we exceed the maximum number of iterations.

Visualisation

Peano writes its own bespoke data file format. There are some scripts delivered to convert them into VTK which you can load into Paraview or VisIt, e.g. we use exactly this script here to convert the outcome into a standard visualisation format. Unfortunately, my pvpython sometimes seems to miss out on environment variables. So, I have to re-add those of interest manually:

import subprocess
import os
my_env = os.environ
my_env["PYTHONPATH"] = "../../../python:" + my_env["PYTHONPATH"]
subprocess.call(["pvpython", "../../../python/peano4/visualisation/render.py", "CollocatedPoisson.rhs.peano-patch-file"], env=my_env)
subprocess.call(["pvpython", "../../../python/peano4/visualisation/render.py", "CollocatedPoisson.value.peano-patch-file"], env=my_env)

Display the outcome

We can now visualise the pvd/vtu files in Paraview or VisIt. As this is all only 2d, I use the PyVista package to do this directly within the Jupyter notebook. The first step grabs the package and therefore is required once and once only. The second one then creates the pics:

import sys
!{sys.executable} -m pip install --break-system-packages pyvista[all]
import pyvista
mesh = pyvista.read( "CollocatedPoisson.rhs.pvd" )
mesh.plot()

No surprises with the right-hand side. We however see that the result is nowhere near the real solution. This is due to the poor convergence rates. In the script below, we might want to pass in clim=[from,to] to limit the range displayed. Note furthermore that we pick a second time step here, and do not load the pvd file. Each solver, by default, writes the initial solution and the last one - unless we make it plot every single mesh sweep.

plotter = pyvista.Plotter()
mesh = pyvista.read( "CollocatedPoisson.value.peano-patch-file-1.vtu" )
plotter.add_mesh(mesh, show_edges=True ) # , clim=[0.0, 0.01]
plotter.show()

Study the convergence behaviour

We close this first tutorial with a plot of the convergence behaviour. For this, we open the output that we'd streamed into a text file and parse the results. As the dumps are kind of standardised, we can use some utility functions shipped with MGHyPE to do the actual parsing:

res_max, res_2, res_h = mghype.api.postprocessing.parse_residuals( "tutorial01.out", solver_name )
du_max, du_2, du_h = mghype.api.postprocessing.parse_solution_updates( "tutorial01.out", solver_name )
import matplotlib.pyplot as plt
plt.plot(res_2, label='$|res|_2$', marker='<')
plt.plot(du_2, label='$|\delta u|_2$', marker='>')
#plt.plot(res_max, label='$|res|_{max}$', marker='>')
#plt.plot(res_h, label='$|res|_h$', marker='v')
plt.yscale('log')
plt.xlabel('iterations')
plt.ylabel('residual')
plt.legend()
plt.savefig( "tutorial01.pdf" )
plt.savefig( "tutorial01.png" )
plt.show()