deal.II version GIT relicensing-6809-ge913b9bb34 2026-09-25 17:20:01+00:00
\(\newcommand{\dealvcentcolon}{\mathrel{\mathop{:}}}\) \(\newcommand{\dealcoloneq}{\dealvcentcolon\mathrel{\mkern-1.2mu}=}\) \(\newcommand{\jump}[1]{\left[\!\left[ #1 \right]\!\right]}\) \(\newcommand{\average}[1]{\left\{\!\left\{ #1 \right\}\!\right\}}\)
Loading...
Searching...
No Matches
step-15.h
Go to the documentation of this file.
1) const
551 *   {
552 *   return std::sin(2 * numbers::PI * (p[0] + p[1]));
553 *   }
554 *  
555 * @endcode
556 *
557 *
558 * <a name="step_15-ThecodeMinimalSurfaceProblemcodeclassimplementation"></a>
559 * <h3>The <code>MinimalSurfaceProblem</code> class implementation</h3>
560 *
561
562 *
563 *
564 * <a name="step_15-MinimalSurfaceProblemMinimalSurfaceProblem"></a>
565 * <h4>MinimalSurfaceProblem::MinimalSurfaceProblem</h4>
566 *
567
568 *
569 * The constructor and destructor of the class are the same as in the first
570 * few tutorials.
571 *
572
573 *
574 *
575 * @code
576 *   template <int dim>
577 *   MinimalSurfaceProblem<dim>::MinimalSurfaceProblem()
578 *   : dof_handler(triangulation)
579 *   , fe(2)
580 *   {}
581 *  
582 *  
583 * @endcode
584 *
585 *
586 * <a name="step_15-MinimalSurfaceProblemsetup_system"></a>
587 * <h4>MinimalSurfaceProblem::setup_system</h4>
588 *
589
590 *
591 * As always in the setup-system function, we set up the variables of the
592 * finite element method. There are some differences to @ref step_6 "step-6", because
593 * we need to construct two AffineConstraint<> objects.
594 *
595 * @code
596 *   template <int dim>
597 *   void MinimalSurfaceProblem<dim>::setup_system()
598 *   {
599 *   dof_handler.distribute_dofs(fe);
600 *   current_solution.reinit(dof_handler.n_dofs());
601 *  
602 *   zero_constraints.clear();
604 *   0,
606 *   zero_constraints);
607 *   DoFTools::make_hanging_node_constraints(dof_handler, zero_constraints);
608 *   zero_constraints.close();
609 *  
610 *   nonzero_constraints.clear();
612 *   0,
613 *   BoundaryValues<dim>(),
614 *   nonzero_constraints);
615 *  
616 *   DoFTools::make_hanging_node_constraints(dof_handler, nonzero_constraints);
617 *   nonzero_constraints.close();
618 *  
619 *   newton_update.reinit(dof_handler.n_dofs());
620 *   system_rhs.reinit(dof_handler.n_dofs());
621 *  
622 *   DynamicSparsityPattern dsp(dof_handler.n_dofs());
623 *   DoFTools::make_sparsity_pattern(dof_handler, dsp, zero_constraints);
624 *  
625 *   sparsity_pattern.copy_from(dsp);
626 *   system_matrix.reinit(sparsity_pattern);
627 *   }
628 *  
629 * @endcode
630 *
631 *
632 * <a name="step_15-MinimalSurfaceProblemassemble_system"></a>
633 * <h4>MinimalSurfaceProblem::assemble_system</h4>
634 *
635
636 *
637 * This function does the same as in the previous tutorials except that now,
638 * of course, the matrix and right hand side functions depend on the
639 * previous iteration's solution. As discussed in the introduction, we need
640 * to use zero boundary values for the Newton updates; this is done by using
641 * the `zero_constraints` object when assembling into the global matrix and
642 * vector.
643 *
644
645 *
646 * The top of the function contains the usual boilerplate code, setting up
647 * the objects that allow us to evaluate shape functions at quadrature
648 * points and temporary storage locations for the local matrices and
649 * vectors, as well as for the gradients of the previous solution at the
650 * quadrature points. We then start the loop over all cells:
651 *
652 * @code
653 *   template <int dim>
654 *   void MinimalSurfaceProblem<dim>::assemble_system()
655 *   {
656 *   const QGauss<dim> quadrature_formula(fe.degree + 1);
657 *  
658 *   system_matrix = 0;
659 *   system_rhs = 0;
660 *  
661 *   FEValues<dim> fe_values(fe,
662 *   quadrature_formula,
663 *   update_gradients | update_quadrature_points |
664 *   update_JxW_values);
665 *  
666 *   const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
667 *   const unsigned int n_q_points = quadrature_formula.size();
668 *  
669 *   FullMatrix<double> cell_matrix(dofs_per_cell, dofs_per_cell);
670 *   Vector<double> cell_rhs(dofs_per_cell);
671 *  
672 *   std::vector<Tensor<1, dim>> old_solution_gradients(n_q_points);
673 *  
674 *   std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
675 *  
676 *   for (const auto &cell : dof_handler.active_cell_iterators())
677 *   {
678 *   cell_matrix = 0;
679 *   cell_rhs = 0;
680 *  
681 *   fe_values.reinit(cell);
682 *  
683 * @endcode
684 *
685 * For the assembly of the linear system, we have to obtain the values
686 * of the previous solution's gradients at the quadrature
687 * points. There is a standard way of doing this: the
688 * FEValues::get_function_gradients function takes a vector that
689 * represents a finite element field defined on a DoFHandler, and
690 * evaluates the gradients of this field at the quadrature points of the
691 * cell with which the FEValues object has last been reinitialized.
692 * The values of the gradients at all quadrature points are then written
693 * into the second argument:
694 *
695 * @code
696 *   fe_values.get_function_gradients(current_solution,
697 *   old_solution_gradients);
698 *  
699 * @endcode
700 *
701 * With this, we can then do the integration loop over all quadrature
702 * points and shape functions. Having just computed the gradients of
703 * the old solution in the quadrature points, we are able to compute
704 * the coefficients @f$a_{n}@f$ in these points. The assembly of the
705 * system itself then looks similar to what we always do with the
706 * exception of the nonlinear terms, as does copying the results from
707 * the local objects into the global ones:
708 *
709 * @code
710 *   for (unsigned int q = 0; q < n_q_points; ++q)
711 *   {
712 *   const double coeff =
713 *   1.0 / std::sqrt(1 + old_solution_gradients[q] *
714 *   old_solution_gradients[q]);
715 *  
716 *   for (unsigned int i = 0; i < dofs_per_cell; ++i)
717 *   {
718 *   for (unsigned int j = 0; j < dofs_per_cell; ++j)
719 *   cell_matrix(i, j) +=
720 *   (((fe_values.shape_grad(i, q) // ((\nabla \phi_i
721 *   * coeff // * a_n
722 *   * fe_values.shape_grad(j, q)) // * \nabla \phi_j)
723 *   - // -
724 *   (fe_values.shape_grad(i, q) // (\nabla \phi_i
725 *   * coeff * coeff * coeff // * a_n^3
726 *   * (fe_values.shape_grad(j, q) // * (\nabla \phi_j
727 *   * old_solution_gradients[q]) // * \nabla u_n)
728 *   * old_solution_gradients[q])) // * \nabla u_n)))
729 *   * fe_values.JxW(q)); // * dx
730 *  
731 *   cell_rhs(i) -= (fe_values.shape_grad(i, q) // \nabla \phi_i
732 *   * coeff // * a_n
733 *   * old_solution_gradients[q] // * \nabla u_n
734 *   * fe_values.JxW(q)); // * dx
735 *   }
736 *   }
737 *  
738 *   cell->get_dof_indices(local_dof_indices);
739 *   zero_constraints.distribute_local_to_global(
740 *   cell_matrix, cell_rhs, local_dof_indices, system_matrix, system_rhs);
741 *   }
742 *   }
743 *  
744 *  
745 *  
746 * @endcode
747 *
748 *
749 * <a name="step_15-MinimalSurfaceProblemsolve"></a>
750 * <h4>MinimalSurfaceProblem::solve</h4>
751 *
752
753 *
754 * The solve function is the same as always. At the end of the solution
755 * process we update the current solution by setting
756 * @f$u^{n+1}=u^n+\alpha^n\;\delta u^n@f$.
757 *
758 * @code
759 *   template <int dim>
760 *   void MinimalSurfaceProblem<dim>::solve()
761 *   {
762 *   SolverControl solver_control(system_rhs.size(),
763 *   system_rhs.l2_norm() * 1e-6);
764 *   SolverCG<Vector<double>> solver(solver_control);
765 *  
767 *   preconditioner.initialize(system_matrix, 1.2);
768 *  
769 *   solver.solve(system_matrix, newton_update, system_rhs, preconditioner);
770 *  
771 *   zero_constraints.distribute(newton_update);
772 *  
773 *   const double alpha = determine_step_length();
774 *   current_solution.add(alpha, newton_update);
775 *   }
776 *  
777 *  
778 * @endcode
779 *
780 *
781 * <a name="step_15-MinimalSurfaceProblemrefine_mesh"></a>
782 * <h4>MinimalSurfaceProblem::refine_mesh</h4>
783 *
784
785 *
786 * The first part of this function is the same as in @ref step_6 "step-6"... However,
787 * after refining the mesh we have to transfer the old solution to the new
788 * one which we do with the help of the SolutionTransfer class. The process
789 * is slightly convoluted, so let us describe it in detail:
790 *
791 * @code
792 *   template <int dim>
793 *   void MinimalSurfaceProblem<dim>::refine_mesh()
794 *   {
795 *   Vector<float> estimated_error_per_cell(triangulation.n_active_cells());
796 *  
798 *   dof_handler,
799 *   QGauss<dim - 1>(fe.degree + 1),
800 *   std::map<types::boundary_id, const Function<dim> *>(),
801 *   current_solution,
802 *   estimated_error_per_cell);
803 *  
805 *   estimated_error_per_cell,
806 *   0.3,
807 *   0.03);
808 *  
809 * @endcode
810 *
811 * Then we need an additional step: if, for example, you flag a cell that
812 * is once more refined than its neighbor, and that neighbor is not
813 * flagged for refinement, we would end up with a jump of two refinement
814 * levels across a cell interface. To avoid these situations, the library
815 * will silently also have to refine the neighbor cell once. It does so by
817 * before actually doing the refinement and coarsening. This function
818 * flags a set of additional cells for refinement or coarsening, to
819 * enforce rules like the one-hanging-node rule. The cells that are
820 * flagged for refinement and coarsening after calling this function are
821 * exactly the ones that will actually be refined or coarsened. Usually,
822 * you don't have to do this by hand
823 * (Triangulation::execute_coarsening_and_refinement does this for
824 * you). However, we need to initialize the SolutionTransfer class and it
825 * needs to know the final set of cells that will be coarsened or refined
826 * in order to store the data from the old mesh and transfer to the new
827 * one. Thus, we call the function by hand:
828 *
829 * @code
830 *   triangulation.prepare_coarsening_and_refinement();
831 *  
832 * @endcode
833 *
834 * With this out of the way, we initialize a SolutionTransfer object with
835 * the present DoFHandler. We make a copy of the solution vector and attach
836 * it to the SolutionTransfer. Now we can actually execute the refinement
837 * and create the new matrices and vectors including the vector
838 * `current_solution`, that will hold the current solution on the new mesh
839 * after calling SolutionTransfer::interpolate():
840 *
841 * @code
842 *   SolutionTransfer<dim> solution_transfer(dof_handler);
843 *   const Vector<double> coarse_solution = current_solution;
844 *   solution_transfer.prepare_for_coarsening_and_refinement(coarse_solution);
845 *  
846 *   triangulation.execute_coarsening_and_refinement();
847 *  
848 *   setup_system();
849 *  
850 *   solution_transfer.interpolate(current_solution);
851 *  
852 * @endcode
853 *
854 * On the new mesh, there are different hanging nodes, computed in
855 * `setup_system()` above. To be on the safe side, we should make sure that
856 * the current solution's vector entries satisfy the hanging node
857 * constraints (see the discussion in the documentation of the
858 * SolutionTransfer class for why this is necessary) and boundary values. As
859 * explained at the end of the introduction, the interpolated solution does
860 * not automatically satisfy the boundary values even if the solution before
861 * refinement had the correct boundary values.
862 *
863 * @code
864 *   nonzero_constraints.distribute(current_solution);
865 *   }
866 *  
867 *  
868 *  
869 * @endcode
870 *
871 *
872 * <a name="step_15-MinimalSurfaceProblemcompute_residual"></a>
873 * <h4>MinimalSurfaceProblem::compute_residual</h4>
874 *
875
876 *
877 * In order to monitor convergence, we need a way to compute the norm of the
878 * (discrete) residual, i.e., the norm of the vector
879 * @f$\left<F(u^n),\varphi_i\right>@f$ with @f$F(u)=-\nabla \cdot \left(
880 * \frac{1}{\sqrt{1+|\nabla u|^{2}}}\nabla u \right)@f$ as discussed in the
881 * introduction. It turns out that (although we don't use this feature in
882 * the current version of the program) one needs to compute the residual
883 * @f$\left<F(u^n+\alpha^n\;\delta u^n),\varphi_i\right>@f$ when determining
884 * optimal step lengths, and so this is what we implement here: the function
885 * takes the step length @f$\alpha^n@f$ as an argument. The original
886 * functionality is of course obtained by passing a zero as argument.
887 *
888
889 *
890 * In the function below, we first set up a vector for the residual, and
891 * then a vector for the evaluation point @f$u^n+\alpha^n\;\delta u^n@f$. This
892 * is followed by the same boilerplate code we use for all integration
893 * operations:
894 *
895 * @code
896 *   template <int dim>
897 *   double MinimalSurfaceProblem<dim>::compute_residual(const double alpha) const
898 *   {
899 *   Vector<double> residual(dof_handler.n_dofs());
900 *  
901 *   Vector<double> evaluation_point(dof_handler.n_dofs());
902 *   evaluation_point = current_solution;
903 *   evaluation_point.add(alpha, newton_update);
904 *  
905 *   const QGauss<dim> quadrature_formula(fe.degree + 1);
906 *   FEValues<dim> fe_values(fe,
907 *   quadrature_formula,
908 *   update_gradients | update_quadrature_points |
909 *   update_JxW_values);
910 *  
911 *   const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
912 *   const unsigned int n_q_points = quadrature_formula.size();
913 *  
914 *   Vector<double> cell_residual(dofs_per_cell);
915 *   std::vector<Tensor<1, dim>> gradients(n_q_points);
916 *  
917 *   std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
918 *  
919 *   for (const auto &cell : dof_handler.active_cell_iterators())
920 *   {
921 *   cell_residual = 0;
922 *   fe_values.reinit(cell);
923 *  
924 * @endcode
925 *
926 * The actual computation is much as in
927 * <code>assemble_system()</code>. We first evaluate the gradients of
928 * @f$u^n+\alpha^n\,\delta u^n@f$ at the quadrature points, then compute
929 * the coefficient @f$a_n@f$, and then plug it all into the formula for
930 * the residual:
931 *
932 * @code
933 *   fe_values.get_function_gradients(evaluation_point, gradients);
934 *  
935 *  
936 *   for (unsigned int q = 0; q < n_q_points; ++q)
937 *   {
938 *   const double coeff =
939 *   1. / std::sqrt(1 + gradients[q] * gradients[q]);
940 *  
941 *   for (unsigned int i = 0; i < dofs_per_cell; ++i)
942 *   cell_residual(i) -= (fe_values.shape_grad(i, q) // \nabla \phi_i
943 *   * coeff // * a_n
944 *   * gradients[q] // * \nabla u_n
945 *   * fe_values.JxW(q)); // * dx
946 *   }
947 *  
948 *   cell->get_dof_indices(local_dof_indices);
949 *   zero_constraints.distribute_local_to_global(cell_residual,
950 *   local_dof_indices,
951 *   residual);
952 *   }
953 *  
954 *   return residual.l2_norm();
955 *   }
956 *  
957 *  
958 *  
959 * @endcode
960 *
961 *
962 * <a name="step_15-MinimalSurfaceProblemdetermine_step_length"></a>
963 * <h4>MinimalSurfaceProblem::determine_step_length</h4>
964 *
965
966 *
967 * As discussed in the introduction, Newton's method frequently does not
968 * converge if we always take full steps, i.e., compute @f$u^{n+1}=u^n+\delta
969 * u^n@f$. Rather, one needs a damping parameter (step length) @f$\alpha^n@f$ and
970 * set @f$u^{n+1}=u^n+\alpha^n\delta u^n@f$. This function is the one called
971 * to compute @f$\alpha^n@f$.
972 *
973
974 *
975 * Here, we simply always return 0.1. This is of course a sub-optimal
976 * choice: ideally, what one wants is that the step size goes to one as we
977 * get closer to the solution, so that we get to enjoy the rapid quadratic
978 * convergence of Newton's method. We will discuss better strategies below
979 * in the results section, and @ref step_77 "step-77" also covers this aspect.
980 *
981 * @code
982 *   template <int dim>
983 *   double MinimalSurfaceProblem<dim>::determine_step_length() const
984 *   {
985 *   return 0.1;
986 *   }
987 *  
988 *  
989 *  
990 * @endcode
991 *
992 *
993 * <a name="step_15-MinimalSurfaceProblemoutput_results"></a>
994 * <h4>MinimalSurfaceProblem::output_results</h4>
995 *
996
997 *
998 * This last function to be called from `run()` outputs the current solution
999 * (and the Newton update) in graphical form as a VTU file. It is entirely the
1000 * same as what has been used in previous tutorials.
1001 *
1002 * @code
1003 *   template <int dim>
1004 *   void MinimalSurfaceProblem<dim>::output_results(
1005 *   const unsigned int refinement_cycle) const
1006 *   {
1007 *   DataOut<dim> data_out;
1008 *  
1009 *   data_out.attach_dof_handler(dof_handler);
1010 *   data_out.add_data_vector(current_solution, "solution");
1011 *   data_out.add_data_vector(newton_update, "update");
1012 *   data_out.build_patches();
1013 *  
1014 *   const std::string filename =
1015 *   "solution-" + Utilities::int_to_string(refinement_cycle, 2) + ".vtu";
1016 *   std::ofstream output(filename);
1017 *   data_out.write_vtu(output);
1018 *   }
1019 *  
1020 *  
1021 * @endcode
1022 *
1023 *
1024 * <a name="step_15-MinimalSurfaceProblemrun"></a>
1025 * <h4>MinimalSurfaceProblem::run</h4>
1026 *
1027
1028 *
1029 * In the run function, we build the first grid and then have the top-level
1030 * logic for the Newton iteration.
1031 *
1032
1033 *
1034 * As described in the introduction, the domain is the unit disk around
1035 * the origin, created in the same way as shown in @ref step_6 "step-6". The mesh is
1036 * globally refined twice followed later on by several adaptive cycles.
1037 *
1038
1039 *
1040 * Before starting the Newton loop, we also need to do
1041 * ensure that the first Newton iterate already has the correct
1042 * boundary values, as discussed in the introduction.
1043 *
1044 * @code
1045 *   template <int dim>
1046 *   void MinimalSurfaceProblem<dim>::run()
1047 *   {
1048 *   GridGenerator::hyper_ball(triangulation);
1049 *   triangulation.refine_global(2);
1050 *  
1051 *   setup_system();
1052 *   nonzero_constraints.distribute(current_solution);
1053 *  
1054 * @endcode
1055 *
1056 * The Newton iteration starts next. We iterate until the (norm of the)
1057 * residual computed at the end of the previous iteration is less than
1058 * @f$10^{-3}@f$, as checked at the end of the `do { ... } while` loop that
1059 * starts here. Because we don't have a reasonable value to initialize
1060 * the variable, we just use the largest value that can be represented
1061 * as a `double`.
1062 *
1063 * @code
1064 *   double last_residual_norm = std::numeric_limits<double>::max();
1065 *   unsigned int refinement_cycle = 0;
1066 *   do
1067 *   {
1068 *   std::cout << "Mesh refinement step " << refinement_cycle << std::endl;
1069 *  
1070 *   if (refinement_cycle != 0)
1071 *   refine_mesh();
1072 *  
1073 * @endcode
1074 *
1075 * On every mesh we do exactly five Newton steps. We print the initial
1076 * residual here and then start the iterations on this mesh.
1077 *
1078
1079 *
1080 * In every Newton step the system matrix and the right hand side have
1081 * to be computed first, after which we store the norm of the right
1082 * hand side as the residual to check against when deciding whether to
1083 * stop the iterations. We then solve the linear system (the function
1084 * also updates @f$u^{n+1}=u^n+\alpha^n\;\delta u^n@f$) and output the
1085 * norm of the residual at the end of this Newton step.
1086 *
1087
1088 *
1089 * After the end of this loop, we then also output the solution on the
1090 * current mesh in graphical form and increment the counter for the
1091 * mesh refinement cycle.
1092 *
1093 * @code
1094 *   std::cout << " Initial residual: " << compute_residual(0) << std::endl;
1095 *  
1096 *   for (unsigned int inner_iteration = 0; inner_iteration < 5;
1097 *   ++inner_iteration)
1098 *   {
1099 *   assemble_system();
1100 *   last_residual_norm = system_rhs.l2_norm();
1101 *  
1102 *   solve();
1103 *  
1104 *   std::cout << " Residual: " << compute_residual(0) << std::endl;
1105 *   }
1106 *  
1107 *   output_results(refinement_cycle);
1108 *  
1109 *   ++refinement_cycle;
1110 *   std::cout << std::endl;
1111 *   }
1112 *   while (last_residual_norm > 1e-2);
1113 *   }
1114 *   } // namespace Step15
1115 *  
1116 * @endcode
1117 *
1118 *
1119 * <a name="step_15-Themainfunction"></a>
1120 * <h4>The main function</h4>
1121 *
1122
1123 *
1124 * Finally the main function. This follows the scheme of all other main
1125 * functions:
1126 *
1127 * @code
1128 *   int main()
1129 *   {
1130 *   try
1131 *   {
1132 *   using namespace Step15;
1133 *  
1134 *   MinimalSurfaceProblem<2> problem;
1135 *   problem.run();
1136 *   }
1137 *   catch (std::exception &exc)
1138 *   {
1139 *   std::cerr << std::endl
1140 *   << std::endl
1141 *   << "----------------------------------------------------"
1142 *   << std::endl;
1143 *   std::cerr << "Exception on processing: " << std::endl
1144 *   << exc.what() << std::endl
1145 *   << "Aborting!" << std::endl
1146 *   << "----------------------------------------------------"
1147 *   << std::endl;
1148 *  
1149 *   return 1;
1150 *   }
1151 *   catch (...)
1152 *   {
1153 *   std::cerr << std::endl
1154 *   << std::endl
1155 *   << "----------------------------------------------------"
1156 *   << std::endl;
1157 *   std::cerr << "Unknown exception!" << std::endl
1158 *   << "Aborting!" << std::endl
1159 *   << "----------------------------------------------------"
1160 *   << std::endl;
1161 *   return 1;
1162 *   }
1163 *   return 0;
1164 *   }
1165 * @endcode
1166@anchor step_15-ResultsSection
1167<a name="step_15-Results"></a><h1>Results</h1>
1168
1169
1170
1171The output of the program looks as follows:
1172@code
1173Mesh refinement step 0
1174 Initial residual: 1.53143
1175 Residual: 1.08746
1176 Residual: 0.966748
1177 Residual: 0.859602
1178 Residual: 0.766462
1179 Residual: 0.685475
1180
1181Mesh refinement step 1
1182 Initial residual: 0.868959
1183 Residual: 0.762125
1184 Residual: 0.677792
1185 Residual: 0.605762
1186 Residual: 0.542748
1187 Residual: 0.48704
1188
1189Mesh refinement step 2
1190 Initial residual: 0.426445
1191 Residual: 0.382731
1192 Residual: 0.343865
1193 Residual: 0.30918
1194 Residual: 0.278147
1195 Residual: 0.250327
1196
1197Mesh refinement step 3
1198 Initial residual: 0.282026
1199 Residual: 0.253146
1200 Residual: 0.227414
1201 Residual: 0.20441
1202 Residual: 0.183803
1203 Residual: 0.165319
1204
1205Mesh refinement step 4
1206 Initial residual: 0.154404
1207 Residual: 0.138723
1208 Residual: 0.124694
1209 Residual: 0.112124
1210 Residual: 0.100847
1211 Residual: 0.0907222
1212
1213....
1214@endcode
1215
1216Obviously, the scheme converges, if not very fast. We will come back to
1217strategies for accelerating the method below.
1218
1219One can visualize the solution after each set of five Newton
1220iterations, i.e., on each of the meshes on which we approximate the
1221solution. This yields the following set of images:
1222
1223<div class="twocolumn" style="width: 80%">
1224 <div>
1225 <img src="https://dealii.org/images/steps/developer/step_15_solution_1.png"
1226 alt="Solution after zero cycles with contour lines." width="230" height="273">
1227 </div>
1228 <div>
1229 <img src="https://dealii.org/images/steps/developer/step_15_solution_2.png"
1230 alt="Solution after one cycle with contour lines." width="230" height="273">
1231 </div>
1232 <div>
1233 <img src="https://dealii.org/images/steps/developer/step_15_solution_3.png"
1234 alt="Solution after two cycles with contour lines." width="230" height="273">
1235 </div>
1236 <div>
1237 <img src="https://dealii.org/images/steps/developer/step_15_solution_4.png"
1238 alt="Solution after three cycles with contour lines." width="230" height="273">
1239 </div>
1240</div>
1241
1242It is clearly visible, that the solution minimizes the surface
1243after each refinement. The solution converges to a picture one
1244would imagine a soap bubble to be that is located inside a wire loop
1245that is bent like
1246the boundary. Also it is visible, how the boundary
1247is smoothed out after each refinement. On the coarse mesh,
1248the boundary doesn't look like a sine, whereas it does the
1249finer the mesh gets.
1250
1251The mesh is mostly refined near the boundary, where the solution
1252increases or decreases strongly, whereas it is coarsened on
1253the inside of the domain, where nothing interesting happens,
1254because there isn't much change in the solution. The ninth
1255solution and mesh are shown here:
1256
1257<div class="onecolumn" style="width: 60%">
1258 <div>
1259 <img src="https://dealii.org/images/steps/developer/step_15_solution_9.png"
1260 alt="Grid and solution of the ninth cycle with contour lines." width="507" height="507">
1261 </div>
1262</div>
1263
1264
1265
1266@anchor step_15-Extensions
1267<a name="step_15-Possibilitiesforextensions"></a><h3>Possibilities for extensions</h3>
1268
1269
1270The program shows the basic structure of a solver for a nonlinear, stationary
1271problem. However, it does not converge particularly fast, for good reasons:
1272
1273- The program always takes a step size of 0.1. This precludes the rapid,
1274 quadratic convergence for which Newton's method is typically chosen.
1275- It does not connect the nonlinear iteration with the mesh refinement
1276 iteration.
1277
1278Obviously, a better program would have to address these two points.
1279We will discuss them in the following.
1280
1281
1282<a name="step_15-Steplengthcontrol"></a><h4> Step length control </h4>
1283
1284
1285Newton's method has two well known properties:
1286- It may not converge from arbitrarily chosen starting points. Rather, a
1287 starting point has to be close enough to the solution to guarantee
1288 convergence. However, we can enlarge the area from which Newton's method
1289 converges by damping the iteration using a <i>step length</i> 0<@f$\alpha^n\le
1290 1@f$.
1291- It exhibits rapid convergence of quadratic order if (i) the step length is
1292 chosen as @f$\alpha^n=1@f$, and (ii) it does in fact converge with this choice
1293 of step length.
1294
1295A consequence of these two observations is that a successful strategy is to
1296choose @f$\alpha^n<1@f$ for the initial iterations until the iterate has come
1297close enough to allow for convergence with full step length, at which point we
1298want to switch to @f$\alpha^n=1@f$. The question is how to choose @f$\alpha^n@f$ in an
1299automatic fashion that satisfies these criteria.
1300
1301We do not want to review the literature on this topic here, but only briefly
1302mention that there are two fundamental approaches to the problem: backtracking
1303line search and trust region methods. The former is more widely used for
1304partial differential equations and essentially does the following:
1305- Compute a search direction
1306- See if the resulting residual of @f$u^n + \alpha^n\;\delta u^n@f$ with
1307 @f$\alpha^n=1@f$ is "substantially smaller" than that of @f$u^n@f$ alone.
1308- If so, then take @f$\alpha^n=1@f$.
1309- If not, try whether the residual is "substantially smaller" with
1310 @f$\alpha^n=2/3@f$.
1311- If so, then take @f$\alpha^n=2/3@f$.
1312- If not, try whether the residual is "substantially smaller" with
1313 @f$\alpha^n=(2/3)^2@f$.
1314- Etc.
1315One can of course choose other factors @f$r, r^2, \ldots@f$ than the @f$2/3,
1316(2/3)^2, \ldots@f$ chosen above, for @f$0<r<1@f$. It is obvious where the term
1317"backtracking" comes from: we try a long step, but if that doesn't work we try
1318a shorter step, and ever shorter step, etc. The function
1319<code>determine_step_length()</code> is written the way it is to support
1320exactly this kind of use case.
1321
1322Whether we accept a particular step length @f$\alpha^n@f$ depends on how we define
1323"substantially smaller". There are a number of ways to do so, but without
1324going into detail let us just mention that the most common ones are to use the
1325Wolfe and Armijo-Goldstein conditions. For these, one can show the following:
1326- There is always a step length @f$\alpha^n@f$ for which the conditions are
1327 satisfied, i.e., the iteration never gets stuck as long as the problem is
1328 convex.
1329- If we are close enough to the solution, then the conditions allow for
1330 @f$\alpha^n=1@f$, thereby enabling quadratic convergence.
1331
1332We will not dwell on this here any further but leave the implementation of
1333such algorithms as an exercise. We note, however, that when implemented
1334correctly then it is a common observation that most reasonably nonlinear
1335problems can be solved in anywhere between 5 and 15 Newton iterations to
1336engineering accuracy &mdash; substantially fewer than we need with the current
1337version of the program.
1338
1339More details on globalization methods including backtracking can be found,
1340for example, in @cite GNS08 and @cite NW99.
1341
1342A separate point, very much worthwhile making, however, is that in practice
1343the implementation of efficient nonlinear solvers is about as complicated as
1344the implementation of efficient finite element methods. One should not
1345attempt to reinvent the wheel by implementing all of the necessary steps
1346oneself. Substantial pieces of the puzzle are already available in
1347the LineMinimization::line_search() function and could be used to this end.
1348But, instead, just like building finite element solvers on libraries
1349such as deal.II, one should be building nonlinear solvers on libraries such
1350as [SUNDIALS](https://computing.llnl.gov/projects/sundials). In fact,
1351deal.II has interfaces to SUNDIALS and in particular to its nonlinear solver
1352sub-package KINSOL through the SUNDIALS::KINSOL class. It would not be
1353very difficult to base the current problem on that interface --
1354indeed, that is what @ref step_77 "step-77" does.
1355
1356
1357
1358<a name="step_15-Integratingmeshrefinementandnonlinearandlinearsolvers"></a><h4> Integrating mesh refinement and nonlinear and linear solvers </h4>
1359
1360
1361We currently do exactly 5 iterations on each mesh. But is this optimal? One
1362could ask the following questions:
1363- Maybe it is worthwhile doing more iterations on the initial meshes since
1364 there, computations are cheap.
1365- On the other hand, we do not want to do too many iterations on every mesh:
1366 yes, we could drive the residual to zero on every mesh, but that would only
1367 mean that the nonlinear iteration error is far smaller than the
1368 discretization error.
1369- Should we use solve the linear systems in each Newton step with higher or
1370 lower accuracy?
1371
1372Ultimately, what this boils down to is that we somehow need to couple the
1373discretization error on the current mesh with the nonlinear residual we want
1374to achieve with the Newton iterations on a given mesh, and to the linear
1375iteration we want to achieve with the CG method within each Newton
1376iterations.
1377
1378How to do this is, again, not entirely trivial, and we again leave it as a
1379future exercise.
1380
1381
1382
1383<a name="step_15-UsingautomaticdifferentiationtocomputetheJacobianmatrix"></a><h4> Using automatic differentiation to compute the Jacobian matrix </h4>
1384
1385
1386As outlined in the introduction, when solving a nonlinear problem of
1387the form
1388 @f[
1389 F(u) \dealcoloneq
1390 -\nabla \cdot \left( \frac{1}{\sqrt{1+|\nabla u|^{2}}}\nabla u \right)
1391 = 0
1392 @f]
1393we use a Newton iteration that requires us to repeatedly solve the
1394linear partial differential equation
1395 @f{align*}{
1396 F'(u^{n},\delta u^{n}) &=- F(u^{n})
1397 @f}
1398so that we can compute the update
1399 @f{align*}{
1400 u^{n+1}&=u^{n}+\alpha^n \delta u^{n}
1401 @f}
1402with the solution @f$\delta u^{n}@f$ of the Newton step. For the problem
1403here, we could compute the derivative @f$F'(u,\delta u)@f$ by hand and
1404obtained
1405 @f[
1406 F'(u,\delta u)
1407 =
1408 - \nabla \cdot \left( \frac{1}{\left(1+|\nabla u|^{2}\right)^{\frac{1}{2}}}\nabla
1409 \delta u \right) +
1410 \nabla \cdot \left( \frac{\nabla u \cdot
1411 \nabla \delta u}{\left(1+|\nabla u|^{2}\right)^{\frac{3}{2}}} \nabla u
1412 \right).
1413 @f]
1414But this is already a sizable expression that is cumbersome both to
1415derive and to implement. It is also, in some sense, duplicative: If we
1416implement what @f$F(u)@f$ is somewhere in the code, then @f$F'(u,\delta u)@f$
1417is not an independent piece of information but is something that, at
1418least in principle, a computer should be able to infer itself.
1419Wouldn't it be nice if that could actually happen? That is, if we
1420really only had to implement @f$F(u)@f$, and @f$F'(u,\delta u)@f$ was then somehow
1421done implicitly? That is in fact possible, and runs under the name
1422"automatic differentiation". @ref step_71 "step-71" discusses this very
1423concept in general terms, and @ref step_72 "step-72" illustrates how this can be
1424applied in practice for the very problem we are considering here.
1425
1426
1427<a name="step_15-StoringtheJacobianmatrixinlowerprecisionfloatingpointvariables"></a><h4> Storing the Jacobian matrix in lower-precision floating point variables </h4>
1428
1429
1430On modern computer systems, *accessing* data in main memory takes far
1431longer than *actually doing* something with it: We can do many floating
1432point operations for the time it takes to load one floating point
1433number from memory onto the processor. Unfortunately, when we do things
1434such as matrix-vector products, we only multiply each matrix entry once
1435with another number (the corresponding entry of the vector) and then we
1436add it to something else -- so two floating point operations for one
1437load. (Strictly speaking, we also have to load the corresponding vector
1438entry, but at least sometimes we get to re-use that vector entry in
1439doing the products that correspond to the next row of the matrix.) This
1440is a fairly low "arithmetic intensity", and consequently we spend most
1441of our time during matrix-vector products waiting for data to arrive
1442from memory rather than actually doing floating point operations.
1443
1444This is of course one of the rationales for the "matrix-free" approach to
1445solving linear systems (see @ref step_37 "step-37", for example). But if you don't quite
1446want to go all that way to change the structure of the program, then
1447here is a different approach: Storing the system matrix (the "Jacobian")
1448in single-precision instead of double precision floating point numbers
1449(i.e., using `float` instead of `double` as the data type). This reduces
1450the amount of memory necessary by a factor of 1.5 (each matrix entry
1451in a SparseMatrix object requires storing the column index -- 4 bytes --
1452and the actual value -- either 4 or 8 bytes), and consequently
1453will speed up matrix-vector products by a factor of around 1.5 as well because,
1454as pointed out above, most of the time is spent loading data from memory
1455and loading 2/3 the amount of data should be roughly 3/2 times as fast. All
1456of this could be done using SparseMatrix<float> as the data type
1457for the system matrix. (In principle, we would then also like it if
1458the SparseDirectUMFPACK solver we use in this program computes and
1459stores its sparse decomposition in `float` arithmetic. This is not
1460currently implemented, though could be done.)
1461
1462Of course, there is a downside to this: Lower precision data storage
1463also implies that we will not solve the linear system of the Newton
1464step as accurately as we might with `double` precision. At least
1465while we are far away from the solution of the nonlinear problem,
1466this may not be terrible: If we can do a Newton iteration in half
1467the time, we can afford to do a couple more Newton steps if the
1468search directions aren't as good.
1469But it turns out that even that isn't typically necessary: Both
1470theory and computational experience shows that it is entirely
1471sufficient to store the Jacobian matrix in single precision
1472*as long as one stores the right hand side in double precision*.
1473A great overview of why this is so, along with numerical
1474experiments that also consider "half precision" floating point
1475numbers, can be found in @cite Kelley2022 .
1476 *
1477 *
1478<a name="step_15-PlainProg"></a>
1479<h1> The plain program</h1>
1480@include "step-15.cc"
1481*/
*  iterator end()
*  const Number height
*  *  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 &parameters=AdditionalData())
virtual bool prepare_coarsening_and_refinement()
Point< 2 > second
Definition grid_out.cc:4640
Point< 2 > first
Definition grid_out.cc:4639
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())
Definition loop.h:562
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
Definition mpi.cc:734
std::size_t size
Definition mpi.cc:733
const Event initial
Definition event.cc:69
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.)
Definition advection.h:72
double norm(const FEValuesBase< dim > &fe, const ArrayView< const std::vector< Tensor< 1, dim > > > &Du)
Definition divergence.h:469
Point< spacedim > point(const gp_Pnt &p, const double tolerance=1e-10)
Definition utilities.cc:210
Tensor< 2, dim, Number > F(const Tensor< 2, dim, Number > &Grad_u)
*  *  if(update_pressure &update_flags) *  compute_pressure(constitutive_request
void interpolate_boundary_values(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const std::map< types::boundary_id, const Function< spacedim, number > * > &function_map, std::map< types::global_dof_index, number > &boundary_values, const ComponentMask &component_mask={})
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)
constexpr double PI
Definition numbers.h:240
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)