Solving PDEs in Python. The FEniCS Tutorial-PYTHON - AWESOME
Solving PDEs in Python. The FEniCS Tutorial-PYTHON - AWESOME
Springer
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1 Preliminaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1 The FEniCS Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 What you will learn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Working with this tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 Obtaining the software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4.1 Installation using Docker containers . . . . . . . . . . . . . . . . 6
1.4.2 Installation using Ubuntu packages . . . . . . . . . . . . . . . . . 7
1.4.3 Testing your installation . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Obtaining the tutorial examples . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6 Background knowledge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6.1 Programming in Python . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6.2 The finite element method . . . . . . . . . . . . . . . . . . . . . . . . . 9
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
vi Contents
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
viii Contents
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Preface
This book gives a concise and gentle introduction to finite element program-
ming in Python based on the popular FEniCS software library. FEniCS can
be programmed in both C++ and Python, but this tutorial focuses exclu-
sively on Python programming, since this is the simplest and most effective
approach for beginners. After having digested the examples in this tutorial,
the reader should be able to learn more from the FEniCS documentation, the
numerous demo programs that come with the software, and the comprehen-
sive FEniCS book Automated Solution of Differential Equations by the Finite
Element Method [26]. This tutorial is a further development of the opening
chapter in [26].
We thank Johan Hake, Kent-Andre Mardal, and Kristian Valen-Sendstad
for many helpful discussions during the preparation of the first version of this
tutorial for the FEniCS book [26]. We are particularly thankful to Professor
Douglas Arnold for very valuable feedback on early versions of the text. ys-
tein Srensen pointed out numerous typos and contributed with many helpful
comments. Many errors and typos were also reported by Mauricio Ange-
les, Ida Drsdal, Miroslav Kuchta, Hans Ekkehard Plesser, Marie Rognes,
Hans Joachim Scroll, Glenn Terje Lines, Simon Funke, Matthew Moelter,
and Magne Nordaas. Ekkehard Ellmann as well as two anonymous reviewers
provided a series of suggestions and improvements. Special thanks go to Ben-
jamin Kehlet for all his work with the mshr tool and for quickly implementing
our requests for this tutorial.
Comments and corrections can be reported as issues for the Git repository
of this book1 , or via email to logg@chalmers.se.
Oslo and Smgen, November 2016 Hans Petter Langtangen, Anders Logg
1
https://github.com/hplgit/fenics-tutorial/
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
Chapter 1
Preliminaries
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
4 1 Preliminaries
The command above will install the program fenicsproject on your sys-
tem. This program lets you easily create FEniCS sessions (containers) on
your system:
Terminal
This command has several useful options, such as easily switching between
the latest release of FEniCS, the latest development version and many more.
To learn more, type fenicsproject help. FEniCS can also be used directly
with Docker, but this typically requires typing a relatively complex Docker
command, for example:
Terminal
1
Running Docker containers on Mac and Windows involves a small performance over-
head compared to running Docker containers on Linux. However, this performance
penalty is typically small and is often compensated for by using the highly tuned and
optimized version of FEniCS that comes with the official FEniCS containers, compared
to building FEniCS and its dependencies from source on Mac or Windows.
2
https://www.docker.com
1.4 Obtaining the software 7
which you run the fenicsproject command) with the FEniCS ses-
sion. When the FEniCS session starts, it will automatically enter into
a directory named shared which will be identical with your current
working directory on your host system. This means that you can eas-
ily edit files and write data inside the FEniCS session, and the files
will be directly accessible on your host system. It is recommended that
you edit your programs using your favorite editor (such as Emacs or
Vim) on your host system and use the FEniCS session only to run your
program(s).
For users of Ubuntu GNU/Linux, FEniCS can also be installed easily via the
standard Ubuntu package manager apt-get. Just copy the following lines
into a terminal window:
Terminal
This will add the FEniCS package archive (PPA) to your Ubuntu com-
puters list of software sources and then install FEniCS. It will will also
automatically install packages for dependencies of FEniCS.
Once you have installed FEniCS, you should make a quick test to see that
your installation works properly. To do this, type the following command in
a FEniCS-enabled3 terminal:
Terminal
If all goes well, you should be able to run this command without any error
message (or any other output).
In this tutorial, you will learn finite element and FEniCS programming
through a number of example programs that demonstrate both how to solve
particular PDEs using the finite element method, how to program solvers in
FEniCS, and how to create well-designed Python code that can later be ex-
tended to solve more complex problems. All example programs are available
from the web page of this book at https://fenicsproject.org/tutorial.
The programs as well as the source code for this text can also be accessed
directly from the Git repository4 for this book.
While you can likely pick up basic Python programming by working through
the examples in this tutorial, you may want to study additional material
on the Python language. A natural starting point for beginners is the classic
Python Tutorial [11], or a tutorial geared towards scientific computing [22]. In
the latter, you will also find pointers to other tutorials for scientific computing
in Python. Among ordinary books we recommend the general introduction
Dive into Python [28] as well as texts that focus on scientific computing with
Python [15, 1821].
3
For users of FEniCS containers, this means first running the command fenicsproject
run.
4
https://github.com/hplgit/fenics-tutorial/
1.6 Background knowledge 9
Python versions
Python comes in two versions, 2 and 3, and these are not compatible.
FEniCS works with both versions of Python. All the programs in this
tutorial are also developed such that they can be run under both Python
2 and 3. Python programs that need to print must then start with
from __future__ import print_function
Many good books have been written on the finite element method. The books
typically fall in either of two categories: the abstract mathematical version
of the method or the engineering structural analysis formulation. FEniCS
builds heavily on concepts from the abstract mathematical exposition. The
first author has a book5 [24] in development that explains all details of the
finite element method in an intuitive way, using the abstract mathematical
formulations that FEniCS employs.
The finite element text by Larson and Bengzon [25] is our recommended
introduction to the finite element method, with a mathematical notation
that goes well with FEniCS. An easy-to-read book, which also provides a
good general background for using FEniCS, is Gockenbach [12]. The book
by Donea and Huerta [8] has a similar style, but aims at readers with an
interest in fluid flow problems. Hughes [14] is also recommended, especially
for readers interested in solid mechanics and heat transfer applications.
Readers with a background in the engineering structural analysis version
of the finite element method may find Bickford [3] an attractive bridge over to
the abstract mathematical formulation that FEniCS builds upon. Those who
have a weak background in differential equations in general should consult
a more fundamental book, and Eriksson et al [9] is a very good choice. On
the other hand, FEniCS users with a strong background in mathematics will
appreciate the texts by Brenner and Scott [5], Braess [4], Ern and Guermond
[10], Quarteroni and Valli [29], or Ciarlet [7].
5
http://hplgit.github.io/fem-book/doc/web/index.html
Chapter 2
Fundamentals: Solving the Poisson
equation
The goal of this chapter is to show how the Poisson equation, the most basic of
all PDEs, can be quickly solved with a few lines of FEniCS code. We introduce
the most fundamental FEniCS objects such as Mesh, Function, FunctionSpace,
TrialFunction, and TestFunction, and learn how to write a basic PDE solver,
including how to formulate the mathematical variational problem, apply boundary
conditions, call the FEniCS solver, and plot the solution.
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
12 2 Fundamentals: Solving the Poisson equation
2u 2u
= f (x, y) . (2.3)
x2 y 2
The unknown u is now a function of two variables, u = u(x, y), defined over
a two-dimensional domain .
The Poisson equation arises in numerous physical contexts, including heat
conduction, electrostatics, diffusion of substances, twisting of elastic rods, in-
viscid fluid flow, and water waves. Moreover, the equation appears in numer-
ical splitting strategies for more complicated systems of PDEs, in particular
the NavierStokes equations.
Solving a boundary-value problem such as the Poisson equation in FEniCS
consists of the following steps:
1. Identify the computational domain (), the PDE, its boundary conditions,
and source terms (f ).
2. Reformulate the PDE as a finite element variational problem.
3. Write a Python program which defines the computational domain, the
variational problem, the boundary conditions, and source terms, using the
corresponding FEniCS abstractions.
4. Call FEniCS to solve the boundary-value problem and, optionally, extend
the program to compute derived quantities such as fluxes and averages,
and visualize the results.
We shall now go through steps 24 in detail. The key feature of FEniCS is
that steps 3 and 4 result in fairly short code, while a similar program in most
other software frameworks for PDEs require much more code and technically
difficult programming.
FEniCS is based on the finite element method, which is a general and efficient
mathematical machinery for the numerical solution of PDEs. The starting
point for the finite element methods is a PDE expressed in variational form.
Readers who are not familiar with variational problems will get a very brief
introduction to the topic in this tutorial, but reading a proper book on the
2.1 Mathematical problem formulation 13
bly different) function space V , the so-called trial space. We refer to (2.6) as
the weak form or variational form of the original boundary-value problem
(2.1)(2.2).
The proper statement of our variational problem now goes as follows: find
u V such that
Z Z
u v dx = f v dx v V . (2.7)
The trial and test spaces V and V are in the present problem defined as
V = {v H 1 () : v = uD on },
V = {v H 1 () : v = 0 on } .
Z
a(u, v) = u v dx, (2.10)
Z
L(v) = f v dx . (2.11)
The Poisson problem (2.1)(2.2) has so far featured a general domain and
general functions uD for the boundary conditions and f for the right-hand
side. For our first implementation we will need to make specific choices for
, uD , and f . It will be wise to construct a problem with a known analytical
solution so that we can easily check that the computed solution is correct.
Solutions that are lower-order polynomials are primary candidates. Standard
finite element function spaces of degree r will exactly reproduce polynomials
of degree r. And piecewise linear elements (r = 1) are able to exactly repro-
duce a quadratic polynomial on a uniformly partitioned mesh. This important
result can be used to verify our implementation. We just manufacture some
quadratic function in 2D as the exact solution, say
ue (x, y) = 1 + x2 + 2y 2 . (2.12)
By inserting (2.12) into the Poisson equation (2.1), we find that ue (x, y) is a
solution if
= [0, 1] [0, 1] .
This simple but very powerful method for constructing test problems is called
the method of manufactured solutions: pick a simple expression for the exact
solution, plug it into the equation to obtain the right-hand side (source term
f ), then solve the equation with this right-hand side and using the exact
solution as a boundary condition, and try to reproduce the exact solution.
A FEniCS program for solving our test problem for the Poisson equation in
2D with the given choices of , uD , and f may look as follows:
from fenics import *
# Compute solution
u = Function(V)
solve(a == L, u, bc)
# Print errors
print(error_L2 =, error_L2)
print(error_max =, error_max)
# Hold plot
interactive()
The FEniCS program must be available in a plain text file, written with a
text editor such as Atom, Sublime Text, Emacs, Vim, or similar. There are
several ways to run a Python program like ft01_poisson.py:
Use a terminal window.
Use an integrated development environment (IDE), e.g., Spyder.
Use a Jupyter notebook.
The statement
mesh = UnitSquareMesh(8, 8)
defines a uniform finite element mesh over the unit square [0, 1] [0, 1]. The
mesh consists of cells, which in 2D are triangles with straight sides. The
parameters 8 and 8 specify that the square should be divided into 8 8
rectangles, each divided into a pair of triangles. The total number of triangles
(cells) thus becomes 128. The total number of vertices in the mesh is 99 = 81.
In later chapters, you will learn how to generate more complex meshes.
Once the mesh has been created, we can create a finite element function space
V:
V = FunctionSpace(mesh, P, 1)
The second argument P specifies the type of element. The type of ele-
ment here is P, implying the standard Lagrange family of elements. You may
also use Lagrange to specify this type of element. FEniCS supports all
simplex element families and the notation defined in the Periodic Table of
the Finite Elements2 [2].
The third argument 1 specifies the degree of the finite element. In this case,
the standard P1 linear Lagrange element, which is a triangle with nodes at
the three vertices. Some finite element practitioners refer to this element
as the linear triangle. The computed solution u will be continuous across
elements and linearly varying in x and y inside each element. Higher-degree
polynomial approximations over each cell are trivially obtained by increasing
the third parameter to FunctionSpace, which will then generate function
spaces of type P2 , P3 , and so forth. Changing the second parameter to DP
creates a function space for discontinuous Galerkin methods.
2
https://www.femtable.org
2.3 Dissection of the program 21
space, so it is sufficient to work with one common space V for both the trial
and test functions in the program:
u = TrialFunction(V)
v = TestFunction(V)
The expression may depend on the variables x[0] and x[1] correspond-
ing to the x and y coordinates. In 3D, the expression may also depend on
the variable x[2] corresponding to the z coordinate. With our choice of
uD (x, y) = 1 + x2 + 2y 2 , the formula string can be written as 1 + x[0]*x[0]
+ 2*x[1]*x[1]:
u_D = Expression(1 + x[0]*x[0] + 2*x[1]*x[1], degree=2)
We set the degree to 2 so that u_D may represent the exact quadratic
solution to our test problem.
The function boundary specifies which points that belong to the part of
the boundary where the boundary condition should be applied:
2.3 Dissection of the program 23
A function like boundary for marking the boundary must return a boolean
value: True if the given point x lies on the Dirichlet boundary and False
otherwise. The argument on_boundary is True if x is on the physical bound-
ary of the mesh, so in the present case, where we are supposed to return
True for all points on the boundary, we can just return the supplied value of
on_boundary. The boundary function will be called for every discrete point
in the mesh, which means that we may define boundaries where u is also
known inside the domain, if desired.
One way to think about the specification of boundaries in FEniCS is that
FEniCS will ask you (or rather the function boundary which you have imple-
mented) whether or not a specific point x is part of the boundary. FEniCS
already knows whether the point belongs to the actual boundary (the math-
ematical boundary of the domain) and kindly shares this information with
you in the variable on_boundary. You may choose to use this information (as
we do here), or ignore it completely.
The argument on_boundary may also be omitted, but in that case we need
to test on the value of the coordinates in x:
def boundary(x):
return x[0] == 0 or x[1] == 0 or x[0] == 1 or x[1] == 1
Comparing floating-point values using an exact match test with == is not good
programming practice, because small round-off errors in the computations of
the x values could make a test x[0] == 1 become false even though x lies on
the boundary. A better test is to check for equality with a tolerance, either
explicitly
tol = 1E-14
def boundary(x):
return abs(x[0]) < tol or abs(x[1]) < tol \
or abs(x[0] - 1) < tol or abs(x[1] - 1) < tol
For numbers of unit size, tolerances as low as 3 1016 can be used (in
fact, this tolerance is known as the constant DOLFIN_EPS in FEniCS).
Otherwise, an appropriately scaled tolerance must be used.
Before defining the bilinear and linear forms a(u, v) and L(v) we have to
specify the source term f :
f = Expression(-6, degree=0)
We now have all the ingredients we need to define the variational problem:
a = dot(grad(u), grad(v))*dx
L = f*v*dx
2.3 Dissection of the program 25
In essence, these two lines specify the PDE to be solved. Note the very close
correspondence between the Python syntax and the mathematical formulas
u v dx and f v dx. This is a key strength of FEniCS: the formulas in
the variational formulation translate directly to very similar Python code,
a feature that makes it easy to specify and solve complicated PDE prob-
lems. The language used to express weak forms is called UFL (Unified Form
Language) [1, 26] and is an integral part of FEniCS.
Having defined the finite element variational problem and boundary condi-
tion, we can now ask FEniCS to compute the solution:
u = Function(V)
solve(a == L, u, bc)
Once the solution has been computed, it can be visualized by the plot com-
mand:
plot(u)
plot(mesh)
26 2 Fundamentals: Solving the Poisson equation
interactive()
Note the call to the function interactive after the plot commands. This
call makes it possible to interact with the plots (rotating and zooming). The
call to interactive is usually placed at the end of a program that creates
plots. Figure 2.1 displays the two plots.
Fig. 2.1 Plot of the mesh and the solution for the Poisson problem created using the
built-in FEniCS visualization tool (plot command).
The plot command is useful for debugging and initial scientific investi-
gations. More advanced visualizations are better created by exporting the
solution to a file and using an advanced visualization tool like ParaView, as
explained in the next section.
By clicking the left mouse button in the plot window, you may rotate the
solution, while the right mouse button is used for zooming. Point the mouse to
the Help text in the lower left corner to display a list of all available shortcut
commands. The help menu may alternatively be activated by typing h in
the plot window. The plot command also accepts a number of additional
arguments, such as for example setting the title of the plot window:
plot(u, title=Finite element solution)
plot(mesh, title=Finite element mesh)
The simple plot command is useful for quick visualizations, but for more
advanced visualizations an external tool must be used. In this section we
demonstrate how to visualize solutions in ParaView. ParaView3 is a powerful
tool for visualizing scalar and vector fields, including those computed by
FEniCS.
The first step is to export the solution in VTK format:
vtkfile = File(poisson/solution.pvd)
vtkfile << u
The following steps demonstrate how to create a plot of the solution of our
Poisson problem in ParaView. The resulting plot is shown in Figure 2.2.
1. Start the ParaView application.
2. Click FileOpen... in the top menu and navigate to the directory con-
taining the exported solution. This should be inside a subdirectory named
poisson below the directory where the FEniCS Python program was
started. Select the file named solution.pvd and then click OK.
3. Click Apply in the Properties pane on the left. This will bring up a plot
of the solution.
4. To make a 3D plot of the solution, we will make use of one of ParaViews
many filters. Click FiltersAlphabeticalWarp By Scalar in the top
menu and then Apply in the Properties pane on the left. This create an
elevated surface with the height determined by the solution value.
5. To show the original plot below the elevated surface, click the little eye
icon to the left of solution.pvd in the Pipeline Browser pane on the left.
Also click the little 2D button at the top of the plot window to change the
visualization to 3D. This lets you interact with the plot by rotating (left
mouse button) and zooming (Ctrl + left mouse button).
6. To show the finite element mesh, click on solution.pvd in the Pipeline
Browser, navigate to Representation in the Properties pane, and select
Surface With Edges. This should make the finite element mesh visible.
7. To change the aspect ratio of the plot, click on WarpByScalar1 in the
Pipeline Browser and navigate to Scale Factor in the Properties pane.
Change the value to 0.2 and click Apply. This will change the scale of the
3
http://www.paraview.org
28 2 Fundamentals: Solving the Poisson equation
Fig. 2.2 Plot of the mesh and the solution for the Poisson problem created using
ParaView.
4
http://www.paraview.org/Wiki/The_ParaView_Tutorial
5
https://vimeo.com/34037236
2.3 Dissection of the program 29
sZ
E= (uD u)2 dx .
Since the exact solution is quadratic and the finite element solution is piece-
wise linear, this error will be nonzero. To compute this error in FEniCS, we
simply write
error_L2 = errornorm(u_D, u, L2)
The errornorm function can also compute other error norms such as the H 1
norm. Type pydoc fenics.errornorm in a terminal window for details.
We also compute the maximum value of the error at all the vertices of the
finite element mesh. As mentioned above, we expect this error to be zero to
within machine precision for this particular example. To compute the error
at the vertices, we first ask FEniCS to compute the value of both u_D and u
at all vertices, and then subtract the results:
vertex_values_u_D = u_D.compute_vertex_values(mesh)
vertex_values_u = u.compute_vertex_values(mesh)
import numpy as np
error_max = np.max(np.abs(vertex_values_u_D - vertex_values_u))
We have here used the maximum and absolute value functions from numpy,
because these are much more efficient for large arrays (a factor of 30) than
Pythons built-in max and abs functions.
With numpy arrays we can write MATLAB-like code to analyze the data.
Indexing is done with square brackets: array_u[j], where the index j al-
ways starts at 0. If the solution is computed with piecewise linear Lagrange
elements (P1 ), then the size of the array array_u is equal to the number of
vertices, and each array_u[j] is the value at some vertex in the mesh. How-
ever, the degrees of freedom are not necessarily numbered in the same way as
the vertices of the mesh. (This is discussed in some detail in Section 5.4.1).
If we therefore want to know the values at the vertices, we need to call the
function u.compute_vertex_values. This function returns the values at all
the vertices of the mesh as a numpy array with the same numbering as for
the vertices of the mesh, for example:
vertex_values_u = u.compute_vertex_values()
Note that for P1 elements, the arrays array_u and vertex_values_u have
the same lengths and contain the same values, albeit in different order.
T 2 D = p in = {(x, y) | x2 + y 2 R} . (2.14)
Here, T is the tension in the membrane (constant), and p is the external
pressure load. The boundary of the membrane has no deflection, implying
D = 0 as a boundary condition. A localized load can be modeled as a Gaussian
function:
2 2 !
A 1 x x0 1 y y0
p(x, y) = exp . (2.15)
2 2 2
There are many physical parameters in this problem, and we can benefit
from grouping them by means of scaling. Let us introduce dimensionless
coordinates x
= x/R, y = y/R, and a dimensionless deflection w = D/Dc ,
0 = R0 /R,
where Dc is a characteristic size of the deflection. Introducing R
we obtain
2w 2w 0 )2 ) for x
2 = exp 2 (
x2 + ( 2 + y2 < 1,
yR
2
x y
where
R2 A R
= , = .
2T Dc 2
With an appropriate scaling, w and its derivatives are of size unity, so the
left-hand side of the scaled PDE is about unity in size, while the right-hand
side has as its characteristic size. This suggest choosing to be unity,
or around unity. We shall in this particular case choose = 4. (One can
also find the analytical solution in scaled coordinates and show that the
maximum deflection D(0, 0) is Dc if we choose = 4 to determine Dc .) With
Dc = AR2 /(8T ) and dropping the bars we obtain the scaled problem
2 w = 4 exp 2 (x2 + (y R0 )2 ) ,
(2.16)
to be solved over the unit disc with w = 0 on the boundary. Now there are
only two parameters to vary: the dimensionless extent of the pressure, , and
32 2 Fundamentals: Solving the Poisson equation
the localization of the pressure peak, R0 [0, 1]. As 0, the solution will
approach the special case w = 1 x2 y 2 .
Given a computed scaled solution w, the physical deflection can be com-
puted by
AR2
D= w.
8T
Just a few modifications are necessary to our previous program to solve
this new problem.
A mesh over the unit disk can be created by the mshr tool in FEniCS:
from mshr import *
domain = Circle(Point(0, 0), 1)
mesh = generate_mesh(domain, 64)
The Circle shape from mshr takes the center and radius of the circle as
arguments. The second argument to the generate_mesh function specifies
the desired mesh resolution. The cell size will be (approximately) equal to
the diameter of the domain divided by the resolution.
The variational problem and the boundary conditions are the same as in
our first Poisson problem, but we may introduce w instead of u as primary
unknown and p instead of f as right-hand side function:
w = TrialFunction(V)
v = TestFunction(V)
a = dot(grad(w), grad(v))*dx
L = p*v*dx
w = Function(V)
solve(a == L, w, bc)
Figure 2.3 shows a visualization of the deflection w and the load p created
with ParaView.
34 2 Fundamentals: Solving the Poisson equation
Fig. 2.3 Plot of the deflection (left) and load (right) for the membrane problem created
using ParaView. The plot uses 10 equispaced isolines for the solution values and the
optional jet colormap.
Another way to compare the deflection and the load is to make a curve plot
along the line x = 0. This is just a matter of defining a set of points along the
y-axis and evaluating the finite element functions w and p at these points:
# Curve plot along x = 0 comparing p and w
import numpy as np
import matplotlib.pyplot as plt
tol = 0.001 # avoid hitting points outside the domain
y = np.linspace(-1 + tol, 1 - tol, 101)
points = [(0, y_) for y_ in y] # 2D points
w_line = np.array([w(point) for point in points])
p_line = np.array([p(point) for point in points])
plt.plot(y, 50*w_line, k, linewidth=2) # magnify w
plt.plot(y, p_line, b--, linewidth=2)
plt.grid(True)
plt.xlabel($y$)
plt.legend([Deflection ($\\times 50$), Load], loc=upper left)
plt.savefig(poisson_membrane/curves.pdf)
plt.savefig(poisson_membrane/curves.png)
Fig. 2.4 Plot of the deflection and load for the membrane problem created using
Matplotlib and sampling of the two functions along the y-axsis.
Chapter 3
A Gallery of finite element solvers
The goal of this chapter is to demonstrate how a range of important PDEs from
science and engineering can be quickly solved with a few lines of FEniCS code.
We start with the heat equation and continue with a nonlinear Poisson equation,
the equations for linear elasticity, the NavierStokes equations, and finally look at
how to solve systems of nonlinear advectiondiffusionreaction equations. These
problems illustrate how to solve time-dependent problems, nonlinear problems,
vector-valued problems, and systems of PDEs. For each problem, we derive the
variational formulation and express the problem in Python in a way that closely
resembles the mathematics.
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
38 3 A Gallery of finite element solvers
u
= 2 u + f in (0, T ], (3.1)
t
u = uD on (0, T ], (3.2)
u = u0 at t = 0 . (3.3)
Here, u varies with space and time, e.g., u = u(x, y, t) if the spatial domain
is two-dimensional. The source function f and the boundary values uD may
also vary with space and time. The initial condition u0 is a function of space
only.
un+1 un
= 2 un+1 + f n+1 . (3.6)
t
This is our time-discrete version of the heat equation (3.1), a so-called back-
ward Euler or implicit Euler discretization.
We may reorder (3.6) so that the left-hand side contains the terms with
the unknown un+1 and the right-hand side contains computed terms only.
The result is a sequence of spatial (stationary) problems for un+1 , assuming
un is known from the previous time step:
3.1 The heat equation 39
u0 = u0 , (3.7)
n+1 2 n+1 n n+1
u t u = u + tf , n = 0, 1, 2, . . . (3.8)
Z
a(u, v) = (uv + tu v) dx, (3.10)
Z
un + tf n+1 v dx .
Ln+1 (v) = (3.11)
Fn+1 (u; v) = 0,
where
Z
uv + tu v (un + tf n+1 )v dx .
Fn+1 (u; v) = (3.12)
a0 (u, v) = L0 (v),
with
40 3 A Gallery of finite element solvers
Z
a0 (u, v) = uv dx, (3.13)
Z
L0 (v) = u0 v dx . (3.14)
Our program needs to implement the time-stepping manually, but can rely
on FEniCS to easily compute a0 , L0 , a, and L (or Fn+1 ), and solve the linear
systems for the unknowns.
Test problem 1: A known analytical solution. Just as for the Poisson
problem from the previous chapter, we construct a test problem that makes
it easy to determine if the calculations are correct. Since we know that our
first-order time-stepping scheme is exact for linear functions, we create a
test problem which has a linear variation in time. We combine this with a
quadratic variation in space. We thus take
u = 1 + x2 + y 2 + t, (3.15)
which yields a function whose computed values at the nodes will be exact,
regardless of the size of the elements and t, as long as the mesh is uniformly
partitioned. By inserting (3.15) into the heat equation (3.1), we find that the
right-hand side f must be given by f (x, y, t) = 22. The boundary value
is uD (x, y, t) = 1 +x2 + y 2 + t and the initial value is u0 (x, y) = 1 +x2 + y 2 .
FEniCS implementation. A new programming issue is how to deal
with functions that vary in space and time, such as the boundary condi-
3.1 The heat equation 41
The essential boundary conditions, along the entire boundary in this case,
are implemented in the same way as we have previously implemented the
boundary conditions for the Poisson problem:
def boundary(x, on_boundary):
return on_boundary
We shall use the variable u for the unknown un+1 at the new time step
and the variable u_n for un at the previous time step. The initial value of
u_n can be computed by either projection or interpolation of u0 . Since we
set t = 0 for the boundary value u_D, we can use u_D to specify the initial
condition:
u_n = project(u_D, V)
# or
u_n = interpolate(u_D, V)
In the last step of the time-stepping loop, we assign the values of the variable
u (the new computed solution) to the variable u_n containing the values at
the previous time step. This must be done using the assign member function.
If we instead try to do u_n = u, we will set the u_n variable to be the same
variable as u which is not what we want. (We need two variables, one for the
values at the previous time step and one for the values at the current time
step.)
The time-stepping loop above does not contain any comparison of the
numerical and the exact solutions, which we must include in order to verify
the implementation. As for the Poisson equation in Section 2.3, we compute
the difference between the array of nodal values for u and the array of nodal
values for the interpolated exact solution. This may be done as follows:
u_e = interpolate(u_D, V)
error = np.abs(u_e.vector().array() - u.vector().array()).max()
print(t = %.2f: error = %.3g % (t, error))
returns the vector of degrees of freedom. For a P1 function space, this vector
of degrees of freedom will be equal to the array of vertex values obtained by
calling compute_vertex_values, albeit possibly in a different order.
The complete program for solving the heat equation goes as follows:
from fenics import *
import numpy as np
# Time-stepping
u = Function(V)
t = 0
for n in range(num_steps):
# Compute solution
solve(a == L, u, bc)
# Plot solution
44 3 A Gallery of finite element solvers
plot(u)
# Hold plot
interactive()
Note that we have used a much higher resolution than before to better re-
solve the features of the solution. We also need to redefine the initial condi-
tion and the boundary condition. Both are easily changed by defining a new
Expression and by setting u = 0 on the boundary.
To be able to visualize the solution in an external program such as Par-
aView, we will save the solution to a file in VTK format in each time step.
We do this by first creating a File with the suffix .pvd:
vtkfile = File(heat_gaussian/solution.pvd)
Inside the time loop, we may then append the solution values to this file:
vtkfile << (u, t)
This line is called in each time step, resulting in the creation of a new file
with suffix .vtu containing all data for the time step (the mesh and the
vertex values). The file heat_gaussian/solution.pvd will contain the time
values and references to the .vtu file, which means that the .pvd file will
be a single small file that points to a large number of .vtu files containing
the actual data. Note that we choose to store the solution to a subdirectory
named heat_gaussian. This is to avoid cluttering our source directory with
3.1 The heat equation 45
all the generated data files. One does not need to create the directory before
running the program as it will be created automatically by FEniCS.
The complete program appears below.
from fenics import *
import time
# Time-stepping
u = Function(V)
t = 0
for n in range(num_steps):
# Compute solution
solve(a == L, u, bc)
u_n.assign(u)
# Hold plot
interactive()
Fig. 3.1 A sequence of snapshots of the solution of the Gaussian hill problem created
with ParaView.
We shall now address how to solve nonlinear PDEs. We will see that nonlinear
problems can be solved just as easily as linear problems in FEniCS, by sim-
ply defining a nonlinear variational problem and calling the solve function.
When doing so, we will encounter a subtle difference in how the variational
problem is defined.
As a model problem for the solution of nonlinear PDEs, we take the following
nonlinear Poisson equation:
3.2 A nonlinear Poisson equation 47
(q(u)u) = f, (3.16)
in , with u = uD on the boundary . The coefficient q = q(u) makes the
equation nonlinear (unless q(u) is constant in u).
F (u; v) = 0 v V , (3.17)
where
Z
F (u; v) = (q(u)u v f v) dx, (3.18)
and
V = {v H 1 () : v = uD on },
V = {v H 1 () : v = 0 on } .
F (u; v) = 0 v V , (3.19)
PN
with u = j=1 Uj j . Since F is nonlinear in u, the variational statement gives
rise to a system of nonlinear algebraic equations in the unknowns U1 , . . . , UN .
is more tedious. However, we may utilize SymPy for symbolic computing and
integrate such computations in the FEniCS solver. This allows us to eas-
ily experiment with different manufactured solutions. The forthcoming code
with SymPy requires some basic familiarity with this package. In particular,
we will use the SymPy functions diff for symbolic differentiation and ccode
for C/C++ code generation.
We take q(u) = 1 + u2 and define a two-dimensional manufactured solution
that is linear in x and y:
# Warning: from fenics import * will import both sym and
# q from FEniCS. We therefore import FEniCS first and then
# overwrite these objects.
from fenics import *
def q(u):
"Return nonlinear coefficient"
return 1 + u**2
Turning the expressions for u and f into C or C++ syntax for FEniCS
Expression objects needs two steps. First, we ask for the C code of the
expressions:
u_code = sym.printing.ccode(u)
f_code = sym.printing.ccode(f)
In some cases, one will need to edit the result to match the required syntax
of Expression objects, but not in this case. (The primary example is that
M_PI for in C/C++ must be replaced by pi for Expression objects.) In
the present case, the output of u_code and f_code is
3.2 A nonlinear Poisson equation 49
x[0] + 2*x[1] + 1
-10*x[0] - 20*x[1] - 10
After having defined the mesh, the function space, and the boundary, we
define the boundary value u_D as
u_D = Expression(u_code, degree=1)
def q(u):
return 1 + u**2
mesh = UnitSquareMesh(8, 8)
V = FunctionSpace(mesh, P, 1)
u_D = Expression(u_code, degree=1)
u = Function(V)
v = TestFunction(V)
f = Expression(f_code, degree=1)
F = q(u)*dot(grad(u), grad(v))*dx - f*v*dx
50 3 A Gallery of finite element solvers
solve(F == 0, u, bc)
A complete version of this example program can be found in the file ft05_
poisson_nonlinear.py.
The major difference from a linear problem is that the unknown function u
in the variational form in the nonlinear case must be defined as a Function,
not as a TrialFunction. In some sense this is a simplification from the linear
case where we must define u first as a TrialFunction and then as a Function.
The solve function takes the nonlinear equations, derives symbolically the
Jacobian matrix, and runs a Newton method to compute the solution.
When we run the code, FEniCS reports on the progress of the Newton
iterations. With 2 (8 8) cells, we reach convergence in eight iterations
with a tolerance of 109 , and the error in the numerical solution is about
1016 . These results bring evidence for a correct implementation. Thinking
in terms of finite differences on a uniform mesh, P1 elements mimic stan-
dard second-order differences, which compute the derivative of a linear or
quadratic function exactly. Here, u is a constant vector, but then multi-
plied by (1 + u2 ), which is a second-order polynomial in x and y, which the
divergence difference operator should compute exactly. We can therefore,
even with P1 elements, expect the manufactured u to be reproduced by the
numerical method. With a nonlinearity like 1 + u4 , this will not be the case,
and we would need to verify convergence rates instead.
The current example shows how easy it is to solve a nonlinear problem
in FEniCS. However, experts on the numerical solution of nonlinear PDEs
know very well that automated procedures may fail for nonlinear problems,
and that it is often necessary to have much better manual control of the
solution process than what we have in the current case. We return to this
problem in [23] and show how we can implement taylored solution algorithms
for nonlinear equations and also how we can steer the parameters in the
automated Newton method used above.
= f in , (3.20)
= tr ()I + 2, (3.21)
1
= u + (u)> , (3.22)
2
where is the stress tensor, f is the body force per unit volume, and are
Lams elasticity parameters for the material in , I is the identity tensor,
tr is the trace operator on a tensor, is the symmetric strain-rate tensor
(symmetric gradient), and u is the displacement vector field. We have here
assumed isotropic elastic conditions.
We combine (3.21) and (3.22) to obtain
part of the boundary, we assume that the value of the displacement is given
as a Dirichlet condition. We thus obtain
Z Z Z
: v dx = f v dx + T v ds .
T
Inserting the expression (3.23) for gives the variational form with u as
unknown. Note that the boundary integral on the remaining part \ T
vanishes due to the Dirichlet condition.
We can now summarize the variational formulation as: find u V such
that
Z
a(u, v) = (u) : v dx, (3.25)
(u) = ( u)I + (u + (u)> ), (3.26)
Z Z
L(v) = f v dx + T v ds . (3.27)
T
One can show that the inner product of a symmetric tensor A and an anti-
symmetric tensor B vanishes. If we express v as a sum of its symmetric and
anti-symmetric parts, only the symmetric part will survive in the product
: v since is a symmetric tensor. Thus replacing u by the symmetric
gradient (u) gives rise to the slightly different variational form
Z
a(u, v) = (u) : (v) dx, (3.28)
where (v) is the symmetric part of v:
1
(v) = v + (v)> .
2
The formulation (3.28) is what naturally arises from minimization of elastic
potential energy and is a more popular formulation than (3.25).
# Scaled variables
L = 1; W = 0.2
mu = 1
rho = 1
delta = W/L
gamma = 0.4*delta**2
beta = 1.25
lambda_ = beta
g = gamma
def epsilon(u):
return 0.5*(nabla_grad(u) + nabla_grad(u).T)
#return sym(nabla_grad(u))
def sigma(u):
return lambda_*nabla_div(u)*Identity(d) + 2*mu*epsilon(u)
# Compute solution
u = Function(V)
solve(a == L, u, bc)
# Plot solution
54 3 A Gallery of finite element solvers
# Plot stress
s = sigma(u) - (1./3)*tr(sigma(u))*Identity(d) # deviatoric stress
von_Mises = sqrt(3./2*inner(s, s))
V = FunctionSpace(mesh, P, 1)
von_Mises = project(von_Mises, V)
plot(von_Mises, title=Stress intensity)
V = FunctionSpace(mesh, P, 1)
von_Mises = project(von_Mises, V)
plot(von_Mises, title=Stress intensity)
( + )( u) 2 u = f,
we insert coordinates made dimensionless by L, and u
= u/U , which results
in the dimensionless governing equation
( u = f,
2u
) f = (0, 0, ),
where = 1 + / is a dimensionless elasticity parameter and where
%gL2
=
U
is a dimensionless variable reflecting the ratio of the load %g and the shear
stress term 2 u U/L2 in the PDE.
One option for the scaling is to chose U such that is of unit size
(U = %gL2 /). However, in elasticity, this leads to displacements of the size
of the geometry, which makes plots look very strange. We therefore want the
characteristic displacement to be a small fraction of the characteristic length
of the geometry. This can be achieved by choosing U equal to the maxi-
mum deflection of a clamped beam, for which there actually exists a formula:
U = 32 %gL2 2 /E, where = L/W is a parameter reflecting how slender the
beam is, and E is the modulus of elasticity. Thus, the dimensionless param-
eter is very important in the problem (as expected, since 1 is what
gives beam theory!). Taking E to be of the same order as , which is the case
for many materials, we realize that 2 is an appropriate choice. Exper-
imenting with the code to find a displacement that looks right in plots of
the deformed geometry, points to = 0.4 2 as our final choice of .
The simulation code implements the problem with dimensions and physical
parameters , , %, g, L, and W . However, we can easily reuse this code for
a scaled problem: just set = % = L = 1, W as W/L ( 1 ), g = , and = .
56 3 A Gallery of finite element solvers
Fig. 3.2 Plot of gravity-induced deflection in a clamped beam for the elasticity prob-
lem.
u
% + u u = (u, p) + f, (3.29)
t
u = 0. (3.30)
The right-hand side f is a given force per unit volume and just as for the
equations of linear elasticity, (u, p) denotes the stress tensor, which for a
Newtonian fluid is given by
3.4 The NavierStokes equations 57
1
h%(u? un )/t, vi + h%un un , vi + h(un+ 2 , pn ), (v)i
1
+ hpn n, vi hun+ 2 n, vi = hf n+1 , vi. (3.32)
58 3 A Gallery of finite element solvers
This notation, suitable for problems with many terms in the variational for-
mulations, requires some explanation. First, we use the short-hand notation
Z Z
hv, wi = vw dx, hv, wi = vw ds.
This allows us to express the variational problem in a more compact way.
1
Second, we use the notation un+ 2 . This notation refers to the value of u at
the midpoint of the interval, usually approximated by an arithmetic mean:
1
un+ 2 (un + un+1 )/2.
Third, we notice that the variational problem (3.32) arises from the integra-
tion by parts of the term h , vi. Just as for the elasticity problem in
Section 3.3, we obtain
h , vi = h, (v)i hT, vi ,
where T = n is the boundary traction. If we solve a problem with a free
boundary, we can take T = 0 on the boundary. However, if we compute the
flow through a channel or a pipe and want to model flow that continues into
an imaginary channel at the outflow, we need to treat this term with some
care. The assumption we then make is that the derivative of the velocity in the
direction of the channel is zero at the outflow, corresponding to a flow that is
fully developed or doesnt change significantly downstream of the outflow.
Doing so, the remaining boundary term at the outflow becomes pn u n,
which is the term appearing in the variational problem (3.32). Note that this
argument and the implementation depends on the exact definition of u,
as either the matrix with components ui /xj or uj /xi . We here choose
the latter, uj /xi , which means that we must use the FEniCS operator
nabla_grad for the implementation. If we use the grad operator and the
definition ui /xj , we must instead keep the terms pn (u)> n!
We now move on to the second step in our splitting scheme for the in-
compressible NavierStokes equations. In the first step, we computed the
tentative velocity u? based on the pressure from the previous time step. We
may now use the computed tentative velocity to compute the new pressure
pn :
u
+ Re u u = p + 2 u,
t
u = 0.
V = VectorFunctionSpace(mesh, P, 2)
Q = FunctionSpace(mesh, P, 1)
The first space V is a vector-valued function space for the velocity and the
second space Q is a scalar-valued function space for the pressure. We use
piecewise quadratic elements for the velocity and piecewise linear elements
for the pressure. When creating a VectorFunctionSpace in FEniCS, the
value-dimension (the length of the vectors) will be set equal to the geometric
dimension of the finite element mesh. One can easily create vector-valued
function spaces with other dimensions in FEniCS by adding the keyword
parameter dim:
V = VectorFunctionSpace(mesh, P, 2, dim=10)
Since we have two different function spaces, we need to create two sets of
trial and test functions:
u = TrialFunction(V)
v = TestFunction(V)
p = TrialFunction(Q)
q = TestFunction(Q)
string of C++ code, much like we have previously defined expressions such
as u_D = Expression(1 + x[0]*x[0] + 2*x[1]*x[1], degree=2). The
above definition of the boundary in terms of a Python function may thus be
replaced by a simple C++ string:
boundary = near(x[0], 0)
This has the advantage of moving the computation of which nodes belong
to the boundary from Python to C++, which improves the efficiency of the
program.
For the current example, we will set three different boundary conditions.
First, we will set u = 0 at the walls of the channel; that is, at y = 0 and y = 1.
Second, we will set p = 8 at the inflow (x = 0) and, finally, p = 0 at the outflow
(x = 1). This will result in a pressure gradient that will accelerate the flow
from the initial state with zero velocity. These boundary conditions may be
defined as follows:
# Define boundaries
inflow = near(x[0], 0)
outflow = near(x[0], 1)
walls = near(x[1], 0) || near(x[1], 1)
At the end, we collect the boundary conditions for the velocity and pressure
in Python lists so we can easily access them in the following computation.
We now move on to the definition of the variational forms. There are three
variational problems to be defined, one for each step in the IPCS scheme. Let
us look at the definition of the first variational problem. We start with some
constants:
U = 0.5*(u_n + u)
n = FacetNormal(mesh)
f = Constant((0, 0))
k = Constant(dt)
mu = Constant(mu)
rho = Constant(rho)
The next step is to set up the variational form for the first step (3.32) in
the solution process. Since the variational problem contains a mix of known
and unknown quantities we will use the following naming convention: u is
the unknown (mathematically un+1 ) as a trial function in the variational
form, u_ is the most recently computed approximation (un+1 available as a
Function object), u_n is un , and the same convention goes for p, p_ (pn+1 ),
and p_n (pn ).
64 3 A Gallery of finite element solvers
In the last step, FEniCS uses the overloaded solve function to solve the
linear system AUP= b where U is the vector of degrees of freedom for the
function u(x) = j=1 Uj j (x).
In our implementation of the splitting scheme, we will make use of these
low-level commands to first assemble and then call solve. This has the ad-
vantage that we may control when we assemble and when we solve the linear
system. In particular, since the matrices for the three variational problems
are all time-independent, it makes sense to assemble them once and for all
outside of the time-stepping loop:
A1 = assemble(a1)
A2 = assemble(a2)
A3 = assemble(a3)
Within the time-stepping loop, we may then assemble only the right-hand
side vectors, apply boundary conditions, and call the solve function as here
for the first of the three steps:
3.4 The NavierStokes equations 65
# Time-stepping
t = 0
for n in range(num_steps):
Fig. 3.3 Plot of the velocity profile at the final time for the NavierStokes channel
flow example.
0.20
0.1
0.21
0.41
0.20
2.20
Fig. 3.4 Geometry for the flow past a cylinder test problem. Notice the slightly per-
turbed and unsymmetric geometry.
1
http://www.featflow.de/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark2_re100.html
3.4 The NavierStokes equations 67
FEniCS implementation. So far all our domains have been simple shapes
such as a unit square or a rectangular box. A number of such simple meshes
may be created using the built-in mesh classes in FEniCS (UnitIntervalMesh,
UnitSquareMesh, UnitCubeMesh, IntervalMesh, RectangleMesh, BoxMesh).
FEniCS supports the creation of more complex meshes via a technique called
constructive solid geometry (CSG), which lets us define geometries in terms
of simple shapes (primitives) and set operations: union, intersection, and set
difference. The set operations are encoded in FEniCS using the operators +
(union), * (intersection), and - (set difference). To access the CSG function-
ality in FEniCS, one must import the FEniCS module mshr which provides
the extended meshing functionality of FEniCS.
The geometry for the cylinder flow test problem can be defined easily by
first defining the rectangular channel and then subtracting the circle:
channel = Rectangle(Point(0, 0), Point(2.2, 0.41))
cylinder = Circle(Point(0.2, 0.2), 0.05)
domain = channel - cylinder
Here the argument 64 indicates that we want to resolve the geometry with
64 cells across its diameter (the channel length).
To solve the cylinder test problem, we only need to make a few minor
changes to the code we wrote for the channel flow test case. Besides defining
the new mesh, the only change we need to make is to modify the boundary
conditions and the time step size. The boundaries are specified as follows:
inflow = near(x[0], 0)
outflow = near(x[0], 2.2)
walls = near(x[1], 0) || near(x[1], 0.41)
cylinder = on_boundary && x[0]>0.1 && x[0]<0.3 && x[1]>0.1 && x[1]<0.3
The last line may seem cryptic before you catch the idea: we want to pick
out all boundary points (on_boundary) that also lie within the 2D domain
[0.1, 0.3] [0.1, 0.3], see Figure 3.4. The only possible points are then the
points on the circular boundary!
In addition to these essential changes, we will make a number of small
changes to improve our solver. First, since we need to choose a relatively
small time step to compute the solution (a time step that is too large will
make the solution blow up) we add a progress bar so that we can follow the
progress of our computation. This can be done as follows:
progress = Progress(Time-stepping)
set_log_level(PROGRESS)
# Time-stepping
t = 0.0
for n in range(num_steps):
68 3 A Gallery of finite element solvers
Since the system(s) of linear equations are significantly larger than for the
simple channel flow test problem, we choose to use an iterative method in-
stead of the default direct (sparse) solver used by FEniCS when calling solve.
Efficient solution of linear systems arising from the discretization of PDEs
requires the choice of both a good iterative (Krylov subspace) method and
a good preconditioner. For this problem, we will simply use the biconjugate
gradient stabilized method (BiCGSTAB) and the conjugate gradient method.
This can be done by adding the keywords bicgstab or cg in the call to solve.
We also specify suitable preconditioners to speed up the computations:
solve(A1, u1.vector(), b1, bicgstab, hypre_amg)
solve(A2, p1.vector(), b2, bicgstab, hypre_amg)
solve(A3, u1.vector(), b3, cg, sor)
For the present example, we will instead choose to save the solution to
XDMF format. This file format works similarly to the .pvd files we have seen
earlier but has several advantages. First, the storage is much more efficient,
both in terms of speed and file sizes. Second, .xdmf files work in parallell,
both for writing and reading (postprocessing). Much like .pvd files, the actual
data will not be stored in the .xdmf file itself, but will instead be stored in
a (single) separate data file with the suffix .hdf5 which is an advanced file
format designed for high-performance computing. We create the XDMF files
as follows:
xdmffile_u = XDMFFile(navier_stokes_cylinder/velocity.xdmf)
xdmffile_p = XDMFFile(navier_stokes_cylinder/pressure.xdmf)
In each time step, we may then store the velocity and pressure by
xdmffile_u.write(u, t)
xdmffile_p.write(p, t)
Fig. 3.5 Plot of the velocity for the cylinder test problem at final time.
The complete code for the cylinder test problem looks as follows:
from fenics import *
from mshr import *
import numpy as np
Fig. 3.6 Plot of the pressure for the cylinder test problem at final time.
rho = 1 # density
# Create mesh
channel = Rectangle(Point(0, 0), Point(2.2, 0.41))
cylinder = Circle(Point(0.2, 0.2), 0.05)
domain = channel - cylinder
mesh = generate_mesh(domain, 64)
# Define boundaries
inflow = near(x[0], 0)
outflow = near(x[0], 2.2)
walls = near(x[1], 0) || near(x[1], 0.41)
cylinder = on_boundary && x[0]>0.1 && x[0]<0.3 && x[1]>0.1 && x[1]<0.3
bcp = [bcp_outflow]
# Assemble matrices
A1 = assemble(a1)
A2 = assemble(a2)
A3 = assemble(a3)
# Time-stepping
t = 0
for n in range(num_steps):
# Plot solution
plot(u_, title=Velocity)
plot(p_, title=Pressure)
# Hold plot
interactive()
u1
+ w u1 (u1 ) = f1 Ku1 u2 , (3.36)
t
u2
+ w u2 (u2 ) = f2 Ku1 u2 , (3.37)
t
u3
+ w u3 (u3 ) = f3 + Ku1 u2 Ku3 . (3.38)
t
74 3 A Gallery of finite element solvers
This system models the chemical reaction between two species A and B
in some domain :
A + B C.
We assume that the reaction is first-order, meaning that the reaction rate
is proportional to the concentrations [A] and [B] of the two species A and B:
d
[C] = K[A][B].
dt
We also assume that the formed species C spontaneously decays with a rate
proportional to the concentration [C]. In the PDE system (3.36)(3.38), we
use the variables u1 , u2 , and u3 to denote the concentrations of the three
species:
w
% + w w = (w, p) + f, (3.39)
t
w = 0, (3.40)
u1
+ w u1 (u1 ) = f1 Ku1 u2 , (3.41)
t
u2
+ w u2 (u2 ) = f2 Ku1 u2 , (3.42)
t
u3
+ w u3 (u3 ) = f3 + Ku1 u2 Ku3 . (3.43)
t
We assume that u1 = u2 = u3 = 0 at t = 0 and inject the species A and B into
the system by specifying nonzero source terms f1 and f2 close to the corners
at the inflow, and take f3 = 0. The result will be that A and B are convected
by advection and diffusion throughout the channel, and when they mix the
species C will be formed.
3.5 A system of advectiondiffusionreaction equations 75
Z
(t1 (un+1
1 un n+1
1 )v1 + w u1 v1 + un+1
1 v1 ) dx (3.44)
Z
+ (t1 (un+1
2 un n+1
2 )v2 + w u2 v2 + un+1
2 v2 ) dx
Z
+ (t1 (un+1
3 un n+1
3 )v3 + w u3 v3 + un+1
3 v3 ) dx
Z
(f1 v1 + f2 v2 + f3 v3 ) dx
Z
(Kun+1
1 un+1
2 v1 Kun+1
1 un+1
2 v2 + Kun+1
1 un+1
2 v3 Kun+1
3 v3 ) dx = 0.
The first step is to read the mesh from file. Luckily, we made sure to save the
mesh to file in the NavierStokes example and can now easily read it back
from file:
mesh = Mesh(navier_stokes_cylinder/cylinder.xml.gz)
The mesh is stored in the native FEniCS XML format (with additional gzip-
ping to decrease the file size).
Next, we need to define the finite element function space. For this problem,
we need to define several spaces. The first space we create is the space for
the velocity field w from the NavierStokes simulation. We call this space W
and define the space by
W = VectorFunctionSpace(mesh, P, 2)
It is important that this space is exactly the same as the space we used for the
velocity field in the NavierStokes solver. To read the values for the velocity
field, we use a TimeSeries:
timeseries_w = TimeSeries(navier_stokes_cylinder/velocity_series)
This will initialize the object timeseries_w which we will call later in the
time-stepping loop to retrieve values from the file velocity_series.h5 (in
binary HDF5 format).
For the three concentrations u1 , u2 , and u3 , we want to create a mixed
space with functions that represent the full system (u1 , u2 , u3 ) as a single
entity. To do this, we need to define a MixedElement as the product space of
three simple finite elements and then used the mixed element to define the
function space:
P1 = FiniteElement(P, triangle, 1)
element = MixedElement([P1, P1, P1])
V = FunctionSpace(mesh, element)
This syntax works great for two elements, but for three or more ele-
ments we meet a subtle issue in how the Python interpreter handles the
* operator. For the reaction system, we create the mixed element by
element = MixedElement([P1, P1, P1]) and one would be tempted
to write
element = P1 * P1 * P1
Once the space has been created, we need to define our test functions and
finite element functions. Test functions for a mixed function space can be
created by replacing TestFunction by TestFunctions:
v_1, v_2, v_3 = TestFunctions(V)
Since the problem is nonlinear, we need to work with functions rather than
trial functions for the unknowns. This can be done by using the corresponding
Functions construction in FEniCS. However, as we will need to access the
Function for the entire system itself, we first need to create that function
and then access its components:
u = Function(V)
u_1, u_2, u_3 = split(u)
t = 0
for n in range(num_steps):
t += dt
timeseries_w.retrieve(w.vector(), t)
solve(F == 0, u)
u_n.assign(u)
In each time step, we first read the current value for the velocity field from the
time series we have previously stored. We then solve the nonlinear system,
and assign the computed values to the left-hand side values for the next
time interval. When retrieving values from a TimeSeries, the values will by
default be interpolated (linearly) to the given time t if the time does not
exactly match a sample in the series.
The solution at the final time is shown in Figure 3.7. We clearly see the
advection of the species A and B and the formation of C along the center of
the channel where A and B meet.
Fig. 3.7 Plot of the concentrations of the three species A, B, and C (from top to
bottom) at final time.
progress = Progress(Time-stepping)
set_log_level(PROGRESS)
# Time-stepping
t = 0
for n in range(num_steps):
# Hold plot
interactive()
Note that u[0] is not really a Function object, but merely a symbolic ex-
pression, just like grad(u) in FEniCS is a symbolic expression and not a
Function representing the gradient. This means that u_1, u_2, u_3 can be
used in a variational problem, but cannot be used for plotting or postpro-
cessing.
To access the components of u for plotting and saving the solution to file,
we need to use a different variant of the split function:
u_1_, u_2_, u_3_ = u.split()
This returns three subfunctions as actual objects with access to the common
underlying data stored in u, which makes plotting and saving to file possible.
Alternatively, we can do
u_1_, u_2_, u_3_ = u.split(deepcopy=True)
which will create u_1_, u_2_, and u_3_ as stand-alone Function objects,
each holding a copy of the subfunction data extracted from u. This is useful
in many situations but is not necessary for plotting and saving solutions to
file.
Chapter 4
Subdomains and boundary conditions
So far, we have only looked briefly at how to specify boundary conditions. In this
chapter, we look more closely at how to specify boundary conditions on specific
parts (subdomains) of the boundary and how to combine multiple boundary con-
ditions. We will also look at how to generate meshes with subdomains and how
to define coefficients with different values in different subdomains.
Let D and N denote the parts of the boundary where the Dirichlet
and Neumann conditions apply, respectively. The complete boundary-value
problem can be written as
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
84 4 Subdomains and boundary conditions
2 u = f in , (4.1)
u = uD on D , (4.2)
u
= g on N . (4.3)
n
Again, we choose u = 1 + x2 + 2y 2 as the exact solution and adjust f , g, and
uD accordingly:
f (x, y) = 6,
0, y = 0,
g(x, y) =
4, y = 1,
uD (x, y) = 1 + x2 + 2y 2 .
g(x, y) = 4y .
The first task is to derive the variational formulation. This time we cannot
omit the boundary term arising from the integration by parts, because v is
only zero on D . We have
Z Z Z
u
(2 u)v dx = u v dx v ds,
n
and since v = 0 on D ,
Z Z Z
u u
v ds = v ds = gv ds,
n n
N
N
How does the Neumann condition impact the implementation? Let us revisit
our previous implementation ft01_poisson.py from Section 2.2 and examine
which changes we need to make to incorporate the Neumann condition. It
turns out that only two changes are necessary:
The function boundary defining the Dirichlet boundary must be modified.
The new boundary term must be added to the expression for L.
The first adjustment can be coded as
tol = 1E-14
2 u = f in ,
L
u = uL on D ,
R
u = uR on D ,
u
=g on N .
n
L R
Here, D is the left boundary x = 0, while D is the right boundary x = 1.
We note that uL (x, y) = 1 + 2y 2 , uR (x, y) = 2 + 2y 2 , and g(x, y) = 4y.
L
For the boundary condition on D , we define the usual triple of an expres-
sion for the boundary value, a function defining the location of the boundary,
and a DirichletBC object:
u_L = Expression(1 + 2*x[1]*x[1], degree=2)
We collect the two boundary conditions in a list which we can pass to the
solve function to compute the solution:
bcs = [bc_L, bc_R]
...
solve(a == L, u, bcs)
In the remainder of this section, we will discuss different strategies for defining
the coefficient kappa as an Expression that takes on different values in the
two subdomains.
88 4 Subdomains and boundary conditions
# Initialize kappa
kappa = K(degree=0)
kappa.set_k_values(1, 0.01)
The eval method gives great flexibility in defining functions, but a downside
is that FEniCS will call eval in Python for each node x, which is a slow
process.
An alternative method is to use a C++ string expression as we have seen
before, which is much more efficient in FEniCS. This can be done using an
inline if test:
tol = 1E-14
k_0 = 1.0
k_1 = 0.01
kappa = Expression(x[1] <= 0.5 + tol ? k_0 : k_1, degree=0,
tol=tol, k_0=k_0, k_1=k_1)
We now address how to specify the subdomains 0 and 1 using a more gen-
eral technique. This technique involves the use of two classes that are essential
in FEniCS when working with subdomains: SubDomain and MeshFunction.
Consider the following definition of the boundary x = 0:
4.3 Defining subdomains for different materials 89
boundary = Boundary()
bc = DirichletBC(V, Constant(0), boundary)
We notice that the inside function of the class Boundary is (almost) iden-
tical to the previous boundary definition in terms of the boundary function.
Technically, our class Boundary is a subclass of the FEniCS class SubDomain.
We will use two SubDomain subclasses to define the two subdomains 0
and 1 :
tol = 1E-14
class Omega_0(SubDomain):
def inside(self, x, on_boundary):
return x[1] <= 0.5 + tol
class Omega_1(SubDomain):
def inside(self, x, on_boundary):
return x[1] >= 0.5 - tol
Notice the use of <= and >= in both tests. FEniCS will call the inside
function for each vertex in a cell to determine whether or not the cell belongs
to a particular subdomain. For this reason, it is important that the test
holds for all vertices in cells aligned with the boundary. In addition, we use a
tolerance to make sure that vertices on the internal boundary at y = 0.5 will
belong to both subdomains. This is a little counter-intuitive, but is necessary
to make the cells both above and below the internal boundary belong to
either 0 or 1 .
To define the variable coefficient , we will use a powerful tool in FEn-
iCS called a MeshFunction. A MeshFunction is a discrete function that can
be evaluated at a set of so-called mesh entities. A mesh entity in FEn-
iCS is either a vertex, an edge, a face, or a cell (triangle or tetrahedron).
A MeshFunction over cells is suitable to represent subdomains (materials),
while a MeshFunction over facets (edges or faces) is used to represent pieces
of external or internal boundaries. A MeshFunction over cells can also be used
to represent boundary markers for mesh refinement. A FEniCS MeshFunction
90 4 Subdomains and boundary conditions
is parameterized both over its data type (like integers or booleans) and its
dimension (0 = vertex, 1 = edge etc.). Special subclasses VertexFunction,
EdgeFunction etc. are provided for easy definition of a MeshFunction of a
particular dimension.
Since we need to define subdomains of in the present example, we make
use of a CellFunction. The constructor is given two arguments: (1) the type
of value: int for integers, size_t for non-negative (unsigned) integers,
double for real numbers, and bool for logical values; (2) a Mesh ob-
ject. Alternatively, the constructor can take just a filename and initialize the
CellFunction from data in a file.
We first create a CellFunction with non-negative integer values (size_t):
materials = CellFunction(size_t, mesh)
Next, we use the two subdomains to mark the cells belonging to each
subdomain:
subdomain_0 = Omega_0()
subdomain_1 = Omega_1()
subdomain_0.mark(materials, 0)
subdomain_1.mark(materials, 1)
This will set the values of the mesh function materials to 0 on each cell
belonging to 0 and 1 on all cells belonging to 1 . Alternatively, we can use
the following equivalent code to mark the cells:
materials.set_all(0)
subdomain_1.mark(materials, 1)
To examine the values of the mesh function and see that we have indeed
defined our subdomains correctly, we can simply plot the mesh function:
plot(materials, interactive=True)
We may also wish to store the values of the mesh function for later use:
File(materials.xml.gz) << materials
Now, to use the values of the mesh function materials to define the
variable coefficient , we create a FEniCS Expression:
class K(Expression):
def __init__(self, materials, k_0, k_1, **kwargs):
self.materials = materials
self.k_0 = k_0
self.k_1 = k_1
values[0] = self.k_0
else:
values[0] = self.k_1
The SubDomain and Expression Python classes are very convenient, but
their use leads to function calls from C++ to Python for each node in the
mesh. Since this involves a significant cost, we need to make use of C++ code
if performance is an issue.
Instead of writing the SubDomain subclass in Python, we may instead use
the CompiledSubDomain tool in FEniCS to specify the subdomain in C++
code and thereby speed up our code. Consider the definition of the classes
Omega_0 and Omega_1 above in Python. The key strings that define these
subdomains can be expressed in C++ syntax and given as arguments to
CompiledSubDomain as follows:
tol = 1E-14
subdomain_0 = CompiledSubDomain(x[1] <= 0.5 + tol, tol=tol)
subdomain_1 = CompiledSubDomain(x[1] >= 0.5 - tol, tol=tol)
Python Expression classes may also be redefined using C++ for more ef-
ficient code. Consider again the definition of the class K above for the variable
coefficient = (x). This may be redefined using a C++ code snippet and
the keyword cppcode to the regular FEniCS Expression class:
cppcode = """
class K : public Expression
{
public:
std::shared_ptr<MeshFunction<std::size_t>> materials;
double k_0;
double k_1;
};
"""
With the notation above, the model problem to be solved with multiple
Dirichlet, Neumann, and Robin conditions can be formulated as follows:
(u) = f in , (4.8)
i i
u = uD on D , i = 0, 1, . . . (4.9)
u
= gi on Ni , i = 0, 1, . . . (4.10)
n
u
= ri (u si ) on Ri , i = 0, 1, . . . (4.11)
n
94 4 Subdomains and boundary conditions
Z Z XZ
u X u u
v ds = ds
ds
n i n i n
i N i R
XZ XZ
= gi ds + ri (u si ) ds .
i i i i
N R
Z XZ XZ Z
F= u v dx + gi v ds + ri (u si )v ds f v dx = 0 .
i i i i
N R
(4.12)
We have been used to writing this variational formulation in the standard
notation a(u, v) = L(v), which requires that we identify all integral depend-
ing on the trial function u, and collect these in a(u, v), while the remaining
integrals go into L(v). The integrals from the Robin condition must for this
reason be split into two parts:
Z Z Z
ri (u si )v ds = ri uv ds ri si v ds .
i i i
R R R
We then have
Z XZ
a(u, v) = u v dx + ri uv ds, (4.13)
i i
R
Z XZ XZ
L(v) = f v dx gi v ds + ri si v ds . (4.14)
i i i i
N R
Alternatively, we may keep the formulation (4.12) and either solve the varia-
tional problem as a nonlinear problem (F == 0) in FEniCS or use the FEniCS
functions lhs and rhs to extract the bilinear and linear parts of F:
a = lhs(F)
4.4 Setting multiple Dirichlet, Neumann, and Robin conditions 95
L = rhs(F)
Note that if we choose to solve this linear problem as a nonlinear problem,
the Newton iteration will converge in a single iteration.
Let us examine how to extend our Poisson solver to handle general combina-
tions of Dirichlet, Neumann, and Robin boundary conditions. Compared to
our previous code, we must consider the following extensions:
Defining markers for the different parts of the boundary.
Splitting the boundary integral into parts using the markers.
A general approach to the first task is to mark each of the desired boundary
parts with markers 0, 1, 2, and so forth. Here we aim at the four sides of the
unit square, marked with 0 (x = 0), 1 (x = 1), 2 (y = 0), and 3 (y = 1). The
markers will be defined using a MeshFunction, but contrary to Section 4.3,
this is not a function over cells, but a function over the facets of the mesh.
We use a FacetFunction for this purpose:
boundary_markers = FacetFunction(size_t, mesh)
As in Section 4.3 we use a subclass of SubDomain to identify the various
parts of the mesh function. Problems with domains of more complicated
geometries may set the mesh function for marking boundaries as part of the
mesh generation. In our case, the boundary x = 0 can be marked as follows:
class BoundaryX0(SubDomain):
tol = 1E-14
def inside(self, x, on_boundary):
return on_boundary and near(x[0], 0, tol)
bx0 = BoundaryX0()
bx0.mark(boundary_markers, 0)
Similarly, we create the classes BoundaryX1 (x = 1), BoundaryY0 (y = 0), and
BoundaryY1 (y = 1) boundary, and mark these as subdomains 1, 2, and 3,
respectively.
For generality of the implementation, we let the user specify what kind
of boundary condition that applies to each of the four boundaries. We set
up a Python dictionary for this purpose, with the key as subdomain number
and the value as a dictionary specifying the kind of condition as key and a
function as its value. For example,
boundary_conditions = {0: {Dirichlet: u_D},
1: {Robin: (r, s)},
2: {Neumann: g},
3: {Neumann, 0}}
96 4 Subdomains and boundary conditions
specifies
a Dirichlet condition u = uD for x = 0;
a Robin condition n u = r(u s) for x = 1;
a Neumann condition n u = g for y = 0;
a Neumann condition n u = 0 for y = 1.
As explained in Section 4.2, multiple Dirichlet conditions must be collected
in a list of DirichletBC objects. Based on the boundary_conditions data
structure above, we can construct this list by the following code snippet:
bcs = []
for i in boundary_conditions:
if Dirichlet in boundary_conditions[i]:
bc = DirichletBC(V, boundary_conditions[i][Dirichlet],
boundary_markers, i)
bcs.append(bc)
In our case, things get a bit more complicated since the information about
integrals in Neumann and Robin conditions are in the boundary_conditions
data structure. We can collect all Neumann conditions by the following code
snippet:
integrals_N = []
for i in boundary_conditions:
4.4 Setting multiple Dirichlet, Neumann, and Robin conditions 97
if Neumann in boundary_conditions[i]:
if boundary_conditions[i][Neumann] != 0:
g = boundary_conditions[i][Neumann]
integrals_N.append(g*v*ds(i))
Alternatively, we may use the FEniCS functions lhs and rhs as mentioned
above to simplify the extraction of terms for the Robin integrals:
integrals_R = []
for i in boundary_conditions:
if Robin in boundary_conditions[i]:
r, s = boundary_conditions[i][Robin]
integrals_R.append(r*(u - s)*v*ds(i))
F = kappa*dot(grad(u), grad(v))*dx + \
sum(integrals_R) - f*v*dx + sum(integrals_N)
a, L = lhs(F), rhs(F)
This time we can more naturally define the integrals from the Robin condition
as r*(u - s)*v*ds(i).
The complete code can be found in the function solver_bcs in the pro-
gram ft10_poisson_extended.py.
We will use the same exact solution ue = 1+x2 +2y 2 as in Chapter 2, and thus
take = 1 and f = 6. Our domain is the unit square, and we assign Dirichlet
conditions at x = 0 and x = 1, a Robin condition at y = 0, and a Neumann
condition at y = 1. With the given exact solution ue , we realize that the
Neumann condition at y = 1 is u/n = u/y = 4y = 4, while the Robin
condition at y = 0 can be selected in many ways. Since u/n = u/y = 0
98 4 Subdomains and boundary conditions
# Collect variables
variables = [u_e, u_00, u_01, f, g, r, s]
# Extract variables
u_e, u_00, u_01, f, g, r, s = variables
The complete code can be found in the function demo_bcs in the program
ft10_poisson_extended.py.
boundary conditions is to run through all vertex coordinates and check if the
SubDomain.inside method marks the vertex as on the boundary. Another
useful method is to list which degrees of freedom that are subject to Dirichlet
conditions, and for first-order Lagrange (P1 ) elements, print the correspond-
ing vertex coordinates as illustrated by the following code snippet:
if debug1:
look at how to use the FEniCS mesh generation tool mshr to generate meshes
and define subdomains.
We will again solve the Poisson equation, but this time for a different applica-
tion. Consider an iron cylinder with copper wires wound around the cylinder
as in Figure 4.2. Through the copper wires a static current J = 1 A is flowing
and we want to compute the magnetic field B in the iron cylinder, the copper
wires, and the surrounding vacuum.
Fig. 4.2 Cross-section of an iron cylinder with copper wires wound around the cylinder,
here with n = 8 windings. The inner circles are cross-sections of the copper wire coming
up (north) and the outer circles are cross-sections of the copper wire going down into
the plane (south).
D = %, (4.15)
B = 0, (4.16)
B
E = , (4.17)
t
D
H = + J. (4.18)
t
Here, D is the displacement field, B is the magnetic field, E is the electric
field, and H is the magnetizing field. In addition to Maxwells equations, we
also need a constitutive relation between B and H,
B = H, (4.19)
which holds for an isotropic linear magnetic medium. Here, is the magnetic
permeability of the material. Now, since B is solenoidal (divergence free)
according to Maxwells equations, we know that B must be the curl of some
vector field A. This field is called the magnetic vector potential. Since the
problem is static and thus D/t = 0, it follows that
J = H = (1 B) = (1 A) = (1 A). (4.20)
In the last step, we have expanded the second derivatives and used the gauge
freedom of A to simplify the equations to a simple vector-valued Poisson
problem for the magnetic vector potential; if B = A, then B = (A +
) for any scalar field (the gauge function). For the current problem, we
thus need to solve the following 2D Poisson problem for the z-component Az
of the magnetic vector potential:
(1 Az ) = Jz in R2 , (4.21)
lim Az = 0. (4.22)
|(x,y)|
Az Az
B(x, y) = , . (4.23)
y x
102 4 Subdomains and boundary conditions
Z
a(Az , v) = 1 Az v dx, (4.25)
Z
L(v) = Jz v dx. (4.26)
The first step is to generate a mesh for the geometry described in Figure 4.2.
We let a and b be the inner and outer radii of the iron cylinder and let c1
and c2 be the radii of the two concentric distributions of copper wire cross-
sections. Furthermore, we let r be the radius of a copper wire, R be the
radius of our domain, and n be the number of windings (giving a total of
2n copper-wire cross-sections). This geometry can be described easily using
mshr and a little bit of Python programming:
# Define geometry for background
domain = Circle(Point(0, 0), R)
The mesh that we generate will be a mesh of the entire disk with radius
R but we need the mesh generation to respect the internal boundaries de-
fined by the iron cylinder and the copper wires. We also want mshr to label
the subdomains so that we can easily specify material parameters () and
currents. To do this, we use the mshr function set_subdomain as follows:
# Set subdomain for iron cylinder
domain.set_subdomain(1, cylinder)
4.5 Generating meshes with subdomains 103
Once the subdomains have been created, we can generate the mesh:
mesh = generate_mesh(domain, 32)
Fig. 4.3 Plot of (part of) the mesh generated for the magnetostatics test problem. The
subdomains for the iron cylinder and copper wires are clearly visible
The mesh generated with mshr will contain information about the sub-
domains we have defined. To use this information in the definition of our
variational problem and subdomain-dependent parameters, we will need to
create a MeshFunction that marks the subdomains. This can be easily created
by a call to the member function mesh.domains, which holds the subdomain
data generated by mshr:
markers = MeshFunction(size_t, mesh, 2, mesh.domains())
This line creates a MeshFunction with unsigned integer values (the subdo-
main numbers) with dimension 2, which is the cell dimension for this 2D
problem.
104 4 Subdomains and boundary conditions
We can now use the markers as we have done before to redefine the inte-
gration measure dx:
dx = Measure(dx, domain=mesh, subdomain_data=markers)
mu = Permeability(mesh, degree=1)
As seen in this code snippet, we have used a somewhat less extreme value for
the magnetic permeability of iron. This is to make the solution a little more
interesting. It would otherwise be completely dominated by the field in the
iron cylinder.
Finally, when Az has been computed, we can compute the magnetic field:
W = VectorFunctionSpace(mesh, P, 1)
B = project(as_vector((A_z.dx(1), -A_z.dx(0))), W)
# Create mesh
mesh = generate_mesh(domain, 32)
mu = Permeability(mesh, degree=1)
a = (1 / mu)*dot(grad(A_z), grad(v))*dx
L_N = sum(J_N*v*dx(i) for i in range(2, 2 + n))
L_S = sum(J_S*v*dx(i) for i in range(2 + n, 2 + 2*n))
L = L_N + L_S
# Plot solution
plot(A_z)
plot(B)
# Hold plot
interactive()
The FEniCS programs we have written so far have been designed as flat Python
scripts. This works well for solving simple demo problems. However, when you
build a solver for an advanced application, you will quickly find the need for more
structured programming. In particular, you may want to reuse your solver to solve
a large number of problems where you vary the boundary conditions, the domain,
and coefficients such as material parameters. In this chapter, we will see how to
write general solver functions to improve the usability of FEniCS programs. We
will also discuss how to utilize iterative solvers with preconditioners for solving
linear systems, how to compute derived quantities, such as, e.g., the flux on a part
of the boundary, and how to compute errors and convergence rates.
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
110 5 Extensions: Improving the Poisson solver
We consider the flat program ft01_poisson.py for solving the Poisson prob-
lem developed in Chapter 2. Some of the code in this program is needed to
solve any Poisson problem 2 u = f on [0, 1] [0, 1] with u = uD on the
boundary, while other statements arise from our simple test problem. Let
us collect the general, reusable code in a function called solver. Our spe-
cial test problem will then just be an application of our solver with some
additional statements. We limit the solver function to just compute the nu-
merical solution. Plotting and comparing the solution with the exact solution
are considered to be problem-specific activities to be performed elsewhere.
We parameterize solver by f , uD , and the resolution of the mesh. Since
it is so trivial to use higher-order finite element functions by changing the
third argument to FunctionSpace, we also add the polynomial degree of the
finite element function space as an argument to solver.
from fenics import *
import numpy as np
# Compute solution
u = Function(V)
solve(a == L, u, bc)
return u
The remaining tasks of our initial program, such as calling the solver
function with problem-specific parameters and plotting, can be placed in
5.1 Refactoring the Poisson solver 111
The solution can now be computed, plotted, and saved to file by simply
calling the run_solver function.
The remaining part of our first program is to compare the numerical and
the exact solutions. Every time we edit the code we must rerun the test
and examine that error_max is sufficiently small so we know that the code
still works. To this end, we shall adopt unit testing, meaning that we create
a mathematical test and corresponding software that can run all our tests
112 5 Extensions: Improving the Poisson solver
automatically and check that all tests pass. Python has several tools for unit
testing. Two very popular ones are pytest and nose. These are almost identical
and very easy to use. More classical unit testing with test classes is offered by
the built-in module unittest, but here we are going to use pytest (or nose)
since that will result in shorter and clearer code.
Mathematically, our unit test is that the finite element solution of our
problem when f = 6 equals the exact solution u = uD = 1 + x2 + 2y 2 at the
vertices of the mesh. We have already created a code that finds the error
at the vertices for our numerical solution. Because of rounding errors, we
cannot demand this error to be zero, but we have to use a tolerance, which
depends on the number of elements and the degrees of the polynomials in
the finite element basis. If we want to test that the solver function works
for meshes up to 2 (20 20) elements and cubic Lagrange elements, 1010
is an appropriate tolerance for testing that the maximum error vanishes.
To make our test case work together with pytest and nose, we have to
make a couple of small adjustments to our program. The simple rule is that
each test must be placed in a function that
has a name starting with test_,
has no arguments, and
implements a test expressed as assert success, msg.
# Compute solution
u = solver(f, u_D, Nx, Ny, degree)
5.1 Refactoring the Poisson solver 113
This will run all functions named test_* (currently only the test_solver
function) found in the file and report the results. For more verbose output,
add the flags -s -v.
We shall make it a habit to encapsulate numerical test problems in unit
tests as above, and we strongly encourage the reader to create similar unit
tests whenever a FEniCS solver is implemented.
This line starts an interactive Python session which lets you print and
plot variables, which can be very helpful for debugging.
114 5 Extensions: Improving the Poisson solver
FEniCS makes it is easy to write a unified simulation code that can op-
erate in 1D, 2D, and 3D. As an appetizer, go back to the previous pro-
grams ft01_poisson.py or ft12_poisson_solver.py and change the mesh
construction from UnitSquareMesh(8, 8) to UnitCubeMesh(8, 8, 8). Now
the domain is the unit cube partitioned into 8 8 8 boxes, and each box
is divided into six tetrahedron-shaped finite elements for computations. Run
the program and observe that we can solve a 3D problem without any other
modifications! (In 1D, expressions must be modified to not depend on x[1].)
The visualization allows you to rotate the cube and observe the function
values as colors on the boundary.
If we want to parameterize the creation of unit interval, unit square, or unit
cube over dimension, we can do so by encapsulating this part of the code in a
function. Given a list or tuple specifying the division into cells in the spatial
coordinates, the following function returns the mesh for a d-dimensional cube:
def UnitHyperCube(divisions):
mesh_classes = [UnitIntervalMesh, UnitSquareMesh, UnitCubeMesh]
d = len(divisions)
mesh = mesh_classes[d - 1](*divisions)
return mesh
The construction mesh_class[d - 1] will pick the right name of the object
used to define the domain and generate the mesh. Moreover, the argument
*divisions sends all the components of the list divisions as separate ar-
guments to the constructor of the mesh construction class picked out by
mesh_class[d - 1]. For example, in a 2D problem where divisions has
two elements, the statement
mesh = mesh_classes[d - 1](*divisions)
is equivalent to
mesh = UnitSquareMesh(divisions[0], divisions[1])
Section 5.2.6 lists the most popular choices of Krylov solvers and precondi-
tioners available in FEniCS.
The actual GMRES and ILU implementations that are brought into action
depend on the choice of linear algebra package. FEniCS interfaces several
linear algebra packages, called linear algebra backends in FEniCS terminology.
PETSc is the default choice if FEniCS is compiled with PETSc. If PETSc is
not available, then FEniCS falls back to using the Eigen backend. The linear
algebra backend in FEniCS can be set using the following command:
parameters.linear_algebra_backend = backendname
116 5 Extensions: Improving the Poisson solver
where backendname is a string. To see which linear algebra backends are avail-
able, you can call the FEniCS function list_linear_algebra_backends.
Similarly, one may check which linear algebra backend is currently being
used by the following command:
print(parameters.linear_algebra_backend)
We will normally want to control the tolerance in the stopping criterion and
the maximum number of iterations when running an iterative method. Such
parameters can be controlled at both a global and a local level. We will start
by looking at how to set global parameters. For more advanced programs,
one may want to use a number of different linear solvers and set different
tolerances and other parameters. Then it becomes important to control the
parameters at a local level. We will return to this issue in Section 5.3.1.
Changing a parameter in the global FEniCS parameter database affects all
linear solvers (created after the parameter has been set). The global FEniCS
parameter database is simply called parameters and it behaves as a nested
dictionary. Write
info(parameters, verbose=True)
to list all parameters and their default values in the database. The nesting of
parameter sets is indicated through indentation in the output from info. Ac-
cording to this output, the relevant parameter set is named krylov_solver,
and the parameters are set like this:
prm = parameters.krylov_solver # short form
prm.absolute_tolerance = 1E-10
prm.relative_tolerance = 1E-6
prm.maximum_iterations = 1000
Stopping criteria for Krylov solvers usually involve some norm of the residual,
which must be smaller than the absolute tolerance parameter or smaller than
the relative tolerance parameter times the initial residual.
We remark that default values for the global parameter database can be
defined in an XML file. To generate such a file from the current set of pa-
rameters in a program, run
File(parameters.xml) << parameters
The XML file can also be in gziped form with the extension .xml.gz.
Which linear solvers and preconditioners that are available in FEniCS de-
pends on how FEniCS has been configured and which linear algebra backend
is currently active. The following table shows an example of which linear
solvers that can be available through FEniCS when the PETSc backend is
active:
118 5 Extensions: Improving the Poisson solver
Name Method
Name Method
An up-to-date list of the available solvers and preconditioners for your FEn-
iCS installation can be produced by
list_linear_solver_methods()
list_krylov_solver_preconditioners()
pared to the high-level solve function interface. This interface uses the two
classes LinearVariationalProblem and LinearVariationalSolver. Using
this interface, the equivalent of solve(a == L, u, bc) looks as follows:
u = Function(V)
problem = LinearVariationalProblem(a, L, u, bc)
solver = LinearVariationalSolver(problem)
solver.solve()
AU = b,
where the entries of A and b are given by
Aij = a(j , i ),
bi = L(i ) .
The examples so far have specified the left- and right-hand sides of the
variational formulation and then asked FEniCS to assemble the linear system
and solve it. An alternative is to explicitly call functions for assembling the
coefficient matrix A and the right-hand side vector b, and then solve the
linear system AU = b for the vector U . Instead of solve(a == L, U, b) we
now write
A = assemble(a)
b = assemble(L)
bc.apply(A, b)
u = Function(V)
U = u.vector()
solve(A, U, b)
The variables a and L are the same as before; that is, a refers to the bilinear
form involving a TrialFunction object u and a TestFunction object v, and
L involves the same TestFunction object v. From a and L, the assemble
function can compute A and b.
Creating the linear system explicitly in a program can have some advan-
tages in more advanced problem settings. For example, A may be constant
throughout a time-dependent simulation, so we can avoid recalculating A at
every time level and save a significant amount of simulation time.
The matrix A and vector b are first assembled without incorporating es-
sential (Dirichlet) boundary conditions. Thereafter, the call bc.apply(A,
b) performs the necessary modifications of the linear system such that u is
guaranteed to equal the prescribed boundary values. When we have multiple
Dirichlet conditions stored in a list bcs, we must apply each condition in bcs
to the system:
for bc in bcs:
bc.apply(A, b)
when using particular linear solvers designed for symmetric systems, such as
the conjugate gradient method.
Once the linear system has been assembled, we need to compute the so-
lution U = A1 b and store the solution U in the vector U = u.vector().
In the same way as linear variational problems can be programmed us-
ing different interfaces in FEniCSthe high-level solve function, the class
LinearVariationalSolver, and the low-level assemble functionlinear
systems can also be programmed using different interfaces in FEniCS. The
high-level interface to solving a linear system in FEniCS is also named solve:
solve(A, U, b)
Note that we must both turn off the default behavior of setting the start
vector (initial guess) to zero, and also set the values of the vector U to
nonzero values. This is useful if we happen to know a good initial guess for
the solution.
122 5 Extensions: Improving the Poisson solver
With access to the elements in A through a numpy array, we can easily per-
form computations on this matrix, such as computing the eigenvalues (using
the eig function in numpy.linalg). We can alternatively dump A.array()
and b.array() to file in MATLAB format and invoke MATLAB or Octave to
analyze the linear system. Dumping the arrays to MATLAB format is done
by
import scipy.io
scipy.io.savemat(Ab.mat, {A: A.array(), b: b.array()})
Writing load Ab.mat in MATLAB or Octave will then make the array vari-
ables A and b available for computations.
Matrix processing in Python or MATLAB/Octave is only feasible for
small PDE problems since the numpy arrays or matrices in MATLAB file
format are dense matrices. FEniCS also has an interface to the eigen-
solver package SLEPc, which is the preferred tool for computing the eigen-
values of large, sparse matrices of the type encountered in PDE prob-
lems (see demo/documented/eigenvalue/python/ in the FEniCS/DOLFIN
source code tree for a demo).
5.4 Degrees of freedom and function evaluation 123
We have seen before how to grab the degrees of freedom array from a finite
element function u:
nodal_values = u.vector().array()
Both nodal_values and vertex_values will be numpy arrays and they will
be of the same length and contain the same values (for P1 elements), but
with possibly different ordering. The array vertex_values will have the same
ordering as the vertices of the mesh, while nodal_values will be ordered in
a way that (nearly) minimizes the bandwidth of the system matrix and thus
improves the efficiency of linear solvers.
A fundamental question is: what are the coordinates of the vertex whose
value is nodal_values[i]? To answer this question, we need to understand
how to get our hands on the coordinates, and in particular, the numbering
of degrees of freedom and the numbering of vertices in the mesh.
The function mesh.coordinates returns the coordinates of the vertices
as a numpy array with shape (M, d), M being the number of vertices in the
mesh and d being the number of space dimensions:
>>> from fenics import *
>>> mesh = UnitSquareMesh(2, 2)
>>> coordinates = mesh.coordinates()
>>> coordinates
array([[ 0. , 0. ],
[ 0.5, 0. ],
[ 1. , 0. ],
[ 0. , 0.5],
[ 0.5, 0.5],
[ 1. , 0.5],
[ 0. , 1. ],
[ 0.5, 1. ],
[ 1. , 1. ]])
We see from this output that for this particular mesh, the vertices are first
numbered along y = 0 with increasing x coordinate, then along y = 0.5, and
so on.
Next we compute a function u on this mesh. Lets take u = x + y:
>>> V = FunctionSpace(mesh, P, 1)
>>> u = interpolate(Expression(x[0] + x[1], degree=1), V)
124 5 Extensions: Improving the Poisson solver
We have seen how to extract the nodal values in a numpy array. If desired,
we can adjust the nodal values too. Say we want to normalize the solution
such that maxj |Uj | = 1. Then we must divide all Uj values by maxj |Uj |. The
following function performs the task:
def normalize_solution(u):
"Normalize u: return u divided by max(u)"
u_array = u.vector().array()
u_max = np.max(np.abs(u_array))
u_array /= u_max
u.vector()[:] = u_array
#u.vector().set_local(u_array) # alternative
return u
126 5 Extensions: Improving the Poisson solver
When using Lagrange elements, this (approximately) ensures that the maxi-
mum value of the function u is 1.
The /= operator implies an in-place modification of the object on the left-
hand side: all elements of the array nodal_values are divided by the value
u_max. Alternatively, we could do nodal_values = nodal_values / u_max,
which implies creating a new array on the right-hand side and assigning this
array to the name nodal_values.
As the final theme in this chapter, we will look at how to postprocess computa-
tions; that is, how to compute various derived quantities from the computed
solution of a PDE. The solution u itself may be of interest for visualizing gen-
eral features of the solution, but sometimes one is interested in computing
the solution of a PDE to compute a specific quantity that derives from the
solution, such as, e.g., the flux, a point-value, or some average of the solution.
(u) = f in , (5.1)
u = uD on . (5.2)
We note that the gradient of a piecewise continuous finite element scalar field
is a discontinuous vector field since the basis functions {j } have discontin-
uous derivatives at the boundaries of the cells. For example, using Lagrange
elements of degree 1, u is linear over each cell, and the gradient becomes a
piecewise constant vector field. On the contrary, the exact gradient is con-
tinuous. For visualization and data analysis purposes, we often want the
computed gradient to be a continuous vector field. Typically, we want each
component of u to be represented in the same way as u itself. To this end,
we can project the components of u onto the same function space as we
used for u. This means that we solve w = u approximately by a finite ele-
ment method, using the same elements for the components of w as we used
for u. This process is known as projection.
Projection is a common operation in finite element analysis and, as we have
already seen, FEniCS has a function for easily performing the projection:
project(expression, W), which returns the projection of some expression
into the space W.
In our case, the flux Q = u is vector-valued and we need to pick W as
the vector-valued function space of the same degree as the space V where u
resides:
V = u.function_space()
mesh = V.mesh()
degree = V.ufl_element().degree()
W = VectorFunctionSpace(mesh, P, degree)
grad_u = project(grad(u), W)
flux_u = project(-k*grad(u), W)
The degrees of freedom of the flux_u vector field can also be reached by
flux_u_nodal_values = flux_u.vector().array()
However, this is a flat numpy array containing the degrees of freedom for both
the x and y components of the flux and the ordering of the components may
be mixed up by FEniCS in order to improve computational efficiency.
The function demo_flux in the program ft10_poisson_extended.py
demonstrates the computations described above.
Manual projection.
Although you will always use project to project a finite element func-
tion, it can be instructive to look at how to formulate the projection
mathematically and implement its steps manually in FEniCS.
Lets say we have an expression g = g(u) that we want to project into
some space W . The mathematical formulation of the (L2 ) projection
w = PW g into W is the variational problem
Z Z
wv dx = gv dx (5.3)
for all test functions v W . In other words, we have a standard varia-
tional problem a(w, v) = L(v) where now
Z
a(w, v) = wv dx, (5.4)
Z
L(v) = gv dx . (5.5)
130 5 Extensions: Improving the Poisson solver
Note that we must here specify the argument domain=mesh to the mea-
sure dx. This is normally not necessary when defining forms in FEniCS
but is necessary here since cos(x[0]) is not associated with any domain
(as is the case when we integrate a Function from some FunctionSpace
defined on some Mesh).
Varying the degree between 0 and 5, the value of | sin(1)I| is 0.036,
0.071, 0.00030, 0.00013, 4.5E-07, and 2.5E-07.
FEniCS also allows expressions to be expressed directly as part of a
form. This requires the creation of a SpatialCoordinate. In this case,
the accuracy is dictated by the accuracy of the integration, which may
be controlled by a degree argument to the integration measure dx.
The degree argument specifies that the integration should be exact for
polynomials of that degree.
The following code snippet shows how to compute the integral
R1
0 cos x dx using this approach:
mesh = UnitIntervalMesh(1)
x = SpatialCoordinate(mesh)
I = assemble(cos(x[0])*dx(degree=degree))
A central question for any numerical method is its convergence rate: how
fast does the error approach zero when the resolution is increased? For finite
element methods, this typically corresponds to proving, theoretically or em-
pirically, that the error e = ue u is bounded by the mesh size h to some
power r; that is, kek Chr for some constant C. The number r is called the
convergence rate of the method. Note that different norms, like the L2 -norm
kek or H01 -norm kek typically have different convergence rates.
To illustrate how to compute errors and convergence rates in FEniCS,
we have included the function compute_convergence_rates in the tutorial
program ft10_poisson_extended.py. This is a tool that is very handy when
verifying finite element codes and will therefore be explained in detail here.
5.5 Postprocessing computations 133
As above, we have used abs in the expression for E above to ensure a positive
value for the sqrt function.
It is important to understand how FEniCS computes the error from the
above code, since we may otherwise run into subtle issues when using the
value for computing convergence rates. The first subtle issue is that if u_e is
not already a finite element function (an object created using Function(V)),
which is the case if u_e is defined as an Expression, FEniCS must interpolate
u_e into some local finite element space on each element of the mesh. The
degree used for the interpolation is determined by the mandatory keyword
argument to the Expression class, for example:
u_e = Expression(sin(x[0]), degree=1)
This means that the error computed will not be equal to the actual error
kue uk but rather the difference between the finite element solution u and
the piecewise linear interpolant of ue . This may yield a too optimistic (too
small) value for the error. A better value may be achieved by interpolating
the exact solution into a higher-order function space, which can be done by
simply increasing the degree:
u_e = Expression(sin(x[0]), degree=3)
The second subtle issue is that when FEniCS evaluates the expression
(u_e - u)**2, this will be expanded into u_e**2 + u**2 - 2*u_e*u. If the
error is small (and the solution itself is of moderate size), this calculation
will correspond to the subtraction of two positive numbers (u_e**2 + u**2
1 and 2*u_e*u 1) yielding a small number. Such a computation is very
prone to round-off errors, which may again lead to an unreliable value for the
error. To make this situation worse, FEniCS may expand this computation
into a large number of terms, in particular for higher order elements, making
the computation very unstable.
To help with these issues, FEniCS provides the built-in function errornorm
which computes the error norm in a more intelligent way. First, both u_e and
u are interpolated into a higher-order function space. Then, the degrees of
freedom of u_e and u are subtracted to produce a new function in the higher-
order function space. Finally, FEniCS integrates the square of the difference
function and then takes the square root to get the value of the error norm.
Using the errornorm function is simple:
E = errornorm(u_e, u, normtype=L2)
V = u.function_space()
mesh = V.mesh()
degree = V.ufl_element().degree()
W = FunctionSpace(mesh, P, degree + 3)
u_e_W = interpolate(u_e, W)
u_W = interpolate(u, W)
e_W = Function(W)
e_W.vector()[:] = u_e_W.vector().array() - u_W.vector().array()
error = e_W**2*dx
return sqrt(abs(assemble(error)))
ln(Ei /Ei1 )
r= .
ln(hi /hi1 )
The r values should approach the expected convergence rate (typically the
polynomial degree + 1 for the L2 -error) as i increases.
The procedure above can easily be turned into Python code. Here we run
through a list of element degrees (P1 , P2 , and P3 ), perform experiments over
a series of refined meshes, and for each experiment report the six error types
as returned by compute_errors.
Test problem. To demonstrate the computation of convergence rates, we
pick an exact solution ue , this time a little more interesting than for the test
problem in Chapter 2:
omega = 1.0
u_e = Expression(sin(omega*pi*x[0])*sin(omega*pi*x[1]),
degree=6, omega=omega)
f = 2*pi**2*omega**2*u_e
element n = 8 n = 16 n = 32 n = 64
An entry like 3.99 for n = 32 and P3 means that we estimate the rate 3.99
by comparing two meshes, with resolutions n = 32 and n = 16, using P3
elements. Note the superconvergence for P2 at the nodes. The best estimates
of the rates appear in the right-most column, since these rates are based
on the finest resolutions and are hence deepest into the asymptotic regime
(until we reach a level where round-off errors and inexact solution of the
linear system starts to play a role).
The L2 -norm errors computed using the FEniCS errornorm function show
the expected hd+1 rate for u:
element n = 8 n = 16 n = 32 n = 64
However, using (u_e - u)**2 for the error computation, with the same de-
gree for the interpolation of u_e as for u, gives strange results:
136 5 Extensions: Improving the Poisson solver
element n = 8 n = 16 n = 32 n = 64
Many readers have extensive experience with visualization and data analysis
of 1D, 2D, and 3D scalar and vector fields on uniform, structured meshes,
while FEniCS solvers exclusively work with unstructured meshes. Since it
can many times be practical to work with structured data, we discuss in this
section how to extract structured data for finite element solutions computed
with FEniCS.
A necessary first step is to transform our Mesh object to an object repre-
senting a rectangle (or a 3D box) with equally-shaped rectangular cells. The
second step is to transform the one-dimensional array of nodal values to a
two-dimensional array holding the values at the corners of the cells in the
structured mesh. We want to access a value by its i and j indices, i counting
cells in the x direction, and j counting cells in the y direction. This trans-
formation is in principle straightforward, yet it frequently leads to obscure
indexing errors, so using software tools to ease the work is advantageous.
In the directory of example programs included with this book, we have
included the Python module boxfield which provides utilities for working
with structured mesh data in FEniCS. Given a finite element function u, the
following function returns a BoxField object that represents u on a structured
mesh:
from boxfield import *
u_box = FEniCSBoxField(u, (nx, ny))
Iterating over points and values. Let us now use the solver function
from the ft10_poisson_extended.py code to compute u, map it onto a
BoxField object for a structured mesh representation, and print the coordi-
nates and function values at all mesh points:
u = solver(p, f, u_b, nx, ny, 1, linear_solver=direct)
u_box = structured_mesh(u, (nx, ny))
u_ = u_box.values
Making surface plots. The ability to access a finite element field as struc-
tured data is handy in many occasions, e.g., for visualization and data anal-
ysis. Using Matplotlib, we can create a surface plot, as shown in Figure 5.1
(upper left):
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # necessary for 3D plotting
from matplotlib import cm
fig = plt.figure()
ax = fig.gca(projection=3d)
cv = u_box.grid.coorv # vectorized mesh coordinates
ax.plot_surface(cv[X], cv[Y], u_, cmap=cm.coolwarm,
rstride=1, cstride=1)
plt.title(Surface plot of solution)
The key issue is to know that the coordinates needed for the surface plot is
in u_box.grid.coorv and that the values are in u_.
Making contour plots. A contour plot can also be made by Matplotlib:
138 5 Extensions: Improving the Poisson solver
1.0 0.8
0.8
0.6 0.000 0.000
0.4 0.6
0.2
0.600
0.400
00
0
0.80
0.0
0.2
0.2 0.4
0.4
1.0
0.8 0.2
0.000
0.0 0.6
0.2 0.4
0.4
0.6 0.2
0.8
1.0 0.0 0.0
0.0 0.2 0.4 0.6 0.8 1.0
0.2 0
u
u
0.1
2
0.0
4
0.1
0.2 6
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
x x
fig = plt.figure()
ax = fig.gca()
levels = [1.5, 2.0, 2.5, 3.5]
cs = ax.contour(cv[X], cv[Y], u_, levels=levels)
plt.clabel(cs) # add labels to contour lines
plt.axis(equal)
plt.title(Contour plot of solution)
The variable snapped is true if the line is snapped onto to nearest gridline
and in that case y_fixed holds the snapped (altered) y value. The keyword
argument snap is by default True to avoid interpolation and force snapping.
A comparison of the numerical and exact solution along the line y 0.41
(snapped from y = 0.4) is made by the following code:
# Plot u along a line y = const and compare with exact solution
start = (0, 0.4)
x, u_val, y_fixed, snapped = u_box.gridline(start, direction=X)
u_e_val = [u_D((x_, y_fixed)) for x_ in x]
plt.figure()
plt.plot(x, u_val, r-)
plt.plot(x, u_e_val, bo)
plt.legend([P1 elements, exact], loc=best)
plt.title(Solution along line y=%g % y_fixed)
plt.xlabel(x); plt.ylabel(u)
See Figure 5.1 (lower left) for the resulting curve plot.
Making curve plots of the flux. Let us also compare the numerical and
exact fluxes u/x along the same line as above:
# Plot the numerical and exact flux along the same line
flux_u = flux(u, kappa)
flux_u_x, flux_u_y = flux_u.split(deepcopy=True)
flux2_x = flux_u_x if flux_u_x.ufl_element().degree() == 1 \
else interpolate(flux_x,
FunctionSpace(u.function_space().mesh(), P, 1))
flux_u_x_box = FEniCSBoxField(flux_u_x, (nx,ny))
x, flux_u_val, y_fixed, snapped = \
flux_u_x_box.gridline(start, direction=X)
y = y_fixed
plt.figure()
plt.plot(x, flux_u_val, r-)
plt.plot(x, flux_u_x_exact(x, y_fixed), bo)
plt.legend([P1 elements, exact], loc=best)
plt.title(Flux along line y=%g % y_fixed)
plt.xlabel(x); plt.ylabel(u)
The function flux called at the beginning of the code snippet is defined in
the example program ft10_poisson_extended.py and interpolates the flux
back into the function space.
Note that Matplotlib is one choice of plotting package. With the unified
interface in the SciTools package1 one can access Matplotlib, Gnuplot, MAT-
LAB, OpenDX, VisIt, and other plotting engines through the same API.
Test problem. The graphics referred to in Figure 5.1 correspond to a test
problem with prescribed solution ue = H(x)H(y), where
1 2
H(x) = e16(x 2 ) sin(3x) .
1
https://github.com/hplgit/scitools
140 5 Extensions: Improving the Poisson solver
H = lambda x: exp(-16*(x-0.5)**2)*sin(3*pi*x)
x, y = sym.symbols(x[0], x[1])
u = H(x)*H(y)
Turning the expression for u into C or C++ syntax for Expression objects
needs two steps. First we ask for the C code of the expression:
u_code = sym.printing.ccode(u)
Printing u_code gives (the output is here manually broken into two lines):
-exp(-16*pow(x[0] - 0.5, 2) - 16*pow(x[1] - 0.5, 2))*
sin(3*M_PI*x[0])*sin(3*M_PI*x[1])
c 2017, Hans Petter Langtangen, Anders Logg.
Released under CC Attribution 4.0 license
144 REFERENCES
145
146 INDEX