Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Poisson’s Equation is one of the most fundamental elliptic partial differential equations (PDEs) out there. Given a region Ω⊆Rn\Omega \subseteq \mathbb{R}^n, and scalar fields u,f:Ω→Ru, f: \Omega \to \mathbb{R}, the Poisson equation reads

−Δu=f,- \Delta u = f,

where Δ\Delta is the Laplace operator, signifying the divergence of the gradient of uu. Solving the Poisson equation numerically qualifies as the classic ‘Hello World’ example of numerics. Hence, it should come as no surprise that in this lesson, we will construct a simple solver for the traditional five-point finite difference discretization of (1).

Lesson Goals

By the end of this lesson, you will have implemented a solver for a boundary value problem of Poisson’s equation in 2D. On the way, you will

  • Use the Patch class to define a numerical domain, set up fields on its vertex grid, and work with its geometry;

  • Manage simulation data for a patch using PatchData;

  • Symbolically derive a Jacobi-style update kernel from a finite difference equation and implement it as a pystencils operator;

  • and use reductions to evaluate a global error measure for your solver.

Let’s start by importing the packages we will need.

Model Problem

We will consider the following model problem for the Poisson equation on the unit square Ω=[0,1]2\Omega = [0, 1]^2:

{−Δu=fon Ω∘u=φon ∂Ω\begin{align} \left\{ \begin{array}{cl} - \Delta u = f & \text{on } \overset{\circ}{\Omega} \\ u = \varphi & \text{on } \partial \Omega \end{array} \right. \end{align}

with the known source function

f(x,y)=π2(4sin⁡(2πx)+16cos⁡(4πy))f(x, y) = \pi^2 \left( 4 \sin(2 \pi x) + 16 \cos(4 \pi y) \right)

and the Dirichlet boundary condition

φ(x,y)=sin⁡(2πx)+cos⁡(4πy).\varphi(x, y) = \sin(2 \pi x) + \cos(4 \pi y).

The problem definition is illustrated in Fig. 1.

The model problem for the Poisson-Equation.

Figure 1:The model problem for the Poisson-Equation.

In this lesson, we will develop a numerical solver for a finite-difference discretization of this problem, based on the Jacobi method. For this, we discretize Ω\Omega with a regular cartesian grid of N×NN \times N points, which gives rise to a grid spacing of h=1N−1h = \frac{1}{N-1} (see Fig. 2):

Ω^={(ih,jh)  ∣  i,j∈{0,…,N−1}}\hat{\Omega} = \{ (ih, jh) \; \vert \; i, j \in \{ 0, \dots, N-1 \} \}
Numerical grid for our solver.
The functions u and f are represented by their values on the grid points,
and the boundary value problem (%s) is discretized
by a system of linear equations, one for each grid point.

Figure 2:Numerical grid for our solver. The functions uu and ff are represented by their values on the grid points, and the boundary value problem (2) is discretized by a system of linear equations, one for each grid point.

Set up Algebraic Objects

We start off by creating symbols and the patch object for our simulation grid.

Notebook Cell
Loading...
Notebook Cell
Loading...

Initialize the Fields

Next, we need to set up the fields uu and ff to reflect the boundary-value problem (2). The entries of f must be set to the corresponding values of ff, while u must be set to φ\varphi only on the boundary points.

Notebook Cell

To run our solver for an actual incarnation of the problem (2), we need to create data structures (i.e. arrays) for the patch Ω\Omega and its fields.

Notebook Cell

We can now run our initialization operator on the data arrays:

Let’s look at our initial state. We will use the following function to visualize the scalar fields as surface plots:

Notebook Cell

When visualizing, we should see that f shows a superposition of two waves as defined in (3), and that u is set to a similar superposition only at the edges:

<Figure size 1200x800 with 2 Axes>

That’s the initial state done. Let’s continue by discretizing the Poisson equation (1) and setting up the Jacobi smoother.

The Jacobi Smoother

Discretizing the Laplace Operator Δ\Delta on the grid Ω^\hat{\Omega} using second-order finite differences yields the following discrete stencil operator Lh:Ω^→RL_h: \hat{\Omega} \to \mathbb{R}:

(Lhu)ij=−4ui,j+ui−1,j+ui+1,j+ui,j−1+ui,j+1h2\left( L_h u \right)_{ij} = \frac{ - 4 u_{i,j} + u_{i-1, j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1} }{h^2}

The discretized boundary-value problem therefore reads

{(−Lhu)ij=fijfor i,j=1,…,N−1,uij=φijfor i,j∈{0,N}.\left\{ \begin{array}{cl} \left( - L_h \boldsymbol{u} \right)_{ij} = f_{ij} & \text{for } i, j = 1, \dots, N - 1, \\ u_{ij} = \varphi_{ij} & \text{for } i, j \in \{ 0, N \}. \end{array} \right.

This is a system of linear equations, which we are going to solve iteratively using the Jacobi method.

Notebook Cell

To run the smoother, we first need to create an array for the new field u_tmp. To honor the boundary conditions, we initialize it as a copy of u:

Let’s now run the Jacobi smoother, say, for 200 iterations. After each step, we must swap the data arrays of u and u_tmp:

After 200 steps, we should see that u has taken a form similar to f, as a superposition of two waves:

<Figure size 640x480 with 1 Axes>

Error Evaluation

As a last step in this lesson, would like to numerically evaluate the accuracy of the approximate solution u by computing the ∞\infty-Norm of the error. To do so, we observe that the model problem (2) is solved exactly by the analytical solution

u∗(x,y)=sin⁡(2πx)+cos⁡(4πy),u^{\ast} (x, y) = \sin(2 \pi x) + \cos(4 \pi y),

which, unsurprisingly, is identical to the boundary function φ\varphi. We would like to compute the error vector e=u−u∗\boldsymbol{e} = \boldsymbol{u} - \boldsymbol{u}^{\ast}, and evaluate its supremum norm

∥e∥∞=max⁡i,j=0,…,N∣eij∣.\Vert \boldsymbol{e} \Vert_{\infty} = \max_{i,j = 0, \dots, N} \vert e_{ij} \vert.

For this, we set up a new field e for the error vector, and a typed symbol e_norm for computing the norm:

Notebook Cell

Let’s add an array for the error vector e\boldsymbol{e} to our patch data:

Let’s plot the progression of the error during the 200 iterations of our solver loop. For this, we set up an array with room for 200 values of ∥e∥\Vert \boldsymbol{e} \Vert":

Next, we reset our arrays by running initialize_model_problem again. In the loop, we call compute_error after each iteration, pointing e_norm at the entry t of our prepared array. Note that we need to use the slice syntax [t:t+1] such that NumPy gives us a view into the array, instead of just copying out the value:

With the following cell, we plot the error progression. It should follow a roughly inverse-exponential curve, as the error is reduced multiplicatively by a roughly constant factor at each step.

<Figure size 640x480 with 1 Axes>

Conclusion

Congratulations! You have constructed your first numerical solver using pystencils, with many common ingredients: The initial setup, an iterative solution routine, and an error evaluation and validation step.

In the next lesson, we’ll apply the same techniques, and more, to solve a time-dependent advection-diffusion problem.