397 *
double return_value = 0.0;
398 *
for (
unsigned int i = 0; i < dim; ++i)
399 *
return_value += 4.0 *
std::pow(p[i], 4.0);
401 *
return return_value;
407 * As boundary
values, we choose @f$x^2+y^2@f$ in 2
d, and @f$x^2+y^2+z^2@f$ in 3
d. This
408 * happens to be
equal to the square of the vector from the origin to the
409 *
point at which we would like to evaluate the function, irrespective of the
410 * dimension. So that is what we
return:
414 *
double BoundaryValues<dim>::value(
const Point<dim> &p,
415 *
const unsigned int )
const
425 * <a name=
"step_4-ImplementationofthecodeStep4codeclass"></a>
426 * <h3>Implementation of the <code>Step4</code>
class</h3>
430 * Next
for the implementation of the
class template that makes use of the
431 *
functions above. As before, we will write everything as templates that have
432 * a formal parameter <code>dim</code> that we assume unknown at the time we
433 * define the
template functions. Only later, the compiler will find a
434 * declaration of <code>Step4@<2@></code> (in the <code>
main</code> function,
435 * actually) and compile the entire
class with <code>dim</code> replaced by 2,
436 * a process referred to as
"instantiation of a template". When doing so, it
437 * will also replace instances of <code>RightHandSide@<dim@></code> by
438 * <code>RightHandSide@<2@></code> and instantiate the latter
class from the
443 * In fact, the compiler will also find a declaration <code>Step4@<3@></code>
444 * in <code>
main()</code>. This will cause it to again go back to the
general
445 * <code>Step4@<dim@></code>
template, replace all occurrences of
446 * <code>dim</code>,
this time by 3, and compile the
class a
second time. Note
447 * that the two instantiations <code>Step4@<2@></code> and
448 * <code>Step4@<3@></code> are completely
independent classes; their only
449 * common feature is that they are both instantiated from the same
general
450 *
template, but they are not convertible into each other,
for example, and
451 * share no code (both instantiations are compiled completely independently).
459 * <a name=
"step_4-Step4Step4"></a>
460 * <h4>Step4::Step4</h4>
464 * After
this introduction, here is the constructor of the <code>Step4</code>
465 *
class. It specifies the desired polynomial degree of the finite elements
466 * and associates the
DoFHandler to the triangulation just as in the previous
467 * example program, @ref step_3
"step-3":
471 *
Step4<dim>::Step4()
473 *
, dof_handler(triangulation)
480 * <a name=
"step_4-Step4make_grid"></a>
481 * <h4>Step4::make_grid</h4>
485 * Grid creation is something inherently dimension dependent. However, as
long
486 * as the domains are sufficiently similar in 2
d or 3
d, the library can
487 * abstract
for you. In our
case, we would like to again solve on the square
488 * @f$[-1,1]\times [-1,1]@f$ in 2
d, or on the cube @f$[-1,1] \times [-1,1] \times
490 * use the same function in whatever dimension we are. Of course, the
491 *
functions that create a hypercube in two and three dimensions are very much
492 * different, but that is something you need not care about. Let the library
493 * handle the difficult things.
497 *
void Step4<dim>::make_grid()
500 *
triangulation.refine_global(4);
502 *
std::cout <<
" Number of active cells: " << triangulation.n_active_cells()
504 *
<<
" Total number of cells: " << triangulation.n_cells()
511 * <a name=
"step_4-Step4setup_system"></a>
512 * <h4>Step4::setup_system</h4>
516 * This function looks exactly like in the previous example, although it
517 * performs actions that in their details are quite different
if
518 * <code>dim</code> happens to be 3. The only significant difference from a
519 * user
's perspective is the number of cells resulting, which is much higher
520 * in three than in two space dimensions!
524 * void Step4<dim>::setup_system()
526 * dof_handler.distribute_dofs(fe);
528 * std::cout << " Number of degrees of freedom: " << dof_handler.n_dofs()
531 * DynamicSparsityPattern dsp(dof_handler.n_dofs());
532 * DoFTools::make_sparsity_pattern(dof_handler, dsp);
533 * sparsity_pattern.copy_from(dsp);
535 * system_matrix.reinit(sparsity_pattern);
537 * solution.reinit(dof_handler.n_dofs());
538 * system_rhs.reinit(dof_handler.n_dofs());
545 * <a name="step_4-Step4assemble_system"></a>
546 * <h4>Step4::assemble_system</h4>
550 * Unlike in the previous example, we would now like to use a non-constant
551 * right hand side function and non-zero boundary values. Both are tasks that
552 * are readily achieved with only a few new lines of code in the assemblage of
553 * the matrix and right hand side.
557 * More interesting, though, is the way we assemble matrix and right hand side
558 * vector dimension independently: there is simply no difference to the
559 * two-dimensional case. Since the important objects used in this function
560 * (quadrature formula, FEValues) depend on the dimension by way of a template
561 * parameter as well, they can take care of setting up properly everything for
562 * the dimension for which this function is compiled. By declaring all classes
563 * which might depend on the dimension using a template parameter, the library
564 * can make nearly all work for you and you don't have to care about most
569 *
void Step4<dim>::assemble_system()
571 *
const QGauss<dim> quadrature_formula(fe.degree + 1);
575 * We wanted to have a non-
constant right hand side, so we use an
object of
576 * the
class declared above to generate the necessary
data. Since this right
577 * hand side
object is only used locally in the present function, we declare
578 * it here as a local variable:
581 *
RightHandSide<dim> right_hand_side;
585 * Compared to the previous example, in order to evaluate the non-
constant
586 * right hand side function we now also need the quadrature points on the
587 * cell we are presently on (previously, we only required values and
588 * gradients of the shape function from the
FEValues object, as well as the
594 *
quadrature_formula,
600 * We then again define the same abbreviation as in the previous program.
601 * The
value of
this variable of course depends on the dimension which we
602 * are presently
using, but the
FiniteElement class does all the necessary
603 * work
for you and you don
't have to care about the dimension dependent
607 * const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
609 * FullMatrix<double> cell_matrix(dofs_per_cell, dofs_per_cell);
610 * Vector<double> cell_rhs(dofs_per_cell);
612 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
616 * Next, we again have to loop over all cells and assemble local
617 * contributions. Note, that a cell is a quadrilateral in two space
618 * dimensions, but a hexahedron in 3d. In fact, the
619 * <code>active_cell_iterator</code> data type is something different,
620 * depending on the dimension we are in, but to the outside world they look
621 * alike and you will probably never see a difference. In any case, the real
622 * type is hidden by using `auto`:
625 * for (const auto &cell : dof_handler.active_cell_iterators())
627 * fe_values.reinit(cell);
634 * Now we have to assemble the local matrix and right hand side. This is
635 * done exactly like in the previous example, but now we revert the
636 * order of the loops (which we can safely do since they are independent
637 * of each other) and merge the loops for the local matrix and the local
638 * vector as far as possible to make things a bit faster.
642 * Assembling the right hand side presents the only significant
643 * difference to how we did things in @ref step_3 "step-3": Instead of using a
644 * constant right hand side with value 1, we use the object representing
645 * the right hand side and evaluate it at the quadrature points:
648 * for (const unsigned int q_index : fe_values.quadrature_point_indices())
649 * for (const unsigned int i : fe_values.dof_indices())
651 * for (const unsigned int j : fe_values.dof_indices())
652 * cell_matrix(i, j) +=
653 * (fe_values.shape_grad(i, q_index) * // grad phi_i(x_q)
654 * fe_values.shape_grad(j, q_index) * // grad phi_j(x_q)
655 * fe_values.JxW(q_index)); // dx
657 * const auto &x_q = fe_values.quadrature_point(q_index);
658 * cell_rhs(i) += (fe_values.shape_value(i, q_index) * // phi_i(x_q)
659 * right_hand_side.value(x_q) * // f(x_q)
660 * fe_values.JxW(q_index)); // dx
664 * As a final remark to these loops: when we assemble the local
665 * contributions into <code>cell_matrix(i,j)</code>, we have to multiply
666 * the gradients of shape functions @f$i@f$ and @f$j@f$ at point number
668 * multiply it with the scalar weights JxW. This is what actually
669 * happens: <code>fe_values.shape_grad(i,q_index)</code> returns a
670 * <code>dim</code> dimensional vector, represented by a
671 * <code>Tensor@<1,dim@></code> object, and the operator* that
672 * multiplies it with the result of
673 * <code>fe_values.shape_grad(j,q_index)</code> makes sure that the
674 * <code>dim</code> components of the two vectors are properly
675 * contracted, and the result is a scalar floating point number that
676 * then is multiplied with the weights. Internally, this operator* makes
677 * sure that this happens correctly for all <code>dim</code> components
678 * of the vectors, whether <code>dim</code> be 2, 3, or any other space
679 * dimension; from a user's perspective,
this is not something worth
680 * bothering with, however, making things a lot simpler
if one wants to
681 * write code dimension independently.
685 * With the local systems assembled, the transfer into the global
matrix
686 * and right hand side is done exactly as before, but here we have again
687 * merged some loops
for efficiency:
690 *
cell->get_dof_indices(local_dof_indices);
691 *
for (
const unsigned int i : fe_values.dof_indices())
693 *
for (
const unsigned
int j : fe_values.dof_indices())
694 *
system_matrix.add(local_dof_indices[i],
695 *
local_dof_indices[j],
698 *
system_rhs(local_dof_indices[i]) += cell_rhs(i);
704 * As the
final step in
this function, we wanted to have non-homogeneous
705 * boundary
values in
this example, unlike the
one before. This is a simple
707 *
object of the
class which describes the boundary
values we would like to
708 * use (i.e. the <code>BoundaryValues</code>
class declared above):
713 * on faces that have been marked with boundary indicator 0 (because that
's
714 * what we say the function should work on with the second argument below).
715 * If there are faces with boundary id other than 0, then the function
716 * interpolate_boundary_values() will do nothing on these faces. For
717 * the Laplace equation doing nothing is equivalent to assuming that
718 * on those parts of the boundary a zero Neumann boundary condition holds.
721 * std::map<types::global_dof_index, double> boundary_values;
722 * VectorTools::interpolate_boundary_values(dof_handler,
723 * types::boundary_id(0),
724 * BoundaryValues<dim>(),
726 * MatrixTools::apply_boundary_values(boundary_values,
736 * <a name="step_4-Step4solve"></a>
737 * <h4>Step4::solve</h4>
741 * Solving the linear system of equations is something that looks almost
742 * identical in most programs. In particular, it is dimension independent, so
743 * this function is copied verbatim from the previous example.
747 * void Step4<dim>::solve()
749 * SolverControl solver_control(1000, 1e-6 * system_rhs.l2_norm());
750 * SolverCG<Vector<double>> solver(solver_control);
751 * solver.solve(system_matrix, solution, system_rhs, PreconditionIdentity());
753 * std::cout << " " << solver_control.last_step()
754 * << " CG iterations needed to obtain convergence." << std::endl;
761 * <a name="step_4-Step4output_results"></a>
762 * <h4>Step4::output_results</h4>
766 * This function also does what the respective one did in @ref step_3 "step-3". No changes
767 * here for dimension independence either.
771 * Since the program will run both 2d and 3d versions of the Laplace solver,
772 * we use the dimension in the filename to generate distinct filenames for
773 * each run (in a better program, one would check whether <code>dim</code> can
774 * have other values than 2 or 3, but we neglect this here for the sake of
779 * void Step4<dim>::output_results() const
781 * DataOut<dim> data_out;
783 * data_out.attach_dof_handler(dof_handler);
784 * data_out.add_data_vector(solution, "solution");
786 * data_out.build_patches();
788 * std::ofstream output(dim == 2 ? "solution-2d.vtk" : "solution-3d.vtk");
789 * data_out.write_vtk(output);
797 * <a name="step_4-Step4run"></a>
798 * <h4>Step4::run</h4>
802 * This is the function which has the top-level control over everything. Apart
803 * from one line of additional output, it is the same as for the previous
808 * void Step4<dim>::run()
810 * std::cout << "Solving problem in " << dim << " space dimensions."
824 * <a name="step_4-Thecodemaincodefunction"></a>
825 * <h3>The <code>main</code> function</h3>
829 * And this is the main function. It also looks mostly like in @ref step_3 "step-3", but if
830 * you look at the code below, note how we first create a variable of type
831 * <code>Step4@<2@></code> (forcing the compiler to compile the class template
832 * with <code>dim</code> replaced by <code>2</code>) and run a 2d simulation,
833 * and then we do the whole thing over in 3d.
837 * In practice, this is probably not what you would do very frequently (you
838 * probably either want to solve a 2d problem, or one in 3d, but not both at
839 * the same time). However, it demonstrates the mechanism by which we can
840 * simply change which dimension we want in a single place, and thereby force
841 * the compiler to recompile the dimension independent class templates for the
842 * dimension we request. The emphasis here lies on the fact that we only need
843 * to change a single place. This makes it rather trivial to debug the program
844 * in 2d where computations are fast, and then switch a single place to a 3 to
845 * run the much more computing intensive program in 3d for "real"
850 * Each of the two blocks is enclosed in braces to make sure that the
851 * <code>laplace_problem_2d</code> variable goes out of scope (and releases
852 * the memory it holds) before we move on to allocate memory for the 3d
853 * case. Without the additional braces, the <code>laplace_problem_2d</code>
854 * variable would only be destroyed at the end of the function, i.e. after
855 * running the 3d problem, and would needlessly hog memory while the 3d run
856 * could actually use it.
862 * Step4<2> laplace_problem_2d;
863 * laplace_problem_2d.run();
867 * Step4<3> laplace_problem_3d;
868 * laplace_problem_3d.run();
874<a name="step_4-Results"></a><h1>Results</h1>
878The output of the program looks as follows (the number of iterations
879may vary by one or two, depending on your computer, since this is
880often dependent on the round-off accuracy of floating point
881operations, which differs between processors):
883Solving problem in 2 space dimensions.
884 Number of active cells: 256
885 Total number of cells: 341
886 Number of degrees of freedom: 289
887 19 CG iterations needed to obtain convergence.
888Solving problem in 3 space dimensions.
889 Number of active cells: 4096
890 Total number of cells: 4681
891 Number of degrees of freedom: 4913
892 20 CG iterations needed to obtain convergence.
894It is obvious that in three spatial dimensions the number of cells and
895therefore also the number of degrees of freedom is
896much higher. What cannot be seen here, is that besides this higher
897number of rows and columns in the matrix, there are also significantly
898more entries per row of the matrix in three space
899dimensions. Together, this leads to a much higher numerical effort for
900solving the system of equation, which you can feel in the run time of the two
901solution steps when you actually run the program.
905The program produces two files: <code>solution-2d.vtk</code> and
906<code>solution-3d.vtk</code>, which can be viewed using the programs
907VisIt or Paraview (in case you do not have these programs, you can easily
909output format in the program to something which you can view more
910easily). Visualizing solutions is a bit of an art, but it can also be fun, so
911you should play around with your favorite visualization tool to get familiar
912with its functionality. Here's what I have come up with
for the 2
d solution:
915 <img src=
"https://dealii.org/images/steps/developer/step-4.solution-2d.png" alt=
"">
918(See also <a href=
"https://www.math.colostate.edu/~bangerth/videos.676.11.html">video lecture 11</a>, <a href=
"https://www.math.colostate.edu/~bangerth/videos.676.32.html">video lecture 32</a>.)
919The picture shows the solution of the problem under consideration as
920a 3D plot. As can be seen, the solution is almost flat in the interior
921of the domain and has a higher curvature near the boundary. This, of
922course, is due to the fact that
for Laplace
's equation the curvature
923of the solution is equal to the right hand side and that was chosen as
924a quartic polynomial which is nearly zero in the interior and is only
925rising sharply when approaching the boundaries of the domain; the
926maximal values of the right hand side function are at the corners of
927the domain, where also the solution is moving most rapidly.
928It is also nice to see that the solution follows the desired quadratic
929boundary values along the boundaries of the domain.
930It can also be useful to verify a computed solution against an analytical
931solution. For an explanation of this technique, see @ref step_7 "step-7".
933On the other hand, even though the picture does not show the mesh lines
934explicitly, you can see them as little kinks in the solution. This clearly
935indicates that the solution hasn't been computed to very high accuracy and
936that to get a better solution, we may have to compute on a finer mesh.
938In three spatial dimensions, visualization is a bit more difficult. The left
939picture shows the solution and the mesh it was computed on on the surface of
940the domain. This is nice, but it has the drawback that it completely hides
941what is happening on the
inside. The picture on the right is an attempt at
942visualizing the interior as well, by showing surfaces where the solution has
943constant values (as indicated by the legend at the top left). Isosurface
944pictures look best
if one makes the individual surfaces slightly transparent
945so that it is possible to see through them and see what
's behind.
947<table width="60%" align="center">
950 <img src="https://dealii.org/images/steps/developer/step-4.solution-3d.png" alt="">
953 <img src="https://dealii.org/images/steps/developer/step-4.contours-3d.png" alt="">
959A final remark on visualization: the idea of visualization is to give insight,
960which is not the same as displaying information. In particular, it is easy to
961overload a picture with information, but while it shows more information it
962makes it also more difficult to glean insight. As an example, the program I
963used to generate these pictures, VisIt, by default puts tick marks on every
964axis, puts a big fat label "X Axis" on the @f$x@f$ axis and similar for the other
965axes, shows the file name from which the data was taken in the top left and
966the name of the user doing so and the time and date on the bottom right. None
968here: the axes are equally easy to make out because the tripod at the bottom
969left is still visible, and we know from the program that the domain is
970@f$[-1,1]^3@f$, so there is no need for tick marks. As a consequence, I have
971switched off all the extraneous stuff in the picture: the art of visualization
972is to reduce the picture to those parts that are important to see what one
973wants to see, but no more.
977<a name="step-4-postprocessing"></a>
978<a name="step_4-PostprocessingWhattodowiththesolution"></a><h3> Postprocessing: What to do with the solution? </h3>
981This tutorial -- like most of the other programs -- principally only shows how
982to numerically approximate the solution of a partial differential equation, and
983then how to visualize this solution graphically. But
984solving a PDE is of course not the goal in most practical applications (unless
985you are a numerical methods developer and the *method* is the goal): We generally
986want to solve a PDE because we want to *extract information* from it. Examples
987for what people are interested in from solutions include the following:
988- Let's say you solve the equations of elasticity (which we will do in @ref step_8
"step-8"),
989 then that
's presumably because you want to know about the deformation of an
990 elastic object under a given load. From an engineering perspective, what you
991 then presumably want to learn is the degree of deformation of the object,
992 say at a specific point; or you may want to know the maximum
993 [stress](https://en.wikipedia.org/wiki/Stress_(mechanics)) in order to
994 determine whether the applied load exceeds the safe maximal stress the
995 material can withstand.
996- If you are solving fluid flow problems (such as in @ref step_22 "step-22", @ref step_57 "step-57", @ref step_67 "step-67",
997 and @ref step_69 "step-69"), then you might be interested in the fluid velocity at specific
998 points, and oftentimes the forces the fluid exerts on the boundary of the
999 fluid domain. The latter is important in many applications: If the fluid
1000 in question is the air flowing around an airplane, then we are typically
1001 interested in the drag and lift forces on the fuselage and wings. If the
1002 fluid is water flowing around a ship, then we typically care about
1003 the drag force on the ship.
1004- If you are solving the Maxwell equations of electromagnetics, you are
1005 typically interested in how much energy is radiated in certain directions
1006 (say, in order to know the range of information transmission via an
1007 antenna, or to determine the
1008 [radar cross section](https://en.wikipedia.org/wiki/Radar_cross_section)
1009 of planes or ships).
1011The point here is that from an engineering perspective, *solving* the
1012PDE is only the first step. The second step is to *evaluate* the computed
1013solution in order to extract relevant numbers that allow us to
1014either *optimize a design*, or to *make decisions*. This second step is often
1015called "postprocessing the solution".
1017This program does not solve a solid or fluid mechanics problem, so we
1018should try to illustrate postprocessing with something that makes sense
1019in the context of the equation we solve here. The Poisson equation in two
1020space dimensions is a model for the vertical deformation of a membrane
1021that is clamped at the boundary and is subject to a vertical force. For this
1022kind of situation, it makes sense to evaluate the *average vertical
1025 \bar u_h = \frac{\int_\Omega u_h(\mathbf x) \, dx}{|\Omega|},
1027where @f$|\Omega| = \int_\Omega 1 \, dx@f$ is the area of the domain. To compute
1028@f$\bar u_h@f$, as usual we replace integrals over the domain by a sum of integrals
1031 \int_\Omega u_h(\mathbf x) \, dx
1033 \sum_K \int_K u_h(\mathbf x) \, dx,
1035and then integrals over cells are approximated by quadrature:
1037 \int_\Omega u_h(\mathbf x) \, dx
1039 \sum_K \int_K u_h(\mathbf x) \, dx,
1042 \sum_K \sum_q u_h(\mathbf x_q^K) w_q^K,
1044where @f$w_q^K@f$ is the weight of the @f$q@f$th quadrature point evaluated on
1045cell @f$K@f$. All of this is as always provided by the FEValues class -- the
1046entry point for all integrals in deal.II.
1048The actual implementation of this is straightforward once you know how
1049to get the values of the solution @f$u@f$ at the quadrature points of a cell.
1050This functionality is provided by FEValues::get_function_values(), a
1051function that takes a global vector of nodal values as input and returns
1052a vector of function values at the quadrature points of the current cell.
1053Using this function, to see how it all works together you can
1054place the following code snippet anywhere in the program after the
1055solution has been computed (the `output_results()` function seems like a good
1056place to also do postprocessing, for example):
1058 QGauss<dim> quadrature_formula(fe.degree + 1);
1059 FEValues<dim> fe_values(fe,
1061 update_values | update_JxW_values);
1063 std::vector<double> solution_values(quadrature_formula.size());
1064 double integral_of_u = 0;
1065 double volume_of_omega = 0;
1067 for (const auto &cell : dof_handler.active_cell_iterators())
1069 fe_values.reinit(cell);
1070 fe_values.get_function_values(solution, solution_values);
1072 for (const unsigned int q_point : fe_values.quadrature_point_indices())
1074 integral_of_u += solution_values[q_point] * fe_values.JxW(q_point);
1075 volume_of_omega += 1 * fe_values.JxW(q_point);
1078 std::cout << " Mean value of u=" << integral_of_u / volume_of_omega
1081In this code snippet, we also compute the volume (or, since we are currently
1082thinking about a two-dimensional situation: the area) @f$|\Omega|@f$ by computing
1083the integral @f$|\Omega| = \int_\Omega 1 \, dx@f$ in exactly the same way, via
1084quadrature. (We could avoid having to compute @f$|\Omega|@f$ by hand here, using the
1085fact that deal.II has a function for this: GridTools::volume(). That said,
1086it is efficient to compute the two integrals
1087concurrently in the same loop, and so that's what we do.)
1089This program of course also solves the same Poisson equation in three space
1090dimensions. In this situation, the Poisson equation is often used as a model
1091for diffusion of either a physical species (say, of ink in a tank of water,
1092or a pollutant in the air) or of energy (specifically, of thermal energy in
1093a solid body). In that context, the quantity
1095 \Phi_h = \int_{\partial\Omega} \nabla u_h(\mathbf x) \cdot \mathbf n(\mathbf x) \;
dx
1097is the *flux* of
this species or energy across the boundary. (In actual
1098physical models,
one would also have to multiply the right hand side by
1099a diffusivity or conductivity
constant, but let us ignore
this here.) In
1100much the same way as before, we compute such integrals by splitting
1101it over integrals of *faces* of cells, and then applying quadrature:
1105 \int_{\partial\Omega} \nabla u_h(\mathbf x) \cdot \mathbf n(\mathbf x) \;
dx
1109 \sum_{f \in \text{faces of @f$K@f$}, f\subset\partial\Omega}
1110 \int_f \nabla u_h(\mathbf x) \cdot \mathbf n(\mathbf x) \;
dx
1114 \sum_{f \in \text{faces of @f$K@f$}, f\subset\partial\Omega}
1115 \sum_q \nabla u_h(\mathbf x_q^f) \cdot \mathbf n(\mathbf x_q^f) w_q^f,
1117where now @f$\mathbf x_q^f@f$ are the quadrature points located on face @f$f@f$,
1118and @f$w_q^f@f$ are the weights associated with these faces. The
second
1119of the
sum symbols loops over all faces of cell @f$K@f$, but restricted to
1120those that are actually at the boundary.
1122This all is easily implemented by the following code that replaces the use of the
1123FEValues class (which is used
for integrating over cells -- i.e., domain integrals)
1124by the
FEFaceValues class (which is used
for integrating over faces -- i.e.,
1127 QGauss<dim - 1> face_quadrature_formula(fe.degree + 1);
1129 face_quadrature_formula,
1133 std::vector<Tensor<1, dim>> solution_gradients(face_quadrature_formula.size());
1136 for (
const auto &cell : dof_handler.active_cell_iterators())
1137 for (
const auto &face : cell->face_iterators())
1138 if (face->at_boundary())
1140 fe_face_values.reinit(cell, face);
1141 fe_face_values.get_function_gradients(solution, solution_gradients);
1143 for (
const unsigned int q_point :
1144 fe_face_values.quadrature_point_indices())
1146 flux += solution_gradients[q_point] *
1147 fe_face_values.normal_vector(q_point) *
1148 fe_face_values.JxW(q_point);
1151 std::cout <<
" Flux=" << flux << std::endl;
1154If you add these two code snippets to the code, you will get output like the
1155following when you
run the program:
1157Solving problem in 2 space dimensions.
1158 Number of active cells: 256
1159 Total number of cells: 341
1160 Number of degrees of freedom: 289
1161 26 CG iterations needed to obtain convergence.
1162 Mean
value of u=1.33303
1164Solving problem in 3 space dimensions.
1165 Number of active cells: 4096
1166 Total number of cells: 4681
1167 Number of degrees of freedom: 4913
1168 30 CG iterations needed to obtain convergence.
1169 Mean
value of u=1.58058
1173This makes some sense: If you look,
for example, at the 2
d output above,
1174the solution varies between
values of 1 and 2, but with a larger part of the
1176is reasonable. For the flux, recall that @f$\nabla u \cdot \mathbf n@f$ is the
1177directional derivative in the normal direction -- in other words, how the
1178solution changes as we move from the interior of the domain towards the
1179boundary. If you look at the 2
d solution, you will realize that
for most parts
1180of the boundary, the solution *decreases* as we approach the boundary, so the
1181normal derivative is
negative -- so
if we integrate along the boundary, we
1182should expect (and obtain!) a negative value.
1186<a name="step-4-extensions"></a>
1187<a name="step_4-Possibilitiesforextensions"></a><h3>Possibilities
for extensions</h3>
1190There are many ways with which one can play with this program. The simpler
1191ones include essentially all the possibilities already discussed in the
1192@ref step_3-Extensions "Possibilities
for extensions" in the documentation of
1193@ref step_3 "step-3", except that you will have to think about whether something now also
1194applies to the 3d case discussed in the current program.
1196It is also worthwhile considering the postprocessing options discussed
1197above. The documentation states two
numbers (the mean value and the
1198normal flux)
for both the 2d and 3d cases. Can we trust these
1199numbers? We have convinced ourselves that at least the mean value
1200is reasonable, and that the sign of the flux is probably correct.
1201But are these
numbers accurate?
1203A general rule is that we should never trust a number unless we have
1204verified it in some way. From the theory of finite element methods,
1205we know that as we make the mesh finer and finer, the numerical
1206solution @f$u_h@f$ we compute here must converge to the exact solution
1207@f$u@f$. As a consequence, we also expect that @f$\bar u_h \rightarrow \bar u@f$
1208and @f$\Phi_h \rightarrow \Phi@f$, but that does not mean that
for any
1209given mesh @f$\bar u_h@f$ or @f$\Phi_h@f$ are particularly accurate approximations.
1211To test this kind of thing, we have already considered the convergence of
1212a point value in @ref step_3 "step-3". We can do the same here by selecting how many
1213times the mesh is globally refined in the `make_grid()` function of this
1214program. For the mean value of the solution, we then get the following
1216 <table align="center" class="doxtable">
1217 <tr> <th>
# of refinements</th>
1218 <th>@f$\bar u_h@f$ in 2
d</th>
1219 <th>@f$\bar u_h@f$ in 3
d</th>
1221 <tr> <td>4</td> <td>1.33303</td> <td>1.58058</td> </tr>
1222 <tr> <td>5</td> <td>1.33276</td> <td>1.57947</td> </tr>
1223 <tr> <td>6</td> <td>1.3327</td> <td>1.5792</td> </tr>
1224 <tr> <td>7</td> <td>1.33269</td> <td>1.57914</td> </tr>
1225 <tr> <td>8</td> <td>1.33268</td> <td></td> </tr>
1226 <tr> <td>9</td> <td>1.33268</td> <td></td> </tr>
1228I did not have the patience to
run the last two
values for the 3
d case --
1229one needs quite a fine mesh
for this, with correspondingly
long run times.
1230But we can be reasonably assured that
values around 1.33 (
for the 2
d case)
1231and 1.58 (
for the 3d
case) are about right -- and at least
for engineering
1232applications, three digits of accuracy are good enough.
1234The situation looks very different
for the flux. Here, we get results
1235such as the following:
1236 <table align=
"center" class=
"doxtable">
1237 <tr> <th># of refinements</th>
1238 <th>@f$\Phi_h@f$ in 2
d</th>
1239 <th>@f$\Phi_h@f$ in 3
d</th>
1241 <tr> <td>4</td> <td>-3.68956</td> <td>-8.29435</td> </tr>
1242 <tr> <td>5</td> <td>-4.90147</td> <td>-13.0691</td> </tr>
1243 <tr> <td>6</td> <td>-5.60745</td> <td>-15.9171</td> </tr>
1244 <tr> <td>7</td> <td>-5.99111</td> <td>-17.4918</td> </tr>
1245 <tr> <td>8</td> <td>-6.19196</td> <td></td> </tr>
1246 <tr> <td>9</td> <td>-6.29497</td> <td></td> </tr>
1247 <tr> <td>10</td> <td>-6.34721</td> <td></td> </tr>
1248 <tr> <td>11</td> <td>-6.37353</td> <td></td> </tr>
1250So
this is not great. For the 2
d case, we might infer that perhaps
1251a
value around -6.4 might be right
if we just
refine the mesh enough --
1252though 11 refinements already leads to some 4,194,304 cells. In any
1253case, the
first number (the one shown in the beginning where we
1254discussed postprocessing) was off by almost a factor of 2!
1256For the 3
d case, the last number shown was on a mesh with 2,097,152
1257cells; the next
one would have had 8 times as many cells. In any
case, the
1259that the first digit of that last number is correct! In other words,
1260it was worth checking, or we would have just believed all of these
1261numbers. In fact, that last column isn't even doing a particularly
1262good job convincing us that the code might be correctly implemented.
1264If you keep reading through the other tutorial programs, you will find many ways
1265to make these sorts of computations more accurate and to come to
1266believe that the flux actually does converge to its correct
value.
1267For example, we can dramatically increase the accuracy of the computation
1268by
using adaptive mesh refinement (@ref step_6
"step-6") near the boundary, and
1269in particular by
using higher polynomial degree finite elements (also
1270@ref step_6
"step-6", but also @ref step_7
"step-7"). Using the latter,
using cubic elements
1271(polynomial degree 3), we can actually compute the flux pretty
1272accurately even in 3
d: @f$\Phi_h=-19.0148@f$ with 4 global refinement steps,
1273and @f$\Phi_h=-19.1533@f$ with 5 refinement steps. These
numbers are already
1274pretty close together and give us a reasonable idea of the
first
1275two correct digits of the
"true" answer.
1277@note We would be remiss to not also comment on the fact that there
1278 are good theoretical reasons why computing the flux accurately
1279 appears to be so much more difficult than the average
value.
1280 This has to
do with the fact that finite element theory
1281 provides us with the estimate
1282 @f$\|u-u_h\|_{L_2(\Omega)} \le
C h^2 \|\nabla^2u\|_{L_2(\Omega)}@f$
1283 when
using the linear elements
this program uses -- that is,
for
1284 every global mesh refinement, @f$h@f$ is reduced by a factor of two
1285 and the error goes down by a factor of 4. Now, the @f$L_2@f$ error is
1286 not equivalent to the error in the
mean value, but the two are
1287 related: They are both integrals over the domain,
using the *
value*
1288 of the solution. We expect the
mean value to converge no worse than
1289 the @f$L_2@f$
norm of the error. At the same time, theory also provides
1290 us with
this estimate:
1291 @f$\|\nabla (u-u_h)\|_{L_2(\partial\Omega)} \le
1292 C h^{1/2} \|\nabla^2u\|_{L_2(\Omega)}@f$. The move from
values to
1293 gradients reduces the convergence rates by
one order, and the move
1294 from domain to boundary by another half order. Here, then, each
1295 refinement step reduces the error not by a factor of 4 any more,
1296 by only by a factor of @f$\sqrt{2} \approx 1.4@f$. It takes a lot
1297 of global refinement steps to
reduce the error by, say, a factor
1298 ten or hundred, and
this is reflected in the very slow convergence
1299 evidenced by the table. On the other hand,
for cubic elements (i.e.,
1300 polynomial degree 3), we would get
1301 @f$\|u-u_h\|_{L_2(\Omega)} \le
C h^4 \|\nabla^4u\|_{L_2(\Omega)}@f$
1302 and after reduction by 1.5 orders, we would still have
1303 @f$\|\nabla (u-u_h)\|_{L_2(\partial\Omega)} \le
1304 C h^{2+1/2} \|\nabla^4u\|_{L_2(\Omega)}@f$. This rate,
1305 @f${\cal
O}(h^{2.5})@f$ is still quite rapid, and it is perhaps not
1306 surprising that we get much better answers with these higher
1307 order elements. This also illustrates that when trying to
1308 approximate anything that relates to a gradient of the solution,
1309 using linear elements (polynomial degree one) is really not a
1312@note In
this very specific
case, it turns out that we can actually
1313 compute the exact
value of @f$\Phi@f$. This is because
for the Poisson
1314 equation we compute the solution of here, @f$-\Delta u = f@f$, we can
1315 integrate over the domain, @f$-\int_\Omega \Delta u = \int_\Omega f@f$,
1316 and then use that @f$\Delta = \text{div}\;\text{grad}@f$;
this allows
1318 divergence theorem followed by multiplying by minus
one to find
1319 @f$\int_{\partial\Omega} \nabla u \cdot n = -\int_\Omega f@f$. The
1320 left hand side happens to be @f$\Phi@f$. For the specific right
1321 hand side @f$f(x_1,x_2)=4(x_1^4+x_2^4)@f$ we use in 2d, we then
1322 get @f$-\int_\Omega f = -\int_{-1}^{1} \int_{-1}^{1} 4(x_1^4+x_2^4) \; dx_2\; dx_1
1323 = -16 \left[\int_{-1}^{1} x^4 \; dx\right] = -16\times\frac 25@f$,
1324 which has a numerical
value of exactly -6.4 -- right on with our
1325 guess above. In 3
d, we can
do the same and get that the exact
1328 -\int_{-1}^{1} \int_{-1}^{1} \int_{-1}^{1} 4(x_1^4+x_2^4+x_3^4) \; dx_3 \; dx_2\; dx_1
1329 = -48\times\frac 25=-19.2@f$. What we found with cubic elements
1330 is then quite close to
this exact
value. Of course, in practice
1331 we almost never have exact
values to compare with: If we could
1332 compute something on a piece of paper, we wouldn
't have to solve
1333 the PDE numerically. But these sorts of situations make for excellent
1334 test cases that help us verify that our numerical solver works
1335 correctly. In many other cases, the literature contains
1336 numbers where others have already computed an answer accurately
1337 using their own software, and these are also often useful to
1338 compare against in verifying the correctness of our codes.
1341<a name="step_4-PlainProg"></a>
1342<h1> The plain program</h1>
* * for(const auto &cell :triangulation.active_cell_iterators())
* * int main(int argc, char **argv)
* * * struct InterferenceTaperTransform *
double JxW(const unsigned int q_point) const
constexpr numbers::NumberTraits< Number >::real_type square() const
@ update_values
Shape function values.
@ update_normal_vectors
Normal vectors.
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
@ update_quadrature_points
Transformed quadrature points.
std::vector< index_type > data
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
void refine(Triangulation< dim, spacedim > &tria, const Vector< Number > &criteria, const double threshold, const unsigned int max_to_mark=numbers::invalid_unsigned_int)
@ matrix
Contents is actually a matrix.
@ general
No special properties.
constexpr types::blas_int one
void cell_matrix(FullMatrix< double > &M, const FEValuesBase< dim > &fe, const FEValuesBase< dim > &fetest, const ArrayView< const std::vector< double > > &velocity, const double factor=1.)
double norm(const FEValuesBase< dim > &fe, const ArrayView< const std::vector< Tensor< 1, dim > > > &Du)
Point< spacedim > point(const gp_Pnt &p, const double tolerance=1e-10)
SymmetricTensor< 2, dim, Number > C(const Tensor< 2, dim, Number > &F)
SymmetricTensor< 2, dim, Number > d(const Tensor< 2, dim, Number > &F, const Tensor< 2, dim, Number > &dF_dt)
* * if(update_pressure &update_flags) * compute_pressure(constitutive_request
* * * RotationFunction< dim, Number >::RotationFunction Number(dim)
* * * * std::vector< Number > ThermoPlasticMaterial< dim, ViscoplasticYieldLaw, Number >::get_state_parameters const
T sum(const T &t, const MPI_Comm mpi_communicator)
T reduce(const T &local_value, const MPI_Comm comm, const std::function< T(const T &, const T &)> &combiner, const unsigned int root_process=0)
void run(const Iterator &begin, const std_cxx20::type_identity_t< Iterator > &end, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length, const unsigned int chunk_size)
int(&) functions(const void *v1, const void *v2)
::VectorizedArray< Number, width > pow(const ::VectorizedArray< Number, width > &, const Number p)