So far we have not paid any attention to the accuracy of the results. In general, a computation on a fixed mesh is not likely to be very accurate. There is a need for adaptive mesh refinement (AMR) that improves the quality of the approximation by refining mesh elements or increases the polynomial degree of approximation where the approximation is bad.
In traditional low-order FEM, refining an element is not algorithmically complicated, and so the most difficult part is to find out what elements should be refined. To do this, people employ various techniques ranging from rigorous guaranteed a-posteriori error estimates to heuristic criteria such as residual error indicators, error indicators based on steep gradients, etc. Unfortunately, none of these approaches is suitable for real-life multiphysics coupled problems or higher-order finite element methods: Rigorous guaranteed error estimates only exist for very simple problems (such as linear elliptic PDE), and moreover only for low-order finite elements (such as piecewise linear approximations). Note that virtually no a-posteriori error estimates capable of guiding automatic hp-adaptivity are available even for simplest elliptic problems, this will be discussed in a moment. The heuristic techniques listed above are not employed in Hermes since they may fail in non-standard situations, and because they lack a transparent relation to the true approximation error.
Adaptive low-order FEM is known to be notoriously inefficient, and practitioners are rightfully skeptical of it. The reason is its extremely slow convergence that makes large computations virtually freeze without getting anywhere. This is illustrated in the following graph that compares a typical convergence of adaptive FEM with linear elements, adaptive FEM with quadratic elements, and adaptive hp-FEM:

Note that the linear FEM would need in the order of 1,000,000,000,000,000,000 degrees of freedom (DOF) to reach a level of accuracy where the hp-FEM is with less than 10,000 DOF. These convergence curves are typical representative examples, confirmed with many numerical experiments of independent researchers, and supported with theory. The horizontal axis shows (in linear scale) the number of degrees of freedom (= size of the stiffness matrix) that increases during automatic adaptivity. The vertical one shows the approximation error (in logarithmic scale). Note that in all three cases, the convergence is similar during a short initial phase. However, with the hp-FEM the convergence becomes faster and faster as the adaptivity progresses. Note that low-order FEM is doomed to such slow convergence by its poor approximation properties - this cannot be fixed no matter how smart the adaptivity algorithm might be.
In order to obtain fast, usable adaptivity (the red curve), one has to resort to adaptive hp-FEM. The hp-FEM takes advantage of the following facts:
and
. Often this is the case with
boundary layers (viscous flows, singularly perturbed problems, etc.). This is illustrated
in benchmarks
smooth-aniso-x and
boundary layer. However,
solutions without boundary layers can have significant anisotropic behavior too, as illustrated
in benchmark line singularity.Automatic adaptivity in the hp-FEM is substantially different from adaptivity in low-order FEM, since every element can be refined in many different ways. The following figure shows several illustrative refinement candidates for a fourth-order element.

Of course, the number of possible element refinements is implementation-dependent.
In general it is very low in
or
adaptivity, much higher in
adaptivity,
and it rises even more when anisotropic refinements are enabled. This is why Hermes
has eight different adaptivity options P_ISO, P_ANISO, H_ISO, H_ANISO,
HP_ISO, HP_ANISO_P, HP_ANISO_H, HP_ANISO. In this ordering, usually P_ISO yields the
worst results and HP_ANISO the best. In the most general HP_ANISO
option, around 100 refinement candidates for each element are considered.
Naturally, the adaptivity algorithm takes progressively more time as more
refinement candidates are considered. The difference between the HP_ANISO_H
option (next best to HP_ANISO) and HP_ANISO is quite significant. So, this is
where the user has to make a choice based on his a-priori knowledge of the
solution behavior.
Due to the large number of refinement options, classical error estimators (that provide a constant error estimate per element) cannot be used to guide automatic hp-adaptivity. For this, one needs to know the shape of the approximation error.
In analogy to the most successful adaptive ODE solvers,
Hermes uses a pair of approximations with different orders of accuracy to obtain
this information: coarse mesh solution and
fine mesh solution. The initial coarse mesh is read from the mesh file,
and the initial fine mesh is created through its global refinement both in
and
.
The fine mesh solution is the approximation of interest both during the adaptive
process and at the end of computation. The coarse mesh solution represents its
low-order part. In all adaptivity examples in Hermes, the coarse mesh solution
can be turned off and a global orthogonal projection of the fine mesh solution
on the coarse mesh can be used instead. In most cases, this yields a better
convergence behavior than using the coarse mesh solve (and the projection
problem is always linear and better conditioned than solving on the coarse mesh).
Note that this approach is PDE independent, which is truly great for multiphysics coupled problems. Currently, Hermes does not use a single analytical error estimate or any other technique that would narrow down its applicability to just some equations or just low-order FEM.
The obvious disadvantage of the Hermes approach to automatic adaptivity is its higher computational cost, especially in 3D. We are aware of this fact and would not mind at all replacing it with some cheaper technique (as long as it also is PDE-independent, works for elements of high orders, and can be successfully used to guide hp-adaptivity). So far, however, no alternatives meeting these criteria exist yet to our best knowledge.
Hermes provides convergence graphs for every adaptive computation. Therefore,
let us spend a short moment explaining their meaning.
The classical notion of
convergence rate is related to sequences of
uniform meshes with a gradually decreasing diameter
. In
spatial dimensions,
the diameter
of a uniform mesh is related to the number of degrees of freedom
through the relation
Therefore a slope of
on the log-log scale means that
or
. When local refinements are enabled, the meaning of
convergence rate loses its meaning, and one should switch to convergence in terms of
the number of degrees of freedom (DOF) or CPU time - Hermes provides both.
-FEM¶When using elements of degree
, the convergence rate of adaptive
-FEM will not exceed the
one predicted for uniformly refined meshes (this can be explained using
mathematical analysis). Nevertheless, the convergence may be faster due to a different
constant in front of the
term. This is illustrated in the following two figures,
both of which are related to a 2D problem with known exact solution. The first pair of
graphs corresponds to adaptive
-FEM with linear elements. The slope on the log-log
graph is -1/2 which means first-order convergence, as predicted by theory.

The next pair of convergence graphs corresponds to adaptive
-FEM with quadratic elements.
The slope on the log-log graph is -1, which means that the convergence is quadratic as
predicted by theory.

Note that one always should look at the end of the convergence curve, not at the beginning. The automatic adaptivity in Hermes is guided with the so-called reference solution, which is an approximation on a globally-refined mesh. In early stages of adaptivity, the reference solution and in turn also the error estimate usually are not sufficiently accurate to deliver the expected convergence rates.
-FEM¶It is predicted by theory that adaptive
-FEM should attain
exponential convergence rate. This means that the slope of the
convergence graph is steadily increasing, as shown in the
following figure.

While this often is the case with adaptive
-FEM, there are
problems whose difficulty is such that the convergence is not
exponential. Or at least not during a long pre-asymptotic
stage of adaptivity. This may happen, for example, when the solution
contains an extremely strong singularity. Then basically all error
is concentrated there, and all adaptive methods will do the same,
which is to throw into the singularity as many small low-order
elements as possible. Then the convergence of adaptive
-FEM
and
-FEM may be very similar (usually quite poor).
Whenever exact solution is available, Hermes provides both estimated error (via the reference solution) as well as the exact error. Thus the user can see the quality of the error estimate. Note that the estimated error usually is slightly less than the exact one, but during adaptivity they quickly converge together and become virtually identical. This is shown in the figure below.

In problems with extremely strong singularities the difference between the exact and estimated error can be significant. This is illustrated in the following graph that belongs to the benchmark kellogg.
Git reference: Tutorial example 10-adapt.
Let us demonstrate the use of adaptive h-FEM and hp-FEM on a linear elliptic problem
concerned with the calculation of
the electrostatic potential in the vicinity of the electrodes of an electrostatic
micromotor. This is a MEMS device free of any coils, and thus resistive to
strong electromagnetic waves (as opposed to classical electromotors).
The following figure shows one half of the domain
(dimensions need to be scaled with
and are in meters):

The subdomain
represents the moving part of the domain and the area bounded by
represents the electrodes that are fixed. The distribution of the electrostatic potential
is governed by the equation
equipped with the Dirichlet boundary conditions
The relative permittivity
is piecewise-constant,
in
and
in
. The weak formulation reads
The piecewise constant parameter
is handled by defining two bilinear forms in the code, one for
and the other for
. The two different materials are distinguished by different
element markers OMEGA_1 = 1 and OMEGA_2 = 2 in the mesh file, and two different weak forms are assigned
to the corresponding markers during the registration of the forms:
// Initialize the weak formulation.
WeakForm wf;
wf.add_matrix_form(callback(biform1), H2D_SYM, OMEGA_1);
wf.add_matrix_form(callback(biform2), H2D_SYM, OMEGA_2);
The principal part of the example is the main adaptivity loop. However, before the loop is entered, a refinement selector should be initialized:
H1ProjBasedSelector selector(CAND_LIST, CONV_EXP, H2DRS_DEFAULT_ORDER);
The selector is used by the class H1Adapt to determine how an element should be refined. For that purpose, the selector does following steps:
By default, the score is
where
and
are an estimated error and an estimated number of DOF of a candidate respectively,
and
are an estimated error and an estimated number of DOF of the examined element respectively, and
is a convergence exponent.
The first parameter CAND_LIST specifies which candidates are generated. In a case of quadrilaterals, all possible values and considered candidates are summarized in the following table:

The second parameter CONV_EXP is a convergence exponent used to calculate the score.
The third parameter specifies the the maximum considered order used in the resulting refinement. In this case, a constant H2DRS_DEFAULT_ORDER is used. The constant is defined by Hermes2D library and it corresponds to the maximum order supported by the selector. In this case, this is 9.
Furthermore, the selector allows you to weight errors though a method set_error_weights(). Error weights are applied before the error of a candidate is passed to the calculation of the score. Through this method it is possible to set a preference for a given type of a candidate, i.e., H-candidate, P-candidate, and ANISO-candidate. The error weights can be set anytime and setting error weights to appropriate values can lead to a lower number of DOF. However, the best values of weights depend on a solved problem.
In this particular case, a default error weights are used. The default weights prefer the P-candidate and they are defined as:
(see a constant H2DRS_DEFAULT_ERR_WEIGHT_H)
(see a constant H2DRS_DEFAULT_ERR_WEIGHT_P)
(see a constant H2DRS_DEFAULT_ERR_WEIGHT_ANISO)Since these weights are default, it is not necessary to express them explicitly. Nevertheless, if expressed, a particular line of the code would be:
selector.set_error_weights(2.0, 1.0, sqrt(2.0));
Besides the error weights, the selector allows you to modify a default behaviour through the method set_option(). The behavior can be modified anytime. Currently, the method accepts following options:
, where
is the convergence exponent, instead of
to evaluate the score. This options is not set by default.In this case, default settings are used. If expressed explicitly, the code would be:
selector.set_option(H2D_PREFER_SYMMETRIC_MESH, true);
selector.set_option(H2D_APPLY_CONV_EXP_DOF, false);
After the selector has been created, the adaptivity can begin. The adaptivity loop is an ordinary while-loop or a for-loop that (for linear problems) usually starts like this:
// Adaptivity loop:
Solution sln_coarse, sln_fine;
int as = 1; bool done = false;
do
{
info("---- Adaptivity step %d:", as);
// Assemble and solve the fine mesh problem.
info("Solving on fine mesh.");
RefSystem rs(&ls);
rs.assemble();
rs.solve(&sln_fine);
// Either solve on coarse mesh or project the fine mesh solution
// on the coarse mesh.
if (SOLVE_ON_COARSE_MESH) {
info("Solving on coarse mesh.");
ls.assemble();
ls.solve(&sln_coarse);
}
else {
info("Projecting fine mesh solution on coarse mesh.");
ls.project_global(&sln_fine, &sln_coarse);
}
The code above creates the pair of coarse and fine mesh approximations, either by solving on both meshes or by just solving on the fine mesh and projecting the fine mesh solution on the coarse mesh. We prefer the latter approach as for us it has worked better in many situations.
The reference (fine mesh) solution is computed on a globally refined copy of the mesh using the class RefSystem. The constructor of the class RefSystem allows the user to choose a different polynomial degree increment (default value 1) and another element refinement (default value 1) - see the file src/refsystem.h:
RefSystem(LinSystem* base, int order_increase = 1, int refinement = 1);
In particular, sometimes one may want to use order_increase = 2 or 3 at the very beginning of computation when the reference mesh is still very coarse and thus the reference solution with order_increase = 1 does not give a meaningful error estimate.
In the third and last step of each iteration, we use the class H1Dadpt to adjust the coarse mesh and polynomial degrees of finite elements stored in the corresponding Space. (Classes HcurlAdapt, HdivAdapt and L2Adapt will be discussed later.) The H1Adapt class has two main functionalities:
norm (user-defined norms for
error calculation will be discussed later),The class H1Adapt is initialized with a pointer to the underlying LinSystem (or NonlinSystem - this will be discussed later). Then the user sets the coarse solution and the fine solution and evaluates the error. By default, the error is calculated as
In the code this looks as follows:
// Calculate element errors and total error estimate.
info("Calculating error.");
H1Adapt hp(&ls);
hp.set_solutions(&sln_coarse, &sln_fine);
double err_est = hp.calc_error() * 100;
Finally, if err_est is still above the threshold ERR_STOP, we perform one adaptivity step:
// If err_est too large, adapt the mesh.
if (err_est < ERR_STOP) done = true;
else {
info("Adapting coarse mesh.");
done = hp.adapt(&selector, THRESHOLD, STRATEGY, MESH_REGULARITY);
if (ls.get_num_dofs() >= NDOF_STOP) done = true;
}
The constants THRESHOLD, STRATEGY and MESH_REGULARITY have the following meaning:
The constant STRATEGY indicates which adaptive strategy is used. In all cases, the strategy is applied to elements in an order defined through the error. If the user request to process an element outside this order, the element is processed regardless the strategy. Currently, Hermes2D supportes following strategies:
The constant MESH_REGULARITY specifies maximum allowed level of hanging nodes: -1 means arbitrary-level hanging nodes (default), and 1, 2, 3, ... means 1-irregular mesh, 2-irregular mesh, etc. Hermes does not support adaptivity on regular meshes because of its extremely poor performance.
It is a good idea to spend some time playing with these parameters to get a feeling for adaptive hp-FEM. Also look at other adaptivity examples in the examples/ directory: layer, lshape deal with elliptic problems and have known exact solutions. So do examples screen, bessel for time-harmonic Maxwell’s equations. These examples allow you to compare the error estimates computed by Hermes with the true error. Examples crack, singpert show how to handle cracks and singularly perturbed problems, respectively. There are also more advanced examples illustrating automatic adaptivity for nonlinear problems solved via the Newton’s method, adaptive multimesh hp-FEM, adaptivity for time-dependent problems on dynamical meshes, etc.
But let’s return to the micromotor example for a moment again: The computation starts with a very coarse mesh consisting of a few quadrilaterals, some of which are moreover very ill-shaped. Thanks to the anisotropic refinement capabilities of the selector, the mesh quickly adapts to the solution and elements of reasonable shape are created near singularities, which occur at the corners of the electrode. Initially, all elements of the mesh are of a low degree, but as the hp-adaptive process progresses, the elements receive different polynomial degrees, depending on the local smoothness of the solution.
The gradient was visualized using the class VectorView. We have seen this in the previous section. We plug in the same solution for both vector components, but specify that its derivatives should be used:
gview.show(&sln, &sln, H2D_EPS_NORMAL, H2D_FN_DX_0, H2D_FN_DY_0);

Convergence graphs of adaptive h-FEM with linear elements, h-FEM with quadratic elements and hp-FEM are shown below.

The following graph shows convergence in terms of CPU time.

In multiphysics PDE systems (or just PDE systems) it can happen that one physical field (solution component) has a singularity or a boundary layer where other fields are smooth. If one approximates all fields on the same mesh, then the necessity to refine the mesh at the singularity or boundary layer implies new degrees of freedom for the smooth fields as well. This can be very wasteful indeed, as we will see in the next example that deals with a simplified Fitzhugh-Nagumo system. But let us first explain briefly the main idea of the multimesh discretization method that we developed to circumvent this problem.
Hermes makes it possible to approximate them on individual meshes. These meshes are not completely independent of each other – they have a common coarse mesh that we call master mesh. The master mesh is there for algorithmic purposes only, it may not even be used for discretization purposes: Every mesh in the system is obtained from it via an arbitrary sequence of elementary refinements. This is illustrated in the following figure, where (A) is the master mesh, (B) - (D) three different meshes (say, for a coupled problem with three equations), and (E) is the virtual union mesh that is used for assembling.

The union mesh is not constructed physically in the computer memory –
merely it serves as a hint to correctly transform integration points
while integrating over sub-elements of the elements of the existing meshes.
The following figure shows the integration over an element
of the
virtual union mesh, and what are the appropriate subelements of the
existing elements where this integration is performed:

As a result, the multimesh discretization of the PDE system is monolithic in the sense that no physics is lost – all integrals in the discrete weak formulations are evaluated exactly up to the error in the numerical quadrature. In particular, we do not perform operator splitting or commit errors while transferring solution data between different meshes. The multimesh assembling in Hermes works with all meshes at the same time, there is no such thing as interpolating or projecting functions between different meshes. More details about this method can be found in the corresponding research article.
In principle, the adaptivity procedure for single PDE could be extended
directly to systems of PDEs. In other words, two spaces can be passed into a constructor of the class H1Adapt,
two coarse and two fine solutions can be passed into set_solutions(),
and finally, calc_error() and adapt() can be called as before. In this way, error estimates in
norm are calculated for elements in both spaces independently and the
worst ones are refined. However, this approach is not optimal if the PDEs are
coupled, since an error caused in one solution component influences the errors
in other components and vice versa.
Recall that in elliptic problems the bilinear form
defines the energetic inner product,
The norm induced by this product,
is called the energy norm. When measuring the error in the energy norm of the entire system, one can reduce the above-mentioned difficulties dramatically. When calculating the error on an element, the energy norm accounts also for the error caused by other solution components.
It is also worth mentioning that the adaptivity algorithm does not make distinctions between various meshes. The elements of all meshes in the system are put into one single array, sorted according to their estimated errors, and then the ones with the largest error are refined. In other words, it may happen that all elements marked for refinement will belong just to one mesh.
If norms of components are substantially different, it is more beneficial to use a relative error of an element rather than an absolute error. The relative error of an element is an absolute error divided by a norm of a component. This behavior can be requested while calling the method calc_error():
hp.calc_error(H2D_TOTAL_ERROR_REL | H2D_ELEMENT_ERROR_REL)
The input parameter of the method calc_error() is a combination that is a pair: one member of the pair has to be a constant `H2D_TOTAL_ERROR_*`, the other member has to be a constant `H2D_ELEMENT_ERROR_*`. If not specified, the default pair is `H2D_TOTAL_ERROR_REL | H2D_ELEMENT_ERROR_ABS`. Currently available contants are:
Git reference: Tutorial example 11-system-adapt.
We consider a simplified version of the Fitzhugh-Nagumo equation. This equation is a~prominent example of activator-inhibitor systems in two-component reaction-diffusion equations, It describes a prototype of an excitable system (e.g., a neuron) and its stationary form is
Here the unknowns
are the voltage and
-gate, respectively,
The nonlinear function
describes how an action potential travels through a nerve. Obviously this system is nonlinear.
In order to make it simpler for this tutorial, we replace the function
with just
:
The original nonlinear version of this example is planned for inclusion in benchmarks.
Our computational domain is the square
and we consider zero Dirichlet conditions
for both
and
. In order to enable fair convergence comparisons, we will use the following
functions as the exact solution:
where
is the exact solution of the one-dimensional singularly perturbed problem
in
, equipped with zero Dirichlet boundary conditions. The functions
and
defined above evidently satisfy the given boundary conditions, and
they also satisfy the equation, since we inserted them into the PDE system
and calculated the source functions
and
from there. These functions
are not extremely pretty, but they are not too bad either:
// Functions g_1 and g_2.
double g_1(double x, double y)
{
return (-cos(M_PI*x/2.)*cos(M_PI*y/2.) + SIGMA*(1. - (exp(K*x) + exp(-K*x))/(exp(K) + exp(-K)))
* (1. - (exp(K*y) + exp(-K*y))/(exp(K) + exp(-K))) + pow(M_PI,2.)*pow(D_u,2.)*cos(M_PI*x/2.)
*cos(M_PI*y/2.)/2.);
}
double g_2(double x, double y)
{
return ((1. - (exp(K*x) + exp(-K*x))/(exp(K) + exp(-K)))*(1. - (exp(K*y) + exp(-K*y))/(exp(K) + exp(-K)))
- pow(D_v,2.)*(-(1 - (exp(K*x) + exp(-K*x))/(exp(K) + exp(-K)))*(pow(K,2.)*exp(K*y) + pow(K,2.)*exp(-K*y))/(exp(K) + exp(-K))
- (1. - (exp(K*y) + exp(-K*y))/(exp(K) + exp(-K)))*(pow(K,2.)*exp(K*x) + pow(K,2.)*exp(-K*x))/(exp(K) + exp(-K))) -
cos(M_PI*x/2.)*cos(M_PI*y/2.));
}
The weak forms can be found in the file forms.cpp and they are registered as follows:
// Initialize the weak formulation.
WeakForm wf(2);
wf.add_matrix_form(0, 0, callback(bilinear_form_0_0));
wf.add_matrix_form(0, 1, callback(bilinear_form_0_1));
wf.add_matrix_form(1, 0, callback(bilinear_form_1_0));
wf.add_matrix_form(1, 1, callback(bilinear_form_1_1));
wf.add_vector_form(0, linear_form_0, linear_form_0_ord);
wf.add_vector_form(1, linear_form_1, linear_form_1_ord);
Beware that although each of the forms is actually symmetric, one cannot use the H2D_SYM flag as in the elasticity equations, since it has a slightly different meaning (see example 08-system).
At the beginning of the adaptivity loop, a coarse and fine mesh approximation on both meshes is obtained as follows:
// Assemble and solve the fine mesh problem.
info("Solving on fine meshes.");
RefSystem rs(&ls);
rs.assemble();
rs.solve(Tuple<Solution*>(&u_sln_fine, &v_sln_fine));
// Either solve on coarse mesh or project the fine mesh solution
// on the coarse mesh.
if (SOLVE_ON_COARSE_MESH) {
info("Solving on coarse meshes.");
ls.assemble();
ls.solve(Tuple<Solution*>(&u_sln_coarse, &v_sln_coarse));
}
else {
info("Projecting fine mesh solutions on coarse meshes.");
ls.project_global(Tuple<MeshFunction*>(&u_sln_fine, &v_sln_fine),
Tuple<Solution*>(&u_sln_coarse, &v_sln_coarse));
}
Error estimate for adaptivity is now calculated using an energetic norm that employs the original weak forms of the problem:
// Calculate element errors and total error estimate.
info("Calculating error (est).");
H1Adapt hp(&ls);
hp.set_solutions(Tuple<Solution*>(&u_sln_coarse, &v_sln_coarse),
Tuple<Solution*>(&u_sln_fine, &v_sln_fine));
hp.set_error_form(0, 0, bilinear_form_0_0<scalar, scalar>, bilinear_form_0_0<Ord, Ord>);
hp.set_error_form(0, 1, bilinear_form_0_1<scalar, scalar>, bilinear_form_0_1<Ord, Ord>);
hp.set_error_form(1, 0, bilinear_form_1_0<scalar, scalar>, bilinear_form_1_0<Ord, Ord>);
hp.set_error_form(1, 1, bilinear_form_1_1<scalar, scalar>, bilinear_form_1_1<Ord, Ord>);
double err_est = hp.calc_error(H2D_TOTAL_ERROR_REL | H2D_ELEMENT_ERROR_ABS) * 100;
We also calculate error wrt. exact solution for comparison purposes:
// Calculate error wrt. exact solution.
info("Calculating error (exact).");
ExactSolution uexact(&umesh, u_exact);
ExactSolution vexact(&vmesh, v_exact);
double u_error = h1_error(&u_sln_coarse, &uexact) * 100;
double v_error = h1_error(&v_sln_coarse, &vexact) * 100;
double error = std::max(u_error, v_error);
The following two figures show the solutions
and
. Notice their
large qualitative differences: While
is smooth in the entire domain,
has a thin boundary layer along the boundary:


Resulting mesh for
and
obtained using conventional (single-mesh) hp-FEM: 12026 DOF
(6013 for each solution).

Resulting mesh for
obtained using the multimesh hp-FEM: 169 DOF

Resulting mesh for
obtained using the multimesh hp-FEM: 3565 DOF

DOF convergence graphs:

CPU time convergence graphs:

Git reference: Tutorial example 12-general-adapt.
This example does not bring anything substantially new and its purpose is solely to save you work adding adaptivity to the tutorial example 07-general. Feel free to adjust the main.cpp file for your own applications.
Solution:

Final hp-mesh:

Convergence graphs of adaptive h-FEM with linear elements, h-FEM with quadratic elements and hp-FEM.

Convergence comparison in terms of CPU time.

Git reference: Tutorial example 13-complex-adapt.
This example solves a complex-valued vector potential problem
in a two-dimensional cross-section containing a conductor and an iron object as shown in the following schematic picture:

The computational domain is a rectangle of height 0.003 and width 0.004. Different material markers are used for the wire, air, and iron (see mesh file domain2.mesh).
Boundary conditions are zero Dirichlet on the top and right edges, and zero Neumann elsewhere.
Solution:

Complex-valued weak forms:
template<typename Real, typename Scalar>
Scalar bilinear_form_iron(int n, double *wt, Func<Scalar> *u_ext[], Func<Real> *u, Func<Real> *v, Geom<Real> *e, ExtData<Scalar> *ext)
{
scalar ii = cplx(0.0, 1.0);
return 1./mu_iron * int_grad_u_grad_v<Real, Scalar>(n, wt, u, v) + ii*omega*gamma_iron*int_u_v<Real, Scalar>(n, wt, u, v);
}
template<typename Real, typename Scalar>
Scalar bilinear_form_wire(int n, double *wt, Func<Scalar> *u_ext[], Func<Real> *u, Func<Real> *v, Geom<Real> *e, ExtData<Scalar> *ext)
{
return 1./mu_0 * int_grad_u_grad_v<Real, Scalar>(n, wt, u, v);
}
template<typename Real, typename Scalar>
Scalar bilinear_form_air(int n, double *wt, Func<Scalar> *u_ext[], Func<Real> *u, Func<Real> *v, Geom<Real> *e, ExtData<Scalar> *ext)
{
return 1./mu_0 * int_grad_u_grad_v<Real, Scalar>(n, wt, u, v); // conductivity gamma is zero
}
template<typename Real, typename Scalar>
Scalar linear_form_wire(int n, double *wt, Func<Scalar> *u_ext[], Func<Real> *v, Geom<Real> *e, ExtData<Scalar> *ext)
{
return J_wire * int_v<Real, Scalar>(n, wt, v);
}
After loading the mesh and performing initial mesh refinements, we create an H1 space:
// Create an H1 space with default shapeset.
H1Space space(&mesh, bc_types, essential_bc_values, P_INIT);
The weak forms are registered as follows:
// Initialize the weak formulation.
WeakForm wf;
wf.add_matrix_form(callback(bilinear_form_iron), H2D_SYM, 3);
wf.add_matrix_form(callback(bilinear_form_wire), H2D_SYM, 2);
wf.add_matrix_form(callback(bilinear_form_air), H2D_SYM, 1);
wf.add_vector_form(callback(linear_form_wire), 2);
Let us compare adaptive
-FEM with linear and quadratic elements and the
-FEM.
Final mesh for
-FEM with linear elements: 18694 DOF, error = 1.02 %

Final mesh for
-FEM with quadratic elements: 46038 DOF, error = 0.018 %

Final mesh for
-FEM: 4787 DOF, error = 0.00918 %

Convergence graphs of adaptive h-FEM with linear elements, h-FEM with quadratic elements and hp-FEM are shown below.

Git reference: Tutorial example 14-hcurl-adapt.
This example solves time-harmonic Maxwell’s equations in an L-shaped domain and it describes the diffraction of an electromagnetic wave from a re-entrant corner. It comes with an exact solution that contains singularity.
Equation solved: Time-harmonic Maxwell’s equations
(1)
Domain of interest is the square
missing the quarter lying in the
fourth quadrant. It is filled with air:

Boundary conditions: Combined essential and natural, see the main.cpp file.
Exact solution:
(2)
where
is the Bessel function of the first kind,
the polar coordinates and
. In
computer code, this reads:
void exact_sol(double x, double y, scalar& e0, scalar& e1)
{
double t1 = x*x;
double t2 = y*y;
double t4 = sqrt(t1+t2);
double t5 = jv(-1.0/3.0,t4);
double t6 = 1/t4;
double t7 = jv(2.0/3.0,t4);
double t11 = (t5-2.0/3.0*t6*t7)*t6;
double t12 = atan2(y,x);
if (t12 < 0) t12 += 2.0*M_PI;
double t13 = 2.0/3.0*t12;
double t14 = cos(t13);
double t17 = sin(t13);
double t18 = t7*t17;
double t20 = 1/t1;
double t23 = 1/(1.0+t2*t20);
e0 = t11*y*t14-2.0/3.0*t18/x*t23;
e1 = -t11*x*t14-2.0/3.0*t18*y*t20*t23;
}
Here jv() is the Bessel function
. For its source code see the
forms.cpp file.
Code for the weak forms:
template<typename Real, typename Scalar>
Scalar bilinear_form(int n, double *wt, Func<Scalar> *u_ext[], Func<Real> *u, Func<Real> *v, Geom<Real> *e, ExtData<Scalar> *ext)
{
return 1.0/mu_r * int_curl_e_curl_f<Real, Scalar>(n, wt, u, v) -
sqr(kappa) * int_e_f<Real, Scalar>(n, wt, u, v);
}
template<typename Real, typename Scalar>
Scalar bilinear_form_surf(int n, double *wt, Func<Scalar> *u_ext[], Func<Real> *u, Func<Real> *v, Geom<Real> *e, ExtData<Scalar> *ext)
{
cplx ii = cplx(0.0, 1.0);
return ii * (-kappa) * int_e_tau_f_tau<Real, Scalar>(n, wt, u, v, e);
}
scalar linear_form_surf(int n, double *wt, Func<scalar> *u_ext[], Func<double> *v, Geom<double> *e, ExtData<scalar> *ext)
{
scalar result = 0;
for (int i = 0; i < n; i++)
{
double r = sqrt(e->x[i] * e->x[i] + e->y[i] * e->y[i]);
double theta = atan2(e->y[i], e->x[i]);
if (theta < 0) theta += 2.0*M_PI;
double j13 = jv(-1.0/3.0, r), j23 = jv(+2.0/3.0, r);
double cost = cos(theta), sint = sin(theta);
double cos23t = cos(2.0/3.0*theta), sin23t = sin(2.0/3.0*theta);
double Etau = e->tx[i] * (cos23t*sint*j13 - 2.0/(3.0*r)*j23*(cos23t*sint + sin23t*cost)) +
e->ty[i] * (-cos23t*cost*j13 + 2.0/(3.0*r)*j23*(cos23t*cost - sin23t*sint));
result += wt[i] * cplx(cos23t*j23, -Etau) * ((v->val0[i] * e->tx[i] + v->val1[i] * e->ty[i]));
}
return result;
}
// Maximal polynomial order to integrate surface linear form.
Ord linear_form_surf_ord(int n, double *wt, Func<Ord> *u_ext[], Func<Ord> *v, Geom<Ord> *e, ExtData<Ord> *ext)
{ return Ord(v->val[0].get_max_order()); }
Solution:

Final mesh (h-FEM with linear elements):

Note that the polynomial order indicated corresponds to the tangential components of approximation on element interfaces, not to polynomial degrees inside the elements (those are one higher).
Final mesh (h-FEM with quadratic elements):

Final mesh (hp-FEM):

DOF convergence graphs:

CPU time convergence graphs:
