This tutorial depends on step-12.
This program was contributed by Jiaqi Zhang and Timo Heister.
This material is based upon work partly supported by the National Science Foundation Award DMS-2028346, OAC-2015848, EAR-1925575, by the Computational Infrastructure in Geodynamics initiative (CIG), through the NSF under Award EAR-0949446 and EAR-1550901 and The University of California – Davis.
- Note
- If you use this program as a basis for your own work, please consider citing it in your list of references. The initial version of this work was contributed to the deal.II project by the authors listed in the following citation:
Symmetric interior penalty Galerkin (SIPG) method for Poisson's equation
Overview
In this tutorial, we display the usage of the FEInterfaceValues class, which is designed for assembling face terms arising from discontinuous Galerkin (DG) methods. The FEInterfaceValues class provides an easy way to obtain the jump and the average of shape functions and of the solution across cell faces. This tutorial includes the following topics.
-
The SIPG method for Poisson's equation, which has already been used in step-39 and step-59.
-
Assembling of face terms using FEInterfaceValues and the system matrix using MeshWorker::mesh_loop(), which is similar to step-12.
-
Adaptive mesh refinement using an error estimator.
-
Two test cases: convergence test for a smooth function and adaptive mesh refinement test for a singular solution.
The equation
In this example, we consider Poisson's equation
\[
- \nabla \cdot \left( \nu \nabla u\right) = f \qquad \mbox{in } \Omega,
\]
subject to the boundary condition
\[
u = g_D \qquad \mbox{on } \partial \Omega.
\]
For simplicity, we assume that the diffusion coefficient \(\nu\) is constant here. Note that if \(\nu\) is discontinuous, we need to take this into account when computing jump terms on cell faces.
We denote the mesh by \({\mathbb T}_h\), and \(K\in{\mathbb T}_h\) is a mesh cell. The sets of interior and boundary faces are denoted by \({\mathbb F}^i_h\) and \({\mathbb F}^b_h\) respectively. Let \(K^0\) and \(K^1\) be the two cells sharing a face \(f\in F_h^i\), and \(\mathbf n\) be the outer normal vector of \(K^0\). Then the jump operator is given by the "here minus there" formula,
\[
\jump{v} = v^0 - v^1
\]
and the averaging operator as
\[
\average{v} = \frac{v^0 + v^1}{2}
\]
respectively. Note that when \(f\subset \partial \Omega\), we define \(\jump{v} = v\) and \(\average{v}=v\). The discretization using the SIPG is given by the following weak formula (more details can be found in [di2011mathematical] and the references therein)
\begin{align*}
&\sum_{K\in {\mathbb T}_h} (\nabla v_h, \nu \nabla u_h)_K\\
&-\sum_{F \in F_h^i} \left\{
\left< \jump{v_h}, \nu\average{ \nabla u_h} \cdot \mathbf n \right>_F
+\left<\average{ \nabla v_h }\cdot \mathbf n,\nu\jump{u_h}\right>_F
-\left<\jump{v_h},\nu \sigma \jump{u_h} \right>_F
\right\}\\
&-\sum_{F \in F_h^b} \left\{
\left<v_h, \nu \nabla u_h\cdot \mathbf n \right>_F
+ \left< \nabla v_h \cdot \mathbf n , \nu u_h\right>_F
- \left< v_h,\nu \sigma u_h\right>_F
\right\}\\
&=(v_h, f)_\Omega
- \sum_{F \in F_h^b} \left\{
\left< \nabla v_h \cdot \mathbf n, \nu g_D\right>_F - \left<v_h,\nu \sigma g_D\right>_F
\right\}.
\end{align*}
The penalty parameter
The penalty parameter is defined as \(\sigma = \gamma/h_f\), where \(h_f\) a local length scale associated with the cell face; here we choose an approximation of the length of the cell in the direction normal to the face: \(\frac 1{h_f} = \frac 12 \left(\frac 1{h_K} + \frac 1{h_{K'}}\right)\), where \(K,K'\) are the two cells adjacent to the face \(f\) and we we compute \(h_K = \frac{|K|}{|f|}\).
In the formula above, \(\gamma\) is the penalization constant. To ensure the discrete coercivity, the penalization constant has to be large enough [ainsworth2007posteriori]. People do not really have consensus on which of the formulas proposed in the literature should be used. (This is similar to the situation discussed in the "Results" section of step-47.) One can just pick a large constant, while other options could be the multiples of \((p+1)^2\) or \(p(p+1)\). In this code, we follow step-39 and use \(\gamma = p(p+1)\).
A posteriori error estimator
In this example, with a slight modification, we use the error estimator by Karakashian and Pascal [karakashian2003posteriori]
\[
\eta^2 = \sum_{K \in {\mathbb T}_h} \eta^2_{K} + \sum_{f_i \in {\mathbb F}^i_h} \eta^2_{f_i} + \sum_{f_b \in F^i_b}\eta^2_{f_b}
\]
where
\begin{align*}
\eta^2_{K} &= h_K^2 \left\| f + \nu \Delta u_h \right\|_K^2,
\\
\eta^2_{f_i} &= \sigma \left\| \jump{u_h} \right\|_f^2 + h_f \left\| \jump{\nu \nabla u_h} \cdot \mathbf n \right\|_f^2,
\\
\eta_{f_b}^2 &= \sigma \left\| u_h-g_D \right\|_f^2.
\end{align*}
Here we use \(\sigma = \gamma/h_f\) instead of \(\gamma^2/h_f\) for the jump terms of \(u_h\) (the first term in \(\eta^2_{f_i}\) and \(\eta_{f_b}^2\)).
In order to compute this estimator, in each cell \(K\) we compute
\begin{align*}
\eta_{c}^2 &= h_K^2 \left\| f + \nu \Delta u_h \right\|_K^2,
\\
\eta_{f}^2 &= \sum_{f\in \partial K}\lbrace \sigma \left\| \jump{u_h} \right\|_f^2 + h_f \left\| \jump{\nu \nabla u_h} \cdot \mathbf n \right\|_f^2 \rbrace,
\\
\eta_{b}^2 &= \sum_{f\in \partial K \cap \partial \Omega} \sigma \left\| (u_h -g_D) \right\|_f^2.
\end{align*}
Then the square of the error estimate per cell is
\[
\eta_\text{local}^2 =\eta_{c}^2+0.5\eta_{f}^2+\eta_{b}^2.
\]
The factor of \(0.5\) results from the fact that the overall error estimator includes each interior face only once, and so the estimators per cell count it with a factor of one half for each of the two adjacent cells. Note that we compute \(\eta_\text{local}^2\) instead of \(\eta_\text{local}\) to simplify the implementation. The error estimate square per cell is then stored in a global vector, whose \(l_1\) norm is equal to \(\eta^2\).
The test case
In the first test problem, we run a convergence test using a smooth manufactured solution with \(\nu =1\) in 2D
\begin{align*}
u&=\sin(2\pi x)\sin(2\pi y), &\qquad\qquad &(x,y)\in\Omega=(0,1)\times (0,1),
\\
u&=0, &\qquad\qquad &\text{on } \partial \Omega,
\end{align*}
and \(f= 8\pi^2 u\). We compute errors against the manufactured solution and evaluate the convergence rate.
In the second test, we choose Functions::LSingularityFunction on a L-shaped domain (GridGenerator::hyper_L) in 2D. The solution is given in the polar coordinates by \(u(r,\phi) = r^{\frac{2}{3}}\sin \left(\frac{2}{3}\phi \right)\), which has a singularity at the origin. An error estimator is constructed to detect the region with large errors, according to which the mesh is refined adaptively.
The commented program
The first few files have already been covered in previous examples and will thus not be further commented on:
const ::Triangulation< dim, spacedim > & tria
Here the discontinuous finite elements and FEInterfaceValues are defined.
Equation data
Here we define two test cases: convergence_rate for a smooth function and l_singularity for the Functions::LSingularityFunction.
A smooth solution for the convergence test:
virtual void value_list(
const std::vector<
Point<dim>> &points,
std::vector<double> & values,
const unsigned int component = 0)
const override;
const unsigned int component = 0)
const override;
std::vector<double> & values,
const unsigned int )
const
for (
unsigned int i = 0; i <
values.size(); ++i)
const unsigned int )
const
static constexpr double PI
::VectorizedArray< Number, width > cos(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
The corresponding right-hand side of the smooth function:
virtual void value_list(
const std::vector<
Point<dim>> &points,
std::vector<double> & values,
const unsigned int )
const override;
std::vector<double> & values,
const unsigned int )
const
for (
unsigned int i = 0; i <
values.size(); ++i)
values[i] = 8. * PI * PI *
std::sin(2. * PI * points[i][0]) *
The right-hand side that corresponds to the function Functions::LSingularityFunction, where we assume that the diffusion coefficient \(\nu = 1\):
virtual void value_list(
const std::vector<
Point<dim>> &points,
std::vector<double> & values,
const unsigned int )
const override;
std::vector<double> & values,
const unsigned int )
const
for (
unsigned int i = 0; i <
values.size(); ++i)
values[i] = -
ref.laplacian(points[i]);
Auxiliary functions
This function computes the penalty \(\sigma\).
{
const unsigned int degree =
std::max(1U, fe_degree);
return degree * (degree + 1.) * 0.5 *
::VectorizedArray< Number, width > max(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)
The CopyData
In the following, we define "Copy" objects for the MeshWorker::mesh_loop(), which is essentially the same as step-12. Note that the "Scratch" object is not defined here because we use MeshWorker::ScratchData<dim> instead. (The use of "Copy" and "Scratch" objects is extensively explained in the WorkStream namespace documentation.
std::array<double, 2> values;
std::vector<types::global_dof_index> local_dof_indices;
std::vector<CopyDataFace> face_data;
template <
class Iterator>
void reinit(
const Iterator &cell,
const unsigned int dofs_per_cell)
{
cell_matrix.reinit(dofs_per_cell, dofs_per_cell);
local_dof_indices.resize(dofs_per_cell);
cell->get_dof_indices(local_dof_indices);
void reinit(value_type *starting_element, const std::size_t n_elements)
The SIPGLaplace class
After these preparations, we proceed with the main class of this program, called SIPGLaplace. The overall structure of the class is as in many of the other tutorial programs. Major differences will only come up in the implementation of the assemble functions, since we use FEInterfaceValues to assemble face terms.
const unsigned int degree;
const QGauss<dim - 1> face_quadrature;
const ::parallel::distributed::Triangulation< dim, spacedim > * triangulation
The remainder of the class's members are used for the following:
- Vectors to store error estimator square and energy norm square per cell.
- Print convergence rate and errors on the screen.
- The fiffusion coefficient \(\nu\) is set to 1.
- Members that store information about the test case to be computed.
The constructor here takes the test case as input and then determines the correct solution and right-hand side classes. The remaining member variables are initialized in the obvious way.
: degree(3)
, face_quadrature(degree + 1)
rhs_function = std::make_unique<const SmoothRightHandSide<dim>>();
std::make_unique<const Functions::LSingularityFunction>();
rhs_function = std::make_unique<const SingularRightHandSide<dim>>();
dof_handler.distribute_dofs(fe);
sparsity_pattern.copy_from(
dsp);
system_matrix.
reinit(sparsity_pattern);
solution.
reinit(dof_handler.n_dofs());
#define AssertThrow(cond, exc)
void make_flux_sparsity_pattern(const DoFHandler< dim, spacedim > &dof_handler, SparsityPatternBase &sparsity_pattern)
The assemble_system function
The assemble function here is similar to that in step-12 and step-47. Different from assembling by hand, we just need to focus on assembling on each cell, each boundary face, and each interior face. The loops over cells and faces are handled automatically by MeshWorker::mesh_loop().
The function starts by defining a local (lambda) function that is used to integrate the cell terms:
[&](
const auto &cell,
auto &scratch_data,
auto ©_data) {
const unsigned int dofs_per_cell =
fe_v.dofs_per_cell;
copy_data.
reinit(cell, dofs_per_cell);
const auto & q_points = scratch_data.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
const std::vector<double> &JxW = scratch_data.get_JxW_values();
std::vector<double>
rhs(n_q_points);
for (
unsigned int point = 0; point < n_q_points; ++point)
for (
unsigned int i = 0; i <
fe_v.dofs_per_cell; ++i)
for (
unsigned int j = 0;
j <
fe_v.dofs_per_cell; ++
j)
copy_data.cell_matrix(i,
j) +=
fe_v.shape_grad(i, point) *
fe_v.shape_grad(
j, point) *
copy_data.cell_rhs(i) +=
fe_v.shape_value(i, point) *
Next, we need a function that assembles face integrals on the boundary:
const auto & q_points = scratch_data.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
const unsigned int dofs_per_cell =
fe_fv.dofs_per_cell;
const std::vector<double> & JxW = scratch_data.get_JxW_values();
const std::vector<Tensor<1, dim>> &normals =
scratch_data.get_normal_vectors();
std::vector<double>
g(n_q_points);
const double extent1 = cell->measure() / cell->face(
face_no)->measure();
for (
unsigned int point = 0; point < n_q_points; ++point)
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
for (
unsigned int j = 0;
j < dofs_per_cell; ++
j)
copy_data.cell_matrix(i,
j) +=
fe_fv.shape_value(i, point) *
(
fe_fv.shape_grad(i, point) *
fe_fv.shape_value(i, point) *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
(
fe_fv.shape_grad(i, point) *
fe_fv.shape_value(i, point) *
g[point]
Finally, a function that assembles face integrals on interior faces. To reinitialize FEInterfaceValues, we need to pass cells, face and subface indices (for adaptive refinement) to the reinit() function of FEInterfaceValues:
copy_data.face_data.emplace_back();
const std::vector<double> & JxW =
fe_iv.get_JxW_values();
const std::vector<Tensor<1, dim>> &normals =
fe_iv.get_normal_vectors();
const double extent1 = cell->measure() / cell->face(f)->measure();
for (
const unsigned int point :
fe_iv.quadrature_point_indices())
fe_iv.jump_in_shape_values(i, point) *
(
fe_iv.average_of_shape_gradients(
j,
(
fe_iv.average_of_shape_gradients(i, point) *
fe_iv.jump_in_shape_values(
j, point)
fe_iv.jump_in_shape_values(i, point) *
fe_iv.jump_in_shape_values(
j, point)
The following lambda function will then copy data into the global matrix and right-hand side. Though there are no hanging node constraints in DG discretization, we define an empty AffineConstraints object that allows us to use the AffineConstraints::distribute_local_to_global() functionality.
const auto copier = [&](
const auto &c) {
constraints.distribute_local_to_global(c.cell_matrix,
Copy data from interior face assembly to the global matrix.
for (
auto &
cdf : c.face_data)
constraints.distribute_local_to_global(
cdf.cell_matrix,
With the assembly functions defined, we can now create ScratchData and CopyData objects, and pass them together with the lambda functions above to MeshWorker::mesh_loop(). In addition, we need to specify that we want to assemble on interior faces exactly once.
ScratchData scratch_data(
mapping, fe, quadrature, cell_flags, face_quadrature, face_flags);
void mesh_loop(const CellIteratorType &begin, const CellIteratorType &end, const CellWorkerFunctionType &cell_worker, const CopierType &copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const AssembleFlags flags=assemble_own_cells, const BoundaryWorkerFunctionType &boundary_worker=BoundaryWorkerFunctionType(), const FaceWorkerFunctionType &face_worker=FaceWorkerFunctionType(), const unsigned int queue_length=2 *MultithreadInfo::n_threads(), const unsigned int chunk_size=8)
@ update_values
Shape function values.
@ update_normal_vectors
Normal vectors.
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
@ update_quadrature_points
Transformed quadrature points.
@ assemble_boundary_faces
@ assemble_own_interior_faces_once
The solve() and output_results() function
The following two functions are entirely standard and without difficulty.
data_out.attach_dof_handler(dof_handler);
data_out.build_patches(mapping);
data_out.write_vtu(output);
std::string int_to_string(const unsigned int value, const unsigned int digits=numbers::invalid_unsigned_int)
The compute_error_estimate() function
The assembly of the error estimator here is quite similar to that of the global matrix and right-had side and can be handled by the MeshWorker::mesh_loop() framework. To understand what each of the local (lambda) functions is doing, recall first that the local cell residual is defined as \(h_K^2 \left\| f + \nu \Delta u_h \right\|_K^2\):
[&](
const auto &cell,
auto &scratch_data,
auto ©_data) {
copy_data.cell_index = cell->active_cell_index();
const auto & q_points =
fe_v.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
const std::vector<double> &JxW =
fe_v.get_JxW_values();
std::vector<Tensor<2, dim>> hessians(n_q_points);
fe_v.get_function_hessians(solution, hessians);
std::vector<double>
rhs(n_q_points);
const double hk = cell->diameter();
for (
unsigned int point = 0; point < n_q_points; ++point)
DEAL_II_HOST constexpr Number trace(const SymmetricTensor< 2, dim2, Number > &)
Next compute boundary terms \(\sum_{f\in \partial K \cap \partial \Omega}
\sigma \left\| [ u_h-g_D ] \right\|_f^2 \):
const auto & q_points =
fe_fv.get_quadrature_points();
const unsigned n_q_points = q_points.
size();
const std::vector<double> &JxW =
fe_fv.get_JxW_values();
std::vector<double>
g(n_q_points);
std::vector<double>
sol_u(n_q_points);
const double extent1 = cell->measure() / cell->face(
face_no)->measure();
for (
unsigned int point = 0; point < q_points.
size(); ++point)
And finally interior face terms \(\sum_{f\in \partial K}\lbrace \sigma
\left\| [u_h] \right\|_f^2 + h_f \left\| [\nu \nabla u_h \cdot
\mathbf n ] \right\|_f^2 \rbrace\):
copy_data.face_data.emplace_back();
const std::vector<double> & JxW =
fe_iv.get_JxW_values();
const std::vector<Tensor<1, dim>> &normals =
fe_iv.get_normal_vectors();
const auto & q_points =
fe_iv.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
std::vector<double> jump(n_q_points);
fe_iv.get_jump_in_function_values(solution, jump);
std::vector<Tensor<1, dim>>
grad_jump(n_q_points);
const double h = cell->face(f)->diameter();
const double extent1 = cell->measure() / cell->face(f)->measure();
for (
unsigned int point = 0; point < n_q_points; ++point)
Having computed local contributions for each cell, we still need a way to copy these into the global vector that will hold the error estimators for all cells:
const auto copier = [&](
const auto ©_data) {
for (
auto &
cdf : copy_data.face_data)
static const unsigned int invalid_unsigned_int
After all of this set-up, let's do the actual work: We resize the vector into which the results will be written, and then drive the whole process using the MeshWorker::mesh_loop() function.
ScratchData scratch_data(
mapping, fe, quadrature, cell_flags, face_quadrature, face_flags);
@ update_hessians
Second derivatives of shape functions.
The compute_energy_norm_error() function
Next, we evaluate the accuracy in terms of the energy norm. This function is similar to the assembling of the error estimator above. Here we compute the square of the energy norm defined by
\[
\|u \|_{1,h}^2 = \sum_{K \in \Gamma_h} \nu\|\nabla u \|_K^2 +
\sum_{f \in F_i} \sigma \| [ u ] \|_f^2 +
\sum_{f \in F_b} \sigma \|u\|_f^2.
\]
Therefore the corresponding error is
\[
\|u -u_h \|_{1,h}^2 = \sum_{K \in \Gamma_h} \nu\|\nabla (u_h - u) \|_K^2
+ \sum_{f \in F_i} \sigma \|[ u_h ] \|_f^2 + \sum_{f \in F_b}\sigma
\|u_h-g_D\|_f^2.
\]
Assemble \(\sum_{K \in \Gamma_h} \nu\|\nabla (u_h - u) \|_K^2 \).
[&](
const auto &cell,
auto &scratch_data,
auto ©_data) {
copy_data.cell_index = cell->active_cell_index();
const auto & q_points =
fe_v.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
const std::vector<double> &JxW =
fe_v.get_JxW_values();
std::vector<Tensor<1, dim>>
grad_u(n_q_points);
std::vector<Tensor<1, dim>>
grad_exact(n_q_points);
for (
unsigned int point = 0; point < n_q_points; ++point)
Assemble \(\sum_{f \in F_b}\sigma \|u_h-g_D\|_f^2\).
const auto & q_points =
fe_fv.get_quadrature_points();
const unsigned n_q_points = q_points.
size();
const std::vector<double> &JxW =
fe_fv.get_JxW_values();
std::vector<double>
g(n_q_points);
std::vector<double>
sol_u(n_q_points);
const double extent1 = cell->measure() / cell->face(
face_no)->measure();
for (
unsigned int point = 0; point < q_points.
size(); ++point)
Assemble \(\sum_{f \in F_i} \sigma \| [ u_h ] \|_f^2\).
copy_data.face_data.emplace_back();
const std::vector<double> &JxW =
fe_iv.get_JxW_values();
const auto & q_points =
fe_iv.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
std::vector<double> jump(n_q_points);
fe_iv.get_jump_in_function_values(solution, jump);
const double extent1 = cell->measure() / cell->face(f)->measure();
for (
unsigned int point = 0; point < n_q_points; ++point)
const auto copier = [&](
const auto ©_data) {
for (
auto &
cdf : copy_data.face_data)
const ScratchData scratch_data(mapping,
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)
The refine_grid() function
void refine_and_coarsen_fixed_number(Triangulation< dim, spacedim > &triangulation, const Vector< Number > &criteria, const double top_fraction_of_cells, const double bottom_fraction_of_cells, const unsigned int max_n_cells=std::numeric_limits< unsigned int >::max())
The compute_errors() function
We compute three errors in the \(L_2\) norm, \(H_1\) seminorm, and the energy norm, respectively. These are then printed to screen, but also stored in a table that records how these errors decay with mesh refinement and which can be output in one step at the end of the program.
std::cout <<
" Error in the L2 norm : " <<
L2_error << std::endl
<<
" Error in the H1 seminorm : " <<
H1_error << std::endl
The run() function
for (
unsigned int cycle = 0; cycle <
max_cycle; ++cycle)
std::cout <<
"Cycle " << cycle << std::endl;
std::cout <<
" Number of active cells : "
std::cout <<
" Number of degrees of freedom : " << dof_handler.n_dofs()
std::cout <<
" Estimated error : "
static ::ExceptionBase & ExcNotImplemented()
#define Assert(cond, exc)
void hyper_L(Triangulation< dim > &tria, const double left=-1., const double right=1., const bool colorize=false)
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
Number l1_norm(const Tensor< 2, dim, Number > &t)
Having run all of our computations, let us tell the convergence table how to format its data and output it to screen:
std::cout <<
"degree = " << degree << std::endl;
The main() function
The following main function is similar to previous examples as well, and need not be commented on.
catch (std::exception &exc)
<<
"----------------------------------------------------"
std::cerr <<
"Exception on processing: " << std::endl
<< exc.what() << std::endl
<<
"Aborting!" << std::endl
<<
"----------------------------------------------------"
<<
"----------------------------------------------------"
std::cerr <<
"Unknown exception!" << std::endl
<<
"Aborting!" << std::endl
<<
"----------------------------------------------------"
Results
The output of this program consist of the console output and solutions in vtu format.
In the first test case, when you run the program, the screen output should look like the following:
Cycle 0
Cycle 1
Cycle 2
.
.
.
When using the smooth case with polynomial degree 3, the convergence table will look like this:
Theoretically, for polynomial degree \(p\), the order of convergence in \(L_2\) norm and \(H^1\) seminorm should be \(p+1\) and \(p\), respectively. Our numerical results are in good agreement with theory.
In the second test case, when you run the program, the screen output should look like the following:
Cycle 0
Cycle 1
Cycle 2
.
.
.
The following figure provides a log-log plot of the errors versus the number of degrees of freedom for this test case on the L-shaped domain. In order to interpret it, let \(n\) be the number of degrees of freedom, then on uniformly refined meshes, \(h\) is of order \(1/\sqrt{n}\) in 2D. Combining the theoretical results in the previous case, we see that if the solution is sufficiently smooth, we can expect the error in the \(L_2\) norm to be of order \(O(n^{-\frac{p+1}{2}})\) and in \(H^1\) seminorm to be \(O(n^{-\frac{p}{2}})\). It is not a priori clear that one would get the same kind of behavior as a function of \(n\) on adaptively refined meshes like the ones we use for this second test case, but one can certainly hope. Indeed, from the figure, we see that the SIPG with adaptive mesh refinement produces asymptotically the kinds of hoped-for results:

In addition, we observe that the error estimator decreases at almost the same rate as the errors in the energy norm and \(H^1\) seminorm, and one order lower than the \(L_2\) error. This suggests its ability to predict regions with large errors.
While this tutorial is focused on the implementation, the step-59 tutorial program achieves an efficient large-scale solver in terms of computing time with matrix-free solution techniques. Note that the step-59 tutorial does not work with meshes containing hanging nodes at this moment, because the multigrid interface matrices are not as easily determined, but that is merely the lack of some interfaces in deal.II, nothing fundamental.
The plain program
{
{
};
template <int dim>
{
public:
{}
virtual void value_list(
const std::vector<
Point<dim>> &points,
std::vector<double> & values,
const unsigned int component = 0) const override;
const unsigned int component = 0) const override;
};
template <int dim>
std::vector<double> & values,
const unsigned int ) const
{
for (
unsigned int i = 0; i <
values.size(); ++i)
values[i] =
}
template <int dim>
const unsigned int ) const
{
return_value[0] =
return_value[1] =
return return_value;
}
template <int dim>
{
public:
{}
virtual void value_list(
const std::vector<
Point<dim>> &points,
std::vector<double> & values,
const unsigned int ) const override;
};
template <int dim>
void
std::vector<double> & values,
const unsigned int ) const
{
for (
unsigned int i = 0; i <
values.size(); ++i)
values[i] = 8. * PI * PI *
std::sin(2. * PI * points[i][0]) *
}
template <int dim>
{
public:
{}
virtual void value_list(
const std::vector<
Point<dim>> &points,
std::vector<double> & values,
const unsigned int ) const override;
private:
};
template <int dim>
void
std::vector<double> & values,
const unsigned int ) const
{
for (
unsigned int i = 0; i <
values.size(); ++i)
values[i] = -
ref.laplacian(points[i]);
}
{
const unsigned int degree =
std::max(1U, fe_degree);
return degree * (degree + 1.) * 0.5 *
}
{
};
struct CopyData
{
std::vector<types::global_dof_index> local_dof_indices;
std::vector<CopyDataFace> face_data;
template <class Iterator>
void reinit(
const Iterator &cell,
const unsigned int dofs_per_cell)
{
local_dof_indices.resize(dofs_per_cell);
cell->get_dof_indices(local_dof_indices);
}
};
template <int dim>
{
public:
private:
void solve();
void refine_grid();
const unsigned int degree;
const QGauss<dim - 1> face_quadrature;
};
template <int dim>
: degree(3)
, quadrature(degree + 1)
, face_quadrature(degree + 1)
, mapping()
, fe(degree)
{
{
rhs_function = std::make_unique<const SmoothRightHandSide<dim>>();
}
{
std::make_unique<const Functions::LSingularityFunction>();
rhs_function = std::make_unique<const SingularRightHandSide<dim>>();
}
else
}
template <int dim>
{
dof_handler.distribute_dofs(fe);
sparsity_pattern.copy_from(
dsp);
system_matrix.reinit(sparsity_pattern);
solution.reinit(dof_handler.n_dofs());
}
template <int dim>
{
[&](const auto &cell, auto &scratch_data, auto ©_data) {
const unsigned int dofs_per_cell =
fe_v.dofs_per_cell;
copy_data.
reinit(cell, dofs_per_cell);
const auto & q_points = scratch_data.get_quadrature_points();
const unsigned int n_q_points = q_points.size();
const std::vector<double> &JxW = scratch_data.get_JxW_values();
std::vector<double>
rhs(n_q_points);
for (
unsigned int point = 0;
point < n_q_points; ++
point)
for (
unsigned int i = 0; i <
fe_v.dofs_per_cell; ++i)
{
for (
unsigned int j = 0;
j <
fe_v.dofs_per_cell; ++
j)
copy_data.cell_matrix(i,
j) +=
fe_v.shape_grad(i, point) *
fe_v.shape_grad(
j, point) *
copy_data.cell_rhs(i) +=
fe_v.shape_value(i, point) *
}
};
auto & scratch_data,
auto & copy_data) {
const auto & q_points = scratch_data.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
const unsigned int dofs_per_cell =
fe_fv.dofs_per_cell;
const std::vector<double> & JxW = scratch_data.get_JxW_values();
const std::vector<Tensor<1, dim>> &normals =
scratch_data.get_normal_vectors();
std::vector<double>
g(n_q_points);
const double extent1 = cell->measure() / cell->face(
face_no)->measure();
for (
unsigned int point = 0;
point < n_q_points; ++
point)
{
for (unsigned int i = 0; i < dofs_per_cell; ++i)
for (
unsigned int j = 0;
j < dofs_per_cell; ++
j)
copy_data.cell_matrix(i,
j) +=
fe_fv.shape_value(i, point) *
(
fe_fv.shape_grad(i, point) *
fe_fv.shape_value(i, point) *
) *
JxW[point];
for (unsigned int i = 0; i < dofs_per_cell; ++i)
copy_data.cell_rhs(i) +=
(
fe_fv.shape_grad(i, point) *
) *
JxW[point];
}
};
const unsigned int &f,
const unsigned int &sf,
auto & scratch_data,
auto & copy_data) {
copy_data.face_data.emplace_back();
const std::vector<double> & JxW =
fe_iv.get_JxW_values();
const std::vector<Tensor<1, dim>> &normals =
fe_iv.get_normal_vectors();
const double extent1 = cell->measure() / cell->face(f)->measure();
for (
const unsigned int point :
fe_iv.quadrature_point_indices())
{
for (
const unsigned int i :
fe_iv.dof_indices())
(
fe_iv.average_of_shape_gradients(
j,
-
) *
}
};
constraints.close();
const auto copier = [&](const auto &c) {
constraints.distribute_local_to_global(c.cell_matrix,
c.cell_rhs,
c.local_dof_indices,
system_matrix,
for (
auto &
cdf : c.face_data)
{
constraints.distribute_local_to_global(
cdf.cell_matrix,
system_matrix);
}
};
ScratchData scratch_data(
mapping, fe, quadrature, cell_flags, face_quadrature, face_flags);
CopyData copy_data;
dof_handler.end(),
copier,
scratch_data,
copy_data,
}
template <int dim>
{
}
template <int dim>
{
".vtu";
data_out.attach_dof_handler(dof_handler);
data_out.build_patches(mapping);
data_out.write_vtu(output);
}
template <int dim>
{
[&](const auto &cell, auto &scratch_data, auto ©_data) {
copy_data.cell_index = cell->active_cell_index();
const auto & q_points =
fe_v.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
const std::vector<double> &JxW =
fe_v.get_JxW_values();
std::vector<Tensor<2, dim>>
hessians(n_q_points);
fe_v.get_function_hessians(solution, hessians);
std::vector<double>
rhs(n_q_points);
const double hk = cell->diameter();
for (
unsigned int point = 0;
point < n_q_points; ++
point)
{
const double residual =
}
};
auto & scratch_data,
auto & copy_data) {
const auto & q_points =
fe_fv.get_quadrature_points();
const unsigned n_q_points = q_points.size();
const std::vector<double> &JxW =
fe_fv.get_JxW_values();
std::vector<double>
g(n_q_points);
std::vector<double>
sol_u(n_q_points);
const double extent1 = cell->measure() / cell->face(
face_no)->measure();
for (
unsigned int point = 0;
point < q_points.size(); ++
point)
{
}
};
const unsigned int &f,
const unsigned int &sf,
auto & scratch_data,
auto & copy_data) {
copy_data.face_data.emplace_back();
const std::vector<double> & JxW =
fe_iv.get_JxW_values();
const std::vector<Tensor<1, dim>> &normals =
fe_iv.get_normal_vectors();
const auto & q_points =
fe_iv.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
std::vector<double> jump(n_q_points);
fe_iv.get_jump_in_function_values(solution, jump);
std::vector<Tensor<1, dim>>
grad_jump(n_q_points);
const double h = cell->face(f)->diameter();
const double extent1 = cell->measure() / cell->face(f)->measure();
for (
unsigned int point = 0;
point < n_q_points; ++
point)
{
}
};
const auto copier = [&](const auto ©_data) {
copy_data.value;
for (
auto &
cdf : copy_data.face_data)
};
ScratchData scratch_data(
mapping, fe, quadrature, cell_flags, face_quadrature, face_flags);
CopyData copy_data;
dof_handler.end(),
copier,
scratch_data,
copy_data,
}
template <int dim>
{
[&](const auto &cell, auto &scratch_data, auto ©_data) {
copy_data.cell_index = cell->active_cell_index();
const auto & q_points =
fe_v.get_quadrature_points();
const unsigned int n_q_points = q_points.size();
const std::vector<double> &JxW =
fe_v.get_JxW_values();
std::vector<Tensor<1, dim>>
grad_u(n_q_points);
std::vector<Tensor<1, dim>>
grad_exact(n_q_points);
double norm_square = 0;
for (
unsigned int point = 0;
point < n_q_points; ++
point)
{
norm_square +=
}
};
auto & scratch_data,
auto & copy_data) {
const auto & q_points =
fe_fv.get_quadrature_points();
const unsigned n_q_points = q_points.
size();
const std::vector<double> &JxW =
fe_fv.get_JxW_values();
std::vector<double>
g(n_q_points);
std::vector<double>
sol_u(n_q_points);
const double extent1 = cell->measure() / cell->face(
face_no)->measure();
for (
unsigned int point = 0;
point < q_points.size(); ++
point)
{
}
};
const unsigned int &f,
const unsigned int &sf,
auto & scratch_data,
auto & copy_data) {
copy_data.face_data.emplace_back();
const std::vector<double> &JxW =
fe_iv.get_JxW_values();
const auto & q_points =
fe_iv.get_quadrature_points();
const unsigned int n_q_points = q_points.
size();
std::vector<double> jump(n_q_points);
fe_iv.get_jump_in_function_values(solution, jump);
const double extent1 = cell->measure() / cell->face(f)->measure();
for (
unsigned int point = 0;
point < n_q_points; ++
point)
{
}
};
const auto copier = [&](const auto ©_data) {
for (
auto &
cdf : copy_data.face_data)
};
const ScratchData scratch_data(mapping,
fe,
cell_flags,
face_flags);
CopyData copy_data;
dof_handler.end(),
copier,
scratch_data,
copy_data,
}
template <int dim>
{
}
template <int dim>
{
{
dof_handler,
solution,
}
{
dof_handler,
solution,
}
{
}
std::cout <<
" Error in the L2 norm : " <<
L2_error << std::endl
<<
" Error in the H1 seminorm : " <<
H1_error << std::endl
<< std::endl;
}
template <int dim>
{
for (
unsigned int cycle = 0; cycle <
max_cycle; ++cycle)
{
std::cout << "Cycle " << cycle << std::endl;
{
{
if (cycle == 0)
{
}
else
{
}
break;
}
{
if (cycle == 0)
{
}
else
{
refine_grid();
}
break;
}
default:
{
}
}
std::cout << " Number of active cells : "
std::cout << " Number of degrees of freedom : " << dof_handler.n_dofs()
<< std::endl;
solve();
{
}
{
std::cout << " Estimated error : "
<< std::endl;
"Estimator",
}
std::cout << std::endl;
}
{
}
{
}
std::cout << "degree = " << degree << std::endl;
}
}
{
try
{
}
catch (std::exception &exc)
{
std::cerr << std::endl
<< std::endl
<< "----------------------------------------------------"
<< std::endl;
std::cerr << "Exception on processing: " << std::endl
<< exc.what() << std::endl
<< "Aborting!" << std::endl
<< "----------------------------------------------------"
<< std::endl;
return 1;
}
catch (...)
{
std::cerr << std::endl
<< std::endl
<< "----------------------------------------------------"
<< std::endl;
std::cerr << "Unknown exception!" << std::endl
<< "Aborting!" << std::endl
<< "----------------------------------------------------"
<< std::endl;
return 1;
};
return 0;
}
void cell_matrix(FullMatrix< double > &M, const FEValuesBase< dim > &fe, const FEValuesBase< dim > &fetest, const ArrayView< const std::vector< double > > &velocity, const double factor=1.)
Point< spacedim > point(const gp_Pnt &p, const double tolerance=1e-10)
void run(const Iterator &begin, const std_cxx20::type_identity_t< Iterator > &end, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length, const unsigned int chunk_size)
void reinit(MatrixBlock< MatrixType > &v, const BlockSparsityPattern &p)