This program was contributed by Andrea Bonito and M. Sebastian Pauletti, with editing and writing by Wolfgang Bangerth.
This material is based upon work supported by the National Science Foundation under Grant No. DMS-0914977. Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation (NSF).

Introduction

In this example, we show how to solve a partial differential equation (PDE) on a codimension one surface \(\Gamma \subset \mathbb R^3\) made of quadrilaterals, i.e. on a surface in 3d or a line in 2d. We focus on the following elliptic second order PDE

which generalized the Laplace equation we have previously solved in several of the early tutorial programs. Our implementation is based on step-4. step-34 also solves problems on lower dimensional surfaces; however, there we only consider integral equations that do not involve derivatives on the solution variable, while here we actually have to investigate what it means to take derivatives of a function only defined on a (possibly curved) surface.

In order to define the above operator, we start by introducing some notations. Let \(\mathbf x_S:\hat S \rightarrow S\) be a parametrization of a surface \(S\) from a reference element \(\hat S \subset \mathbb R^2\), i.e. each point \(\hat{\mathbf x}\in\hat S\) induces a point \({\mathbf x}_S(\hat{\mathbf x}) \in S\). Then let

\[ G_S:= (D \mathbf{x}_S)^T \ D \mathbf{x}_S \]

denotes the corresponding first fundamental form, where \(D \mathbf{x}_S=\left(\frac{\partial x_{S,i}(\hat{\mathbf x})}{\partial \hat x_j}\right)_{ij}\) is the derivative (Jacobian) of the mapping. In the following, \(S\) will be either the entire surface \(\Gamma\) or, more convenient for the finite element method, any face \(S \in {\mathbb T}\), where \({\mathbb T}\) is a partition (triangulation) of \(\Gamma\) constituted of quadrilaterals. We are now in position to define the tangential gradient of a function \(v : S \rightarrow \mathbb R\) by

The surface Laplacian (also called the Laplace-Beltrami operator) is then defined as \(\Delta_S:= \nabla_S \cdot \nabla_S\). Note that an alternate way to compute the surface gradient on smooth surfaces \(\Gamma\) is

As usual, we are only interested in weak solutions for which we can use \(C^0\) finite elements (rather than requiring \(C^1\) continuity as for strong solutions). We therefore resort to the weak formulation

Fortunately, deal.II has already all the tools to compute the above expressions. In fact, they barely differ from the ways in which we solve the usual Laplacian, only requiring the surface coordinate mapping to be provided in the constructor of the FEValues class. This surface description given, in the codimension one surface case, the two routines FEValues::shape_grad and FEValues::JxW return

On a more general note, details for the finite element approximation on surfaces can be found for instance in [Dziuk, in Partial differential equations and calculus of variations 1357, Lecture Notes in Math., 1988], [Demlow, SIAM J. Numer. Anal. 47(2), 2009] and [Bonito, Nochetto, and Pauletti, SIAM J. Numer. Anal. 48(5), 2010].

Testcase

In general when you want to test numerically the accuracy and/or order of convergence of an algorithm you need to provide an exact solution. The usual trick is to pick a function that we want to be the solution, then apply the differential operator to it that defines a forcing term for the right hand side. This is what we do in this example. In the current case, the form of the domain is obviously also essential.

We produce one test case for a 2d problem and another one for 3d:

In 2d, let's choose as domain a half circle. On this domain, we choose the function \(u(\mathbf x)=-2x_1x_2\) as the solution. To compute the right hand side, we have to compute the surface Laplacian of the solution function. There are (at least) two ways to do that. The first one is to project away the normal derivative as described above using the natural extension of \(u(\mathbf x)\) (still denoted by \(u\)) over \(\mathbb R^d\), i.e. to compute

where \(\kappa\) is the total curvature of \(\Gamma\). Since we are on the unit circle, \(\mathbf n=\mathbf x\) and \(\kappa = 1\) so that

\[ -\Delta_\Gamma u = -8 x_1x_2. \]

A somewhat simpler way, at least for the current case of a curve in two-dimensional space, is to note that we can map the interval \(t \in [0,\pi]\) onto the domain \(\Omega\) using the transformation \(\mathbf x(t)= \left(\begin{array}{c} \cos t \\ \sin t \end{array}\right)\). At position \(\mathbf x=\mathbf x(t)\), the value of the solution is then \(u(\mathbf x(t)) = -2\cos t \sin t\). Taking into account that the transformation is length preserving, i.e. a segment of length \(dt\) is mapped onto a piece of curve of exactly the same length, the tangential Laplacian then satisfies

In 3d, the domain is again half of the surface of the unit ball, i.e. a half sphere or dome. We choose \(u(\mathbf x)=-2\sin(\pi x_1)\cos(\pi x_2)e^z\) as the solution. We can compute the right hand side of the equation, \(f=-\Delta_\Gamma u\), in the same way as the method above (with \(\kappa = 2\)), yielding an awkward and lengthy expression. You can find the full expression in the source code.

In the program, we will also compute the \(H^1\) seminorm error of the solution. Since the solution function and its numerical approximation are only defined on the manifold, the obvious definition of this error functional is \(| e |_{H^1(\Gamma)} = | \nabla_\Gamma e |_{L_2(\Gamma)} = \left( \int_\Gamma | \nabla_\Gamma (u-u_h) |^2 \right)^{1/2}\). This requires us to provide the tangential gradient \(\nabla_\Gamma u\) to the function VectorTools::integrate_difference (first introduced in step-7), which we will do by implementing the function Solution::gradient in the program below.

Implementation

If you've read through step-4 and understand the discussion above of how solution and right hand side correspond to each other, you will be immediately familiar with this program as well. In fact, there are only two things that are of significance:

The way we generate the mesh that triangulates the computational domain.

The way we use Mapping objects to describe that the domain on which we solve the partial differential equation is not planar but in fact curved.

Mapping objects were already introduced in step-10 and step-11 and as explained there, there is usually not a whole lot you have to know about how they work as long as you have a working description of how the boundary looks. In essence, we will simply declare an appropriate object of type MappingQ that will automatically obtain the boundary description from the Triangulation. The mapping object will then be passed to the appropriate functions, and we will get a boundary description for half circles or half spheres that is predefined in the library.

The rest of the program follows closely step-4 and, as far as computing the error, step-7. Some aspects of this program, in particular the use of two template arguments on the classes Triangulation, DoFHandler, and similar, are already described in detail in step-34; you may wish to read through this tutorial program as well.

The commented program

Include files

If you've read through step-4 and step-7, you will recognize that we have used all of the following include files there already. Consequently, we will not explain their meaning here again.

The LaplaceBeltramiProblem class template

This class is almost exactly similar to the LaplaceProblem class in step-4.

The essential differences are these:

The template parameter now denotes the dimensionality of the embedding space, which is no longer the same as the dimensionality of the domain and the triangulation on which we compute. We indicate this by calling the parameter spacedim , and introducing a constant dim equal to the dimensionality of the domain – here equal to spacedim-1.

All member variables that have geometric aspects now need to know about both their own dimensionality as well as that of the embedding space. Consequently, we need to specify both of their template parameters one for the dimension of the mesh dim, and the other for the dimension of the embedding space, spacedim. This is exactly what we did in step-34, take a look there for a deeper explanation.

We need an object that describes which kind of mapping to use from the reference cell to the cells that the triangulation is composed of. The classes derived from the Mapping base class do exactly this. Throughout most of deal.II, if you don't do anything at all, the library assumes that you want an object of kind MappingQ1 that uses a (bi-, tri-)linear mapping. In many cases, this is quite sufficient, which is why the use of these objects is mostly optional: for example, if you have a polygonal two-dimensional domain in two-dimensional space, a bilinear mapping of the reference cell to the cells of the triangulation yields an exact representation of the domain. If you have a curved domain, one may want to use a higher order mapping for those cells that lie at the boundary of the domain – this is what we did in step-11, for example. However, here we have a curved domain, not just a curved boundary, and while we can approximate it with bilinearly mapped cells, it is really only prudent to use a higher order mapping for all cells. Consequently, this class has a member variable of type MappingQ; we will choose the polynomial degree of the mapping equal to the polynomial degree of the finite element used in the computations to ensure optimal approximation, though this iso-parametricity is not required.

Equation data

Next, let us define the classes that describe the exact solution and the right hand sides of the problem. This is in analogy to step-4 and step-7 where we also defined such objects. Given the discussion in the introduction, the actual formulas should be self-explanatory. A point of interest may be how we define the value and gradient functions for the 2d and 3d cases separately, using explicit specializations of the general template. An alternative to doing it this way might have been to define the general template and have a switch statement (or a sequence of ifs) for each possible value of the spatial dimension.

Implementation of the LaplaceBeltramiProblem class

The rest of the program is actually quite unspectacular if you know step-4. Our first step is to define the constructor, setting the polynomial degree of the finite element and mapping, and associating the DoF handler to the triangulation:

template <int spacedim>

LaplaceBeltramiProblem<spacedim>::

LaplaceBeltramiProblem (constunsigned degree)

:

fe (degree),

dof_handler(triangulation),

mapping (degree)

{}

LaplaceBeltramiProblem::make_grid_and_dofs

The next step is to create the mesh, distribute degrees of freedom, and set up the various variables that describe the linear system. All of these steps are standard with the exception of how to create a mesh that describes a surface. We could generate a mesh for the domain we are interested in, generate a triangulation using a mesh generator, and read it in using the GridIn class. Or, as we do here, we generate the mesh using the facilities in the GridGenerator namespace.

In particular, what we're going to do is this (enclosed between the set of braces below): we generate a spacedim dimensional mesh for the half disk (in 2d) or half ball (in 3d), using the GridGenerator::half_hyper_ball function. This function sets the boundary indicators of all faces on the outside of the boundary to zero for the ones located on the perimeter of the disk/ball, and one on the straight part that splits the full disk/ball into two halves. The next step is the main point: The GridGenerator::extract_boundary_mesh function creates a mesh that consists of those cells that are the faces of the previous mesh, i.e. it describes the surface cells of the original (volume) mesh. However, we do not want all faces: only those on the perimeter of the disk or ball which carry boundary indicator zero; we can select these cells using a set of boundary indicators that we pass to GridGenerator::extract_boundary_mesh.

There is one point that needs to be mentioned. In order to refine a surface mesh appropriately if the manifold is curved (similarly to refining the faces of cells that are adjacent to a curved boundary), the triangulation has to have an object attached to it that describes where new vertices should be located. If you don't attach such a boundary object, they will be located halfway between existing vertices; this is appropriate if you have a domain with straight boundaries (e.g. a polygon) but not when, as here, the manifold has curvature. So for things to work properly, we need to attach a manifold object to our (surface) triangulation, in much the same way as we've already done in 1d for the boundary. We create such an object (with indefinite, static, lifetime) at the top of the function and attach it to the triangulation for all cells with boundary indicator zero that will be created henceforth.

The final step in creating the mesh is to refine it a number of times. The rest of the function is the same as in previous tutorial programs.

LaplaceBeltramiProblem::assemble_system

The following is the central function of this program, assembling the matrix that corresponds to the surface Laplacian (Laplace-Beltrami operator). Maybe surprisingly, it actually looks exactly the same as for the regular Laplace operator discussed in, for example, step-4. The key is that the FEValues::shape_gradient function does the magic: It returns the surface gradient \(\nabla_K \phi_i(x_q)\) of the \(i\)th shape function at the \(q\)th quadrature point. The rest then does not need any changes either:

LaplaceBeltramiProblem::output_result

This is the function that generates graphical output from the solution. Most of it is boilerplate code, but there are two points worth pointing out:

The DataOut::add_data_vector function can take two kinds of vectors: Either vectors that have one value per degree of freedom defined by the DoFHandler object previously attached via DataOut::attach_dof_handler; and vectors that have one value for each cell of the triangulation, for example to output estimated errors for each cell. Typically, the DataOut class knows to tell these two kinds of vectors apart: there are almost always more degrees of freedom than cells, so we can differentiate by the two kinds looking at the length of a vector. We could do the same here, but only because we got lucky: we use a half sphere. If we had used the whole sphere as domain and \(Q_1\) elements, we would have the same number of cells as vertices and consequently the two kinds of vectors would have the same number of elements. To avoid the resulting confusion, we have to tell the DataOut::add_data_vector function which kind of vector we have: DoF data. This is what the third argument to the function does.

The DataOut::build_patches function can generate output that subdivides each cell so that visualization programs can resolve curved manifolds or higher polynomial degree shape functions better. We here subdivide each element in each coordinate direction as many times as the polynomial degree of the finite element in use.

LaplaceBeltramiProblem::compute_error

This is the last piece of functionality: we want to compute the error in the numerical solution. It is a verbatim copy of the code previously shown and discussed in step-7. As mentioned in the introduction, the Solution class provides the (tangential) gradient of the solution. To avoid evaluating the error only a superconvergence points, we choose a quadrature rule of sufficiently high order.

Results

By playing around with the number of global refinements in the LaplaceBeltrami::make_grid_and_dofs function you increase or decrease mesh refinement. For example, doing one more refinement and only running the 3d surface problem yields the following output:

This is what we expect: make the mesh size smaller by a factor of two and the error goes down by a factor of four (remember that we use bi-quadratic elements). The full sequence of errors from one to five refinements looks like this, neatly following the theoretically predicted pattern:

0.360759
0.0888008
0.0221245
0.00552639
0.0013813

Finally, the program produces graphical output that we can visualize. Here is a plot of the results:

The program also works for 1d curves in 2d, not just 2d surfaces in 3d. You can test this by changing the template argument in main() like so:

LaplaceBeltramiProblem<2> laplace_beltrami;

The domain is a curve in 2d, and we can visualize the solution by using the third dimension (and color) to denote the value of the function \(u(x)\). This then looks like so (the white curve is the domain, the colored curve is the solution extruded into the third dimension, clearly showing the change in sign as the curve moves from one quadrant of the domain into the adjacent one):

Possibilities for extensions

Computing on surfaces only becomes interesting if the surface is more interesting than just a half sphere. To achieve this, deal.II can read meshes that describe surfaces through the usual GridIn class. Or, in case you have an analytic description, a simple mesh can sometimes be stretched and bent into a shape we are interested in.

Let us consider a relatively simple example: we take the half sphere we used before, we stretch it by a factor of 10 in the z-direction, and then we jumble the x- and y-coordinates a bit. Let's show the computational domain and the solution first before we go into details of the implementation below:

The way to produce such a mesh is by using the GridTools::transform function. It needs a way to transform each individual mesh point to a different position. Let us here use the following, rather simple function (remember: stretch in one direction, jumble in the other two):

If we followed the LaplaceBeltrami::make_grid_and_dofs function, we would extract the half spherical surface mesh as before, warp it into the shape we want, and refine as often as necessary. This is not quite as simple as we'd like here, though: refining requires that we have an appropriate manifold object attached to the triangulation that describes where new vertices of the mesh should be located upon refinement. I'm sure it's possible to describe this manifold in a not-too-complicated way by simply undoing the transformation above (yielding the spherical surface again), finding the location of a new point on the sphere, and then re-warping the result. But I'm a lazy person, and since doing this is not really the point here, let's just make our lives a bit easier: we'll extract the half sphere, refine it as often as necessary, get rid of the object that describes the manifold since we now no longer need it, and then finally warp the mesh. With the function above, this would look as follows:

Note that the only essential addition has been the three lines marked with asterisks. It is worth pointing out one other thing here, though: because we detach the manifold description from the surface mesh, whenever we use a mapping object in the rest of the program, it has no curves boundary description to go on any more. Rather, it will have to use the implicit, StraightBoundary class that is used on all parts of the boundary not explicitly assigned a different manifold object. Consequently, whether we use MappingQ(2), MappingQ(15) or MappingQ1, each cell of our mesh will be mapped using a bilinear approximation.

All these drawbacks aside, the resulting pictures are still pretty. The only other differences to what's in step-38 is that we changed the right hand side to \(f(\mathbf x)=\sin x_3\) and the boundary values (through the Solution class) to \(u(\mathbf x)|_{\partial\Omega}=\cos x_3\). Of course, we now non longer know the exact solution, so the computation of the error at the end of LaplaceBeltrami::run will yield a meaningless number.