Final Project
Final Project
ln M
Simulationsumgebungen im Ingenieurwesen
This file contains all the tasks and information you need for the final project. If you have any questions,
please reach out to me via email or the lecture forum on Moodle as soon as possible. Please read the
whole document carefully and make sure you understand what is expected of you. Please read especially
this first part carefully, as it contains the organizational information for the project and the grading
criteria. Afterward, you can find the two parts containing the tasks.
The tasks are split into two separate parts.
• The first part is the main task of the final project. It is mandatory for everyone to do this part.
This is the only required task for the final project. It is sufficient to get the best grade for the
project if done well.
• The second part contains optional tasks which you can choose to do. Doing these tasks can
improve the grade of your project and make up marks lost in the first part. However, you are
not required to do any of them. All the optional tasks are independent of each other and can be
done in any order, however they build on the first part.
Note that the project will be graded as a whole, meaning that messy code in the second part will have
a negative impact on the grade even if the first part is done well.
The project will be handed in via GitLab. You all have been added to a GitLab repository for the
final project, to which only you and your group members have access.
The submission deadline for the final project is the 9th February 2024 23:59.
This is a hard deadline, and your access to the GitLab will run out once it is reached. The grad-
ing of the actual code will be done based on the last commit, before the deadline on the main
branch. However, the whole repository will be considered to assess your use of version control and
the development process, so keep that in mind when working on the project.
Grading Criteria
Before we get to the individual grading criteria, let me give you a rough guideline to the grading of
the project.
1
The most important thing is that you write clean and well-structured code. This means that you
should use the concepts and features we discussed in class and apply them where appropriate. The
goal is to assess your ability to write a clean and modern C++ code base and not to assess your ability
to write a numerical solver. Correctness, performance, and the implementation of the optional tasks
are secondary to this. While you should strive to implement the optional tasks correctly, it is better
to have a clean and well-structured code base without the optional tasks than a messy code base with
all the optional tasks.
• Code Quality: The code should be well structured and easy to read. This includes the use of
comments, variable names, and the overall structure of the code.
• Version Control: The code should be version-controlled using git. This includes the use of
branches, commits, and pull requests for major changes. Discussions about decisions should be
documented via GitLab. Commits and their messages should be meaningful and understandable.
• Testing: The code should be tested. This includes unit tests for the individual functions and
integration tests for the overall code.
• Modern C++: The code should be written in modern C++. This includes the use of smart
pointers, templates, and of the C++ standard library and containers. You should be aware of
the features discussed in class and use them where appropriate. However, this does not mean
that you must use all of them. For example, if you do not need to use pointers, then you don’t
have to; however, the use of raw pointers instead of smart pointers would be penalized.
• Correctness: The final code you hand in should be correct, easily buildable, and runnable. This
includes the use of the build system CMake for the Code. If there are any special commands
needed to build or run the code, they should be documented in the README file.
Furthermore, there are some further secondary criteria that will be considered when grading the project.
These include the following:
• Completeness of the Project: The project should be complete. This includes the implemen-
tation of all the required features and the implementation of the optional features you choose to
implement. However, messy code that is not well structured will have a stronger negative impact
on the grade than missing some features.
• Preformance: The performance of the code is not the main focus of the project. However,
obvious and easy-to-fix performance issues might be penalized.
2
Your final grade will then consist to 50% of the grade you get for the project and to 50% of the grade
you get for the presentation.
We as the Lecture Team wish you all the best for the project and hope you have fun
implementing it. We are looking forward to seeing your results and implementations.
3
Part 1: Required Task
The goal of the final project will be to implement a simple solver for partial differential equations. The
problem we consider and want to solve is the so-called Poisson equation given by
−∆u = f in Ω,
u=g on ∂Ω,
where Ω ⊂ Rd is a bounded domain with boundary ∂Ω and f and g are given functions. The Laplace
operator ∆ is given by
d
X
∆u = ∂x2i u.
i=1
Theoretical Background
The approach with which we will solve this problem is the so-called Finite Diffrence Method (FD).
The idea is to approximate the differential operator ∆ by a finite difference operator ∆h and then
solve the resulting linear system of equations. To construct the finite difference operator, we first need
to discretize the domain Ω. For simplicity, we will only consider the case where Ω = (0, 1)2 is the
two-dimensional unit square. We then discretize the domain by a uniform grid with N points in each
direction.
We then get grid points
xi,j = (ih, jh)
for i, j = 0, . . . , N with h = N.
1
and will define
ui,j := u(xi,j )
for the unknowns we want to solve for. Note that this definition includes the boundary points for i or
j ∈ {0, N }. For these points, the value is given by the boundary condition g and we know that
From here, we can define the one-dimensional central finite difference operator ∂1,h
2 and ∂ 2 along the
2,h
4
Summed up this gives us the finite difference operator ∆h for the Laplace operator defined by
1
2
∆h ui,j := ∂1,h 2
ui,j + ∂2,h ui,j = (ui−1,j + ui,j−1 − 4ui,j + ui+1,j + ui,j+1 ) . (3)
h2
This is a linear operator in the unknowns ui,j and can be written in matrix form and assembled. The
matrix ∆h is then roughly of the form
−4 1 0 . . . 1 0 0
1 −4 1 . . . 0 1 0
0 1 −4 . . . 0 0 1
. . . . . .
∆h = 2
1 .. .. .. . . . .. .. .. .
h
1
0 0 . . . −4 1 0
0 1 0 . . . 1 −4 1
0 0 1 ... 0 1 −4
Note that the unknown indices i and j do NOT correspond to the matrix indices k and l. Instead,
every pair (i, j) corresponds to a single index k of the matrix. This means that you will need to define
a mapping from the pair (i, j) to the index k and vice versa, which flattens the 2 dimensional grid into
a 1 dimensional array. You can then assemble the matrix ∆h by looping over all the unknowns and
adding the corresponding entries to the matrix.
Finally, the boundary degrees of freedom and rhs values need to be handled. For the RHS you will need
to define a vector f with the values fi,j = f (xi,j ) for the inner points plus possible contributions from
the boundary conditions. This is done by bringing the Boundary DOFs to the RHS in the equation
5
and removing the corresponding rows and columns from the matrix. Assuming in this example that
the red values are on the boundary, we get the following equation for one row of the matrix:
−∆h u = fi,j
1
− (ui−1,j + ui,j−1 − 4ui,j + ui+1,j + ui,j+1 ) = fi,j
h2
1
− 2 (ui−1,j + ui,j−1 − 4ui,j + ui+1,j + ui,j+1 ) = fi,j
h
1 1
− 2 (ui−1,j + ui,j−1 − 4ui,j ) = fi,j + 2 (ui+1,j + ui,j+1 )
h h
1 1
− 2 (ui−1,j + ui,j−1 − 4ui,j ) = fi,j + 2 (gi+1,j + gi,j+1 )
h h
In this case, the matrix is only assembled for the inner points, but the RHS must be adjusted accord-
ingly.
In total, this gives us a linear system of equations of the form
−∆h u = f
which we can solve for u using a linear solver like Jacobi, Gauss-Seidel, or the CG method since the
resulting matrix is symmetric and positive definite.
6
Tasks
Your task is to implement the above-described finite difference method to solve the Poisson equation.
You should recognize a lot of pieces you can use from the previous sheets and the exercises. You are
free to use any of the code you have written so far as well as any of the reference solutions for the
previous sheets. Note, however, that the reference solutions for sheet 4 will only be released after the
Christmas break.
More specifically, you should implement functionality that, for a given right-hand side f and boundary
condition g, assembles the system matrix for the Poisson equation, solves the linear system, and returns
the solution u.
In the end, there should be two executables which solve the problem for the following two cases:
1. Polynomial Case:
g(x, y) = 1 + x2 + 2y 2
f (x, y) = −6
2. Trigonometric Case:
It is sufficient to consider grids of the sizes N ≤ 10 in each direction. These two cases should call the
output code provided in the psi-external repository on GitLab. As on the previous sheets, you should
use the build system CMake to gain access to the external code. The output code is documented, and
understanding how to use it is part of the task.
If there are any special commands needed to build or run the code, they should be documented in the
README file. We will not look around in the code to find out how to build or run it when grading it.
7
Part 2: Optional Tasks
Below is a list of optional tasks of which you can choose one or more to implement. Each of them
extends a different part of the code base and is marked with a difficulty level. Note that these are
subjective and are meant only as a rough guideline.
All the tasks are independent of each other and can be done in any order. There will be no difference
in the grading between the different tasks.
In real life, the domains we want to solve problems on are typically not squares or cubes but rather
more complicated shapes. While really complex domains are beyond the scope of this course, we can
still extend the code to handle rectangular domains. Instead of the unit square, we will consider the
domain Ω = (a, b) × (c, d). There are now three obvious ways to discretize this.
• The first one is to discretize the domain still by N points in every direction. This leads to a grid
with N 2 points that are not uniformly distributed anymore.
• The second one is to discretize the domain by points that uniformly spaced in every direction.
Assuming we chose this distance h we then get Nx = b−a
h points in the x direction and Ny = d−c
h
points in the y direction. This leads to a grid with Nx · Ny points. For this to work the number
of points in each direction has to be an integer.
• The third one is a mixture of the former two in which we discretize the domain by Nx points in
the x direction and Ny points in the y direction. This leads to a grid with Nx · Ny points, which
are not uniformly distributed anymore.
You should implement all three of these discretizations and extend the code to handle them. For this to
work, we will need to look at the discretization of the Laplace operator in Equations (1) and (3). The
first thing to notice is that the distance h between the grid points is not constant anymore. Instead,
we will need to define a distance hx in the x direction and a distance hy in the y direction. This leads
8
to
2 1
∂1,h ui,j := (ui−1,j − 2ui,j + ui+1,j )
h2x
2 1
∂2,h ui,j := 2 (ui,j−1 − 2ui,j + ui,j+1 )
hy
The combined operator is then obtained by summing up the two operators :
2 2 1 1
∆h ui,j := ∂1,h ui,j + ∂2,h ui,j = 2
(ui−1,j − 2ui,j + ui+1,j ) + 2 (ui,j−1 − 2ui,j + ui,j+1 ) .
hx hy
However, it is not symmetric anymore; therefore, the matrix we assemble will not be symmetric. Still,
for hx ≈ hy it will be close to symmetric, and we can use the same solvers as before. Big differences
will, however, lead to deteriorations in the solvers’ performance. The most reasonable approach is thus
to use the second discretization, where we have a uniform distance h in both directions. If this is not
possible, you should use the third approach with a minimal difference between hx and hy . Think of a
simple way to implement this and to find suitable values for Nx and Ny .
In this task, you should extend the code to solve the heat equation given by
∂t u − ∆u = f in Ω × (0, T ),
u=g on ∂Ω × (0, T ),
u = u0 at t = 0.
Here, as before, u is the unknown we want to solve for, and f and g are given functions. The main
difference to the Poisson equation is the additional time derivative ∂t u and the initial condition u0 . We
will use the so-called Implicit Euler Method to handle the time derivative. The idea is the same
as for the finite differences which we now use on the time derivative. We discretize the time domain
[0, T ] by a uniform grid with M points and define tm = m · δt for m = 0, . . . , M with δt = M.
T
for um+1 where um+1 is the solution at the next time step and um is the solution at the current time
step. Discretizing the Laplace operator as before and moving the known values to the rhs we get the
following linear system of equations in each time step:
um+1 = gm+1
9
where I is the identity matrix. Take care to impose the boundary conditions correctly.
As before, your task is to implement this method and solve the heat equation for the following case:
• We will consider the domain Ω = (0, 1)2 and the time domain [0, 1] and simulate a simple heating
of a plate at the top edge. The boundary condition is given by
1 if y = 1
g(x, y, t) =
0 else
As you might have noticed, the matrices we assemble in the finite difference method contain a lot of
zero entries. This is a common property of matrices that arise from the discretization of differential
equations, stemming from the locality of the differential operators and methods used. Such matrices
are therefore called sparse matrices, and there are special data structures and algorithms to handle
them. Typically, one can expect that the number of non-zero (i.e. relevant) entries in the matrix
scales linearly with the number of unknowns. However, the memory demand of a dense matrix scales
quadratically with the number of unknowns and quickly becomes infeasible for large problems.
In this task, you should implement a sparse matrix data structure and use it to assemble the system
matrix for the Poisson equation. This means that you will need to do the following things:
• Implement a method to assemble the system matrix using the sparse matrix data structure
CSR Format
In the following, we will discuss one possible sparse matrix data structure called the Compressed
Sparse Row (CSR) format. It is geared towards the efficient implementation of matrix-vector products
10
from the right. It is, therefore, well suited for the finite difference method since the solvers we use are
iterative, and the main performance bottleneck are matrix-vector products.
The idea of the CSR format is to store the matrix in three arrays.
1. entries : The first array stores the ordered non-zero entries of the matrix. Here, we order the
entries by row and then by column. Meaning
2. col_indices : The second one stores the corresponding column indices of the non-zero entries,
meaning that if entries[k] corresponds to the matrix entry ai,j then col_indices[k]= j is the
column index of that entry.
3. row_ptr : The third one stores the index of the first non-zero entry in each row. By convention,
the last entry contains the number of non-zero entries in the matrix. This results in the relations
as well as
To make this clearer, let us look at an example. Consider the following matrix:
1 0 2
0 0 3
4 5 6
This matrix has six non-zero entries; therefore, we need six entries in the entries and col_indices
arrays. The row_ptr array will have four entries, one for each row plus one for the total number of
non-zero entries. The arrays can be obtained as follows: The entries array is given by ordering the
matrix entries
entries = [1, 2, 3, 4, 5, 6].
11
To get this, start with the first row and count the number of non-zero entries. In this case, there are
two. Therefore, the first entry in the row_ptr array is 0 and the second entry is 2. The next row has
one non-zero entry and therefore the third entry i.e. row_ptr[3] = row_ptr[3 - 1] + 1 array is 3.
The final entry fullfills the same relation but with 3 entries and is given by row_ptr[4] = row_ptr[3]
+ 3 = 6, which is the total number of non-zero entries in the matrix. The total memory demand of
the CSR format is given
which is a significant improvement over the dense matrix format. Let us make a final remark on the
most important operation, the matrix-vector product and its implementation. Given a matrix A in
CSR format and a vector v, we want to compute the matrix-vector product w = Av. This can be
achieved over the following loop:
This means we loop over all the rows, and for each row, we loop over all the non-zero entries in that row
and add the corresponding contribution to the result vector w. This allows us to compute the matrix-
vector product without ever having to store the full matrix and without unnecessary computations in
which we multiply zero entries with the vector v.
12