the quality of the solution.
Note: To begin this lesson…
cd HandsOnLessons/mfem_convergence
In this lesson, we demonstrate the discretization of a simple Poisson problem using the MFEM library and examine the finite element approximation error under uniform refinement. An example of this equation is steady-state heat conduction.
The Poisson Equation is a partial differential equation (PDE) that can be used to model steady-state heat conduction, electric potentials and gravitational fields. In mathematical terms … \[-\nabla^2u = f\]
where u is the potential field and f is the source function. This PDE is a generalization of the Laplace Equation.
To solve the above continuous equation using computers we need to discretize it by introducing a finite (discrete) number of unknowns to compute for. In the Finite Element Method (FEM), this is done using the concept of basis functions.
Instead of calculating the exact analytic solution u, consider approximating it by \[u \approx \sum_{j=1}^n c_j \phi_j\]
where \(c_j\) are scalar unknown coefficients and \(\phi_j\) are known basis functions. They are typically piecewise-polynomial functions which are only non-zero on small portions of the computational mesh.
With finite elements, the mesh can be totally unstructured, curved and non-conforming.
To solve for the unknown coefficients, we multiply Poisson’s equation by another (test) basis function \(\phi_i\) and integrate by parts to obtain \[\sum_{j=1}^n\int_\Omega c_j \nabla \phi_j \cdot \nabla \phi_i dV = \int_\Omega f \phi_i\]
for every basis function \(\phi_i\). (Here we are assuming homogeneous Dirichlet boundary conditions corresponding, for example, to zero temperature on the whole boundary.)
Since the basis functions are known, we can rewrite (3) as \[\mathbf{Ax} = \mathbf{b}\]
where \[A_{ij} = \int_\Omega \nabla \phi_i \cdot \nabla \phi_j dV\] \[b_i = \int_\Omega f \phi_i dV\] \[x_j = c_j\]
This is a \(n \times n\) linear system that can be solved directly or iteratively for the unknown coefficients. Note that we are free to choose the basis functions \(\phi_i\) as we see fit.
The mfem_convergence/
directory contains the convergence.cpp
source code for solving the Poisson problem using a variety of grids and orders. You can also view the code online on GitHub.
To define the system we need to solve, we need three things. First, we need to define our basis functions which live on the computational mesh.
// order is the FEM basis functions polynomial order
FiniteElementCollection *fec = new H1_FECollection(order, dim);
// pmesh is the parallel computational mesh
ParFiniteElementSpace *fespace = new ParFiniteElementSpace(pmesh, fec);
This defines a collection of H1 functions (meaning they have well-defined gradient) of a given polynomial order on a parallel computational mesh pmesh
. Next, we need to define the integrals in Equation (5)
ParBilinearForm *a = new ParBilinearForm(fespace);
ConstantCoefficient one(1.0);
a->AddDomainIntegrator(new DiffusionIntegrator(one));
a->Assemble();
and Equation (6)
// f_exact is a C function defining the source
FunctionCoefficient f(f_exact);
ParLinearForm *b = new ParLinearForm(fespace);
b->AddDomainIntegrator(new DomainLFIntegrator(f));
b->Assemble();
This defines the matrix A and the vector b. We then solve the linear system for our solution vector x using AMG-preconditioned PCG iteration.
// FEM -> Linear System
HypreParMatrix A;
Vector B, X;
a->FormLinearSystem(ess_tdof_list, x, *b, A, X, B);
// AMG preconditioner
HypreBoomerAMG *amg = new HypreBoomerAMG(A);
amg->SetPrintLevel(0);
// PCG Krylov solver
HyprePCG *pcg = new HyprePCG(A);
pcg->SetTol(1e-12);
pcg->SetMaxIter(200);
pcg->SetPrintLevel(0);
pcg->SetPreconditioner(*amg);
// Solve the system A X = B
pcg->Mult(B, X);
// Linear System -> FEM
a->RecoverFEMSolution(X, *b, x);
In this lesson we know what the exact solution is, so we can measure the amount of error in our approximate solution in two ways: \[\left \| u_{\mbox{exact}} - u_{\mbox{h}} \right \|_{L_2}^2 = \int_\Omega \left| u_{\mbox{exact}} - u_{\mbox{h}} \right |^2\] \[\left \| u_{\mbox{exact}} - u_{\mbox{h}} \right \|_{H^1}^2 = \left \| u_{\mbox{exact}} - u_{\mbox{h}} \right \|_{L_2}^2 + \left \| \nabla u_{\mbox{exact}} - \nabla u_{\mbox{h}} \right \|_{L_2}^2\]
We expect the error to behave like \[\left \| u_{\mbox{exact}} - u_{\mbox{h}} \right \|_{L_2} \leq Ch^{r}\]
where \(h\) is the mesh size, \(C\) is a mesh-independent constant and \(r\) is the convergence rate.
Given approximations at two different mesh resolutions, we can estimate the convergence rate as follows (\(C\) doesn’t change when we refine the mesh and compare runs): \[r \approx \frac{\log\ \frac{ \left \| u_{\mbox{exact}} - u_{h_{\mbox{new}}} \right \|_{L_2}}{\left \| u_{\mbox{exact}} - u_{h_{\mbox{old}}} \right \|_{L_2}}}{ \log \frac{h_{\mbox{new}}}{h_{\mbox{old}}}}\]
In code, this is implemented in a refinement loop as follows:
double l2_err = x.ComputeL2Error(u);
double h1_err = x.ComputeH1Error(&u, &u_grad, &one, 1.0, 1);
pmesh->GetCharacteristics(h_min, h_max, kappa_min, kappa_max);
l2_rate = log(l2_err/l2_err_prev) / log(h_min/h_prev);
h1_rate = log(h1_err/h1_err_prev) / log(h_min/h_prev);
The convergence study has the following options.
./convergence --help
Usage: ./convergence [options] ...
Options:
-h, --help
Print this help message and exit.
-m <string>, --mesh <string>, current value: star.mesh
Mesh file to use.
-o <int>, --order <int>, current value: 1
Finite element order (polynomial degree).
-sc, --static-condensation, -no-sc, --no-static-condensation, current option: --no-static-condensation
Enable static condensation.
-r <int>, --refinements <int>, current value: 4
Number of total uniform refinements
-sr <int>, --serial-refinements <int>, current value: 2
Maximum number of serial uniform refinements
-f <double>, --frequency <double>, current value: 1
Set the frequency for the exact solution.
In this run, we will examine the error after seven uniform refinements in both the L2 and H1 norms using first order (linear) basis functions. We use the star.mesh
2D mesh file.
./convergence -r 7
Options used:
--mesh star.mesh
--order 1
--no-static-condensation
--refinements 7
--serial-refinements 2
--frequency 1
----------------------------------------------------------------------------------------
DOFs h L^2 error L^2 rate H^1 error H^1 rate
----------------------------------------------------------------------------------------
31 0.4876 0.3252 0 2.631 0
101 0.2438 0.09293 1.807 1.387 0.9229
361 0.1219 0.02393 1.957 0.7017 0.9836
1361 0.06095 0.006027 1.989 0.3518 0.996
5281 0.03048 0.00151 1.997 0.176 0.999
20801 0.01524 0.0003776 1.999 0.08803 0.9997
82561 0.007619 9.441e-05 2 0.04402 0.9999
Note that the L2 error is converging at a rate of 2 while the H1 error is only converging at a rate of 1.
Now consider the same run, only we are using 3rd order (cubic) basis functions instead.
./convergence -r 7 -o 3
Options used:
--mesh star.mesh
--order 3
--no-static-condensation
--refinements 7
--serial-refinements 2
--frequency 1
----------------------------------------------------------------------------------------
DOFs h L^2 error L^2 rate H^1 error H^1 rate
----------------------------------------------------------------------------------------
211 0.4876 0.004777 0 0.118 0
781 0.2438 0.0003178 3.91 0.01576 2.905
3001 0.1219 2.008e-05 3.984 0.001995 2.982
11761 0.06095 1.258e-06 3.997 0.0002501 2.996
46561 0.03048 7.864e-08 4 3.129e-05 2.999
185281 0.01524 4.915e-09 4 3.912e-06 3
739201 0.007619 3.072e-10 4 4.891e-07 3
The L2 error is now converging at a rate of 4, and the H1 error is converging at a rate of 3. This is because the exact solution in these runs is smooth, so higher-order methods approximate it better.
We need only 3001 unknowns compared to 82561 unknowns for the low-order method!
The high-order method is more efficient.
The previous two runs used a 2D mesh in serial, but the same code can be used to run a 3D problem in parallel.
mpiexec -n 4 ./convergence -r 4 -o 2 -m inline-hex.mesh
Options used:
--mesh inline-hex.mesh
--order 2
--no-static-condensation
--refinements 4
--serial-refinements 2
--frequency 1
----------------------------------------------------------------------------------------
DOFs h L^2 error L^2 rate H^1 error H^1 rate
----------------------------------------------------------------------------------------
729 0.25 0.001386 0 0.02215 0
4913 0.125 0.0001772 2.967 0.005532 2.002
35937 0.0625 2.227e-05 2.993 0.001377 2.007
274625 0.03125 2.787e-06 2.998 0.0003441 2
Experiment with different orders in 2D and 3D.
For a smooth exact solution, the convergence rate in energy norm (H1) is p. Using the so-called Nitsche Trick, one can prove that we pick an additional order in L2, so the convergence rate there is p+1.
We demonstrated the ease of implementing a order- and dimension-independent finite element code in MFEM. We discussed the basics of the finite element method as well as demonstrated the effect of the polynomial order of the basis functions on convergence rates.
For more information, visit the MFEM website, http://mfem.org, including the
You may also be interested in visiting the websites of the related GLVis, CEED, and BLAST projects.
The evening hands on exercise will be dedicated on getting more experience with MFEM and on reviewing finite element discretization methods for additional physics.
spack install mfem glvis
.MFEM includes a number of well-documented example codes & miniapps that can be used as tutorials, as well as simple starting points for user applications.
These examples and miniapps are available in the mfem/
subdirectory of your copy of HandsOnLessons/mfem_convergence
or as top-level sub-directories in the MFEM source code.
The full list of examples is below. Feel free to explore any of them depending on your interests, but we recommend starting with the ones marked with a “⭐”
Most of the examples have a serial and a parallel version, illustrating the ease of transition and the minimal code changes between the two.
Many of the examples also have modifications that take advantage of optional third-party libraries such as PETSc, SUNDIALS and PUMI.
Beyond the examples, a number of miniapps are available that are more representative of the advanced usage of the library in physics/application codes. Some of the included miniapps are:
In addition, the sources for several external benchmark/proxy-apps build on top of MFEM are available:
Modify the miniapps and example codes, either in your local copy on Cooley, or on your laptop to create a simple simulation of your own. In both cases you should be able to edit the source code and rebuild the binary simply with make
.
For example, you can solve a steady-state heat conduction problem in 2D and 3D using the shaper
miniapp (modified for the cable shape) to define the mesh and ex1
or ex1p
to solve it (modified to include separate coefficients for air and cable).
We want to see your creativity – the best simulations will enter for a chance to be featured on MFEM’s gallery page!
Please consult the MFEM code documentation and don’t hesitate to ask if you have any implementation questions.
To get points, please submit screenshots, simulation images and any additional material you have generated via Show Your Work using the hands-on activity name MFEM_HandsOn
.