554 * <a name=
"ThecodeMinimalSurfaceProblemcodeclassimplementation"></a>
560 * <a name=
"MinimalSurfaceProblemMinimalSurfaceProblem"></a>
582 * <a name=
"MinimalSurfaceProblemsetup_system"></a>
592 * argument passed to this function thus indicates whether we can
593 * distributed degrees of freedom (plus compute constraints) and set the
594 * solution vector to zero or whether this has happened elsewhere already
595 * (specifically, in <code>refine_mesh()</code>).
602 * void MinimalSurfaceProblem<dim>::setup_system(const bool initial_step)
606 * dof_handler.distribute_dofs(fe);
607 * current_solution.reinit(dof_handler.n_dofs());
609 * hanging_node_constraints.clear();
610 * DoFTools::make_hanging_node_constraints(dof_handler,
611 * hanging_node_constraints);
612 * hanging_node_constraints.close();
618 * The remaining parts of the function are the same as in @ref step_6 "step-6".
624 * newton_update.reinit(dof_handler.n_dofs());
625 * system_rhs.reinit(dof_handler.n_dofs());
627 * DynamicSparsityPattern dsp(dof_handler.n_dofs());
628 * DoFTools::make_sparsity_pattern(dof_handler, dsp);
630 * hanging_node_constraints.condense(dsp);
632 * sparsity_pattern.copy_from(dsp);
633 * system_matrix.reinit(sparsity_pattern);
639 * <a name="MinimalSurfaceProblemassemble_system"></a>
640 * <h4>MinimalSurfaceProblem::assemble_system</h4>
644 * This function does the same as in the previous tutorials except that now,
645 * of course, the matrix and right hand side functions depend on the
672 *
const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
680 *
std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
682 *
for (
const auto &cell : dof_handler.active_cell_iterators())
693 * points. There is a standard way of doing this: the
694 * FEValues::get_function_gradients function takes a vector that
695 * represents a finite element field defined on a DoFHandler, and
696 * evaluates the gradients of this field at the quadrature points of the
697 * cell with which the FEValues object has last been reinitialized.
698 * The values of the gradients at all quadrature points are then written
699 * into the second argument:
702 * fe_values.get_function_gradients(current_solution,
703 * old_solution_gradients);
707 * With this, we can then do the integration loop over all quadrature
708 * points and shape functions. Having just computed the gradients of
709 * the old solution in the quadrature points, we are able to compute
710 * the coefficients @f$a_{n}@f$ in these points. The assembly of the
711 * system itself then looks similar to what we always do with the
712 * exception of the nonlinear terms, as does copying the results from
713 * the local objects into the global ones:
716 * for (unsigned int q = 0; q < n_q_points; ++q)
718 * const double coeff =
719 * 1.0 / std::sqrt(1 + old_solution_gradients[q] *
720 * old_solution_gradients[q]);
722 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
724 * for (unsigned int j = 0; j < dofs_per_cell; ++j)
725 * cell_matrix(i, j) +=
726 * (((fe_values.shape_grad(i, q) // ((\nabla \phi_i
728 * * fe_values.shape_grad(j, q)) // * \nabla \phi_j)
730 * (fe_values.shape_grad(i, q) // (\nabla \phi_i
731 * * coeff * coeff * coeff // * a_n^3
732 * * (fe_values.shape_grad(j, q) // * (\nabla \phi_j
733 * * old_solution_gradients[q]) // * \nabla u_n)
734 * * old_solution_gradients[q])) // * \nabla u_n)))
735 * * fe_values.JxW(q)); // * dx
737 * cell_rhs(i) -= (fe_values.shape_grad(i, q) // \nabla \phi_i
739 * * old_solution_gradients[q] // * \nabla u_n
740 * * fe_values.JxW(q)); // * dx
744 * cell->get_dof_indices(local_dof_indices);
745 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
747 * for (unsigned int j = 0; j < dofs_per_cell; ++j)
748 * system_matrix.add(local_dof_indices[i],
749 * local_dof_indices[j],
750 * cell_matrix(i, j));
752 * system_rhs(local_dof_indices[i]) += cell_rhs(i);
758 * Finally, we remove hanging nodes from the system and apply zero
759 * boundary values to the linear system that defines the Newton updates
763 * hanging_node_constraints.condense(system_matrix);
764 * hanging_node_constraints.condense(system_rhs);
766 * std::map<types::global_dof_index, double> boundary_values;
767 * VectorTools::interpolate_boundary_values(dof_handler,
769 * Functions::ZeroFunction<dim>(),
771 * MatrixTools::apply_boundary_values(boundary_values,
782 * <a name="MinimalSurfaceProblemsolve"></a>
783 * <h4>MinimalSurfaceProblem::solve</h4>
787 * The solve function is the same as always. At the end of the solution
788 * process we update the current solution by setting
789 * @f$u^{n+1}=u^n+\alpha^n\;\delta u^n@f$.
793 * void MinimalSurfaceProblem<dim>::solve()
795 * SolverControl solver_control(system_rhs.size(),
796 * system_rhs.l2_norm() * 1e-6);
797 * SolverCG<Vector<double>> solver(solver_control);
799 * PreconditionSSOR<SparseMatrix<double>> preconditioner;
800 * preconditioner.initialize(system_matrix, 1.2);
802 * solver.solve(system_matrix, newton_update, system_rhs, preconditioner);
804 * hanging_node_constraints.distribute(newton_update);
806 * const double alpha = determine_step_length();
807 * current_solution.add(alpha, newton_update);
814 * <a name="MinimalSurfaceProblemrefine_mesh"></a>
815 * <h4>MinimalSurfaceProblem::refine_mesh</h4>
819 * The first part of this function is the same as in @ref step_6 "step-6"... However,
820 * after refining the mesh we have to transfer the old solution to the new
821 * one which we do with the help of the SolutionTransfer class. The process
822 * is slightly convoluted, so let us describe it in detail:
826 * void MinimalSurfaceProblem<dim>::refine_mesh()
828 * Vector<float> estimated_error_per_cell(triangulation.n_active_cells());
830 * KellyErrorEstimator<dim>::estimate(
832 * QGauss<dim - 1>(fe.degree + 1),
833 * std::map<types::boundary_id, const Function<dim> *>(),
835 * estimated_error_per_cell);
837 * GridRefinement::refine_and_coarsen_fixed_number(triangulation,
838 * estimated_error_per_cell,
844 * Then we need an additional step: if, for example, you flag a cell that
845 * is once more refined than its neighbor, and that neighbor is not
846 * flagged for refinement, we would end up with a jump of two refinement
847 * levels across a cell interface. To avoid these situations, the library
848 * will silently also have to refine the neighbor cell once. It does so by
849 * calling the Triangulation::prepare_coarsening_and_refinement function
850 * before actually doing the refinement and coarsening. This function
851 * flags a set of additional cells for refinement or coarsening, to
852 * enforce rules like the one-hanging-node rule. The cells that are
853 * flagged for refinement and coarsening after calling this function are
854 * exactly the ones that will actually be refined or coarsened. Usually,
878 *
dof_handler.distribute_dofs(fe);
899 *
current solution
's vector entries satisfy the hanging node constraints
900 * (see the discussion in the documentation of the SolutionTransfer class
901 * for why this is necessary). We could do this by calling
902 * `hanging_node_constraints.distribute(current_solution)` explicitly; we
903 * omit this step because this will happen at the end of the call to
904 * `set_boundary_values()` below, and it is not necessary to do it twice.
907 * hanging_node_constraints.clear();
909 * DoFTools::make_hanging_node_constraints(dof_handler,
910 * hanging_node_constraints);
911 * hanging_node_constraints.close();
915 * Once we have the interpolated solution and all information about
916 * hanging nodes, we have to make sure that the @f$u^n@f$ we now have
917 * actually has the correct boundary values. As explained at the end of
918 * the introduction, this is not automatically the case even if the
919 * solution before refinement had the correct boundary values, and so we
920 * have to explicitly make sure that it now has:
923 * set_boundary_values();
927 * We end the function by updating all the remaining data structures,
928 * indicating to <code>setup_dofs()</code> that this is not the first
929 * go-around and that it needs to preserve the content of the solution
933 * setup_system(false);
941 * <a name="MinimalSurfaceProblemset_boundary_values"></a>
942 * <h4>MinimalSurfaceProblem::set_boundary_values</h4>
946 * The next function ensures that the solution vector's entries
respect the
952 * solution vector
explicit to the right
value.
981 * <a name=
"MinimalSurfaceProblemcompute_residual"></a>
982 * <
h4>MinimalSurfaceProblem::compute_residual</
h4>
991 * the current version of the program) one needs to compute the residual
992 * @f$\left<F(u^n+\alpha^n\;\delta u^n),\varphi_i\right>@f$ when determining
993 * optimal step lengths, and so this is what we implement here: the function
994 * takes the step length @f$\alpha^n@f$ as an argument. The original
995 * functionality is of course obtained by passing a zero as argument.
999 * In the function below, we first set up a vector for the residual, and
1000 * then a vector for the evaluation point @f$u^n+\alpha^n\;\delta u^n@f$. This
1001 * is followed by the same boilerplate code we use for all integration
1005 * template <int dim>
1006 * double MinimalSurfaceProblem<dim>::compute_residual(const double alpha) const
1008 * Vector<double> residual(dof_handler.n_dofs());
1010 * Vector<double> evaluation_point(dof_handler.n_dofs());
1011 * evaluation_point = current_solution;
1012 * evaluation_point.add(alpha, newton_update);
1014 * const QGauss<dim> quadrature_formula(fe.degree + 1);
1015 * FEValues<dim> fe_values(fe,
1016 * quadrature_formula,
1017 * update_gradients | update_quadrature_points |
1018 * update_JxW_values);
1020 * const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
1021 * const unsigned int n_q_points = quadrature_formula.size();
1023 * Vector<double> cell_residual(dofs_per_cell);
1024 * std::vector<Tensor<1, dim>> gradients(n_q_points);
1026 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
1028 * for (const auto &cell : dof_handler.active_cell_iterators())
1030 * cell_residual = 0;
1031 * fe_values.reinit(cell);
1035 * The actual computation is much as in
1036 * <code>assemble_system()</code>. We first evaluate the gradients of
1037 * @f$u^n+\alpha^n\,\delta u^n@f$ at the quadrature points, then compute
1038 * the coefficient @f$a_n@f$, and then plug it all into the formula for
1042 * fe_values.get_function_gradients(evaluation_point, gradients);
1045 * for (unsigned int q = 0; q < n_q_points; ++q)
1047 * const double coeff =
1048 * 1. / std::sqrt(1 + gradients[q] * gradients[q]);
1050 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
1051 * cell_residual(i) -= (fe_values.shape_grad(i, q) // \nabla \phi_i
1053 * * gradients[q] // * \nabla u_n
1054 * * fe_values.JxW(q)); // * dx
1057 * cell->get_dof_indices(local_dof_indices);
1058 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
1059 * residual(local_dof_indices[i]) += cell_residual(i);
1064 * At the end of this function we also have to deal with the hanging node
1065 * constraints and with the issue of boundary values. With regard to the
1066 * latter, we have to set to zero the elements of the residual vector for
1067 * all entries that correspond to degrees of freedom that sit at the
1068 * boundary. The reason is that because the value of the solution there is
1069 * fixed, they are of course no "real" degrees of freedom and so, strictly
1073 * of whether a particular degree of freedom sits at the boundary in the
1074 * integration above. Rather, we will simply set to zero these entries
1075 * after the fact. To this end, we need to determine which degrees
1076 * of freedom do in fact belong to the boundary and then loop over all of
1077 * those and set the residual entry to zero. This happens in the following
1078 * lines which we have already seen used in @ref step_11 "step-11", using the appropriate
1079 * function from namespace DoFTools:
1082 * hanging_node_constraints.condense(residual);
1084 * for (const types::global_dof_index i :
1085 * DoFTools::extract_boundary_dofs(dof_handler))
1090 * At the end of the function, we return the norm of the residual:
1093 * return residual.l2_norm();
1101 * <a name="MinimalSurfaceProblemdetermine_step_length"></a>
1102 * <h4>MinimalSurfaceProblem::determine_step_length</h4>
1117 *
convergence of Newton
's method. We will discuss better strategies below
1118 * in the results section, and @ref step_77 "step-77" also covers this aspect.
1121 * template <int dim>
1122 * double MinimalSurfaceProblem<dim>::determine_step_length() const
1132 * <a name="MinimalSurfaceProblemoutput_results"></a>
1133 * <h4>MinimalSurfaceProblem::output_results</h4>
1137 * This last function to be called from `run()` outputs the current solution
1138 * (and the Newton update) in graphical form as a VTU file. It is entirely the
1139 * same as what has been used in previous tutorials.
1142 * template <int dim>
1143 * void MinimalSurfaceProblem<dim>::output_results(
1144 * const unsigned int refinement_cycle) const
1146 * DataOut<dim> data_out;
1148 * data_out.attach_dof_handler(dof_handler);
1149 * data_out.add_data_vector(current_solution, "solution");
1150 * data_out.add_data_vector(newton_update, "update");
1151 * data_out.build_patches();
1153 * const std::string filename =
1154 * "solution-" + Utilities::int_to_string(refinement_cycle, 2) + ".vtu";
1155 * std::ofstream output(filename);
1156 * data_out.write_vtu(output);
1163 * <a name="MinimalSurfaceProblemrun"></a>
1164 * <h4>MinimalSurfaceProblem::run</h4>
1168 * In the run function, we build the first grid and then have the top-level
1169 * logic for the Newton iteration.
1173 * As described in the introduction, the domain is the unit disk around
1174 * the origin, created in the same way as shown in @ref step_6 "step-6". The mesh is
1175 * globally refined twice followed later on by several adaptive cycles.
1179 * Before starting the Newton loop, we also need to do a bit of
1180 * setup work: We need to create the basic data structures and
1181 * ensure that the first Newton iterate already has the correct
1182 * boundary values, as discussed in the introduction.
1185 * template <int dim>
1186 * void MinimalSurfaceProblem<dim>::run()
1188 * GridGenerator::hyper_ball(triangulation);
1189 * triangulation.refine_global(2);
1191 * setup_system(/*first time=*/true);
1192 * set_boundary_values();
1196 * The Newton iteration starts next. We iterate until the (norm of the)
1197 * residual computed at the end of the previous iteration is less than
1198 * @f$10^{-3}@f$, as checked at the end of the `do { ... } while` loop that
1234 *
std::cout <<
" Initial residual: " << compute_residual(0) << std::endl;
1244 *
std::cout <<
" Residual: " << compute_residual(0) << std::endl;
1250 *
std::cout << std::endl;
1259 * <a name=
"Themainfunction"></a>
1272 *
using namespace Step15;
1277 *
catch (std::exception &exc)
1279 *
std::cerr << std::endl
1281 *
<<
"----------------------------------------------------"
1283 *
std::cerr <<
"Exception on processing: " << std::endl
1284 *
<< exc.what() << std::endl
1285 *
<<
"Aborting!" << std::endl
1286 *
<<
"----------------------------------------------------"
1293 *
std::cerr << std::endl
1295 *
<<
"----------------------------------------------------"
1297 *
std::cerr <<
"Unknown exception!" << std::endl
1298 *
<<
"Aborting!" << std::endl
1299 *
<<
"----------------------------------------------------"
1362<
div class=
"twocolumn" style=
"width: 80%">
1364 <
img src=
"https://www.dealii.org/images/steps/developer/step_15_solution_1.png"
1365 alt=
"Solution after zero cycles with contour lines." width=
"230" height=
"273">
1368 <
img src=
"https://www.dealii.org/images/steps/developer/step_15_solution_2.png"
1369 alt=
"Solution after one cycle with contour lines." width=
"230" height=
"273">
1372 <
img src=
"https://www.dealii.org/images/steps/developer/step_15_solution_3.png"
1373 alt=
"Solution after two cycles with contour lines." width=
"230" height=
"273">
1376 <
img src=
"https://www.dealii.org/images/steps/developer/step_15_solution_4.png"
1377 alt=
"Solution after three cycles with contour lines." width=
"230" height=
"273">
1387the boundary
doesn't look like a sine, whereas it does the
1390The mesh is mostly refined near the boundary, where the solution
1391increases or decreases strongly, whereas it is coarsened on
1392the inside of the domain, where nothing interesting happens,
1396<
div class=
"onecolumn" style=
"width: 60%">
1398 <
img src=
"https://www.dealii.org/images/steps/developer/step_15_solution_9.png"
1399 alt=
"Grid and solution of the ninth cycle with contour lines." width=
"507" height=
"507">
1405<a name=
"extensions"></a>
1406<a name=
"Possibilitiesforextensions"></a><
h3>Possibilities
for extensions</
h3>
1414- It does not connect the nonlinear iteration with the mesh refinement
1417Obviously, a better program would have to address these two points.
1418We will discuss them in the following.
1421<a name="Steplengthcontrol"></a><h4> Step length control </h4>
1428 converges by damping the iteration using a <i>step length</i> 0<@f$\alpha^n\le
1430- It exhibits rapid convergence of quadratic order if (i) the step length is
1431 chosen as @f$\alpha^n=1@f$, and (ii) it does in fact converge with this choice
1434A consequence of these two observations is that a successful strategy is to
1435choose @f$\alpha^n<1@f$ for the initial iterations until the iterate has come
1436close enough to allow for convergence with full step length, at which point we
1437want to switch to @f$\alpha^n=1@f$. The question is how to choose @f$\alpha^n@f$ in an
1438automatic fashion that satisfies these criteria.
1440We do not want to review the literature on this topic here, but only briefly
1441mention that there are two fundamental approaches to the problem: backtracking
1442line search and trust region methods. The former is more widely used for
1443partial differential equations and essentially does the following:
1444- Compute a search direction
1445- See if the resulting residual of @f$u^n + \alpha^n\;\delta u^n@f$ with
1446 @f$\alpha^n=1@f$ is "substantially smaller" than that of @f$u^n@f$ alone.
1447- If so, then take @f$\alpha^n=1@f$.
1448- If not, try whether the residual is "substantially smaller" with
1450- If so, then take @f$\alpha^n=2/3@f$.
1451- If not, try whether the residual is "substantially smaller" with
1452 @f$\alpha^n=(2/3)^2@f$.
1454One can of course choose other factors @f$r, r^2, \ldots@f$ than the @f$2/3,
1455(2/3)^2, \ldots@f$ chosen above, for @f$0<r<1@f$. It is obvious where the term
1456"backtracking" comes from: we try a long step, but if that doesn't work
we try
1535 F'(u^{n},\delta u^{n}) &=- F(u^{n})
1537so that we can compute the update
1539 u^{n+1}&=u^{n}+\alpha^n \delta u^{n}
1541with the solution @f$\delta u^{n}@f$ of the Newton step. For the problem
1542here, we could compute the derivative @f$F'(
u,\delta
u)@
f$ by hand and
1547 - \nabla \cdot \left( \frac{1}{\left(1+|\nabla u|^{2}\right)^{\frac{1}{2}}}\nabla
1549 \nabla \cdot \left( \frac{\nabla u \cdot
1550 \nabla \delta u}{\left(1+|\nabla u|^{2}\right)^{\frac{3}{2}}} \nabla u
1553But this is already a sizable expression that is cumbersome both to
1554derive and to implement. It is also, in some sense, duplicative: If we
1555implement what @f$F(u)@f$ is somewhere in the code, then @f$F'(
u,\delta
u)@
f$
1558Wouldn
't it be nice if that could actually happen? That is, if we
1585want to go all that way to change the structure of the program, then
1586here is a different approach: Storing the system matrix (the "Jacobian")
1587in single-precision instead of double precision floating point numbers
1588(i.e., using `float` instead of `double` as the data type). This reduces
1589the amount of memory necessary by a factor of 1.5 (each matrix entry
1590in a SparseMatrix object requires storing the column index -- 4 bytes --
1591and the actual value -- either 4 or 8 bytes), and consequently
1592will speed up matrix-vector products by a factor of around 1.5 as well because,
1593as pointed out above, most of the time is spent loading data from memory
1594and loading 2/3 the amount of data should be roughly 3/2 times as fast. All
1595of this could be done using SparseMatrix<float> as the data type
1596for the system matrix. (In principle, we would then also like it if
1597the SparseDirectUMFPACK solver we use in this program computes and
1598stores its sparse decomposition in `float` arithmetic. This is not
1599currently implemented, though could be done.)
1601Of course, there is a downside to this: Lower precision data storage
1602also implies that we will not solve the linear system of the Newton
1603step as accurately as we might with `double` precision. At least
1604while we are far away from the solution of the nonlinear problem,
1605this may not be terrible: If we can do a Newton iteration in half
1606the time, we can afford to do a couple more Newton steps if the
1607search directions aren't
as good.
1609theory and computational experience shows that it is entirely
1610sufficient to store the Jacobian matrix in single precision
1611*as long as one stores the right hand side in double precision*.
1612A great overview of why this is so, along with numerical
1613experiments that also consider "half precision" floating point
1614numbers, can be found in @cite Kelley2022 .
1617<a name="PlainProg"></a>
1618<h1> The plain program</h1>
1619@include "step-15.cc"
value_type * data() const noexcept
void reinit(value_type *starting_element, const std::size_t n_elements)
virtual void execute_coarsening_and_refinement()
__global__ void set(Number *val, const Number s, const size_type N)
void loop(ITERATOR begin, std_cxx20::type_identity_t< ITERATOR > end, DOFINFO &dinfo, INFOBOX &info, const std::function< void(DOFINFO &, typename INFOBOX::CellInfo &)> &cell_worker, const std::function< void(DOFINFO &, typename INFOBOX::CellInfo &)> &boundary_worker, const std::function< void(DOFINFO &, DOFINFO &, typename INFOBOX::CellInfo &, typename INFOBOX::CellInfo &)> &face_worker, ASSEMBLER &assembler, const LoopControl &lctrl=LoopControl())
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
@ update_quadrature_points
Transformed quadrature points.
std::vector< value_type > preserve(const typename ::Triangulation< dim, spacedim >::cell_iterator &parent, const value_type parent_value)
void approximate(SynchronousIterators< std::tuple< typename DoFHandler< dim, spacedim >::active_cell_iterator, Vector< float >::iterator > > const &cell, const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof_handler, const InputVector &solution, const unsigned int component)
@ matrix
Contents is actually a matrix.
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)
void call(const std::function< RT()> &function, internal::return_value< RT > &ret_val)
bool check(const ConstraintKinds kind_in, const unsigned int dim)
int(&) functions(const void *v1, const void *v2)
static constexpr double PI
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)
const ::parallel::distributed::Triangulation< dim, spacedim > * triangulation