This program was contributed by Siarhei Uzunbajakau, CEM Books, 2025.
Introduction
Problem definition
This tutorial illustrates application of the FE_Nedelec and FE_RaviartThomas finite elements to problems in electromagnetics. To this end, we will compute the magnetic field induced by a coil with a magnetic core and compare the result to an exact closed-form analytical expression of the magnetic field. We will repeat the comparison on a set of progressively refined meshes and observe that the error norm decreases with the size of mesh cells. We will also vary the degree of the finite elements and observe that the higher-degree finite elements imply faster convergence of the error norms. Experimenting with boundary conditions is suggested as a possibility for extension.
The first figure below illustrates a cross section of the coil. The second figure illustrates the corresponding problem domain, coordinate system, and the relevant notations. The coil consists of two spherical shells. The inner shell, depicted in green, is the magnetic core. The walls of the core are filled with a soft magnetic material of permeability . The magnetic material is assumed to be homogeneous, linear, lossless, and isotropic. The permeability of the rest of the space (the ball-shaped space inside the core and the space outside the core) equals to that of the free space, . The permeability of the entire space can be expressed as
The outer shell, depicted in blue, contains the current-carrying windings of the coil. The windings are modeled by a prescribed free-current density. The prescribed free-current density fills all the space available in the wall of the shell. In the figure below, however, only three cross sections of the free-current density are shown. The free-current density is the highest on the equator and equals zero at the two poles. It can be described as
in the Cartesian coordinate system.
There is no consensus in the literature on what to call a magnetic field. On this page we adopt the nomenclature from [griffiths2017b]. Consequently, we call a magnetic field the vector quantity such that the Ampere's law in free space in static approximation reads
where is the total current density, free plus bound.
Problem domain and coordinate systems
We adopt the Cartesian coordinate system, the coordinates being , , and , as shown in the figure below. We denote the corresponding unitary basis vectors as , , and . The problem domain consists of a ball centered at the origin and four spherical shells around it. The spheres and are interfaces between the magnetic material and free space. Strictly speaking, the magnetic field induced by the coil extends to infinity. Therefore, the problem we would like to solve is unbounded. As the mesh needs to have finite dimensions, we truncate the space with the sphere . This sphere will represent infinity. We denote the spherical coordinates as , , and , with and being polar and azimuthal angles, respectively. The figure below illustrates the basis vectors of the spherical coordinate system, , , and .
Exact solution
The problem of a permeable spherical shell exposed to a uniform magnetic field is ubiquitous in the literature on electromagnetics. The problem of the magnetic field generated by a spherical coil can be found in the literature as well. Remarkably, the magnetic field inside the coil is uniform. Due to this fact one can combine the solution to these two problems to get the exact closed-form analytical expressions for the magnetic field generated by the coil described above. The exact solution presented below is a combination of the solutions to the problems of permeable shell and spherical coil. The derivation of the equations presented below can be found in [uzunbajakau2024b].
We express the magnetic field induced by the coil as a sum of the magnetic field induced by the free-current density in free space, , and the magnetic field induced by the magnetization of the magnetic core, ,
It is convenient to express the term in a spherical coordinate system,
where
We, however, need to program in the Cartesian coordinate system. The expressions above can be converted into the Cartesian coordinate system by invoking the following identities:
and
The magnetic field induced by the magnetic core can be expressed in Cartesian coordinates as
where
and
The following constants were used in the last equation:
The relative permeability in the last five equations is computed as
Boundary value problems
Magnetic vector potential - A
Ampere's law in static approximation can be written in terms of the auxiliary vector field as
where is the free current and is given by
We introduce the magnetic vector potential, , as
By combining the last three equations we arrive at
which is the curl-curl partial differential equation we would like to solve. There are, however, two subtleties we need to address before solving any curl-curl equations: the gauge and compatibility condition.
Gauge
The Helmholtz decomposition theorem suggests that any vector field studied in electromagnetics, say , can be represented as a sum of a conservative and a solenoidal vector field,
The curl of a conservative vector field always equals zero (conservative fields are curl free),
Consequently, any vector field derived by adding a conservative vector field to a solution to the curl-curl equation is also a solution,
We can restrict this freedom by specifying the divergence of the solution. Indeed, the divergence of a solenoidal vector field always equals zero (solenoidal fields are divergence free),
Then the divergence of a solution can be expressed as
By specifying the divergence of the solution, we specify the divergence of the conservative part of the solution, and, thus, we specify the conservative part of the solution itself. In short: the magnetic vector potential consists of conservative and solenoidal components; the curl-curl equation fixes the solenoidal component, the gauge fixes the conservative component. For example, the Coulomb gauge,
specifies that there is no conservative component in . That is, the Coulomb gauge selects a purely solenoidal solution. We are interested in the magnetic field, . In the realm of classical electromagnetics, the magnetic vector potential, , has an instrumental value - we just use it to compute the magnetic field, . The magnetic vector potential is a useful mathematical tool but it has no physical meaning as it cannot be measured. On the contrary, the magnetic field, , can be measured and, as a consequence, has a distinctive physical meaning. From a physical perspective, there are (infinitely) many magnetic vector potentials that lead to the same physically measurable magnetic field and for a physicist, it doesn't matter which of these potentials we choose. But from a mathematical perspective, we need to pick one to make the problem well-posed. A gauge is a way to pick one. Because the magnetic field is computed as
we are interested in the solenoidal part of the solution. Which conservative component of the solution is brought by the gauge is not important.
A good question is: how to enforce the gauge? The literature suggests that the best approach is not to gauge the magnetic vector potential explicitly. Instead, one should invoke an advanced linear solver (tree-cotree splitting, domain decomposition, geometric multigrid, algebraic multigrid, etc.). For example, in [manges1995a] it is suggested that the tree-cotree splitting can be used us a gauge. The hypre AMS utilizes a more modern approach [hypre1998b]. It can solve positive semidefinite linear systems yielded by the ungauged curl-curl equation. In general, if a solver is able to choose one solution out of many ungauged solutions, we can claim that such solution is implicitly gauged by the linear solver. The linear solver will not communicate to us which gauge has been applied, i.e., the conservative part of the solution will be fixed, but we will not be able to tell what it is, exactly. We will call such gauge an implicit gauge. An implicit gauge is good enough for our purpose.
We will use the CG solver which is the next best choice to the advanced linear solvers mentioned above. The CG solver is designed to handle symmetric positive definite matrices. This means that all eigenvalues of the system matrix are expected to be positive. On the other hand, the curl-curl equation above being augmented with proper boundary conditions will always yield a symmetric positive semidefinite matrix. The last means that most of the eigenvalues of the system matrix will be positive but some of them will be zero. We elevate the zero eigenvalues above zero just a bit by adding a very small gauging term, , to the partial differential equation,
so all eigenvalues become positive. Formally, the system matrix with elevated eigenvalues is symmetric positive definite. We can feed it to the CG solver. The partial differential equation is different now. We can expect a different solution. Moreover, belongs to the function space, while the rest of the terms of the partial differential equation belong to the function space, see the Bossavit's diagram below. They exhibit different behavior on interfaces between dissimilar materials. Strictly speaking, we destroy the compatibility between the two sides of the curl-curl equation by adding the gauging term. However, by choosing the parameter to be small enough we can make these horrible crimes insignificant. In practice, we need to set to zero. If the CG solver chokes, we will increase just a bit. Setting can save the day. The -trick is an implicit gauge: it helps the CG solver to select one solution out of many, but it is impossible to predict what the conservative part of the solution will look like. The last is not important for the problem we would like to solve because, as mentioned above, the vector potential has no physical reality and all choices of a gauge (including the implicit gauge used here) lead to the same physical fields.
Compatibility condition
To solve the curl-curl equation numerically, we need to feed to the solver the right-hand side of the equation, . Whatever we do, we will end up feeding to the solver a discretized version of which will be a combination of conservative and solenoidal vector fields even if the problem we would like to solve specifies a closed-form analytical expression of a purely solenoidal . The conservative component will be added implicitly by discretization. On the other hand, the left-hand side of the curl-curl equation is purely solenoidal as the curl of any vector field is always solenoidal (assume for a moment). Therefore, the two sides of the equation will be somewhat incompatible: pure solenoidal vector field on the left-hand side and a combination of a conservative and solenoidal vector fields on the right-hand side. As a consequence, we cannot expect that we can get the norm of the residual to zero. In other words, the linear solver cannot converge to a solution that has zero residual. In practice, this means that CG will either not converge at all, or terminate only after an unreasonably large number of iterations. To make the two sides of the equation compatible, we derive the free-current density from a current vector potential, , as
Consequently, the curl-curl equation becomes
This equation contains purely solenoidal vector fields on both sides if . Now, however, we need to convert into . There are a number of ways of doing so. One of them is solving another curl-curl equation as discussed below. For now we assume that is known.
Boundary and interface conditions
Even if the magnetic vector potential is gauged and the compatibility issue is solved, one still needs to set up the boundary conditions to pick up one solution out of many gauged vector potentials that satisfy the curl-curl equation. Moreover, the behavior of the magnetic field on interfaces between dissimilar materials is defined by the Maxwell's equations. Consequently, the behavior of the magnetic vector potential on interfaces is defined by the Maxwell's equations as well. For this reason, we need to add the interface conditions next to the boundary conditions to ensure the correct behavior of the current vector potential on interfaces. The curl-curl partial differential equation augmented with the boundary and interface conditions constitutes the boundary value problem. We compose the following boundary value problem for the problem discussed above:
where
In this boundary value problem the vector is the unit vector normal to the surface on which the boundary or interface condition is defined. By convention, always points outside a closed surface. The subscript "+" refers to the space immediately next to the interface in the direction of vector . The subscript "-" refers to the space immediately next to the interface in the direction opposite to .
Equation (i) in the boundary value problem has been discussed above and needs no further comments.
The surface represents infinity. As most problems in magnetics, the problem we would like to solve is unbounded. That is to say, the magnetic field as well as the magnetic vector potential extend to infinity. A straightforward modeling of an unbounded domain by means of finite elements is impossible as such modeling will require an infinite amount of cells. To overcome this unfortunate predicament, we artificially truncate the unbounded problem domain with the surface . This truncation will introduce a simulation error. We, however, will try to minimize this error by making the interior of the artificial surface as spacious as possible. The magnetic field induced by the coil vanishes at infinity, so does the magnetic vector potential. For this reason, the homogeneous Dirichlet,
and homogeneous Neumann,
boundary conditions are used on most often. For this approach to be efficient, one needs to choose the radius of the sphere large enough so that the tangential component of the magnetic vector potential (Dirichlet) or the tangential component of the magnetic field (Neumann) is negligibly small on . There is, however, a more efficient approach.
The first term of the multipole expansion of magnetic vector potential is the magnetic vector potential of a magnetic dipole moment unless the source of the magnetic field is explicitly configured as a higher-order multipole. For this reason, the magnetic vector potential induced by the coil looks more like that of a magnetic dipole at a sufficient distance. If a magnetic dipole is placed at the center of a sphere, the magnetic vector potential induced by the dipole on the sphere satisfies the following identity:
Note that this is a purely geometric statement. It does not depend on how we choose our coordinate system. Consequently, the identity given by the last equation is true for a magnetic dipole of any orientation as long as it is situated at the center of the sphere. By multiplying the last equation by and rearranging the terms we arrive into
which is, essentially, the Robin boundary condition (ii) in the boundary value problem above. The Robin boundary condition in the current context is also called the first-order asymptotic boundary condition, first-order ABC for short. The Robin boundary condition is expected to be superior to both Dirichlet and Neumann boundary conditions as it allows to reach the same level of simulation error with a sphere of a smaller radius.
All three boundary conditions, Dirichlet, Neumann, and Robin, guarantee the uniqueness of the curl of the solution, . The uniqueness of the solution itself, , can only be guaranteed if one of the boundary conditions is applied in combination with a gauge, see the discussion above. The program uses the Robin boundary condition by default, but can easily be switched to Dirichlet or Robin boundary conditions.
The behavior of the magnetic field on the interfaces between dissimilar materials can be described by
where is a surface free-current density that can be present on an interface. No free currents flow on the surface of the magnetic core of the coil described above. Therefore, we set . Then by substituting the constitutive relation for the magnetic field and the definition of the magnetic vector potential, see above, into the last two equations we get
Next, we simply replace the first condition with another condition and rearrange the terms of the second condition:
The last two equations are identical to equations (iii) and (iv) in the boundary value problem above. These are the interface conditions that must be observed on the inner and outer surfaces of the magnetic core, .
We have replaced condition A with condition B. This replacement deserves an explanation. The are two reasons for this replacement. The first reason is the following. A magnetic flux through an infinitesimally small closed loop must be infinitesimally small even if an interface between dissimilar magnetic materials passes through the loop. We can guarantee this only if we require that condition B holds. Condition A will not do. The second reason is the uniqueness of the curl of the solution. If we include condition A into the boundary value problem above, the uniqueness of the curl of the solution, , cannot be guaranteed. It is possible to guarantee it if condition B is included instead. A good question is: does this replacement disturb the behavior of the magnetic field on the interfaces? Not at all. Consider the following. The normal component of the curl, , is defined by the derivatives of the tangential components of the vector potential. If two vector potentials, and , have the same tangential components on the interface, i.e., condition B, the derivatives of these tangential components along the interface will be the same as well. Therefore, condition B implicitly implies condition A and, thus, the replacement is justified. We can conclude that condition B is more restrictive than condition A, so the behavior of the magnetic vector potential on the interfaces is observed. Note, that the interface conditions (iii) and (iv) in the boundary value problem above have absolutely no influence on the uniqueness of the solution regardless which boundary condition, Dirichlet, Neumann, or Robin, is used.
It remains to comment on the labels (e) and (n) in the boundary value problem above. They label (e)ssential and (n)atural boundary and interface conditions. How to sort conditions in these two categories and why it is important to do so is discussed below in the section on variational formulations.
Current vector potential - T
To be able to solve the boundary value problem above, we need to convert the free current density, , into current vector potential, . We define the current vector potential as
By taking the curl of the last equation and rearranging the terms we arrive into
We will compute the current vector potential by solving this equation. As soon as there are curls of vector fields on both sides of the equation, the compatibility condition is observed. We add to this equation a gauging term,
The discussion on the gauging term in the preceding section is valid in the current context as well.
The current vector potential is computed in free space, i.e., everywhere. For this reason, there is no need for interface conditions as there are no interfaces in free space.
We still need to apply a boundary condition to the surface . It is beneficial to apply the homogeneous Dirichlet boundary condition as it allows to nullify the integral in the recipe for calculating the magnetic vector potential, see below. Consequently, we can neglect this integral when programming and this makes the code more efficient.
We compose a boundary value problem by combining the curl-curl equation for the current vector potential with the Dirichlet boundary condition:
Variational formulations
Magnetic vector potential
The problem of solving the boundary value problem for the magnetic vector potential can be replaced by the problem of minimizing the following functional:
Any magnetic vector potential that minimizes this functional will satisfy the curl-curl equation (i), the boundary condition (ii) and the interface condition (iv). On the other hand, this functional is invariant to the interface condition (iii). This can be verified as follows: The first variation of the functional at the minimum vanishes,
We can express the first variation and deduce that the identities given by equations (i), (ii), and (iv) in the boundary value problem must be satisfied for the last equation to be true. The interface condition (iii), however, is not needed for the last equation to be satisfied. The boundary and interface conditions that are needed to drive the first variation of the functional to zero are called natural and are labeled by (n) in the boundary value problem. If a condition is not natural, it is essential. We label essential conditions by (e).
The natural conditions are enforced by minimization of the functional. The essential boundary and interface conditions must be enforced by other means. The interface condition (iii) in the boundary value problem above is enforced by choosing the finite elements wisely. The FE_Nedelec finite elements ensure continuity of the tangential component of the vector field they model on the faces of the cells. Therefore, if we construct the mesh such that all interfaces are made of cell faces, i.e., no interface runs through a cell, and choose the FE_Nedelec finite elements to model the magnetic vector potential, the interface condition (iii) will be enforced automatically. The Dirichlet boundary condition is essential. It is enforced by constraining the degrees of freedom in the system of linear equations. This can be done by invoking the deal.II function VectorTools::project_boundary_values_curl_conforming_l2(). The other two boundary conditions, Neumann and Robin, are natural.
Note that the homogeneous Neumann boundary condition is embedded into the first integral of the functional. This integral is present in all functionals related to the curl-curl equation, even in the most minimalistic ones. Therefore, application of no boundary condition is quite impossible: the homogeneous Neumann boundary condition,
will be applied implicitly by default as a result of the functional minimization.
We can switch between the three boundary conditions as the following.
The functional above as it is corresponds to a boundary value problem with Robin boundary condition, i.e., the boundary value problem given above.
To switch to Neumann boundary condition, we need to discard the second integral from the functional. Such functional will correspond to the boundary value problem above with equation (ii) replaced by the Neumann boundary condition.
To switch to Dirichlet boundary condition, we use the functional with discarded second integral and use VectorTools::project_boundary_values_curl_conforming_l2() to constrain the system of linear equations. Such configuration will correspond to the boundary value problem above with equation (ii) replaced by the Dirichlet boundary condition.
In general, we need to sort all boundary and interface conditions into two categories, natural and essential. The reason for doing so is simple: we need to know which conditions are not taken care of by the functional (the essential conditions), so we can take care of them by other means (choice of finite elements, restricting dofs). Interrogating the first variation of the functional as discussed above is a common method of sorting the conditions. A more detailed discussion on boundary conditions can be found in step-22. (See also video lecture 21.5, video lecture 21.55, video lecture 21.6, video lecture 21.65.)
Next, we need to convert the functional above into a numerical recipe that can be programmed into a computer. To do so, we note that the final result, i.e., the magnetic vector potential computed by the finite element method, is represented as a sum,
where are the degrees of freedom and are the vector-valued shape functions of the FE_Nedelec finite elements. We convert the functional into a multivariate function by substituting the last equation into the functional,
Then we compose the system of linear equations by computing the partial derivatives of the multivariate function:
This system of linear equations can be written in matrix form as
where is a column vector filled with the degrees of freedom,
The system matrix and the right-hand side column vector are computed as
and
The last two equations are implemented by the computer code. The integral equals zero as the tangential component of the current vector potential, , is forced to zero on the boundary by the Dirichlet boundary condition. We will neglect this integral in the computer code.
Current vector potential
The problem of solving the boundary value problem for the current vector potential can be replaced by the problem of minimizing the following functional:
This functional is minimized by solving a system of linear equations with the following system matrix and right-hand side column vector:
Here again, the integral equals zero as the free-current density, , equals zero at the boundary by definition of the problem. We will neglect this integral in the computer code.
Converting potentials into fields
As discussed above, we are interested in the magnetic field induced by the coil. For this reason, we convert the numerically computed vector potential into magnetic field as
The problem of computing this equation can be replaced by the problem of minimizing the following functional:
This functional is minimized by solving a system of linear equations with the system matrix
and right-hand side column vector
This time, however, are the shape functions of the FE_RaviartThomas finite elements. The vector field in the last equation is the numerically computed magnetic vector potential, i.e., a linear combination of the shape functions of the FE_Nedelec finite elements.
It is informative to verify the quality of the computed current vector potential, . We can do this by converting it back into the free-current density, , and comparing the result to the closed-form analytical expression given by the definition of the problem, see above. The current vector potential relates to the free-current density as
This equation is, essentially, the same as the equation for converting into , see the first equation of this section. Consequently, we can simply adapt the equations for the functional, system matrix, and the right-hand side given above by making the following substitutions:
The two conversions, into and into , can be done by the same piece of code. We will call it a projector from to .
Selecting finite elements
The preceding sections leave an impression that we are going to model and by the FE_Nedelec finite elements, while and by the FE_RaviartThomas finite elements. It makes sense to discuss how this kind of choice can be made.
The simplest method of assigning a particular type of finite elements to a physical quantity studied in electromagnetics is a contemplation of the Bossavit's diagram. It is presented below. The Bossavit's diagram describes the relations between physical quantities as they are given by Maxwell's equations and constitutive relations. Most equations derived from Maxwell's equations can be captured by this diagram.
Let us consider assigning the correct type of finite elements to the magnetic vector potential, . First, we contemplate the Bossavit's diagram and observe that belongs to the function space. Second, we look at the table below and conclude that the physical quantities that belong to the function space are modeled by the FE_Nedelec finite elements. Therefore, we need to model the magnetic vector potential, , by the FE_Nedelec finite elements. The same assigning procedure can be applied to the rest of the physical quantities, , , and .
Alternatively, one of the types of the finite elements listed in the table can be assigned to a physical quantity by considering the behavior of the physical quantity on the interfaces between dissimilar materials. Let us consider the magnetic field as an example. The normal component of the magnetic field is continuous on interfaces. On the contrary, the tangential component is discontinuous, see the first figure on this page. This type of behavior can be modeled by the FE_RaviartThomas finite elements as they guarantee the continuity of the normal component of the vector field on the faces of mesh cells.
Mesh
The mesh is, essentially, a discretization of the problem domain shown above. The mesh is constructed such that all spherical surfaces of the problem domain are delineated by cell faces. That is, no spherical surface runs through a mesh cell. The figure below illustrates the mesh. The main elements of the mesh are listed below.
The cube with a side of in the middle of the mesh. The purpose of this cube is instrumental. This is the only way to construct a mesh that has spherical interfaces and contains no tetrahedral cells as the cells around the origin must be hexahedral.
The spherical shell nr.1. This shell represents the magnetic core. The permeability of this region is . The permeability outside this region is .
The spherical shell nr.2. This region contains the free current. There is no free current outside this region.
Sphere nr.1. The error norms are computed inside the region delineated by this surface.
Sphere nr.2. The boundary of the problem domain. Represents infinity. The boundary condition is applied to this surface.
The program uses four meshes created with a help of gmsh. Each mesh has a different degree of refinement. The degree of refinement in each mesh is defined by a number of mesh nodes on the transfinite lines, . The figure below illustrates a mesh with . The program uses four meshes with the amount of nodes on the transfinite lines in the interval .
Note that this tutorial uses only globally refined meshes. There are no non-conforming cells, hanging nodes, and hanging node constrains in this tutorial. We will use constrains only for the purpose of enforcing the Dirichlet boundary condition.
Overview of the program
The program runs in a loop. In each iteration of the loop a different parameter (the mesh refinement parameter, i.e., the amount of nodes on transfinite lines) is assumed. Each iteration consists of four stages:
The current vector potential, , is computed given the closed-form analytical expression for the free-current density, , see above. No error norms are computed. The mesh is loaded at this stage. This mesh is simply reused at all other stages.
The current vector potential computed at the preceding stage, , is converted numerically back into free-current density, , for verification. The error norm is computed by comparing the numerical result with the closed-form analytical expression. The error norm is saved in a convergence table table_Jf.
The magnetic vector potential, , is computed given the current vector potential computed at the first stage, . No error norms are computed at this stage.
The magnetic vector potential computed at the preceding stage, , is converted into magnetic field, . The norm is computed by comparing the numerical result with the closed-form analytical solution. The error norm is saved in a convergence table table_B.
The first stage is implemented by the code contained in the name space SolverT. The third stage is implemented by the code contained in the name space SolverA. The second and the fourth stages are implemented by the code contained in the name space ProjectorHcurlToHdiv. The name space ExactSolutions contains exact closed-form analytical expressions for and .
No error norms are computed at the first and the third stages. Computing error norms at these stages makes no sense as we apply implicit gauges and the conservative portion of the solution is unknown. We can, however, judge the quality of the simulations at these two stages indirectly by evaluating the quality of the simulations at the second and the fourth stages.
This enumeration is used to switch between boundary conditions when solving for the magnetic vector potential, . The boundary condition is fixed (Dirichlet) when solving for the current vector potential, .
 enum BoundaryConditionType
 {
 Dirichlet,
 Neumann,
 Robin
 };
Â
Settings
The following is the control panel of the program. The scaling of the program can be changed by setting mu_0 = 1.0. Then the computed magnetic field, , will have to be multiplied by a factor of . The free-current density, , does not depend on scaling. The setting boundary_condition_type_A can be used to switch between Dirichlet, Neumann, and Robin (ABC) boundary conditions. This has an effect only on the solver that solves for the magnetic vector potential, . The solver that solves for the current vector potential, , applies the Dirichlet boundary condition. This cannot be changed. Recall, that forcing to zero the tangential component of on the boundary allows us to skip the boundary integral . The boundary ID is set in the geo files that describe the mesh geometry. This is done by specifying the physical surface, i.e.,
The boundary ID in the geo file must match the boundary ID setting below. If project_exact_solution = true, the program projects the exact solutions for and onto function space and saves the results into corresponding vtu files next to the numerical solutions. If the projected exact solution and the numerical solution look alike, it is a good sign. In case of problems with the convergence of the CG solver the parameter must be increased just a bit.
 namespace Settings
 {
 constdouble permeability_fs = 1.2566370614359172954e-6;
 constdouble mu_0 = permeability_fs; // Permeability of free space.
Â
 constdouble mu_r = 4; // Relative permeability of the magnetic material.
 constdouble mu_1 = mu_0 * mu_r; // Permeability of the magnetic material.
 constdouble d1 = 0.1; // Half-side of the cube in the center of the mesh.
 constdouble a1 = 0.3; // Inner radius of the magnetic core.
 constdouble b1 = 0.6; // Outer radius of the magnetic core.
 constdouble a2 = 0.9; // Inner radius of the free-current region.
 constdouble b2 = 1.2; // Outer radius of the free-current region.
 constdouble d2 = 2.0; // Error norms are computed if r < d2.
 constdouble K0 = 1.0; // Magnitude of the free-current density.
 constdouble H0 = // Magnitude of H-field inside the coil, core removed.
 B = magnetic_field_coil(p[i], K0, mu_0, a2, b2) +
 magnetic_field_core(p[i], H0, mu_r, mu_0, a1, b1);
Â
 values[i][0] = B[0];
 values[i][1] = B[1];
 values[i][2] = B[2];
 }
 }
 };
Â
 } // namespace ExactSolutions
Â
Solver - T
This name space contains all the code related to the computation of the current vector potential, .
 namespace SolverT
 {
This class describes the free-current density, , on the right-hand side of the curl-curl equation, see equation (i) in the boundary value problem for . The free-current density is given as a closed-form analytical expression by the definition of the problem.
virtual void value_list(const std::vector< Point< dim > > &points, std::vector< RangeNumberType > &values, const unsigned int component=0) const
This class implements the solver that minimizes the functional , see the introduction. The mesh is loaded in this class. All other solvers use a reference to this mesh.
 class Solver
 {
 public:
 Solver() = delete;
 Solver(constunsignedint p, // Degree of the Nedelec finite elements.
 constunsignedint r, // The mesh refinement parameter.
 constunsignedint mapping_degree,
 constdouble eta_squared = 0.0,
 const std::string &fname = "data");
Â
 void make_mesh(); // Loads the mesh. Assigns mat. IDs. Attaches manifold.
 void setup(); // Initializes dofs, vectors, matrices.
 void assemble(); // Assembles the system of linear equations.
 void solve(); // Solves the system of linear equations.
 voidsave() const; // Saves computed T into a vtu file.
The following data members are typical for all deal.II simulations: triangulation, finite elements, dof handlers, etc. The constraints are used to enforce the Dirichlet boundary conditions. The names of the data members are self-explanatory.
The program utilizes the WorkStream technology. The step-9 tutorial does a much better job of explaining the workings of WorkStream. Reading the "WorkStream paper", see the glossary, is recommended. The following structures and functions are related to WorkStream.
The following function loads the mesh, assigns material IDs to all cells, and attaches the spherical manifold to the mesh. The material IDs are assigned on the basis of the distance from the center of a cell to the origin. The spherical manifold is attached to a face if all vertices of the face are at the same distance from the origin provided the cell is outside the cube in the center of the mesh, see mesh description in the introduction.
This function initializes the dofs, applies the Dirichlet boundary condition, and initializes the vectors and matrices.
 void Solver::setup()
 {
 dof_handler.reinit(triangulation);
 dof_handler.distribute_dofs(fe);
Â
The following segment of the code applies the homogeneous Dirichlet boundary condition. As discussed in the introduction, the Dirichlet boundary condition is an essential condition and must be enforced by constraining the system matrix. This segment of the code does the constraining.
Formally, the following function assembles the system of linear equations. In reality, however, it just spells all the magic words to get the WorkStream going. The interesting part, i.e., the actual assembling of the system matrix and the right-hand side, happens below in the function Solver::system_matrix_local().
This function assembles a fraction of the system matrix and the system right-hand side related to a single cell. These fractions are copy_data.cell_matrix and copy_data.cell_rhs. They are copied into the system matrix, , and the right-hand side, , by the function Solver::copy_local_to_global().
First, we reinitialize the matrices and vectors related to the current cell and compute the FE values.
 copy_data.cell_matrix.reinit(scratch_data.dofs_per_cell,
 scratch_data.dofs_per_cell);
Â
 copy_data.cell_rhs.reinit(scratch_data.dofs_per_cell);
Â
 copy_data.local_dof_indices.resize(scratch_data.dofs_per_cell);
Â
 scratch_data.fe_values.reinit(cell);
Â
Second, we compute the free-current density, , at the quadrature points.
 scratch_data.Jf.value_list(scratch_data.fe_values.get_quadrature_points(),
 cell->material_id(),
 scratch_data.Jf_list);
Â
Third, we compute the components of the cell matrix and cell right-hand side. The labels of the integrals are the same as in the introduction to this tutorial.
 for (unsignedint q_index = 0; q_index < scratch_data.n_q_points; ++q_index)
 {
 for (unsignedint i = 0; i < scratch_data.dofs_per_cell; ++i)
 {
 for (unsignedint j = 0; j < scratch_data.dofs_per_cell; ++j)
 {
 copy_data.cell_matrix(i, j) += // Integral I_a1+I_a3.
 (scratch_data.fe_values[scratch_data.ve].curl(
 i, q_index) * // curl N_i
 scratch_data.fe_values[scratch_data.ve].curl(
 j, q_index) // curl N_j
 + scratch_data.eta_squared * // eta^2
 scratch_data.fe_values[scratch_data.ve].value(
 i, q_index) * // N_i
 scratch_data.fe_values[scratch_data.ve].value(
 j, q_index) // N_j
 ) *
 scratch_data.fe_values.JxW(q_index); // dV
 }
 copy_data.cell_rhs(i) += // Integral I_b3-1.
 (scratch_data.Jf_list[q_index] *
 scratch_data.fe_values[scratch_data.ve].curl(i, q_index)) *
 scratch_data.fe_values.JxW(q_index); // J_f.(curl N_i)dV.
 }
 }
Â
Finally, we query the dof indices on the current cell and store them in the copy data structure, so we know to which locations of the system matrix and right-hand side the components of the cell matrix and cell right-hand side must be copied.
 cell->get_dof_indices(copy_data.local_dof_indices);
 }
Â
This function copies the components of a cell matrix and a cell right-hand side into the system matrix, , and the system right-hand side, .
 constraints.distribute_local_to_global(copy_data.cell_matrix,
 copy_data.cell_rhs,
 copy_data.local_dof_indices,
 system_matrix,
 system_rhs);
 }
Â
This function solves the system of linear equations. If Settings::log_cg_convergence == true, the convergence data is saved into a file. In theory, a CG solver can solve an system of linear equations in at most steps. In practice, it can take more steps to converge. The convergence of the algorithm depends on the spectral properties of the system matrix. The best case is if the eigenvalues form a compact cluster away from zero. In our case, however, the eigenvalues are spread in between zero and the maximal eigenvalue. Consequently, we expect a poor convergence and increase the maximal number of iteration steps by a factor of 10, i.e., 10*system_rhs.size(). The stopping condition is
As soon as we use constraints, we must not forget to distribute them.
This name space contains all the code related to the computation of the magnetic vector potential, . The main difference between this solver and the solver for the current vector potential, , is in how the information on the source is fed to respective solvers. The solver for is fed data sampled from the analytical closed-form expression for . The solver for is fed a field function, i.e., a numerically computed current vector potential, .
 namespace SolverA
 {
This class describes the permeability in the entire problem domain. The permeability is given by the definition of the problem, see the introduction.
 if ((mid == Settings::material_id_free_space) ||
 (mid == Settings::material_id_free_current))
 std::fill(values.begin(), values.end(), Settings::mu_0);
Â
 if (mid == Settings::material_id_core)
 std::fill(values.begin(), values.end(), Settings::mu_1);
 }
 };
Â
This class describes the parameter in the Robin boundary condition. As soon as it is evaluated on the boundary, the permeability equals to that of free space. Therefore, we evaluate the parameter gamma as
 values[i] = 1.0 / (Settings::mu_0 * r[i].norm());
 }
 };
Â
This class implements the solver that minimizes the functional . The numerically computed current vector potential, , is fed to this solver by means of the input parameters dof_handler_T and solution_T. Moreover, this solver reuses the mesh on which has been computed. The reference to the mesh is passed via the input parameter triangulation_T.
 class Solver
 {
 public:
 Solver() = delete;
 Solver(constunsignedint p, // Degree of the Nedelec finite elements.
The following data members are typical for all deal.II simulations: triangulation, finite elements, dof handlers, etc. The constraints are used to enforce the Dirichlet boundary conditions. The names of the data members are self-explanatory.
This time we have two dof handlers, dof_handler_T for and dof_handler for . The WorkStream needs to walk through the two dof handlers synchronously. For this purpose we will pair two active cell iterators (one from dof_handler_T, another from dof_handler). For that we need the IteratorPair type.
 using IteratorTuple =
 std::tuple<typename DoFHandler<3>::active_cell_iterator,
The program utilizes the WorkStream technology. The step-9 tutorial does a much better job of explaining the workings of WorkStream. Reading the "WorkStream paper", see the glossary, is recommended. The following structures and functions are related to WorkStream.
This function initializes the dofs, applies the Dirichlet boundary condition, and initializes the vectors and matrices.
 void Solver::setup()
 {
 dof_handler.reinit(triangulation_T);
 dof_handler.distribute_dofs(fe);
Â
The following segment of the code applies the homogeneous Dirichlet boundary condition. As discussed in the introduction, the Dirichlet boundary condition is an essential condition and must be enforced by constraining the system matrix. This segment of code does the constraining.
Formally, this function assembles the system of linear equations. In reality, however, it just spells all the magic words to get the WorkStream going. The interesting part, i.e., the actual assembling of the system matrix and the right-hand side happens below in the Solver::system_matrix_local function. Note that this time the first two input parameters to WorkStream::run are pairs of iterators, not iterators themselves as per usual. Note also the order in which we package the iterators: first the iterator of dof_handler, then the iterator of the dof_handler_T. We will extract them in the same order.
 void Solver::assemble()
 {
 WorkStream::run(IteratorPair(IteratorTuple(dof_handler.begin_active(),
 dof_handler_T.begin_active())),
 IteratorPair(
 IteratorTuple(dof_handler.end(), dof_handler_T.end())),
 *this,
 &Solver::system_matrix_local,
 &Solver::copy_local_to_global,
 AssemblyScratchData(fe,
 dof_handler_T,
 solution_T,
 mapping_degree,
 eta_squared,
 Settings::boundary_condition_type_A),
 AssemblyCopyData());
 }
Â
The following two constructors initialize scratch data from the input parameters and from another object of the same type, i.e., a copy constructor.
 Solver::AssemblyScratchData::AssemblyScratchData(
This function assembles a fraction of the system matrix and the system right-hand side related to a single cell. These fractions are copy_data.cell_matrix and copy_data.cell_rhs. They are copied into to the system matrix, , and the right-hand side, , by the function Solver::copy_local_to_global().
 void Solver::system_matrix_local(const IteratorPair &IP,
First we reinitialize the matrices and vectors related to the current cell.
 copy_data.cell_matrix.reinit(scratch_data.dofs_per_cell,
 scratch_data.dofs_per_cell);
Â
 copy_data.cell_rhs.reinit(scratch_data.dofs_per_cell);
Â
 copy_data.local_dof_indices.resize(scratch_data.dofs_per_cell);
Â
Second, we extract the cells from the pair. We extract them in the correct order, see above.
 auto cell = std::get<0>(*IP);
 auto cell_T = std::get<1>(*IP);
Â
Third, we compute the ordered FE values, the permeability, and the values of the current vector potential, , on the cell.
 scratch_data.fe_values.reinit(cell);
 scratch_data.fe_values_T.reinit(cell_T);
Â
 scratch_data.permeability.value_list(cell->material_id(),
 scratch_data.permeability_list);
Â
 scratch_data.fe_values_T[scratch_data.ve].get_function_values(
 scratch_data.dofs_T, scratch_data.T_values);
Â
Fourth, we compute the components of the cell matrix and cell right-hand side. The labels of the integrals are the same as in the introduction to this tutorial.
 for (unsignedint q_index = 0; q_index < scratch_data.n_q_points; ++q_index)
 {
 for (unsignedint i = 0; i < scratch_data.dofs_per_cell; ++i)
 {
 for (unsignedint j = 0; j < scratch_data.dofs_per_cell; ++j)
 {
 copy_data.cell_matrix(i, j) += // Integral I_a1+I_a3.
 (1.0 / scratch_data.permeability_list[q_index]) * // 1 / mu
 (scratch_data.fe_values[scratch_data.ve].curl(
 i, q_index) * // curl N_i
 scratch_data.fe_values[scratch_data.ve].curl(
 j, q_index) // curl N_j
 + scratch_data.eta_squared * // eta^2
 scratch_data.fe_values[scratch_data.ve].value(
 i, q_index) * // N_i
 scratch_data.fe_values[scratch_data.ve].value(
 j, q_index) // N_j
 ) *
 scratch_data.fe_values.JxW(q_index); // dV
 }
 copy_data.cell_rhs(i) += // Integral I_b3-1.
 (scratch_data.T_values[q_index] *
 scratch_data.fe_values[scratch_data.ve].curl(i, q_index)) *
 scratch_data.fe_values.JxW(q_index); // T.(curl N_i)dV
 }
 }
Â
If the Robin boundary condition (first-order ABC) is ordered, we compute an extra integral over the boundary.
 if (scratch_data.boundary_condition_type == BoundaryConditionType::Robin)
 {
 for (unsignedint f = 0; f < cell->n_faces(); ++f)
 {
 if (cell->face(f)->at_boundary())
 {
 scratch_data.fe_face_values.reinit(cell, f);
Â
 for (unsignedint q_index_face = 0;
 q_index_face < scratch_data.n_q_points_face;
 ++q_index_face)
 {
 for (unsignedint i = 0; i < scratch_data.dofs_per_cell;
 ++i)
 {
 scratch_data.gamma.value_list(
 scratch_data.fe_face_values.get_quadrature_points(),
 scratch_data.gamma_list);
Â
 for (unsignedint j = 0; j < scratch_data.dofs_per_cell;
 ++j)
 {
 copy_data.cell_matrix(i, j) += // Integral I_a2.
 scratch_data.gamma_list[q_index_face] * // gamma
Finally, we query the dof indices on the current cell and store them in the copy data structure, so we know to which locations of the system matrix and right-hand side the components of the cell matrix and cell right-hand side must be copied.
 cell->get_dof_indices(copy_data.local_dof_indices);
 }
Â
This function copies the components of a cell matrix and a cell right-hand side into the system matrix, , and the system right-hand side, .
 constraints.distribute_local_to_global(copy_data.cell_matrix,
 copy_data.cell_rhs,
 copy_data.local_dof_indices,
 system_matrix,
 system_rhs);
 }
Â
This function solves the system of linear equations. If Settings::log_cg_convergence == true, the convergence data is saved into a file. In theory, a CG solver can solve an system of linear equations in at most steps. In practice, it can take more steps to converge. The convergence of the algorithm depends on the spectral properties of the system matrix. The best case is if the eigenvalues form a compact cluster away from zero. In our case, however, the eigenvalues are spread in between zero and the maximal eigenvalue. Consequently, we expect a poor convergence and increase the maximal number of iteration steps by a factor of 10, i.e., 10*system_rhs.size(). The stopping condition is
As soon as we use constraints, we must not forget to distribute them.
This name space contains all the code related to the conversion of the magnetic vector potential, , into magnetic field, . The magnetic vector potential is modeled by the FE_Nedelec finite elements, while the magnetic field is modeled by the FE_RaviartThomas finite elements. This code is also used for converting the current vector potential, into the free-current density, .
 namespace ProjectorHcurlToHdiv
 {
Â
This class implements the solver that minimizes the functional or , see the introduction. The input vector field, or , is fed to the solver by means of the input parameters dof_handler_Hcurl and solution_Hcurl. Moreover, this solver reuses the mesh on which the input vector field has been computed. The reference to the mesh is passed via the input parameter triangulation_Hcurl. There are no constraints this time around as we are not going to apply the Dirichlet boundary condition.
 class Solver
 {
 public:
 Solver() = delete;
 Solver(constunsignedint p, // Degree of the Raviart-Thomas finite elements
The following data members are typical for all deal.II simulations: triangulation, finite elements, dof handlers, etc. The constraints are used to enforce the Dirichlet boundary conditions. The names of the data members are self-explanatory.
This time we have two dof handlers, dof_handler_Hcurl for the input vector field and dof_handler_Hdiv for the output vector field. The WorkStream needs to walk through the two dof handlers synchronously. For this purpose we will pair two active cells iterators (one from dof_handler_Hcurl, another from dof_handler_Hdiv) to be walked through synchronously. For that we need the IteratorPair type.
 using IteratorTuple =
 std::tuple<typename DoFHandler<3>::active_cell_iterator,
The program utilizes the WorkStream technology. The step-9 tutorial does a much better job of explaining the workings of WarkStream. Reading the "WorkStream paper", see the glossary, is recommended. The following structures and functions are related to WorkStream.
 solution_Hdiv.reinit(dof_handler_Hdiv.n_dofs());
 system_rhs.reinit(dof_handler_Hdiv.n_dofs());
Â
 if (Settings::project_exact_solution && exact_solution)
 projected_exact_solution.reinit(dof_handler_Hdiv.n_dofs());
Â
 if (exact_solution)
 L2_per_cell.reinit(triangulation_Hcurl.n_active_cells());
 }
Â
Formally, this function assembles the system of linear equations. In reality, however, it just spells all the magic words to get the WorkStream going. The interesting part, i.e., the actual assembling of the system matrix and the right-hand side happens below in the Solver::system_matrix_local function.
 , n_q_points(fe_values_Hdiv.get_quadrature().size())
 , curl_vec_in_Hcurl(scratch_data.n_q_points)
 , ve(0)
 , dof_hand_Hcurl(scratch_data.dof_hand_Hcurl)
 , dofs_Hcurl(scratch_data.dofs_Hcurl)
 {}
Â
This function assembles a fraction of the system matrix and the system right-hand side related to a single cell. These fractions are copy_data.cell_matrix and copy_data.cell_rhs. They are copied into to the system matrix, , and the right-hand side, , by the function Solver::copy_local_to_global().
 void Solver::system_matrix_local(const IteratorPair &IP,
First we reinitialize the matrices and vectors related to the current cell, update the FE values, and compute the curl of the input vector field.
 copy_data.cell_matrix.reinit(scratch_data.dofs_per_cell,
 scratch_data.dofs_per_cell);
Â
 copy_data.cell_rhs.reinit(scratch_data.dofs_per_cell);
Â
 copy_data.local_dof_indices.resize(scratch_data.dofs_per_cell);
Â
 scratch_data.fe_values_Hdiv.reinit(std::get<0>(*IP));
 scratch_data.fe_values_Hcurl.reinit(std::get<1>(*IP));
Â
The variable curl_vec_in_Hcurl denotes the curl of the input vector field, or , depending on the context.
 scratch_data.fe_values_Hcurl[scratch_data.ve].get_function_curls(
 scratch_data.dofs_Hcurl, scratch_data.curl_vec_in_Hcurl);
Â
Second, we compute the components of the cell matrix and cell right-hand side. The labels of the integrals are the same as in the introduction to this tutorial.
 for (unsignedint q_index = 0; q_index < scratch_data.n_q_points; ++q_index)
 {
 for (unsignedint i = 0; i < scratch_data.dofs_per_cell; ++i)
 {
 for (unsignedint j = 0; j < scratch_data.dofs_per_cell; ++j)
 {
 copy_data.cell_matrix(i, j) += // Integral I_a
 scratch_data.fe_values_Hdiv[scratch_data.ve].value(i,
 q_index) *
 scratch_data.fe_values_Hdiv[scratch_data.ve].value(j,
 q_index) *
 scratch_data.fe_values_Hdiv.JxW(q_index);
 }
Â
 copy_data.cell_rhs(i) += // Integral I_b
 scratch_data.curl_vec_in_Hcurl[q_index] *
 scratch_data.fe_values_Hdiv[scratch_data.ve].value(i, q_index) *
 scratch_data.fe_values_Hdiv.JxW(q_index);
 }
 }
Â
Finally, we query the dof indices on the current cell and store them in the copy data structure, so we know to which locations of the system matrix and right-hand side the components of the cell matrix and cell right-hand side must be copied.
 std::get<0>(*IP)->get_dof_indices(copy_data.local_dof_indices);
 }
Â
This function copies the components of a cell matrix and a cell right-hand side into the system matrix, , and the system right-hand side, .
This function solves the system of linear equations. This time we are dealing with a mass matrix. It has good spectral properties. Consequently, we do not use the factor of 10 as in preceding two solvers. The stopping condition is
 cg.solve(system_matrix, solution_Hdiv, system_rhs, preconditioner);
Â
 if (Settings::log_cg_convergence)
 {
 const std::vector<double> history_data = control.get_history_data();
Â
 std::ofstream ofs(fname + "_cg_convergence.csv");
Â
 for (unsignedint i = 1; i < history_data.size(); i++)
 ofs << i << ", " << history_data[i] << "\n";
 }
 }
Â
This function saves the computed fields into a vtu file. This time we also save the projected exact solution and the error norm. The exact solution is only saved if the Settings::project_exact_solution = true
 void Solver::save() const
 {
 std::vector<std::string> solution_names(3, "VectorField");
 std::vector<DataComponentInterpretation::DataComponentInterpretation>
 "T_p" + std::to_string(Settings::fe_degree) + "_r" +
 std::to_string(r));
Â
 T.run();
Â
Stage 2. Computing .
 std::cout << "Jf " << std::flush;
Â
 ExactSolutions::FreeCurrentDensity Jf_exact;
Â
 ProjectorHcurlToHdiv::Solver Jf(Settings::fe_degree,
 Settings::mapping_degree,
 T.get_tria(),
 T.get_dof_handler(),
 T.get_solution(),
 "Jf_p" +
 std::to_string(Settings::fe_degree) +
 "_r" + std::to_string(r),
 &Jf_exact);
Â
 Jf.run();
Â
 table_Jf.add_value("ndofs", Jf.get_n_dofs());
 table_Jf.add_value("ncells", Jf.get_n_cells());
 table_Jf.add_value("L2", Jf.get_L2_norm());
Â
Stage 3. Computing .
 std::cout << "A " << std::flush;
Â
 SolverA::Solver A(Settings::fe_degree,
 Settings::mapping_degree,
 T.get_tria(),
 T.get_dof_handler(),
 T.get_solution(),
 Settings::eta_squared_A,
 "A_p" + std::to_string(Settings::fe_degree) + "_r" +
 std::to_string(r));
Â
 A.run();
Â
Stage 4. Computing .
 std::cout << "B " << std::flush;
Â
 ExactSolutions::MagneticField B_exact;
Â
 ProjectorHcurlToHdiv::Solver B(Settings::fe_degree,
 Settings::mapping_degree,
 T.get_tria(),
 A.get_dof_handler(),
 A.get_solution(),
 "B_p" +
 std::to_string(Settings::fe_degree) +
 "_r" + std::to_string(r),
 &B_exact);
 B.run();
Â
 table_B.add_value("ndofs", B.get_n_dofs());
 table_B.add_value("ncells", B.get_n_cells());
 table_B.add_value("L2", B.get_L2_norm());
End stage 4.
 table_Jf.save("table_Jf_p" + std::to_string(Settings::fe_degree));
 table_B.save("table_B_p" + std::to_string(Settings::fe_degree));
 }
 std::cout << std::endl;
 }
 };
Â
 int main()
 {
 try
 {
 MagneticProblem problem;
 problem.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
The program generates the following output in the command line interface by default.
Solving for (p = 0): T Jf A B T Jf A B T Jf A B T Jf A B
The program assumes the finite elements of the lowermost degree, . To change the degree of the finite elements, say , one needs to change the setting Settings::fe_degree = 2 and rebuild the program.
The program also dumps a number of files in the current directory. In the default configuration these files are:
vtu files. They contain the computed vector fields. Recall that the spherical manifold is attached to many cell faces. Consequently, these cell faces are curved. They look more like patches of a sphere. Furthermore, the shape functions are mapped from the reference cell to the real mesh cells by the second-order mapping to accommodate the cells with curved faces. For these reasons, one needs to use a visualization software that can deal with curved faces and the higher-order mapping. A fresh version of ParaView is recommended. Visit will not do. The Notes on visualizing high order output provide more information on this topic.
tex files. These files contain the convergence tables.
The following provides examples of the convergence tables simulated with the default settings for three different degrees of the finite elements, .
The following notations were used in the headers of the tables:
p - the degree of the finite elements.
r - the mesh refinement parameter, i.e., the number of nodes on the transfinite lines.
cells - the total amount of active cells.
dofs - the amount of degrees of freedom.
- the error norm.
- the order of convergence of the error norm.
If Settings::log_cg_convergence = true, the program saves the convergence data of the CG solver into csv files.
The vector representations of the calculated vector fields, and , are illustrated above by the first figure on this page. The figures below illustrate slices of the magnitudes of these fields. The figures below were simulated with and . Visual inspection of the vector potentials is not very informative as their conservative portions are unknown.
Possibilities for extensions
Repeat the simulations for the three types of the boundary conditions, Dirichlet, Neumann, and Robin. The Robin boundary condition is supposed to be superior to the other two. Look at the simulated data to see that this is indeed the case. You can save the projected exact solution next to the simulated solutions into the vtu files, just set Settings::project_exact_solution = true. ParaView has "Plot Over Line" filter. You can use this filter to visualize the difference between the exact solution and a solution simulated with a particular boundary condition. You can also draw conclusions by observing the convergence tables. Keep in mind the parameter. Increase it if the CG solver chokes while you are experimenting. Note that the benefits offered by the Robin boundary condition are observed the best when higher-order finite elements are used, i.e., and .
The Robin boundary condition as described above is also called the first-order asymptotic boundary condition (ABC). There exist ABCs of higher orders [gratkowski2010p]. Implement and test the second-order ABC to see if it performs any better. There exist improvised asymptotic boundary conditions, IABCs, [meeker2013a]. Try to implement the first order IABC.