Poisson Example 2D#
Authors: Kidus Teshome, Cian Wilson
A specific example#
In this case we use a manufactured solution (that is, one that is not necessarily an example of a solution to a PDE representing a naturally occurring physical problem) where we take a known analytical solution \(T(x,y)\) and substitute this into the original equation to find \(h\), then use this as the right-hand side in our numerical test. We choose \(T(x,y) = \exp\left(x+\tfrac{y}{2}\right)\), which is the solution to
Solving the Poisson equation numerically in a unit square, \(\Omega=[0,1]\times[0,1]\), for the approximate solution \(\tilde{T} \approx T\), we impose the boundary conditions
representing an essential Dirichlet condition on the value of \(\tilde{T}\) and natural Neumann conditions on \(\nabla\tilde{T}\).
Implementation#
This example was presented by Wilson & van Keken, 2023 using FEniCS v2019.1.0 and TerraFERMA, a GUI-based model building framework that also uses FEniCS v2019.1.0. Here we reproduce these results using the latest version of FEniCS, FEniCSx.
Preamble#
We start by loading all the modules we will require and initializing our plotting preferences through pyvista.
from mpi4py import MPI
import dolfinx as df
import dolfinx.fem.petsc
import numpy as np
import ufl
import matplotlib.pyplot as pl
import sys, os
basedir = ''
if "__file__" in globals(): basedir = os.path.dirname(__file__)
sys.path.append(os.path.join(basedir, os.path.pardir, 'python'))
import utils
import pyvista as pv
if __name__ == "__main__" and "__file__" in globals():
pv.OFF_SCREEN = True
import pathlib
if __name__ == "__main__":
output_folder = pathlib.Path(os.path.join(basedir, "output"))
output_folder.mkdir(exist_ok=True, parents=True)
Solution#
We then declare a python function solve_poisson_2d
that contains a complete description of the discrete Poisson equation problem.
This function follows much the same flow as described in the introduction
we describe the unit square domain \(\Omega = [0,1]\times[0,1]\) and discretize it into \(2 \times\)
ne
\(\times\)ne
triangular elements or cells to make amesh
we declare the function space,
V
, to use Lagrange polynomials of degreep
using this function space we declare trial,
T_a
, and test,T_t
, functionswe define the Dirichlet boundary condition,
bc
at the boundaries where \(x=0\) or \(y=0\), setting the desired value there to the known exact solutionwe define a finite element
Function
,gN
, containing the values of \(\nabla \tilde{T}\) on the Neumann boundaries where \(x=1\) or \(y=1\) (note that this will be used in the weak form rather than as a boundary condition object)we define the right hand side forcing function \(h\),
h
we describe the discrete weak forms,
S
andf
, that will be used to assemble the matrix \(\mathbf{S}\) and vector \(\mathbf{f}\)we solve the matrix problem using a linear algebra back-end and return the solution
For a more detailed description of solving the Poisson equation using FEniCSx, see the FEniCSx tutorial.
def solve_poisson_2d(ne, p=1):
"""
A python function to solve a two-dimensional Poisson problem
on a unit square domain.
Parameters:
* ne - number of elements in each dimension
* p - polynomial order of the solution function space
"""
# Describe the domain (a unit square)
# and also the tessellation of that domain into ne
# equally spaced squares in each dimension which are
# subdivided into two triangular elements each
mesh = df.mesh.create_unit_square(MPI.COMM_WORLD, ne, ne)
# Define the solution function space using Lagrange polynomials
# of order p
V = df.fem.functionspace(mesh, ("Lagrange", p))
# Define the trial and test functions on the same function space (V)
T_a = ufl.TrialFunction(V)
T_t = ufl.TestFunction(V)
# Define the location of the boundary condition, x=0 and y=0
def boundary(x):
return np.logical_or(np.isclose(x[0], 0), np.isclose(x[1], 0))
boundary_dofs = df.fem.locate_dofs_geometrical(V, boundary)
# Specify the value and define a Dirichlet boundary condition (bc)
gD = df.fem.Function(V)
gD.interpolate(lambda x: np.exp(x[0] + x[1]/2.))
bc = df.fem.dirichletbc(gD, boundary_dofs)
# Get the coordinates
x = ufl.SpatialCoordinate(mesh)
# Define the Neumann boundary condition function
gN = ufl.as_vector((ufl.exp(x[0] + x[1]/2.), 0.5*ufl.exp(x[0] + x[1]/2.)))
# Define the right hand side function, h
h = -5./4.*ufl.exp(x[0] + x[1]/2.)
# Get the unit vector normal to the facets
n = ufl.FacetNormal(mesh)
# Define the integral to be assembled into the stiffness matrix
S = ufl.inner(ufl.grad(T_t), ufl.grad(T_a))*ufl.dx
# Define the integral to be assembled into the forcing vector,
# incorporating the Neumann boundary condition weakly
f = T_t*h*ufl.dx + T_t*ufl.inner(gN, n)*ufl.ds
# Compute the solution (given the boundary condition, bc)
problem = df.fem.petsc.LinearProblem(S, f, bcs=[bc], \
petsc_options={"ksp_type": "preonly", \
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps"})
T_i = problem.solve()
return T_i
We can now numerically solve the equations using, e.g., 4 elements and piecewise linear polynomials.
if __name__ == "__main__":
ne = 4
p = 1
T_P1 = solve_poisson_2d(ne, p=p)
T_P1.name = "T (P1)"
main
Note that this code block starts with if __name__ == "__main__":
to prevent it from being run unless being run as a script or in a Jupyter notebook. This prevents unecessary computations when this code is used as a python module.
And use some utility functions (see ../python/utils.py
) to plot it.
if __name__ == "__main__":
# plot the solution as a colormap
plotter_P1 = utils.plot_scalar(T_P1, gather=True)
# plot the mesh
utils.plot_mesh(T_P1.function_space.mesh, plotter=plotter_P1, gather=True, show_edges=True, style="wireframe", color='k', line_width=2)
# plot the values of the solution at the nodal points
utils.plot_scalar_values(T_P1, plotter=plotter_P1, gather=True, point_size=15, font_size=22, shape_color='w', text_color='k', bold=False)
# show the plot
utils.plot_show(plotter_P1)
# save the plot
utils.plot_save(plotter_P1, output_folder / "2d_poisson_P1_solution.png")
comm = T_P1.function_space.mesh.comm
if comm.size > 1:
# if we're running in parallel (e.g. from a script) then save an image per process as well
plotter_P1_p = utils.plot_scalar(T_P1)
utils.plot_mesh(T_P1.function_space.mesh, plotter=plotter_P1_p, show_edges=True, style="wireframe", color='k', line_width=2)
utils.plot_scalar_values(T_P1, plotter=plotter_P1_p, point_size=15, font_size=22, shape_color='w', text_color='k', bold=False)
utils.plot_save(plotter_P1_p, output_folder / "2d_poisson_P1_solution_p{:d}.png".format(comm.rank,))
Similarly, we can solve the equation using quadratic elements (p=2
).
if __name__ == "__main__":
ne = 4
p = 2
T_P2 = solve_poisson_2d(ne, p=p)
T_P2.name = "T (P2)"
if __name__ == "__main__":
# plot the solution as a colormap
plotter_P2 = utils.plot_scalar(T_P2, gather=True)
# plot the mesh
utils.plot_mesh(T_P2.function_space.mesh, plotter=plotter_P2, gather=True, show_edges=True, style="wireframe", color='k', line_width=2)
# plot the values of the solution at the nodal points
utils.plot_scalar_values(T_P2, plotter=plotter_P2, gather=True, point_size=15, font_size=12, shape_color='w', text_color='k', bold=False)
# show the plot
utils.plot_show(plotter_P2)
# save the plot
utils.plot_save(plotter_P2, output_folder / "2d_poisson_P2_solution.png")
comm = T_P2.function_space.mesh.comm
if comm.size > 1:
# if we're running in parallel (e.g. from a script) then save an image per process as well
plotter_P2_p = utils.plot_scalar(T_P2)
utils.plot_mesh(T_P2.function_space.mesh, plotter=plotter_P2_p, show_edges=True, style="wireframe", color='k', line_width=2)
utils.plot_scalar_values(T_P2, plotter=plotter_P2_p, point_size=15, font_size=12, shape_color='w', text_color='k', bold=False)
utils.plot_save(plotter_P2_p, output_folder / "2d_poisson_P2_solution_p{:d}.png".format(comm.rank,))
Themes and variations#
Some suggested interactive tasks.
Given that we know the exact solution to this problem is \(T(x,y)\)=\(\exp\left(x+\tfrac{y}{2}\right)\) write a python function to evaluate the error in our numerical solution.
Loop over a variety of
ne
s andp
s and check that the numerical solution converges with an increasing number of degrees of freedom.
Note that, aside from the analytic solution being different, these tasks should be very similar to the 1D case in notebooks/poisson_1d.ipynb
.
Finally,
To try writing your own weak form, write an equation for the gradient of \(\tilde{T}\), describe it using ufl, solve it, and plot the solution.
Finish up#
Convert this notebook to a python script (making sure to save first)
if __name__ == "__main__" and "__file__" not in globals():
from ipylab import JupyterFrontEnd
app = JupyterFrontEnd()
app.commands.execute('docmanager:save')
!jupyter nbconvert --NbConvertApp.export_format=script --ClearOutputPreprocessor.enabled=True poisson_2d.ipynb
[NbConvertApp] Converting notebook poisson_2d.ipynb to script
[NbConvertApp] Writing 10508 bytes to poisson_2d.py