552 * <a name=
"step_15-ThecodeMinimalSurfaceProblemcodeclassimplementation"></a>
553 * <h3>The <code>MinimalSurfaceProblem</code>
class implementation</h3>
558 * <a name=
"step_15-MinimalSurfaceProblemMinimalSurfaceProblem"></a>
559 * <h4>MinimalSurfaceProblem::MinimalSurfaceProblem</h4>
563 * The constructor and destructor of the
class are the same as in the
first
571 *
MinimalSurfaceProblem<dim>::MinimalSurfaceProblem()
572 *
: dof_handler(triangulation)
580 * <a name=
"step_15-MinimalSurfaceProblemsetup_system"></a>
581 * <h4>MinimalSurfaceProblem::setup_system</h4>
585 * As
always in the setup-system function, we
set up the variables of the
586 * finite element method. There are some differences to @ref step_6
"step-6", because
591 *
void MinimalSurfaceProblem<dim>::setup_system()
593 *
dof_handler.distribute_dofs(fe);
594 *
current_solution.
reinit(dof_handler.n_dofs());
596 *
zero_constraints.clear();
602 *
zero_constraints.close();
604 *
nonzero_constraints.clear();
607 *
BoundaryValues<dim>(),
608 *
nonzero_constraints);
611 *
nonzero_constraints.close();
613 *
newton_update.reinit(dof_handler.n_dofs());
614 *
system_rhs.reinit(dof_handler.n_dofs());
619 *
sparsity_pattern.copy_from(dsp);
620 *
system_matrix.reinit(sparsity_pattern);
626 * <a name=
"step_15-MinimalSurfaceProblemassemble_system"></a>
627 * <h4>MinimalSurfaceProblem::assemble_system</h4>
631 * This function does the same as in the previous tutorials except that now,
633 * previous iteration
's solution. As discussed in the introduction, we need
634 * to use zero boundary values for the Newton updates; this is done by using
635 * the `zero_constraints` object when assembling into the global matrix and
640 * The top of the function contains the usual boilerplate code, setting up
641 * the objects that allow us to evaluate shape functions at quadrature
642 * points and temporary storage locations for the local matrices and
643 * vectors, as well as for the gradients of the previous solution at the
644 * quadrature points. We then start the loop over all cells:
648 * void MinimalSurfaceProblem<dim>::assemble_system()
650 * const QGauss<dim> quadrature_formula(fe.degree + 1);
655 * FEValues<dim> fe_values(fe,
656 * quadrature_formula,
657 * update_gradients | update_quadrature_points |
658 * update_JxW_values);
660 * const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
661 * const unsigned int n_q_points = quadrature_formula.size();
663 * FullMatrix<double> cell_matrix(dofs_per_cell, dofs_per_cell);
664 * Vector<double> cell_rhs(dofs_per_cell);
666 * std::vector<Tensor<1, dim>> old_solution_gradients(n_q_points);
668 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
670 * for (const auto &cell : dof_handler.active_cell_iterators())
675 * fe_values.reinit(cell);
679 * For the assembly of the linear system, we have to obtain the values
680 * of the previous solution's
gradients at the quadrature
681 * points. There is a standard way of doing
this: the
683 * represents a finite element field defined on a
DoFHandler, and
684 * evaluates the
gradients of
this field at the quadrature points of the
685 * cell with which the
FEValues object has last been reinitialized.
686 * The
values of the
gradients at all quadrature points are then written
687 * into the
second argument:
690 *
fe_values.get_function_gradients(current_solution,
691 *
old_solution_gradients);
695 * With
this, we can then
do the integration
loop over all quadrature
697 * the old solution in the quadrature points, we are able to compute
698 * the coefficients @f$a_{n}@f$ in these points. The assembly of the
699 * system itself then looks similar to what we
always do with the
700 * exception of the nonlinear terms, as does copying the results from
701 * the local objects into the global ones:
704 *
for (
unsigned int q = 0; q < n_q_points; ++q)
706 *
const double coeff =
707 *
1.0 /
std::sqrt(1 + old_solution_gradients[q] *
708 *
old_solution_gradients[q]);
710 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
712 *
for (
unsigned int j = 0; j < dofs_per_cell; ++j)
714 *
(((fe_values.shape_grad(i, q)
716 *
* fe_values.shape_grad(j, q))
718 *
(fe_values.shape_grad(i, q)
719 *
* coeff * coeff * coeff
720 *
* (fe_values.shape_grad(j, q)
721 *
* old_solution_gradients[q])
722 *
* old_solution_gradients[q]))
723 *
* fe_values.JxW(q));
725 *
cell_rhs(i) -= (fe_values.shape_grad(i, q)
727 *
* old_solution_gradients[q]
728 *
* fe_values.JxW(q));
732 *
cell->get_dof_indices(local_dof_indices);
733 *
zero_constraints.distribute_local_to_global(
734 *
cell_matrix, cell_rhs, local_dof_indices, system_matrix, system_rhs);
743 * <a name=
"step_15-MinimalSurfaceProblemsolve"></a>
744 * <h4>MinimalSurfaceProblem::solve</h4>
748 * The solve function is the same as
always. At the
end of the solution
749 * process we update the current solution by setting
750 * @f$u^{n+1}=u^n+\alpha^n\;\delta u^n@f$.
754 *
void MinimalSurfaceProblem<dim>::solve()
757 *
system_rhs.l2_norm() * 1e-6);
761 *
preconditioner.
initialize(system_matrix, 1.2);
763 *
solver.solve(system_matrix, newton_update, system_rhs, preconditioner);
765 *
zero_constraints.distribute(newton_update);
767 *
const double alpha = determine_step_length();
768 *
current_solution.add(alpha, newton_update);
775 * <a name=
"step_15-MinimalSurfaceProblemrefine_mesh"></a>
776 * <h4>MinimalSurfaceProblem::refine_mesh</h4>
780 * The
first part of
this function is the same as in @ref step_6
"step-6"... However,
781 * after refining the mesh we have to transfer the old solution to the
new
783 * is slightly convoluted, so let us describe it in detail:
787 *
void MinimalSurfaceProblem<dim>::refine_mesh()
789 *
Vector<float> estimated_error_per_cell(triangulation.n_active_cells());
796 *
estimated_error_per_cell);
799 *
estimated_error_per_cell,
805 * Then we need an additional step:
if,
for example, you flag a cell that
806 * is once more refined than its neighbor, and that neighbor is not
807 * flagged
for refinement, we would
end up with a jump of two refinement
808 * levels across a cell interface. To avoid these situations, the library
809 * will silently also have to
refine the neighbor cell once. It does so by
811 * before actually doing the refinement and coarsening. This function
812 * flags a
set of additional cells
for refinement or coarsening, to
813 * enforce rules like the
one-hanging-node rule. The cells that are
814 * flagged
for refinement and coarsening after calling
this function are
815 * exactly the ones that will actually be refined or coarsened. Usually,
816 * you don
't have to do this by hand
817 * (Triangulation::execute_coarsening_and_refinement does this for
818 * you). However, we need to initialize the SolutionTransfer class and it
819 * needs to know the final set of cells that will be coarsened or refined
820 * in order to store the data from the old mesh and transfer to the new
821 * one. Thus, we call the function by hand:
824 * triangulation.prepare_coarsening_and_refinement();
828 * With this out of the way, we initialize a SolutionTransfer object with
829 * the present DoFHandler. We make a copy of the solution vector and attach
830 * it to the SolutionTransfer. Now we can actually execute the refinement
831 * and create the new matrices and vectors including the vector
832 * `current_solution`, that will hold the current solution on the new mesh
833 * after calling SolutionTransfer::interpolate():
836 * SolutionTransfer<dim> solution_transfer(dof_handler);
837 * const Vector<double> coarse_solution = current_solution;
838 * solution_transfer.prepare_for_coarsening_and_refinement(coarse_solution);
840 * triangulation.execute_coarsening_and_refinement();
844 * solution_transfer.interpolate(current_solution);
848 * On the new mesh, there are different hanging nodes, computed in
849 * `setup_system()` above. To be on the safe side, we should make sure that
850 * the current solution's vector entries satisfy the hanging node
851 * constraints (see the discussion in the documentation of the
853 * explained at the
end of the introduction, the interpolated solution does
854 * not automatically satisfy the boundary
values even
if the solution before
855 * refinement had the correct boundary
values.
858 *
nonzero_constraints.distribute(current_solution);
866 * <a name=
"step_15-MinimalSurfaceProblemcompute_residual"></a>
867 * <h4>MinimalSurfaceProblem::compute_residual</h4>
871 * In order to monitor convergence, we need a way to compute the
norm of the
872 * (discrete) residual, i.e., the norm of the vector
873 * @f$\left<
F(u^n),\varphi_i\right>@f$ with @f$F(u)=-\nabla \cdot \left(
874 * \frac{1}{\
sqrt{1+|\nabla u|^{2}}}\nabla u \right)@f$ as discussed in the
875 * introduction. It turns out that (although we don
't use this feature in
876 * the current version of the program) one needs to compute the residual
877 * @f$\left<F(u^n+\alpha^n\;\delta u^n),\varphi_i\right>@f$ when determining
878 * optimal step lengths, and so this is what we implement here: the function
879 * takes the step length @f$\alpha^n@f$ as an argument. The original
880 * functionality is of course obtained by passing a zero as argument.
884 * In the function below, we first set up a vector for the residual, and
885 * then a vector for the evaluation point @f$u^n+\alpha^n\;\delta u^n@f$. This
886 * is followed by the same boilerplate code we use for all integration
891 * double MinimalSurfaceProblem<dim>::compute_residual(const double alpha) const
893 * Vector<double> residual(dof_handler.n_dofs());
895 * Vector<double> evaluation_point(dof_handler.n_dofs());
896 * evaluation_point = current_solution;
897 * evaluation_point.add(alpha, newton_update);
899 * const QGauss<dim> quadrature_formula(fe.degree + 1);
900 * FEValues<dim> fe_values(fe,
901 * quadrature_formula,
902 * update_gradients | update_quadrature_points |
903 * update_JxW_values);
905 * const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
906 * const unsigned int n_q_points = quadrature_formula.size();
908 * Vector<double> cell_residual(dofs_per_cell);
909 * std::vector<Tensor<1, dim>> gradients(n_q_points);
911 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
913 * for (const auto &cell : dof_handler.active_cell_iterators())
916 * fe_values.reinit(cell);
920 * The actual computation is much as in
921 * <code>assemble_system()</code>. We first evaluate the gradients of
922 * @f$u^n+\alpha^n\,\delta u^n@f$ at the quadrature points, then compute
923 * the coefficient @f$a_n@f$, and then plug it all into the formula for
927 * fe_values.get_function_gradients(evaluation_point, gradients);
930 * for (unsigned int q = 0; q < n_q_points; ++q)
932 * const double coeff =
933 * 1. / std::sqrt(1 + gradients[q] * gradients[q]);
935 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
936 * cell_residual(i) -= (fe_values.shape_grad(i, q) // \nabla \phi_i
938 * * gradients[q] // * \nabla u_n
939 * * fe_values.JxW(q)); // * dx
942 * cell->get_dof_indices(local_dof_indices);
943 * zero_constraints.distribute_local_to_global(cell_residual,
948 * return residual.l2_norm();
956 * <a name="step_15-MinimalSurfaceProblemdetermine_step_length"></a>
957 * <h4>MinimalSurfaceProblem::determine_step_length</h4>
961 * As discussed in the introduction, Newton's method frequently does not
962 * converge
if we always take full steps, i.e., compute @f$u^{n+1}=u^n+\delta
963 * u^n@f$. Rather, one needs a damping parameter (step length) @f$\alpha^n@f$ and
964 *
set @f$u^{n+1}=u^n+\alpha^n\delta u^n@f$. This function is the
one called
965 * to compute @f$\alpha^n@f$.
969 * Here, we simply
always return 0.1. This is of course a sub-optimal
970 * choice: ideally, what
one wants is that the step
size goes to
one as we
971 * get closer to the solution, so that we get to enjoy the rapid quadratic
972 * convergence of Newton
's method. We will discuss better strategies below
973 * in the results section, and @ref step_77 "step-77" also covers this aspect.
977 * double MinimalSurfaceProblem<dim>::determine_step_length() const
987 * <a name="step_15-MinimalSurfaceProblemoutput_results"></a>
988 * <h4>MinimalSurfaceProblem::output_results</h4>
992 * This last function to be called from `run()` outputs the current solution
993 * (and the Newton update) in graphical form as a VTU file. It is entirely the
994 * same as what has been used in previous tutorials.
998 * void MinimalSurfaceProblem<dim>::output_results(
999 * const unsigned int refinement_cycle) const
1001 * DataOut<dim> data_out;
1003 * data_out.attach_dof_handler(dof_handler);
1004 * data_out.add_data_vector(current_solution, "solution");
1005 * data_out.add_data_vector(newton_update, "update");
1006 * data_out.build_patches();
1008 * const std::string filename =
1009 * "solution-" + Utilities::int_to_string(refinement_cycle, 2) + ".vtu";
1010 * std::ofstream output(filename);
1011 * data_out.write_vtu(output);
1018 * <a name="step_15-MinimalSurfaceProblemrun"></a>
1019 * <h4>MinimalSurfaceProblem::run</h4>
1023 * In the run function, we build the first grid and then have the top-level
1024 * logic for the Newton iteration.
1028 * As described in the introduction, the domain is the unit disk around
1029 * the origin, created in the same way as shown in @ref step_6 "step-6". The mesh is
1030 * globally refined twice followed later on by several adaptive cycles.
1034 * Before starting the Newton loop, we also need to do
1035 * ensure that the first Newton iterate already has the correct
1036 * boundary values, as discussed in the introduction.
1039 * template <int dim>
1040 * void MinimalSurfaceProblem<dim>::run()
1042 * GridGenerator::hyper_ball(triangulation);
1043 * triangulation.refine_global(2);
1046 * nonzero_constraints.distribute(current_solution);
1050 * The Newton iteration starts next. We iterate until the (norm of the)
1051 * residual computed at the end of the previous iteration is less than
1052 * @f$10^{-2}@f$, as checked at the end of the `do { ... } while` loop that
1053 * starts here. Because we don't have a reasonable
value to initialize
1054 * the variable, we just use the largest
value that can be represented
1058 *
double last_residual_norm = std::numeric_limits<double>::max();
1059 *
unsigned int refinement_cycle = 0;
1062 *
std::cout <<
"Mesh refinement step " << refinement_cycle << std::endl;
1064 *
if (refinement_cycle != 0)
1069 * On every mesh we
do exactly five Newton steps. We print the
initial
1070 * residual here and then start the iterations on
this mesh.
1074 * In every Newton step the system
matrix and the right hand side have
1075 * to be computed
first, after which we store the
norm of the right
1076 * hand side as the residual to
check against when deciding whether to
1077 * stop the iterations. We then solve the linear system (the function
1078 * also updates @f$u^{n+1}=u^n+\alpha^n\;\delta u^n@f$) and output the
1079 * norm of the residual at the
end of
this Newton step.
1083 * After the
end of
this loop, we then also output the solution on the
1084 * current mesh in graphical form and increment the counter
for the
1085 * mesh refinement cycle.
1088 *
std::cout <<
" Initial residual: " << compute_residual(0) << std::endl;
1090 *
for (
unsigned int inner_iteration = 0; inner_iteration < 5;
1091 *
++inner_iteration)
1093 *
assemble_system();
1094 *
last_residual_norm = system_rhs.l2_norm();
1098 *
std::cout <<
" Residual: " << compute_residual(0) << std::endl;
1101 *
output_results(refinement_cycle);
1103 *
++refinement_cycle;
1104 *
std::cout << std::endl;
1106 *
while (last_residual_norm > 1e-2);
1113 * <a name=
"step_15-Themainfunction"></a>
1114 * <h4>The
main function</h4>
1118 * Finally the
main function. This follows the scheme of all other
main
1126 *
using namespace Step15;
1128 *
MinimalSurfaceProblem<2> problem;
1131 *
catch (std::exception &exc)
1133 *
std::cerr << std::endl
1135 *
<<
"----------------------------------------------------"
1137 *
std::cerr <<
"Exception on processing: " << std::endl
1138 *
<< exc.what() << std::endl
1139 *
<<
"Aborting!" << std::endl
1140 *
<<
"----------------------------------------------------"
1147 *
std::cerr << std::endl
1149 *
<<
"----------------------------------------------------"
1151 *
std::cerr <<
"Unknown exception!" << std::endl
1152 *
<<
"Aborting!" << std::endl
1153 *
<<
"----------------------------------------------------"
1160@anchor step_15-ResultsSection
1161<a name=
"step_15-Results"></a><h1>Results</h1>
1165The output of the program looks as follows:
1167Mesh refinement step 0
1168 Initial residual: 1.53143
1175Mesh refinement step 1
1176 Initial residual: 0.868959
1183Mesh refinement step 2
1184 Initial residual: 0.426445
1191Mesh refinement step 3
1192 Initial residual: 0.282026
1199Mesh refinement step 4
1200 Initial residual: 0.154404
1210Obviously, the scheme converges,
if not very fast. We will come back to
1211strategies
for accelerating the method below.
1213One can visualize the solution after each
set of five Newton
1214iterations, i.e., on each of the meshes on which we
approximate the
1215solution. This yields the following
set of images:
1217<div
class=
"twocolumn" style=
"width: 80%">
1219 <img src=
"https://dealii.org/images/steps/developer/step_15_solution_1.png"
1220 alt=
"Solution after zero cycles with contour lines." width=
"230" height=
"273">
1223 <img src=
"https://dealii.org/images/steps/developer/step_15_solution_2.png"
1224 alt=
"Solution after one cycle with contour lines." width=
"230" height=
"273">
1227 <img src=
"https://dealii.org/images/steps/developer/step_15_solution_3.png"
1228 alt=
"Solution after two cycles with contour lines." width=
"230" height=
"273">
1231 <img src=
"https://dealii.org/images/steps/developer/step_15_solution_4.png"
1232 alt=
"Solution after three cycles with contour lines." width=
"230" height=
"273">
1236It is clearly visible, that the solution minimizes the surface
1237after each refinement. The solution converges to a picture
one
1238would imagine a soap bubble to be that is located
inside a wire
loop
1240the boundary. Also it is visible, how the boundary
1241is smoothed out after each refinement. On the coarse mesh,
1242the boundary doesn
't look like a sine, whereas it does the
1245The mesh is mostly refined near the boundary, where the solution
1246increases or decreases strongly, whereas it is coarsened on
1247the inside of the domain, where nothing interesting happens,
1248because there isn't much change in the solution. The ninth
1249solution and mesh are shown here:
1251<div
class=
"onecolumn" style=
"width: 60%">
1253 <img src=
"https://dealii.org/images/steps/developer/step_15_solution_9.png"
1254 alt=
"Grid and solution of the ninth cycle with contour lines." width=
"507" height=
"507">
1260@anchor step_15-Extensions
1261<a name=
"step_15-Possibilitiesforextensions"></a><h3>Possibilities
for extensions</h3>
1264The program shows the basic structure of a solver
for a nonlinear, stationary
1265problem. However, it does not converge particularly fast,
for good reasons:
1267- The program
always takes a step
size of 0.1. This precludes the rapid,
1268 quadratic convergence
for which Newton
's method is typically chosen.
1269- It does not connect the nonlinear iteration with the mesh refinement
1272Obviously, a better program would have to address these two points.
1273We will discuss them in the following.
1276<a name="step_15-Steplengthcontrol"></a><h4> Step length control </h4>
1279Newton's method has two well known properties:
1280- It may not converge from arbitrarily chosen starting points. Rather, a
1281 starting
point has to be close enough to the solution to guarantee
1282 convergence. However, we can enlarge the area from which Newton
's method
1283 converges by damping the iteration using a <i>step length</i> 0<@f$\alpha^n\le
1285- It exhibits rapid convergence of quadratic order if (i) the step length is
1286 chosen as @f$\alpha^n=1@f$, and (ii) it does in fact converge with this choice
1289A consequence of these two observations is that a successful strategy is to
1290choose @f$\alpha^n<1@f$ for the initial iterations until the iterate has come
1291close enough to allow for convergence with full step length, at which point we
1292want to switch to @f$\alpha^n=1@f$. The question is how to choose @f$\alpha^n@f$ in an
1293automatic fashion that satisfies these criteria.
1295We do not want to review the literature on this topic here, but only briefly
1296mention that there are two fundamental approaches to the problem: backtracking
1297line search and trust region methods. The former is more widely used for
1298partial differential equations and essentially does the following:
1299- Compute a search direction
1300- See if the resulting residual of @f$u^n + \alpha^n\;\delta u^n@f$ with
1301 @f$\alpha^n=1@f$ is "substantially smaller" than that of @f$u^n@f$ alone.
1302- If so, then take @f$\alpha^n=1@f$.
1303- If not, try whether the residual is "substantially smaller" with
1305- If so, then take @f$\alpha^n=2/3@f$.
1306- If not, try whether the residual is "substantially smaller" with
1307 @f$\alpha^n=(2/3)^2@f$.
1309One can of course choose other factors @f$r, r^2, \ldots@f$ than the @f$2/3,
1310(2/3)^2, \ldots@f$ chosen above, for @f$0<r<1@f$. It is obvious where the term
1311"backtracking" comes from: we try a long step, but if that doesn't work we
try
1312a shorter step, and ever shorter step, etc. The function
1313<code>determine_step_length()</code> is written the way it is to support
1314exactly
this kind of use
case.
1316Whether we accept a particular step length @f$\alpha^n@f$ depends on how we define
1317"substantially smaller". There are a number of ways to
do so, but without
1318going into detail let us just mention that the most common ones are to use the
1319Wolfe and Armijo-Goldstein conditions. For these,
one can show the following:
1320- There is
always a step length @f$\alpha^n@f$
for which the conditions are
1321 satisfied, i.e., the iteration never gets stuck as
long as the problem is
1323- If we are close enough to the solution, then the conditions allow
for
1324 @f$\alpha^n=1@f$, thereby enabling quadratic convergence.
1326We will not dwell on
this here any further but leave the implementation of
1327such algorithms as an exercise. We note, however, that when implemented
1328correctly then it is a common observation that most reasonably nonlinear
1329problems can be solved in anywhere between 5 and 15 Newton iterations to
1330engineering accuracy — substantially fewer than we need with the current
1331version of the program.
1333More details on globalization methods including backtracking can be found,
1334for example, in @cite GNS08 and @cite NW99.
1336A separate
point, very much worthwhile making, however, is that in practice
1337the implementation of efficient nonlinear solvers is about as complicated as
1338the implementation of efficient finite element methods. One should not
1339attempt to reinvent the wheel by implementing all of the necessary steps
1340oneself. Substantial pieces of the puzzle are already available in
1342But, instead, just like building finite element solvers on libraries
1343such as deal.II, one should be building nonlinear solvers on libraries such
1345deal.II has interfaces to
SUNDIALS and in particular to its nonlinear solver
1346sub-package KINSOL through the
SUNDIALS::KINSOL class. It would not be
1347very difficult to base the current problem on that interface --
1348indeed, that is what @ref step_77 "step-77" does.
1352<a name="step_15-Integratingmeshrefinementandnonlinearandlinearsolvers"></a><h4> Integrating mesh refinement and nonlinear and linear solvers </h4>
1355We currently do exactly 5 iterations on each mesh. But is this optimal? One
1356could ask the following questions:
1357- Maybe it is worthwhile doing more iterations on the initial meshes since
1358 there, computations are cheap.
1359- On the other hand, we do not want to do too many iterations on every mesh:
1360 yes, we could drive the residual to zero on every mesh, but that would only
1361 mean that the nonlinear iteration error is far smaller than the
1362 discretization error.
1363- Should we use solve the linear systems in each Newton step with higher or
1366Ultimately, what this boils down to is that we somehow need to couple the
1367discretization error on the current mesh with the nonlinear residual we want
1368to achieve with the Newton iterations on a given mesh, and to the linear
1369iteration we want to achieve with the CG method within each Newton
1372How to do this is, again, not entirely trivial, and we again leave it as a
1377<a name="step_15-UsingautomaticdifferentiationtocomputetheJacobianmatrix"></a><h4> Using automatic differentiation to compute the Jacobian matrix </h4>
1380As outlined in the introduction, when solving a nonlinear problem of
1384 -\nabla \cdot \left( \frac{1}{\
sqrt{1+|\nabla u|^{2}}}\nabla u \right)
1387we use a Newton iteration that
requires us to repeatedly solve the
1388linear partial differential equation
1390 F'(u^{n},\delta u^{n}) &=- F(u^{n})
1392so that we can compute the update
1394 u^{n+1}&=u^{n}+\alpha^n \delta u^{n}
1396with the solution @f$\delta u^{n}@f$ of the Newton step. For the problem
1397here, we could compute the derivative @f$F'(u,\delta u)@f$ by hand and
1402 - \nabla \cdot \left( \frac{1}{\left(1+|\nabla u|^{2}\right)^{\frac{1}{2}}}\nabla
1404 \nabla \cdot \left( \frac{\nabla u \cdot
1405 \nabla \delta u}{\left(1+|\nabla u|^{2}\right)^{\frac{3}{2}}} \nabla u
1408But this is already a sizable expression that is cumbersome both to
1409derive and to implement. It is also, in some sense, duplicative: If we
1410implement what @f$F(u)@f$ is somewhere in the code, then @f$F'(u,\delta u)@f$
1411is not an
independent piece of information but is something that, at
1412least in principle, a computer should be able to infer itself.
1413Wouldn
't it be nice if that could actually happen? That is, if we
1414really only had to implement @f$F(u)@f$, and @f$F'(u,\delta u)@f$ was then somehow
1415done implicitly? That is in fact possible, and runs under the name
1416"automatic differentiation". @ref step_71
"step-71" discusses
this very
1417concept in general terms, and @ref step_72
"step-72" illustrates how
this can be
1418applied in practice
for the very problem we are considering here.
1421<a name=
"step_15-StoringtheJacobianmatrixinlowerprecisionfloatingpointvariables"></a><h4> Storing the Jacobian matrix in lower-precision floating point variables </h4>
1424On modern computer systems, *accessing*
data in
main memory takes far
1425longer than *actually doing* something with it: We can do many floating
1427number from memory onto the processor. Unfortunately, when we do things
1428such as
matrix-vector products, we only multiply each
matrix entry once
1429with another number (the corresponding entry of the vector) and then we
1431load. (Strictly speaking, we also have to
load the corresponding vector
1432entry, but at least sometimes we get to re-use that vector entry in
1433doing the products that correspond to the next row of the
matrix.) This
1434is a fairly low
"arithmetic intensity", and consequently we spend most
1435of our time during
matrix-vector products waiting
for data to arrive
1436from memory rather than actually doing floating
point operations.
1438This is of course
one of the rationales
for the
"matrix-free" approach to
1439solving linear systems (see @ref step_37
"step-37",
for example). But
if you don
't quite
1440want to go all that way to change the structure of the program, then
1441here is a different approach: Storing the system matrix (the "Jacobian")
1442in single-precision instead of double precision floating point numbers
1443(i.e., using `float` instead of `double` as the data type). This reduces
1444the amount of memory necessary by a factor of 1.5 (each matrix entry
1445in a SparseMatrix object requires storing the column index -- 4 bytes --
1446and the actual value -- either 4 or 8 bytes), and consequently
1447will speed up matrix-vector products by a factor of around 1.5 as well because,
1448as pointed out above, most of the time is spent loading data from memory
1449and loading 2/3 the amount of data should be roughly 3/2 times as fast. All
1450of this could be done using SparseMatrix<float> as the data type
1451for the system matrix. (In principle, we would then also like it if
1452the SparseDirectUMFPACK solver we use in this program computes and
1453stores its sparse decomposition in `float` arithmetic. This is not
1454currently implemented, though could be done.)
1456Of course, there is a downside to this: Lower precision data storage
1457also implies that we will not solve the linear system of the Newton
1458step as accurately as we might with `double` precision. At least
1459while we are far away from the solution of the nonlinear problem,
1460this may not be terrible: If we can do a Newton iteration in half
1461the time, we can afford to do a couple more Newton steps if the
1462search directions aren't as good.
1463But it turns out that even that isn
't typically necessary: Both
1464theory and computational experience shows that it is entirely
1465sufficient to store the Jacobian matrix in single precision
1466*as long as one stores the right hand side in double precision*.
1467A great overview of why this is so, along with numerical
1468experiments that also consider "half precision" floating point
1469numbers, can be found in @cite Kelley2022 .
1472<a name="step_15-PlainProg"></a>
1473<h1> The plain program</h1>
1474@include "step-15.cc"
* * for(const auto &cell :triangulation.active_cell_iterators())
* * int main(int argc, char **argv)
* x_component_mask set(0, true)
* * * struct InterferenceTaperTransform *
void get_function_gradients(const ReadVector< Number > &fe_function, std::vector< Tensor< 1, spacedim, Number > > &gradients) const
static void estimate(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const Quadrature< dim - 1 > &quadrature, const std::map< types::boundary_id, const Function< spacedim, Number > * > &neumann_bc, const ReadVector< Number > &solution, Vector< float > &error, const ComponentMask &component_mask={}, const Function< spacedim > *coefficients=nullptr, const unsigned int n_threads=numbers::invalid_unsigned_int, const types::subdomain_id subdomain_id=numbers::invalid_subdomain_id, const types::material_id material_id=numbers::invalid_material_id, const Strategy strategy=cell_diameter_over_24)
void initialize(const MatrixType &A, const AdditionalData ¶meters=AdditionalData())
virtual bool prepare_coarsening_and_refinement()
void loop(IteratorType begin, std_cxx20::type_identity_t< IteratorType > end, DOFINFO &dinfo, INFOBOX &info, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &)> &cell_worker, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &)> &boundary_worker, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &, typename INFOBOX::CellInfo &)> &face_worker, AssemblerType &assembler, const LoopControl &lctrl=LoopControl())
void make_hanging_node_constraints(const DoFHandler< dim, spacedim > &dof_handler, AffineConstraints< number > &constraints)
void make_sparsity_pattern(const DoFHandler< dim, spacedim > &dof_handler, SparsityPatternBase &sparsity_pattern, const AffineConstraints< number > &constraints={}, const bool keep_constrained_dofs=true, const types::subdomain_id subdomain_id=numbers::invalid_subdomain_id)
std::vector< index_type > data
void approximate(const SynchronousIterators< std::tuple< typename DoFHandler< dim, spacedim >::active_cell_iterator, Vector< float >::iterator > > &cell, const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof_handler, const InputVector &solution, const unsigned int component)
void refine(Triangulation< dim, spacedim > &tria, const Vector< Number > &criteria, const double threshold, const unsigned int max_to_mark=numbers::invalid_unsigned_int)
void refine_and_coarsen_fixed_number(Triangulation< dim, spacedim > &triangulation, const Vector< Number > &criteria, const double top_fraction_of_cells, const double bottom_fraction_of_cells, const unsigned int max_n_cells=std::numeric_limits< unsigned int >::max())
@ matrix
Contents is actually a matrix.
constexpr types::blas_int one
std::pair< NumberType, unsigned int > line_search(const std::function< std::pair< NumberType, NumberType >(const NumberType x)> &func, const NumberType f0, const NumberType g0, const std::function< NumberType(const NumberType x_low, const NumberType f_low, const NumberType g_low, const NumberType x_hi, const NumberType f_hi, const NumberType g_hi, const FiniteSizeHistory< NumberType > &x_rec, const FiniteSizeHistory< NumberType > &f_rec, const FiniteSizeHistory< NumberType > &g_rec, const std::pair< NumberType, NumberType > bounds)> &interpolate, const NumberType a1, const NumberType eta=0.9, const NumberType mu=0.01, const NumberType a_max=std::numeric_limits< NumberType >::max(), const unsigned int max_evaluations=20, const bool debug_output=false)
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)
Tensor< 2, dim, Number > F(const Tensor< 2, dim, Number > &Grad_u)
* * if(update_pressure &update_flags) * compute_pressure(constitutive_request
void load(Archive &ar, ::std_cxx26::inplace_vector< T, N > &vec, const unsigned int)
bool check(const ConstraintKinds kind_in, const unsigned int dim)
int(&) functions(const void *v1, const void *v2)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)