$$ \newcommand{\dt}{\Delta t} \newcommand{\tp}{\thinspace .} \newcommand{\uex}{{u_{\small\mbox{e}}}} \newcommand{\x}{\boldsymbol{x}} \renewcommand{\u}{\boldsymbol{u}} \newcommand{\dx}{\, \mathrm{d}x} \newcommand{\ds}{\, \mathrm{d}s} \newcommand{\Real}{\mathbb{R}} \newcommand{\ub}{u_{_\mathrm{D}}} \newcommand{\GD}{\Gamma_{_\mathrm{D}}} \newcommand{\GN}{\Gamma_{_\mathrm{N}}} \newcommand{\GR}{\Gamma_{_\mathrm{R}}} \newcommand{\inner}[2]{\langle #1, #2 \rangle} \newcommand{\renni}[2]{\langle #2, #1 \rangle} $$

 

 

 

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 the specification of the mathematical variational problem, applying boundary conditions, calling the FEniCS solver, and plotting the solution.

Mathematical problem formulation

Most books on a programming language start with a "Hello, World!" program. That is, one is curious about how a very fundamental task is expressed in the language, and writing a text to the screen can be such a task. In the world of finite element methods for PDEs, the most fundamental task must be to solve the Poisson equation. Our counterpart to the classical "Hello, World!" program therefore solves $$ \begin{align} - \nabla^2 u(\x) &= f(\x),\quad \x\mbox{ in } \Omega, \tag{2.1}\\ u(\x) &= \ub(\x),\quad \x\mbox{ on } \partial \Omega\tp \tag{2.2} \end{align} $$ Here, \( u = u(\x) \) is the unknown function, \( f = f(\x) \) is a prescribed function, \( \nabla^2 \) is the Laplace operator (also often written as \( \Delta \)), \( \Omega \) is the spatial domain, and \( \partial\Omega \) is the boundary of \( \Omega \). A stationary PDE like this, together with a complete set of boundary conditions, constitute a boundary-value problem, which must be precisely stated before it makes sense to start solving it with FEniCS.

In two space dimensions with coordinates \( x \) and \( y \), we can write out the Poisson equation as $$ \begin{equation} - {\partial^2 u\over\partial x^2} - {\partial^2 u\over\partial y^2} = f(x,y)\tp \tag{2.3} \end{equation} $$ The unknown \( u \) is now a function of two variables, \( u = u(x,y) \), defined over a two-dimensional domain \( \Omega \).

The Poisson equation arises in numerous physical contexts, including heat conduction, electrostatics, diffusion of substances, twisting of elastic rods, inviscid fluid flow, and water waves. Moreover, the equation appears in numerical splitting strategies for more complicated systems of PDEs, in particular the Navier–Stokes equations.

Solving a PDE such as the Poisson equation in FEniCS consists of the following steps:

  1. Identify the computational domain (\( \Omega \)), 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 PDE and, optionally, extend the program to compute derived quantities such as fluxes and averages, and visualize the results.
We shall now go through steps 2–4 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 more technically difficult programming.

What makes FEniCS attractive? Although many frameworks have a really elegant "Hello, World!" example on the Poisson equation, FEniCS is to our knowledge the only framework where the code stays compact and nice, very close to the mathematical formulation, also when the complexity increases with, e.g., systems of PDEs and mixed finite elements for computing on massively high-performance parallel platforms.

Finite element variational formulation

FEniCS is based on the finite element method, which is a general and efficient mathematical machinery for 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 finite element method in addition is encouraged. The section The finite element method contains a list of some suitable books. Experience shows that you can work with FEniCS as a tool to solve your PDEs even without thorough knowledge of the finite element method, as long as you get somebody to help you with formulating the PDE as a variational problem.

The basic recipe for turning a PDE into a variational problem is to multiply the PDE by a function \( v \), integrate the resulting equation over the domain \( \Omega \), and perform integration by parts of terms with second-order derivatives. The function \( v \) which multiplies the PDE is called a test function. The unknown function \( u \) to be approximated is referred to as a trial function. The terms test and trial function are used in FEniCS programs too. Suitable function spaces must be specified for the test and trial functions. For standard PDEs arising in physics and mechanics such spaces are well known.

In the present case, we first multiply the Poisson equation by the test function \( v \) and integrate over \( \Omega \): $$ \begin{equation} \tag{2.4} -\int_\Omega (\nabla^2 u)v \dx = \int_\Omega fv \dx\tp \end{equation} $$ A common rule when we derive variational formulations is that we try to keep the order of the derivatives of \( u \) and \( v \) as low as possible (this will enlarge the collection of finite elements that can be used in the problem). Here, we have a second-order spatial derivative of \( u \), which can be transformed to a first-derivative of \( u \) and \( v \) by applying the technique of integration by parts. A Laplace term will always be subject to integration by parts . The formula reads $$ \begin{equation} \tag{2.5} -\int_\Omega (\nabla^2 u)v \dx = \int_\Omega\nabla u\cdot\nabla v \dx - \int_{\partial\Omega}{\partial u\over \partial n}v \ds , \end{equation} $$ where \( \frac{\partial u}{\partial n} = \nabla u \cdot n \) is the derivative of \( u \) in the outward normal direction \( n \) on the boundary.

3: Integration by parts in more than one space dimension is based on Gauss' divergence theorem. Simply take (2.5) as the formula to be used.

Another feature of variational formulations is that the test function \( v \) is required to vanish on the parts of the boundary where the solution \( u \) is known (the book [10] explains in detail why this requirement is necessary). In the present problem, this means that \( v=0 \) on the whole boundary \( \partial\Omega \). The second term on the right-hand side of (2.5) therefore vanishes. From (2.4) and (2.5) it follows that $$ \begin{equation} \int_\Omega\nabla u\cdot\nabla v \dx = \int_\Omega fv \dx\tp \tag{2.6} \end{equation} $$ If we require that this equation holds for all test functions \( v \) in some suitable space \( \hat V \), the so-called test space, we obtain a well-defined mathematical problem that uniquely determines the solution \( u \) which lies in some (possibly 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 \in V \) such that $$ \begin{equation} \tag{2.7} \int_{\Omega} \nabla u \cdot \nabla v \dx = \int_{\Omega} fv \dx \quad \forall v \in \hat{V}\tp \end{equation} $$ The trial and test spaces \( V \) and \( \hat V \) are in the present problem defined as $$ \begin{align*} V &= \{v \in H^1(\Omega) : v = \ub \mbox{ on } \partial\Omega\}, \\ \hat{V} &= \{v \in H^1(\Omega) : v = 0 \mbox{ on } \partial\Omega\}\tp \end{align*} $$

In short, \( H^1(\Omega) \) is the mathematically well-known Sobolev space containing functions \( v \) such that \( v^2 \) and \( |\nabla v|^2 \) have finite integrals over \( \Omega \) (essentially meaning that the functions are continuous). The solution of the underlying PDE must lie in a function space where also the derivatives are continuous, but the Sobolev space \( H^1(\Omega) \) allows functions with discontinuous derivatives. This weaker continuity requirement of \( u \) in the variational statement (2.7), as a result of the integration by parts, has great practical consequences when it comes to constructing finite element function spaces. In particular, it allows the use of piecewise polynomial function spaces; i.e., function spaces constructed by stitching together polynomial functions on simple domains such as intervals, triangles, or tetrahedrons.

The variational problem (2.7) is a continuous problem: it defines the solution \( u \) in the infinite-dimensional function space \( V \). The finite element method for the Poisson equation finds an approximate solution of the variational problem (2.7) by replacing the infinite-dimensional function spaces \( V \) and \( \hat{V} \) by discrete (finite-dimensional) trial and test spaces \( V_h\subset{V} \) and \( \hat{V}_h\subset\hat{V} \). The discrete variational problem reads: Find \( u_h \in V_h \subset V \) such that $$ \begin{equation} \tag{2.8} \int_{\Omega} \nabla u_h \cdot \nabla v \dx = \int_{\Omega} fv \dx \quad \forall v \in \hat{V}_h \subset \hat{V}\tp \end{equation} $$ This variational problem, together with a suitable definition of the function spaces \( V_h \) and \( \hat{V}_h \), uniquely define our approximate numerical solution of Poisson's equation (2.1). The mathematical framework may seem complicated at first glance, but the good news is that the finite element variational problem (2.8) looks the same as the continuous variational problem (2.7), and FEniCS can automatically solve variational problems like (2.8)!

What we mean by the notation \( u \) and \( V \). The mathematics literature on variational problems writes \( u_h \) for the solution of the discrete problem and \( u \) for the solution of the continuous problem. To obtain (almost) a one-to-one relationship between the mathematical formulation of a problem and the corresponding FEniCS program, we shall drop the subscript \( _h \) and use \( u \) for the solution of the discrete problem and \( \uex \) for the exact solution of the continuous problem, if we need to explicitly distinguish between the two. Similarly, we will let \( V \) denote the discrete finite element function space in which we seek our solution.

Abstract finite element variational formulation

It turns out to be convenient to introduce the following canonical notation for variational problems: $$ \begin{equation} a(u, v) = L(v)\tp \tag{2.9} \end{equation} $$ For the Poisson equation, we have: $$ \begin{align} a(u, v) &= \int_{\Omega} \nabla u \cdot \nabla v \dx, \tag{2.10}\\ L(v) &= \int_{\Omega} fv \dx\tp \tag{2.11} \end{align} $$ From the mathematics literature, \( a(u,v) \) is known as a bilinear form and \( L(v) \) as a linear form. We shall in every linear problem we solve identify the terms with the unknown \( u \) and collect them in \( a(u,v) \), and similarly collect all terms with only known functions in \( L(v) \). The formulas for \( a \) and \( L \) are then coded directly in the program.

FEniCS provides all the necessary mathematical notation needed to express the variational problem \( a(u, v) = L(v) \). To solve a linear PDE in FEniCS, such as the Poisson equation, a user thus needs to perform only two steps: