deal.II version GIT relicensing-1936-ge55124254b 2024-10-04 21:10:00+00:00
\(\newcommand{\dealvcentcolon}{\mathrel{\mathop{:}}}\) \(\newcommand{\dealcoloneq}{\dealvcentcolon\mathrel{\mkern-1.2mu}=}\) \(\newcommand{\jump}[1]{\left[\!\left[ #1 \right]\!\right]}\) \(\newcommand{\average}[1]{\left\{\!\left\{ #1 \right\}\!\right\}}\)
Loading...
Searching...
No Matches
The step-11 tutorial program

This tutorial depends on step-10.

Table of contents
  1. Introduction
  2. The commented program

  1. Results
  2. The plain program

Introduction

The problem we will be considering is the solution of Laplace's problem with Neumann boundary conditions only:

\begin{eqnarray*} -\Delta u &=& f \qquad \mathrm{in}\ \Omega, \\ \partial_n u &=& g \qquad \mathrm{on}\ \partial\Omega. \end{eqnarray*}

It is well known that if this problem is to have a solution, then the forces need to satisfy the compatibility condition

\[ \int_\Omega f\; dx + \int_{\partial\Omega} g\; ds = 0. \]

We will consider the special case that \(\Omega\) is the circle of radius 1 around the origin, and \(f=-2\), \(g=1\). This choice satisfies the compatibility condition.

The compatibility condition allows a solution of the above equation, but it nevertheless retains an ambiguity: since only derivatives of the solution appear in the equations, the solution is only determined up to a constant. For this reason, we have to pose another condition for the numerical solution, which fixes this constant.

For this, there are various possibilities:

  1. Fix one node of the discretization to zero or any other fixed value. This amounts to an additional condition \(u_h(x_0)=0\). Although this is common practice, it is not necessarily a good idea, since we know that the solutions of Laplace's equation are only in \(H^1\), which does not allow for the definition of point values because it is not a subset of the continuous functions. Therefore, even though fixing one node is allowed for discretized functions, it is not for continuous functions, and one can often see this in a resulting error spike at this point in the numerical solution.

  2. Fixing the mean value over the domain to zero or any other value. This is allowed on the continuous level, since \(H^1(\Omega)\subset L^1(\Omega)\) by Sobolev's inequality, and thus also on the discrete level since we there only consider subsets of \(H^1\).

  3. Fixing the mean value over the boundary of the domain to zero or any other value. This is also allowed on the continuous level, since \(H^{1/2}(\partial\Omega)\subset L^1(\partial\Omega)\), again by Sobolev's inequality.

We will choose the last possibility, since we want to demonstrate another technique with it.

While this describes the problem to be solved, we still have to figure out how to implement it. Basically, except for the additional mean value constraint, we have solved this problem several times, using Dirichlet boundary values, and we only need to drop the treatment of Dirichlet boundary nodes. The use of higher order mappings is also rather trivial and will be explained at the various places where we use it; in almost all conceivable cases, you will only consider the objects describing mappings as a black box which you need not worry about, because their only uses seem to be to be passed to places deep inside the library where functions know how to handle them (i.e. in the FEValues classes and their descendants).

The tricky point in this program is the use of the mean value constraint. Fortunately, there is a class in the library which knows how to handle such constraints, and we have used it quite often already, without mentioning its generality. Note that if we assume that the boundary nodes are spaced equally along the boundary, then the mean value constraint

\[ \int_{\partial \Omega} u(x) \; ds = 0 \]

can be written as

\[ \sum_{i\in\partial\Omega_h} u_i = 0, \]

where the sum shall run over all degree of freedom indices which are located on the boundary of the computational domain. Let us denote by \(i_0\) that index on the boundary with the lowest number (or any other conveniently chosen index), then the constraint can also be represented by

\[ u_{i_0} = \sum_{i\in\partial\Omega_h\backslash i_0} -u_i. \]

This, luckily, is exactly the form of constraints for which the AffineConstraints class was designed. Note that we have used this class in several previous examples for the representation of hanging nodes constraints, which also have this form: there, the middle vertex shall have the mean of the values of the adjacent vertices. In general, the AffineConstraints class is designed to handle affine constraints of the form

\[ CU = b \]

where \(C\) denotes a matrix, \(b\) denotes a vector, and \(U\) the vector of nodal values. In this case, since \(C\) represents one homogeneous constraint, \(b\) is the zero vector.

In this example, the mean value along the boundary allows just such a representation, with \(C\) being a matrix with just one row (i.e. there is only one constraint). In the implementation, we will create an AffineConstraints object, add one constraint (i.e. add another row to the matrix) referring to the first boundary node \(i_0\), and insert the weights with which all the other nodes contribute, which in this example happens to be just \(-1\).

Later, we will use this object to eliminate the first boundary node from the linear system of equations, reducing it to one which has a solution without the ambiguity of the constant shift value. One of the problems of the implementation will be that the explicit elimination of this node results in a number of additional elements in the matrix, of which we do not know in advance where they are located and how many additional entries will be in each of the rows of the matrix. We will show how we can use an intermediate object to work around this problem.

But now on to the implementation of the program solving this problem...

The commented program

As usual, the program starts with a rather long list of include files which you are probably already used to by now:

  #include <deal.II/base/quadrature_lib.h>
  #include <deal.II/base/function.h>
  #include <deal.II/base/numbers.h>
  #include <deal.II/base/table_handler.h>
  #include <deal.II/lac/vector.h>
  #include <deal.II/lac/sparse_matrix.h>
  #include <deal.II/lac/solver_cg.h>
  #include <deal.II/lac/precondition.h>
  #include <deal.II/lac/affine_constraints.h>
  #include <deal.II/grid/tria.h>
  #include <deal.II/grid/grid_generator.h>
  #include <deal.II/dofs/dof_handler.h>
  #include <deal.II/dofs/dof_tools.h>
  #include <deal.II/fe/fe_q.h>
  #include <deal.II/fe/fe_values.h>
  #include <deal.II/fe/mapping_q.h>
  #include <deal.II/numerics/vector_tools.h>
  #include <deal.II/numerics/matrix_tools.h>
  #include <deal.II/numerics/data_out.h>
 

Just this one is new: it declares a class DynamicSparsityPattern, which we will use and explain further down below.

  #include <deal.II/lac/dynamic_sparsity_pattern.h>
 

We will make use of the std::find algorithm of the C++ standard library, so we have to include the following file for its declaration:

  #include <algorithm>
  #include <iostream>
  #include <iomanip>
  #include <cmath>
 

The last step is as in all previous programs:

  namespace Step11
  {
  using namespace dealii;
 

Then we declare a class which represents the solution of a Laplace problem. As this example program is based on step-5, the class looks rather the same, with the sole structural difference that the functions assemble_system now calls solve itself, and is thus called assemble_and_solve, and that the output function was dropped since the solution function is so boring that it is not worth being viewed.

The only other noteworthy change is that the constructor takes a value representing the polynomial degree of the mapping to be used later on, and that it has another member variable representing exactly this mapping. In general, this variable will occur in real applications at the same places where the finite element is declared or used.

  template <int dim>
  class LaplaceProblem
  {
  public:
  LaplaceProblem(const unsigned int mapping_degree);
  void run();
 
  private:
  void setup_system();
  void assemble_and_solve();
  void solve();
  void write_high_order_mesh(const unsigned cycle);
 
  const FE_Q<dim> fe;
  DoFHandler<dim> dof_handler;
  const MappingQ<dim> mapping;
 
  SparsityPattern sparsity_pattern;
  SparseMatrix<double> system_matrix;
  AffineConstraints<double> mean_value_constraints;
 
  Vector<double> solution;
  Vector<double> system_rhs;
 
  TableHandler output_table;
  };
 
 
 
Definition fe_q.h:554
const ::parallel::distributed::Triangulation< dim, spacedim > * triangulation

Construct such an object, by initializing the variables. Here, we use linear finite elements (the argument to the fe variable denotes the polynomial degree), and mappings of given order. Print to screen what we are about to do.

  template <int dim>
  LaplaceProblem<dim>::LaplaceProblem(const unsigned int mapping_degree)
  : fe(1)
  , dof_handler(triangulation)
  , mapping(mapping_degree)
  {
  std::cout << "Using mapping with degree " << mapping_degree << ':'
  << std::endl
  << "============================" << std::endl;
  }
 
 
 
STL namespace.

The first task is to set up the variables for this problem. This includes generating a valid DoFHandler object, as well as the sparsity patterns for the matrix, and the object representing the constraints that the mean value of the degrees of freedom on the boundary be zero.

  template <int dim>
  void LaplaceProblem<dim>::setup_system()
  {

The first task is trivial: generate an enumeration of the degrees of freedom, and initialize solution and right hand side vector to their correct sizes:

  dof_handler.distribute_dofs(fe);
  solution.reinit(dof_handler.n_dofs());
  system_rhs.reinit(dof_handler.n_dofs());
 

The next task is to construct the object representing the constraint that the mean value of the degrees of freedom on the boundary shall be zero. For this, we first want a list of those nodes that are actually at the boundary. The DoFTools namespace has a function that returns an IndexSet object that contains the indices of all those degrees of freedom that are at the boundary.

Once we have this index set, we wanted to know which is the first index corresponding to a degree of freedom on the boundary. We need this because we wanted to constrain one of the nodes on the boundary by the values of all other DoFs on the boundary. To get the index of this "first" degree of freedom is easy enough using the IndexSet class:

  const IndexSet boundary_dofs = DoFTools::extract_boundary_dofs(dof_handler);
 
  const types::global_dof_index first_boundary_dof =
  boundary_dofs.nth_index_in_set(0);
 
IndexSet extract_boundary_dofs(const DoFHandler< dim, spacedim > &dof_handler, const ComponentMask &component_mask={}, const std::set< types::boundary_id > &boundary_ids={})
Definition dof_tools.cc:616

Then generate a constraints object with just this one constraint. First clear all previous content (which might reside there from the previous computation on a once coarser grid), then add this one constraint, constraining the first_boundary_dof to the sum of other boundary DoFs each with weight -1. Finally, close the constraints object, i.e. do some internal bookkeeping on it for faster processing of what is to come later:

  mean_value_constraints.clear();
  std::vector<std::pair<types::global_dof_index, double>> rhs;
  for (const types::global_dof_index i : boundary_dofs)
  if (i != first_boundary_dof)
  rhs.emplace_back(i, -1.);
  mean_value_constraints.add_constraint(first_boundary_dof, rhs);
  mean_value_constraints.close();
 

Next task is to generate a sparsity pattern. This is indeed a tricky task here. Usually, we just call DoFTools::make_sparsity_pattern and condense the result using the hanging node constraints. We have no hanging node constraints here (since we only refine globally in this example), but we have this global constraint on the boundary. This poses one severe problem in this context: the SparsityPattern class wants us to state beforehand the maximal number of entries per row, either for all rows or for each row separately. There are functions in the library which can tell you this number in case you just have hanging node constraints (namely DoFHandler::max_couplings_between_dofs), but how is this for the present case? The difficulty arises because the elimination of the constrained degree of freedom requires a number of additional entries in the matrix at places that are not so simple to determine. We would therefore have a problem had we to give a maximal number of entries per row here.

Since this can be so difficult that no reasonable answer can be given that allows allocation of only a reasonable amount of memory, there is a class DynamicSparsityPattern, that can help us out here. It does not require that we know in advance how many entries rows could have, but allows just about any length. It is thus significantly more flexible in case you do not have good estimates of row lengths, however at the price that building up such a pattern is also significantly more expensive than building up a pattern for which you had information in advance. Nevertheless, as we have no other choice here, we'll just build such an object by initializing it with the dimensions of the matrix and calling another function DoFTools::make_sparsity_pattern to get the sparsity pattern due to the differential operator, then condense it with the constraints object which adds those positions in the sparsity pattern that are required for the elimination of the constraint.

  DynamicSparsityPattern dsp(dof_handler.n_dofs(), dof_handler.n_dofs());
  DoFTools::make_sparsity_pattern(dof_handler, dsp);
  mean_value_constraints.condense(dsp);
 
void make_sparsity_pattern(const DoFHandler< dim, spacedim > &dof_handler, SparsityPatternBase &sparsity_pattern, const AffineConstraints< number > &constraints={}, const bool keep_constrained_dofs=true, const types::subdomain_id subdomain_id=numbers::invalid_subdomain_id)

Finally, once we have the full pattern, we can initialize an object of type SparsityPattern from it and in turn initialize the matrix with it. Note that this is actually necessary, since the DynamicSparsityPattern is so inefficient compared to the SparsityPattern class due to the more flexible data structures it has to use, that we can impossibly base the sparse matrix class on it, but rather need an object of type SparsityPattern, which we generate by copying from the intermediate object.

As a further sidenote, you will notice that we do not explicitly have to compress the sparsity pattern here. This, of course, is due to the fact that the copy_from function generates a compressed object right from the start, to which you cannot add new entries anymore. The compress call is therefore implicit in the copy_from call.

  sparsity_pattern.copy_from(dsp);
  system_matrix.reinit(sparsity_pattern);
  }
 
 
 

The next function then assembles the linear system of equations, solves it, and evaluates the solution. This then makes three actions, and we will put them into eight true statements (excluding declaration of variables, and handling of temporary vectors). Thus, this function is something for the very lazy. Nevertheless, the functions called are rather powerful, and through them this function uses a good deal of the whole library. But let's look at each of the steps.

  template <int dim>
  void LaplaceProblem<dim>::assemble_and_solve()
  {

First, we have to assemble the matrix and the right hand side. In all previous examples, we have investigated various ways how to do this manually. However, since the Laplace matrix and simple right hand sides appear so frequently in applications, the library provides functions for actually doing this for you, i.e. they perform the loop over all cells, setting up the local matrices and vectors, and putting them together for the end result.

The following are the two most commonly used ones: creation of the Laplace matrix and creation of a right hand side vector from body or boundary forces. They take the mapping object, the DoFHandler object representing the degrees of freedom and the finite element in use, a quadrature formula to be used, and the output object. The function that creates a right hand side vector also has to take a function object describing the (continuous) right hand side function.

Let us look at the way the matrix and body forces are integrated:

  const unsigned int gauss_degree =
  std::max(static_cast<unsigned int>(
  std::ceil(1. * (mapping.get_degree() + 1) / 2)),
  2U);
  dof_handler,
  QGauss<dim>(gauss_degree),
  system_matrix);
  dof_handler,
  QGauss<dim>(gauss_degree),
  system_rhs);
void create_laplace_matrix(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const Quadrature< dim > &q, MatrixType &matrix, const Function< spacedim, typename MatrixType::value_type > *const a=nullptr, const AffineConstraints< typename MatrixType::value_type > &constraints=AffineConstraints< typename MatrixType::value_type >())
void create_right_hand_side(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const Quadrature< dim > &q, const Function< spacedim, typename VectorType::value_type > &rhs, VectorType &rhs_vector, const AffineConstraints< typename VectorType::value_type > &constraints=AffineConstraints< typename VectorType::value_type >())
::VectorizedArray< Number, width > max(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)

That's quite simple, right?

Two remarks are in order, though: First, these functions are used in a lot of contexts. Maybe you want to create a Laplace or mass matrix for a vector values finite element; or you want to use the default Q1 mapping; or you want to assembled the matrix with a coefficient in the Laplace operator. For this reason, there are quite a large number of variants of these functions in the MatrixCreator and MatrixTools namespaces. Whenever you need a slightly different version of these functions than the ones called above, it is certainly worthwhile to take a look at the documentation and to check whether something fits your needs.

The second remark concerns the quadrature formula we use: we want to integrate over bilinear shape functions, so we know that we have to use at least an order two Gauss quadrature formula. On the other hand, we want the quadrature rule to have at least the order of the boundary approximation. Since the order of Gauss rule with \(r\) points is \(2r - 1\), and the order of the boundary approximation using polynomials of degree \(p\) is \(p+1\), we know that \(2r \geq p\). Since r has to be an integer and (as mentioned above) has to be at least \(2\), this makes up for the formula above computing gauss_degree.

Since the generation of the body force contributions to the right hand side vector was so simple, we do that all over again for the boundary forces as well: allocate a vector of the right size and call the right function. The boundary function has constant values, so we can generate an object from the library on the fly, and we use the same quadrature formula as above, but this time of lower dimension since we integrate over faces now instead of cells:

  Vector<double> tmp(system_rhs.size());
  mapping,
  dof_handler,
  QGauss<dim - 1>(gauss_degree),
  tmp);
void create_boundary_right_hand_side(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const Quadrature< dim - 1 > &q, const Function< spacedim, typename VectorType::value_type > &rhs, VectorType &rhs_vector, const std::set< types::boundary_id > &boundary_ids=std::set< types::boundary_id >())

Then add the contributions from the boundary to those from the interior of the domain:

  system_rhs += tmp;

For assembling the right hand side, we had to use two different vector objects, and later add them together. The reason we had to do so is that the VectorTools::create_right_hand_side and VectorTools::create_boundary_right_hand_side functions first clear the output vector, rather than adding up their results to previous contents. This can reasonably be called a design flaw in the library made in its infancy, but unfortunately things are as they are for some time now and it is difficult to change such things that silently break existing code, so we have to live with that.

Now, the linear system is set up, so we can eliminate the one degree of freedom which we constrained to the other DoFs on the boundary for the mean value constraint from matrix and right hand side vector, and solve the system. After that, distribute the constraints again, which in this case means setting the constrained degree of freedom to its proper value

  mean_value_constraints.condense(system_matrix);
  mean_value_constraints.condense(system_rhs);
 
  solve();
  mean_value_constraints.distribute(solution);
 

Finally, evaluate what we got as solution. As stated in the introduction, we are interested in the H1 semi-norm of the solution. Here, as well, we have a function in the library that does this, although in a slightly non-obvious way: the VectorTools::integrate_difference function integrates the norm of the difference between a finite element function and a continuous function. If we therefore want the norm of a finite element field, we just put the continuous function to zero. Note that this function, just as so many other ones in the library as well, has at least two versions, one which takes a mapping as argument (which we make us of here), and the one which we have used in previous examples which implicitly uses MappingQ1. Also note that we take a quadrature formula of one degree higher, in order to avoid superconvergence effects where the solution happens to be especially close to the exact solution at certain points (we don't know whether this might be the case here, but there are cases known of this, and we just want to make sure):

  Vector<float> norm_per_cell(triangulation.n_active_cells());
  dof_handler,
  solution,
  norm_per_cell,
  QGauss<dim>(gauss_degree + 1),
void integrate_difference(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const ReadVector< Number > &fe_function, const Function< spacedim, Number > &exact_solution, OutVector &difference, const Quadrature< dim > &q, const NormType &norm, const Function< spacedim, double > *weight=nullptr, const double exponent=2.)

Then, the function just called returns its results as a vector of values each of which denotes the norm on one cell. To get the global norm, we do the following:

  const double norm =
  norm_per_cell,
 
double compute_global_error(const Triangulation< dim, spacedim > &tria, const InVector &cellwise_error, const NormType &norm, const double exponent=2.)

Last task – generate output:

  output_table.add_value("cells", triangulation.n_active_cells());
  output_table.add_value("|u|_1", norm);
  output_table.add_value("error", std::fabs(norm - std::sqrt(numbers::PI_2)));
  }
 
 
 
static constexpr double PI_2
Definition numbers.h:259
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)

The following function solving the linear system of equations is copied from step-5 and is explained there in some detail:

  template <int dim>
  void LaplaceProblem<dim>::solve()
  {
  SolverControl solver_control(1000, 1e-12);
  SolverCG<Vector<double>> cg(solver_control);
 
  preconditioner.initialize(system_matrix, 1.2);
 
  cg.solve(system_matrix, solution, system_rhs, preconditioner);
  }
 
 
 
void initialize(const MatrixType &A, const AdditionalData &parameters=AdditionalData())

Next, we write the solution as well as the material ids to a VTU file. This is similar to what was done in many other tutorial programs. The new ingredient presented in this tutorial program is that we want to ensure that the data written to the file used for visualization is actually a faithful representation of what is used internally by deal.II. That is because most of the visualization data formats only represent cells by their vertex coordinates, but have no way of representing the curved boundaries that are used in deal.II when using higher order mappings – in other words, what you see in the visualization tool is not actually what you are computing on. (The same, incidentally, is true when using higher order shape functions: Most visualization tools only render bilinear/trilinear representations. This is discussed in detail in DataOut::build_patches().)

So we need to ensure that a high-order representation is written to the file. We need to consider two particular topics. Firstly, we tell the DataOut object via the DataOutBase::VtkFlags that we intend to interpret the subdivisions of the elements as a high-order Lagrange polynomial rather than a collection of bilinear patches. Recent visualization programs, like ParaView version 5.5 or newer, can then render a high-order solution (see a wiki page for more details). Secondly, we need to make sure that the mapping is passed to the DataOut::build_patches() method. Finally, the DataOut class only prints curved faces for boundary cells by default, so we need to ensure that also inner cells are printed in a curved representation via the mapping.

Note
As of 2023, Visit 3.3.3 can still not deal with higher-order cells. Rather, it simply reports that there is no data to show. To view the results of this program with Visit, you will want to comment out the line that sets flags.write_higher_order_cells = true;. On the other hand, Paraview is able to understand VTU files with higher order cells just fine.
  template <int dim>
  void LaplaceProblem<dim>::write_high_order_mesh(const unsigned cycle)
  {
  DataOut<dim> data_out;
 
  flags.write_higher_order_cells = true;
  data_out.set_flags(flags);
 
  data_out.attach_dof_handler(dof_handler);
  data_out.add_data_vector(solution, "solution");
 
  data_out.build_patches(mapping,
  mapping.get_degree(),
 
  std::ofstream file("solution-c=" + std::to_string(cycle) +
  ".p=" + std::to_string(mapping.get_degree()) + ".vtu");
 
  data_out.write_vtu(file);
  }
 
 

Finally the main function controlling the different steps to be performed. Its content is rather straightforward, generating a triangulation of a circle, associating a boundary to it, and then doing several cycles on subsequently finer grids. Note again that we have put mesh refinement into the loop header; this may be something for a test program, but for real applications you should consider that this implies that the mesh is refined after the loop is executed the last time since the increment clause (the last part of the three-parted loop header) is executed before the comparison part (the second one), which may be rather costly if the mesh is already quite refined. In that case, you should arrange code such that the mesh is not further refined after the last loop run (or you should do it at the beginning of each run except for the first one).

  template <int dim>
  void LaplaceProblem<dim>::run()
  {
 
  for (unsigned int cycle = 0; cycle < 6; ++cycle)
  {
  setup_system();
  assemble_and_solve();
  write_high_order_mesh(cycle);
 
  triangulation.refine_global();
  }
 
void hyper_ball(Triangulation< dim > &tria, const Point< dim > &center=Point< dim >(), const double radius=1., const bool attach_spherical_manifold_on_boundary_cells=false)

After all the data is generated, write a table of results to the screen:

  output_table.set_precision("|u|_1", 6);
  output_table.set_precision("error", 6);
  output_table.write_text(std::cout);
  std::cout << std::endl;
  }
  } // namespace Step11
 
 
 

Finally the main function. It's structure is the same as that used in several of the previous examples, so probably needs no more explanation.

  int main()
  {
  try
  {
  std::cout.precision(5);
 

This is the main loop, doing the computations with mappings of linear through cubic mappings. Note that since we need the object of type LaplaceProblem<2> only once, we do not even name it, but create an unnamed such object and call the run function of it, subsequent to which it is immediately destroyed again.

  for (unsigned int mapping_degree = 1; mapping_degree <= 3;
  ++mapping_degree)
  Step11::LaplaceProblem<2>(mapping_degree).run();
  }
  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;
  }

Results

This is what the program outputs:

Using mapping with degree 1:
============================
cells |u|_1 error
5 0.680402 0.572912
20 1.088141 0.165173
80 1.210142 0.043172
320 1.242375 0.010939
1280 1.250569 0.002745
5120 1.252627 0.000687
Using mapping with degree 2:
============================
cells |u|_1 error
5 1.177062 0.076252
20 1.228978 0.024336
80 1.245175 0.008139
320 1.250881 0.002433
1280 1.252646 0.000668
5120 1.253139 0.000175
Using mapping with degree 3:
============================
cells |u|_1 error
5 1.193493 0.059821
20 1.229825 0.023489
80 1.245221 0.008094
320 1.250884 0.002430
1280 1.252646 0.000668
5120 1.253139 0.000175

As we expected, the convergence order for each of the different mappings is clearly quadratic in the mesh size. What is interesting, though, is that the error for a bilinear mapping (i.e. degree 1) is more than three times larger than that for the higher order mappings; it is therefore clearly advantageous in this case to use a higher order mapping, not because it improves the order of convergence but just to reduce the constant before the convergence order. On the other hand, using a cubic mapping only improves the result further by insignificant amounts, except on very coarse grids.

We can also visualize the underlying meshes by using, for instance, ParaView. The image below shows initial meshes for different mapping degrees:

Clearly, the effect is most pronounced when we go from the linear to quadratic mapping. This is also reflected in the error values given in the table above. The effect of going from quadratic to cubic degree is less dramatic, but still tangible owing to a more accurate description of the circular boundary.

Next, let's look at the meshes after three global refinements

Here, the differences are much less visible, especially for higher order mappings. Indeed, at this refinement level the error values reported in the table are essentially identical between mappings of degrees two and three.

The plain program

/* ------------------------------------------------------------------------
*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright (C) 2001 - 2024 by the deal.II authors
*
* This file is part of the deal.II library.
*
* Part of the source code is dual licensed under Apache-2.0 WITH
* LLVM-exception OR LGPL-2.1-or-later. Detailed license information
* governing the source code and code contributions can be found in
* LICENSE.md and CONTRIBUTING.md at the top level directory of deal.II.
*
* ------------------------------------------------------------------------
*
* Author: Wolfgang Bangerth, University of Heidelberg, 2001
*/
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <cmath>
namespace Step11
{
using namespace dealii;
template <int dim>
class LaplaceProblem
{
public:
LaplaceProblem(const unsigned int mapping_degree);
void run();
private:
void setup_system();
void assemble_and_solve();
void solve();
void write_high_order_mesh(const unsigned cycle);
const FE_Q<dim> fe;
DoFHandler<dim> dof_handler;
const MappingQ<dim> mapping;
SparsityPattern sparsity_pattern;
SparseMatrix<double> system_matrix;
AffineConstraints<double> mean_value_constraints;
Vector<double> solution;
Vector<double> system_rhs;
TableHandler output_table;
};
template <int dim>
LaplaceProblem<dim>::LaplaceProblem(const unsigned int mapping_degree)
: fe(1)
, dof_handler(triangulation)
, mapping(mapping_degree)
{
std::cout << "Using mapping with degree " << mapping_degree << ':'
<< std::endl
<< "============================" << std::endl;
}
template <int dim>
void LaplaceProblem<dim>::setup_system()
{
dof_handler.distribute_dofs(fe);
solution.reinit(dof_handler.n_dofs());
system_rhs.reinit(dof_handler.n_dofs());
const IndexSet boundary_dofs = DoFTools::extract_boundary_dofs(dof_handler);
const types::global_dof_index first_boundary_dof =
boundary_dofs.nth_index_in_set(0);
mean_value_constraints.clear();
std::vector<std::pair<types::global_dof_index, double>> rhs;
for (const types::global_dof_index i : boundary_dofs)
if (i != first_boundary_dof)
rhs.emplace_back(i, -1.);
mean_value_constraints.add_constraint(first_boundary_dof, rhs);
mean_value_constraints.close();
DynamicSparsityPattern dsp(dof_handler.n_dofs(), dof_handler.n_dofs());
mean_value_constraints.condense(dsp);
sparsity_pattern.copy_from(dsp);
system_matrix.reinit(sparsity_pattern);
}
template <int dim>
void LaplaceProblem<dim>::assemble_and_solve()
{
const unsigned int gauss_degree =
std::max(static_cast<unsigned int>(
std::ceil(1. * (mapping.get_degree() + 1) / 2)),
2U);
dof_handler,
QGauss<dim>(gauss_degree),
system_matrix);
dof_handler,
QGauss<dim>(gauss_degree),
system_rhs);
Vector<double> tmp(system_rhs.size());
mapping,
dof_handler,
QGauss<dim - 1>(gauss_degree),
tmp);
system_rhs += tmp;
mean_value_constraints.condense(system_matrix);
mean_value_constraints.condense(system_rhs);
solve();
mean_value_constraints.distribute(solution);
Vector<float> norm_per_cell(triangulation.n_active_cells());
dof_handler,
solution,
norm_per_cell,
QGauss<dim>(gauss_degree + 1),
const double norm =
norm_per_cell,
output_table.add_value("cells", triangulation.n_active_cells());
output_table.add_value("|u|_1", norm);
output_table.add_value("error", std::fabs(norm - std::sqrt(numbers::PI_2)));
}
template <int dim>
void LaplaceProblem<dim>::solve()
{
SolverControl solver_control(1000, 1e-12);
SolverCG<Vector<double>> cg(solver_control);
preconditioner.initialize(system_matrix, 1.2);
cg.solve(system_matrix, solution, system_rhs, preconditioner);
}
template <int dim>
void LaplaceProblem<dim>::write_high_order_mesh(const unsigned cycle)
{
DataOut<dim> data_out;
data_out.set_flags(flags);
data_out.attach_dof_handler(dof_handler);
data_out.add_data_vector(solution, "solution");
data_out.build_patches(mapping,
mapping.get_degree(),
std::ofstream file("solution-c=" + std::to_string(cycle) +
".p=" + std::to_string(mapping.get_degree()) + ".vtu");
data_out.write_vtu(file);
}
template <int dim>
void LaplaceProblem<dim>::run()
{
for (unsigned int cycle = 0; cycle < 6; ++cycle)
{
setup_system();
assemble_and_solve();
write_high_order_mesh(cycle);
triangulation.refine_global();
}
output_table.set_precision("|u|_1", 6);
output_table.set_precision("error", 6);
output_table.write_text(std::cout);
std::cout << std::endl;
}
} // namespace Step11
int main()
{
try
{
std::cout.precision(5);
for (unsigned int mapping_degree = 1; mapping_degree <= 3;
++mapping_degree)
Step11::LaplaceProblem<2>(mapping_degree).run();
}
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 write_vtu(std::ostream &out) const
void set_flags(const FlagType &flags)
void attach_dof_handler(const DoFHandler< dim, spacedim > &)
void add_data_vector(const VectorType &data, const std::vector< std::string > &names, const DataVectorType type=type_automatic, const std::vector< DataComponentInterpretation::DataComponentInterpretation > &data_component_interpretation={})
virtual void build_patches(const unsigned int n_subdivisions=0)
Definition data_out.cc:1062
size_type nth_index_in_set(const size_type local_index) const
Definition index_set.h:1982
double norm(const FEValuesBase< dim > &fe, const ArrayView< const std::vector< Tensor< 1, dim > > > &Du)
Definition divergence.h:471