680 *
const unsigned int = 0) const override
689 * hessian_list(
const std::vector<
Point<dim>> & points,
691 *
const unsigned int = 0) const override
693 *
for (
unsigned i = 0; i < points.size(); ++i)
695 *
const double x = points[i][0];
696 *
const double y = points[i][1];
707 *
class RightHandSide :
public Function<dim>
710 *
static_assert(dim == 2,
"Only dim==2 is implemented");
713 *
const unsigned int = 0) const override
727 * <a name=
"Themainclass"></a>
728 * <h3>The main
class</h3>
732 * The following is the principal
class of this tutorial program. It has
733 * the structure of many of the other tutorial programs and there should
734 * really be
nothing particularly surprising about its contents or
735 * the constructor that follows it.
739 *
class BiharmonicProblem
742 * BiharmonicProblem(
const unsigned int fe_degree);
748 *
void setup_system();
749 *
void assemble_system();
751 *
void compute_errors();
752 *
void output_results(
const unsigned int iteration)
const;
772 * BiharmonicProblem<dim>::BiharmonicProblem(
const unsigned int fe_degree)
783 * unit square) and
set up the constraints, vectors, and matrices on
784 * each mesh. Again, both of these are essentially unchanged from many
785 * previous tutorial programs.
789 *
void BiharmonicProblem<dim>::make_grid()
794 * std::cout <<
"Number of active cells: " <<
triangulation.n_active_cells()
803 *
void BiharmonicProblem<dim>::setup_system()
805 * dof_handler.distribute_dofs(fe);
807 * std::cout <<
" Number of degrees of freedom: " << dof_handler.n_dofs()
810 * constraints.clear();
815 * ExactSolution::Solution<dim>(),
817 * constraints.close();
822 * sparsity_pattern.copy_from(dsp);
823 * system_matrix.reinit(sparsity_pattern);
825 * solution.reinit(dof_handler.n_dofs());
826 * system_rhs.reinit(dof_handler.n_dofs());
834 * <a name=
"Assemblingthelinearsystem"></a>
835 * <h4>Assembling the linear system</h4>
839 * The following pieces of code are more interesting. They all relate to the
840 * assembly of the linear system. While assembling the cell-interior terms
841 * is not of great difficulty -- that works in essence like the assembly
842 * of the corresponding terms of the Laplace equation, and you have seen
843 * how
this works in @ref step_4
"step-4" or @ref step_6
"step-6",
for example -- the difficulty
844 * is with the penalty terms in the formulation. These require the evaluation
846 * one would therefore need to use two
FEFaceValues objects, but
if one of the
847 * two sides is adaptively refined, then one actually needs an
FEFaceValues
849 * shape
functions live where, and
finally we need to ensure that every
850 * face is visited only once. All of
this is a substantial overhead to the
851 * logic we really want to implement (namely the penalty terms in the
852 * bilinear form). As a consequence, we will make use of the
855 * directly access what we really care about: jumps, averages, etc.
859 * But
this doesn
't yet solve our problem of having to keep track of
860 * which faces we have already visited when we loop over all cells and
861 * all of their faces. To make this process simpler, we use the
862 * MeshWorker::mesh_loop() function that provides a simple interface
863 * for this task: Based on the ideas outlined in the WorkStream
864 * namespace documentation, MeshWorker::mesh_loop() requires three
865 * functions that do work on cells, interior faces, and boundary
866 * faces. These functions work on scratch objects for intermediate
867 * results, and then copy the result of their computations into
868 * copy data objects from where a copier function copies them into
869 * the global matrix and right hand side objects.
873 * The following structures then provide the scratch and copy objects
874 * that are necessary for this approach. You may look up the WorkStream
875 * namespace as well as the
876 * @ref threads "Parallel computing with multiple processors"
877 * module for more information on how they typically work.
883 * ScratchData(const Mapping<dim> & mapping,
884 * const FiniteElement<dim> &fe,
885 * const unsigned int quadrature_degree,
886 * const UpdateFlags update_flags,
887 * const UpdateFlags interface_update_flags)
888 * : fe_values(mapping, fe, QGauss<dim>(quadrature_degree), update_flags)
889 * , fe_interface_values(mapping,
891 * QGauss<dim - 1>(quadrature_degree),
892 * interface_update_flags)
896 * ScratchData(const ScratchData<dim> &scratch_data)
897 * : fe_values(scratch_data.fe_values.get_mapping(),
898 * scratch_data.fe_values.get_fe(),
899 * scratch_data.fe_values.get_quadrature(),
900 * scratch_data.fe_values.get_update_flags())
901 * , fe_interface_values(scratch_data.fe_values.get_mapping(),
902 * scratch_data.fe_values.get_fe(),
903 * scratch_data.fe_interface_values.get_quadrature(),
904 * scratch_data.fe_interface_values.get_update_flags())
907 * FEValues<dim> fe_values;
908 * FEInterfaceValues<dim> fe_interface_values;
915 * CopyData(const unsigned int dofs_per_cell)
916 * : cell_matrix(dofs_per_cell, dofs_per_cell)
917 * , cell_rhs(dofs_per_cell)
918 * , local_dof_indices(dofs_per_cell)
922 * CopyData(const CopyData &) = default;
925 * CopyData(CopyData &&) = default;
928 * ~CopyData() = default;
931 * CopyData &operator=(const CopyData &) = default;
934 * CopyData &operator=(CopyData &&) = default;
939 * FullMatrix<double> cell_matrix;
940 * std::vector<types::global_dof_index> joint_dof_indices;
943 * FullMatrix<double> cell_matrix;
944 * Vector<double> cell_rhs;
945 * std::vector<types::global_dof_index> local_dof_indices;
946 * std::vector<FaceData> face_data;
953 * The more interesting part is where we actually assemble the linear system.
954 * Fundamentally, this function has five parts:
955 * - The definition of the `cell_worker` lambda function, a small
956 * function that is defined within the `assemble_system()`
957 * function and that will be responsible for computing the local
958 * integrals on an individual cell. It will work on a copy of the
959 * `ScratchData` class and put its results into the corresponding
961 * - The definition of the `face_worker` lambda function that does
962 * the integration of all terms that live on the interfaces between
964 * - The definition of the `boundary_worker` function that does the
965 * same but for cell faces located on the boundary of the domain.
966 * - The definition of the `copier` function that is responsible
967 * for copying all of the data the previous three functions have
968 * put into copy objects for a single cell, into the global matrix
969 * and right hand side.
973 * The fifth part is the one where we bring all of this together.
977 * Let us go through each of these pieces necessary for the assembly
982 * void BiharmonicProblem<dim>::assemble_system()
984 * using Iterator = typename DoFHandler<dim>::active_cell_iterator;
988 * The first piece is the `cell_worker` that does the assembly
989 * on the cell interiors. It is a (lambda) function that takes
990 * a cell (input), a scratch object, and a copy object (output)
991 * as arguments. It looks like the assembly functions of many
992 * other of the tutorial programs, or at least the body of the
993 * loop over all cells.
997 * The terms we integrate here are the cell contribution
999 * A^K_{ij} = \int_K \nabla^2\varphi_i(x) : \nabla^2\varphi_j(x) dx
1001 * to the global matrix, and
1003 * f^K_i = \int_K \varphi_i(x) f(x) dx
1005 * to the right hand side vector.
1009 * We use the same technique as used in the assembly of @ref step_22 "step-22"
1010 * to accelerate the function: Instead of calling
1011 * `fe_values.shape_hessian(i, qpoint)` in the innermost loop,
1012 * we create a variable `hessian_i` that evaluates this
1013 * value once in the loop over `i` and re-use the so-evaluated
1014 * value in the loop over `j`. For symmetry, we do the same with a
1015 * variable `hessian_j`, although it is indeed only used once and
1016 * we could have left the call to `fe_values.shape_hessian(j,qpoint)`
1017 * in the instruction that computes the scalar product between
1021 * auto cell_worker = [&](const Iterator & cell,
1022 * ScratchData<dim> &scratch_data,
1023 * CopyData & copy_data) {
1024 * copy_data.cell_matrix = 0;
1025 * copy_data.cell_rhs = 0;
1027 * FEValues<dim> &fe_values = scratch_data.fe_values;
1028 * fe_values.reinit(cell);
1030 * cell->get_dof_indices(copy_data.local_dof_indices);
1032 * const ExactSolution::RightHandSide<dim> right_hand_side;
1034 * const unsigned int dofs_per_cell =
1035 * scratch_data.fe_values.get_fe().n_dofs_per_cell();
1037 * for (unsigned int qpoint = 0; qpoint < fe_values.n_quadrature_points;
1040 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
1042 * const Tensor<2, dim> &hessian_i =
1043 * fe_values.shape_hessian(i, qpoint);
1045 * for (unsigned int j = 0; j < dofs_per_cell; ++j)
1047 * const Tensor<2, dim> &hessian_j =
1048 * fe_values.shape_hessian(j, qpoint);
1050 * copy_data.cell_matrix(i, j) +=
1051 * scalar_product(hessian_i, // nabla^2 phi_i(x)
1052 * hessian_j) * // nabla^2 phi_j(x)
1053 * fe_values.JxW(qpoint); // dx
1056 * copy_data.cell_rhs(i) +=
1057 * fe_values.shape_value(i, qpoint) * // phi_i(x)
1058 * right_hand_side.value(
1059 * fe_values.quadrature_point(qpoint)) * // f(x)
1060 * fe_values.JxW(qpoint); // dx
1068 * The next building block is the one that assembles penalty terms on each
1069 * of the interior faces of the mesh. As described in the documentation of
1070 * MeshWorker::mesh_loop(), this function receives arguments that denote
1071 * a cell and its neighboring cell, as well as (for each of the two
1072 * cells) the face (and potentially sub-face) we have to integrate
1073 * over. Again, we also get a scratch object, and a copy object
1074 * for putting the results in.
1078 * The function has three parts itself. At the top, we initialize
1079 * the FEInterfaceValues object and create a new `CopyData::FaceData`
1080 * object to store our input in. This gets pushed to the end of the
1081 * `copy_data.face_data` variable. We need to do this because
1082 * the number of faces (or subfaces) over which we integrate for a
1083 * given cell differs from cell to cell, and the sizes of these
1084 * matrices also differ, depending on what degrees of freedom
1085 * are adjacent to the face or subface. As discussed in the documentation
1086 * of MeshWorker::mesh_loop(), the copy object is reset every time a new
1087 * cell is visited, so that what we push to the end of
1088 * `copy_data.face_data()` is really all that the later `copier` function
1089 * gets to see when it copies the contributions of each cell to the global
1090 * matrix and right hand side objects.
1093 * auto face_worker = [&](const Iterator & cell,
1094 * const unsigned int &f,
1095 * const unsigned int &sf,
1096 * const Iterator & ncell,
1097 * const unsigned int &nf,
1098 * const unsigned int &nsf,
1099 * ScratchData<dim> & scratch_data,
1100 * CopyData & copy_data) {
1101 * FEInterfaceValues<dim> &fe_interface_values =
1102 * scratch_data.fe_interface_values;
1103 * fe_interface_values.reinit(cell, f, sf, ncell, nf, nsf);
1105 * copy_data.face_data.emplace_back();
1106 * CopyData::FaceData ©_data_face = copy_data.face_data.back();
1108 * copy_data_face.joint_dof_indices =
1109 * fe_interface_values.get_interface_dof_indices();
1111 * const unsigned int n_interface_dofs =
1112 * fe_interface_values.n_current_interface_dofs();
1113 * copy_data_face.cell_matrix.reinit(n_interface_dofs, n_interface_dofs);
1117 * The second part deals with determining what the penalty
1118 * parameter should be. By looking at the units of the various
1119 * terms in the bilinear form, it is clear that the penalty has
1120 * to have the form @f$\frac{\gamma}{h_K}@f$ (i.e., one over length
1121 * scale), but it is not a priori obvious how one should choose
1122 * the dimension-less number @f$\gamma@f$. From the discontinuous
1123 * Galerkin theory for the Laplace equation, one might
1124 * conjecture that the right choice is @f$\gamma=p(p+1)@f$ is the
1125 * right choice, where @f$p@f$ is the polynomial degree of the
1126 * finite element used. We will discuss this choice in a bit
1127 * more detail in the results section of this program.
1131 * In the formula above, @f$h_K@f$ is the size of cell @f$K@f$. But this
1132 * is not quite so straightforward either: If one uses highly
1133 * stretched cells, then a more involved theory says that @f$h@f$
1134 * should be replaced by the diameter of cell @f$K@f$ normal to the
1135 * direction of the edge in question. It turns out that there
1136 * is a function in deal.II for that. Secondly, @f$h_K@f$ may be
1137 * different when viewed from the two different sides of a face.
1141 * To stay on the safe side, we take the maximum of the two values.
1142 * We will note that it is possible that this computation has to be
1143 * further adjusted if one were to use hanging nodes resulting from
1144 * adaptive mesh refinement.
1147 * const unsigned int p = fe.degree;
1148 * const double gamma_over_h =
1149 * std::max((1.0 * p * (p + 1) /
1150 * cell->extent_in_direction(
1151 * GeometryInfo<dim>::unit_normal_direction[f])),
1152 * (1.0 * p * (p + 1) /
1153 * ncell->extent_in_direction(
1154 * GeometryInfo<dim>::unit_normal_direction[nf])));
1158 * Finally, and as usual, we loop over the quadrature points and
1159 * indices `i` and `j` to add up the contributions of this face
1160 * or sub-face. These are then stored in the
1161 * `copy_data.face_data` object created above. As for the cell
1162 * worker, we pull the evaluation of averages and jumps out of
1163 * the loops if possible, introducing local variables that store
1164 * these results. The assembly then only needs to use these
1165 * local variables in the innermost loop. Regarding the concrete
1166 * formula this code implements, recall that the interface terms
1167 * of the bilinear form were as follows:
1169 * -\sum_{e \in \mathbb{F}} \int_{e}
1170 * \jump{ \frac{\partial v_h}{\partial \mathbf n}}
1171 * \average{\frac{\partial^2 u_h}{\partial \mathbf n^2}} \ ds
1172 * -\sum_{e \in \mathbb{F}} \int_{e}
1173 * \average{\frac{\partial^2 v_h}{\partial \mathbf n^2}}
1174 * \jump{\frac{\partial u_h}{\partial \mathbf n}} \ ds
1175 * + \sum_{e \in \mathbb{F}}
1176 * \frac{\gamma}{h_e}
1178 * \jump{\frac{\partial v_h}{\partial \mathbf n}}
1179 * \jump{\frac{\partial u_h}{\partial \mathbf n}} \ ds.
1183 * for (unsigned int qpoint = 0;
1184 * qpoint < fe_interface_values.n_quadrature_points;
1187 * const auto &n = fe_interface_values.normal(qpoint);
1189 * for (unsigned int i = 0; i < n_interface_dofs; ++i)
1191 * const double av_hessian_i_dot_n_dot_n =
1192 * (fe_interface_values.average_of_shape_hessians(i, qpoint) * n *
1194 * const double jump_grad_i_dot_n =
1195 * (fe_interface_values.jump_in_shape_gradients(i, qpoint) * n);
1197 * for (unsigned int j = 0; j < n_interface_dofs; ++j)
1199 * const double av_hessian_j_dot_n_dot_n =
1200 * (fe_interface_values.average_of_shape_hessians(j, qpoint) *
1202 * const double jump_grad_j_dot_n =
1203 * (fe_interface_values.jump_in_shape_gradients(j, qpoint) *
1206 * copy_data_face.cell_matrix(i, j) +=
1207 * (-av_hessian_i_dot_n_dot_n // - {grad^2 v n n }
1208 * * jump_grad_j_dot_n // [grad u n]
1209 * - av_hessian_j_dot_n_dot_n // - {grad^2 u n n }
1210 * * jump_grad_i_dot_n // [grad v n]
1212 * gamma_over_h * // gamma/h
1213 * jump_grad_i_dot_n * // [grad v n]
1214 * jump_grad_j_dot_n) * // [grad u n]
1215 * fe_interface_values.JxW(qpoint); // dx
1224 * The third piece is to do the same kind of assembly for faces that
1225 * are at the boundary. The idea is the same as above, of course,
1226 * with only the difference that there are now penalty terms that
1227 * also go into the right hand side.
1231 * As before, the first part of the function simply sets up some
1235 * auto boundary_worker = [&](const Iterator & cell,
1236 * const unsigned int &face_no,
1237 * ScratchData<dim> & scratch_data,
1238 * CopyData & copy_data) {
1239 * FEInterfaceValues<dim> &fe_interface_values =
1240 * scratch_data.fe_interface_values;
1241 * fe_interface_values.reinit(cell, face_no);
1242 * const auto &q_points = fe_interface_values.get_quadrature_points();
1244 * copy_data.face_data.emplace_back();
1245 * CopyData::FaceData ©_data_face = copy_data.face_data.back();
1247 * const unsigned int n_dofs =
1248 * fe_interface_values.n_current_interface_dofs();
1249 * copy_data_face.joint_dof_indices =
1250 * fe_interface_values.get_interface_dof_indices();
1252 * copy_data_face.cell_matrix.reinit(n_dofs, n_dofs);
1254 * const std::vector<double> &JxW = fe_interface_values.get_JxW_values();
1255 * const std::vector<Tensor<1, dim>> &normals =
1256 * fe_interface_values.get_normal_vectors();
1259 * const ExactSolution::Solution<dim> exact_solution;
1260 * std::vector<Tensor<1, dim>> exact_gradients(q_points.size());
1261 * exact_solution.gradient_list(q_points, exact_gradients);
1266 * Positively, because we now only deal with one cell adjacent to the
1267 * face (as we are on the boundary), the computation of the penalty
1268 * factor @f$\gamma@f$ is substantially simpler:
1271 * const unsigned int p = fe.degree;
1272 * const double gamma_over_h =
1273 * (1.0 * p * (p + 1) /
1274 * cell->extent_in_direction(
1275 * GeometryInfo<dim>::unit_normal_direction[face_no]));
1279 * The third piece is the assembly of terms. This is now
1280 * slightly more involved since these contains both terms for
1281 * the matrix and for the right hand side. The former is exactly
1282 * the same as for the interior faces stated above if one just
1283 * defines the jump and average appropriately (which is what the
1284 * FEInterfaceValues class does). The latter requires us to
1285 * evaluate the boundary conditions @f$j(\mathbf x)@f$, which in the
1286 * current case (where we know the exact solution) we compute
1287 * from @f$j(\mathbf x) = \frac{\partial u(\mathbf x)}{\partial
1288 * {\mathbf n}}@f$. The term to be added to the right hand side
1290 * @f$\frac{\gamma}{h_e}\int_e
1291 * \jump{\frac{\partial v_h}{\partial \mathbf n}} j \ ds@f$.
1294 * for (unsigned int qpoint = 0; qpoint < q_points.size(); ++qpoint)
1296 * const auto &n = normals[qpoint];
1298 * for (unsigned int i = 0; i < n_dofs; ++i)
1300 * const double av_hessian_i_dot_n_dot_n =
1301 * (fe_interface_values.average_of_shape_hessians(i, qpoint) * n *
1303 * const double jump_grad_i_dot_n =
1304 * (fe_interface_values.jump_in_shape_gradients(i, qpoint) * n);
1306 * for (unsigned int j = 0; j < n_dofs; ++j)
1308 * const double av_hessian_j_dot_n_dot_n =
1309 * (fe_interface_values.average_of_shape_hessians(j, qpoint) *
1311 * const double jump_grad_j_dot_n =
1312 * (fe_interface_values.jump_in_shape_gradients(j, qpoint) *
1315 * copy_data_face.cell_matrix(i, j) +=
1316 * (-av_hessian_i_dot_n_dot_n // - {grad^2 v n n}
1317 * * jump_grad_j_dot_n // [grad u n]
1319 * - av_hessian_j_dot_n_dot_n // - {grad^2 u n n}
1320 * * jump_grad_i_dot_n // [grad v n]
1322 * + gamma_over_h // gamma/h
1323 * * jump_grad_i_dot_n // [grad v n]
1324 * * jump_grad_j_dot_n // [grad u n]
1326 * JxW[qpoint]; // dx
1329 * copy_data.cell_rhs(i) +=
1330 * (-av_hessian_i_dot_n_dot_n * // - {grad^2 v n n }
1331 * (exact_gradients[qpoint] * n) // (grad u_exact . n)
1333 * gamma_over_h // gamma/h
1334 * * jump_grad_i_dot_n // [grad v n]
1335 * * (exact_gradients[qpoint] * n) // (grad u_exact . n)
1337 * JxW[qpoint]; // dx
1344 * Part 4 is a small function that copies the data produced by the
1345 * cell, interior, and boundary face assemblers above into the
1346 * global matrix and right hand side vector. There really is not
1347 * very much to do here: We distribute the cell matrix and right
1348 * hand side contributions as we have done in almost all of the
1349 * other tutorial programs using the constraints objects. We then
1350 * also have to do the same for the face matrix contributions
1351 * that have gained content for the faces (interior and boundary)
1352 * and that the `face_worker` and `boundary_worker` have added
1353 * to the `copy_data.face_data` array.
1356 * auto copier = [&](const CopyData ©_data) {
1357 * constraints.distribute_local_to_global(copy_data.cell_matrix,
1358 * copy_data.cell_rhs,
1359 * copy_data.local_dof_indices,
1363 * for (auto &cdf : copy_data.face_data)
1365 * constraints.distribute_local_to_global(cdf.cell_matrix,
1366 * cdf.joint_dof_indices,
1374 * Having set all of this up, what remains is to just create a scratch
1375 * and copy data object and call the MeshWorker::mesh_loop() function
1376 * that then goes over all cells and faces, calls the respective workers
1377 * on them, and then the copier function that puts things into the
1378 * global matrix and right hand side. As an additional benefit,
1379 * MeshWorker::mesh_loop() does all of this in parallel, using
1380 * as many processor cores as your machine happens to have.
1383 * const unsigned int n_gauss_points = dof_handler.get_fe().degree + 1;
1384 * ScratchData<dim> scratch_data(mapping,
1387 * update_values | update_gradients |
1388 * update_hessians | update_quadrature_points |
1389 * update_JxW_values,
1390 * update_values | update_gradients |
1391 * update_hessians | update_quadrature_points |
1392 * update_JxW_values | update_normal_vectors);
1393 * CopyData copy_data(dof_handler.get_fe().n_dofs_per_cell());
1394 * MeshWorker::mesh_loop(dof_handler.begin_active(),
1395 * dof_handler.end(),
1400 * MeshWorker::assemble_own_cells |
1401 * MeshWorker::assemble_boundary_faces |
1402 * MeshWorker::assemble_own_interior_faces_once,
1412 * <a name="Solvingthelinearsystemandpostprocessing"></a>
1413 * <h4>Solving the linear system and postprocessing</h4>
1417 * The show is essentially over at this point: The remaining functions are
1418 * not overly interesting or novel. The first one simply uses a direct
1419 * solver to solve the linear system (see also @ref step_29 "step-29"):
1422 * template <int dim>
1423 * void BiharmonicProblem<dim>::solve()
1425 * std::cout << " Solving system..." << std::endl;
1427 * SparseDirectUMFPACK A_direct;
1428 * A_direct.initialize(system_matrix);
1429 * A_direct.vmult(solution, system_rhs);
1431 * constraints.distribute(solution);
1438 * The next function evaluates the error between the computed solution
1439 * and the exact solution (which is known here because we have chosen
1440 * the right hand side and boundary values in a way so that we know
1441 * the corresponding solution). In the first two code blocks below,
1442 * we compute the error in the @f$L_2@f$ norm and the @f$H^1@f$ semi-norm.
1445 * template <int dim>
1446 * void BiharmonicProblem<dim>::compute_errors()
1449 * Vector<float> norm_per_cell(triangulation.n_active_cells());
1450 * VectorTools::integrate_difference(mapping,
1453 * ExactSolution::Solution<dim>(),
1455 * QGauss<dim>(fe.degree + 2),
1456 * VectorTools::L2_norm);
1457 * const double error_norm =
1458 * VectorTools::compute_global_error(triangulation,
1460 * VectorTools::L2_norm);
1461 * std::cout << " Error in the L2 norm : " << error_norm
1466 * Vector<float> norm_per_cell(triangulation.n_active_cells());
1467 * VectorTools::integrate_difference(mapping,
1470 * ExactSolution::Solution<dim>(),
1472 * QGauss<dim>(fe.degree + 2),
1473 * VectorTools::H1_seminorm);
1474 * const double error_norm =
1475 * VectorTools::compute_global_error(triangulation,
1477 * VectorTools::H1_seminorm);
1478 * std::cout << " Error in the H1 seminorm : " << error_norm
1484 * Now also compute an approximation to the @f$H^2@f$ seminorm error. The actual
1485 * @f$H^2@f$ seminorm would require us to integrate second derivatives of the
1486 * solution @f$u_h@f$, but given the Lagrange shape functions we use, @f$u_h@f$ of
1487 * course has kinks at the interfaces between cells, and consequently second
1488 * derivatives are singular at interfaces. As a consequence, we really only
1489 * integrate over the interior of cells and ignore the interface
1490 * contributions. This is *not* an equivalent norm to the energy norm for
1491 * the problem, but still gives us an idea of how fast the error converges.
1495 * We note that one could address this issue by defining a norm that
1496 * is equivalent to the energy norm. This would involve adding up not
1497 * only the integrals over cell interiors as we do below, but also adding
1498 * penalty terms for the jump of the derivative of @f$u_h@f$ across interfaces,
1499 * with an appropriate scaling of the two kinds of terms. We will leave
1500 * this for later work.
1504 * const QGauss<dim> quadrature_formula(fe.degree + 2);
1505 * ExactSolution::Solution<dim> exact_solution;
1506 * Vector<double> error_per_cell(triangulation.n_active_cells());
1508 * FEValues<dim> fe_values(mapping,
1510 * quadrature_formula,
1511 * update_values | update_hessians |
1512 * update_quadrature_points | update_JxW_values);
1514 * const FEValuesExtractors::Scalar scalar(0);
1515 * const unsigned int n_q_points = quadrature_formula.size();
1517 * std::vector<SymmetricTensor<2, dim>> exact_hessians(n_q_points);
1518 * std::vector<Tensor<2, dim>> hessians(n_q_points);
1519 * for (auto &cell : dof_handler.active_cell_iterators())
1521 * fe_values.reinit(cell);
1522 * fe_values[scalar].get_function_hessians(solution, hessians);
1523 * exact_solution.hessian_list(fe_values.get_quadrature_points(),
1526 * double local_error = 0;
1527 * for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
1530 * ((exact_hessians[q_point] - hessians[q_point]).norm_square() *
1531 * fe_values.JxW(q_point));
1533 * error_per_cell[cell->active_cell_index()] = std::sqrt(local_error);
1536 * const double error_norm =
1537 * VectorTools::compute_global_error(triangulation,
1539 * VectorTools::L2_norm);
1540 * std::cout << " Error in the broken H2 seminorm: " << error_norm
1549 * Equally uninteresting is the function that generates graphical output.
1550 * It looks exactly like the one in @ref step_6 "step-6", for example.
1553 * template <int dim>
1555 * BiharmonicProblem<dim>::output_results(const unsigned int iteration) const
1557 * std::cout << " Writing graphical output..." << std::endl;
1559 * DataOut<dim> data_out;
1561 * data_out.attach_dof_handler(dof_handler);
1562 * data_out.add_data_vector(solution, "solution");
1563 * data_out.build_patches();
1565 * const std::string filename =
1566 * ("output_" + Utilities::int_to_string(iteration, 6) + ".vtu");
1567 * std::ofstream output_vtu(filename);
1568 * data_out.write_vtu(output_vtu);
1575 * The same is true for the `run()` function: Just like in previous
1579 * template <int dim>
1580 * void BiharmonicProblem<dim>::run()
1584 * const unsigned int n_cycles = 4;
1585 * for (unsigned int cycle = 0; cycle < n_cycles; ++cycle)
1587 * std::cout << "Cycle " << cycle << " of " << n_cycles << std::endl;
1589 * triangulation.refine_global(1);
1592 * assemble_system();
1595 * output_results(cycle);
1598 * std::cout << std::endl;
1601 * } // namespace Step47
1608 * <a name="Themainfunction"></a>
1609 * <h3>The main() function</h3>
1613 * Finally for the `main()` function. There is, again, not very much to see
1614 * here: It looks like the ones in previous tutorial programs. There
1615 * is a variable that allows selecting the polynomial degree of the element
1616 * we want to use for solving the equation. Because the C0IP formulation
1617 * we use requires the element degree to be at least two, we check with
1618 * an assertion that whatever one sets for the polynomial degree actually
1626 * using namespace dealii;
1627 * using namespace Step47;
1629 * const unsigned int fe_degree = 2;
1630 * Assert(fe_degree >= 2,
1631 * ExcMessage("The C0IP formulation for the biharmonic problem "
1632 * "only works if one uses elements of polynomial "
1633 * "degree at least 2."));
1635 * BiharmonicProblem<2> biharmonic_problem(fe_degree);
1636 * biharmonic_problem.run();
1638 * catch (std::exception &exc)
1640 * std::cerr << std::endl
1642 * << "----------------------------------------------------"
1644 * std::cerr << "Exception on processing: " << std::endl
1645 * << exc.what() << std::endl
1646 * << "Aborting!" << std::endl
1647 * << "----------------------------------------------------"
1654 * std::cerr << std::endl
1656 * << "----------------------------------------------------"
1658 * std::cerr << "Unknown exception!" << std::endl
1659 * << "Aborting!" << std::endl
1660 * << "----------------------------------------------------"
1668<a name="Results"></a><h1>Results</h1>
1671We run the program with right hand side and boundary values as
1672discussed in the introduction. These will produce the
1673solution @f$u = \sin(\pi x) \sin(\pi y)@f$ on the domain @f$\Omega = (0,1)^2@f$.
1674We test this setup using @f$Q_2@f$, @f$Q_3@f$, and @f$Q_4@f$ elements, which one can
1675change via the `fe_degree` variable in the `main()` function. With mesh
1676refinement, the @f$L_2@f$ convergence rates, @f$H^1@f$-seminorm rate,
1677and @f$H^2@f$-seminorm convergence of @f$u@f$
1678should then be around 2, 2, 1 for @f$Q_2@f$ (with the @f$L_2@f$ norm
1679sub-optimal as discussed in the introduction); 4, 3, 2 for
1680@f$Q_3@f$; and 5, 4, 3 for @f$Q_4@f$, respectively.
1682From the literature, it is not immediately clear what
1683the penalty parameter @f$\gamma@f$ should be. For example,
1684@cite Brenner2009 state that it needs to be larger than one, and
1685choose @f$\gamma=5@f$. The FEniCS/Dolphin tutorial chooses it as
1687https://fenicsproject.org/docs/dolfin/1.6.0/python/demo/documented/biharmonic/python/documentation.html
1688. @cite Wells2007 uses a value for @f$\gamma@f$ larger than the
1689number of edges belonging to an element for Kirchhoff plates (see
1690their Section 4.2). This suggests that maybe
1691@f$\gamma = 1@f$, @f$2@f$, are too small; on the other hand, a value
1692@f$p(p+1)@f$ would be reasonable,
1693where @f$p@f$ is the degree of polynomials. The last of these choices is
1694the one one would expect to work by comparing
1695to the discontinuous Galerkin formulations for the Laplace equation
1696(see, for example, the discussions in @ref step_39 "step-39" and @ref step_74 "step-74"),
1697and it will turn out to also work here.
1698But we should check what value of @f$\gamma@f$ is right, and we will do so
1699below; changing @f$\gamma@f$ is easy in the two `face_worker` and
1700`boundary_worker` functions defined in `assemble_system()`.
1703<a name="TestresultsoniQsub2subiwithigammapp1i"></a><h3>Test results on <i>Q<sub>2</sub></i> with <i>γ = p(p+1)</i> </h3>
1706We run the code with differently refined meshes
1707and get the following convergence rates.
1709<table align="center" class="doxtable">
1711 <th>Number of refinements </th><th> @f$\|u-u_h\|_{L_2}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|_{H^1}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|^\circ_{H^2}@f$ </th><th> Conv. rates </th>
1714 <td> 2 </td><td> 8.780e-03 </td><td> </td><td> 7.095e-02 </td><td> </td><td> 1.645 </td><td> </td>
1717 <td> 3 </td><td> 3.515e-03 </td><td> 1.32 </td><td> 2.174e-02 </td><td> 1.70 </td><td> 8.121e-01 </td><td> 1.018 </td>
1720 <td> 4 </td><td> 1.103e-03 </td><td> 1.67 </td><td> 6.106e-03 </td><td> 1.83 </td><td> 4.015e-01 </td><td> 1.016 </td>
1723 <td> 5 </td><td> 3.084e-04 </td><td> 1.83 </td><td> 1.622e-03 </td><td> 1.91 </td><td> 1.993e-01 </td><td> 1.010 </td>
1726We can see that the @f$L_2@f$ convergence rates are around 2,
1727@f$H^1@f$-seminorm convergence rates are around 2,
1728and @f$H^2@f$-seminorm convergence rates are around 1. The latter two
1729match the theoretically expected rates; for the former, we have no
1730theorem but are not surprised that it is sub-optimal given the remark
1734<a name="TestresultsoniQsub3subiwithigammapp1i"></a><h3>Test results on <i>Q<sub>3</sub></i> with <i>γ = p(p+1)</i> </h3>
1738<table align="center" class="doxtable">
1740 <th>Number of refinements </th><th> @f$\|u-u_h\|_{L_2}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|_{H^1}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|^\circ_{H^2}@f$ </th><th> Conv. rates </th>
1743 <td> 2 </td><td> 2.045e-04 </td><td> </td><td> 4.402e-03 </td><td> </td><td> 1.641e-01 </td><td> </td>
1746 <td> 3 </td><td> 1.312e-05 </td><td> 3.96 </td><td> 5.537e-04 </td><td> 2.99 </td><td> 4.096e-02 </td><td> 2.00 </td>
1749 <td> 4 </td><td> 8.239e-07 </td><td> 3.99 </td><td> 6.904e-05 </td><td> 3.00 </td><td> 1.023e-02 </td><td> 2.00 </td>
1752 <td> 5 </td><td> 5.158e-08 </td><td> 3.99 </td><td> 8.621e-06 </td><td> 3.00 </td><td> 2.558e-03 </td><td> 2.00 </td>
1755We can see that the @f$L_2@f$ convergence rates are around 4,
1756@f$H^1@f$-seminorm convergence rates are around 3,
1757and @f$H^2@f$-seminorm convergence rates are around 2.
1758This, of course, matches our theoretical expectations.
1761<a name="TestresultsoniQsub4subiwithigammapp1i"></a><h3>Test results on <i>Q<sub>4</sub></i> with <i>γ = p(p+1)</i> </h3>
1764<table align="center" class="doxtable">
1766 <th>Number of refinements </th><th> @f$\|u-u_h\|_{L_2}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|_{H^1}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|^\circ_{H^2}@f$ </th><th> Conv. rates </th>
1769 <td> 2 </td><td> 6.510e-06 </td><td> </td><td> 2.215e-04 </td><td> </td><td> 1.275e-02 </td><td> </td>
1772 <td> 3 </td><td> 2.679e-07 </td><td> 4.60 </td><td> 1.569e-05 </td><td> 3.81 </td><td> 1.496e-03 </td><td> 3.09 </td>
1775 <td> 4 </td><td> 9.404e-09 </td><td> 4.83 </td><td> 1.040e-06 </td><td> 3.91 </td><td> 1.774e-04 </td><td> 3.07 </td>
1778 <td> 5 </td><td> 7.943e-10 </td><td> 3.56 </td><td> 6.693e-08 </td><td> 3.95 </td><td> 2.150e-05 </td><td> 3.04 </td>
1781We can see that the @f$L_2@f$ norm convergence rates are around 5,
1782@f$H^1@f$-seminorm convergence rates are around 4,
1783and @f$H^2@f$-seminorm convergence rates are around 3.
1784On the finest mesh, the @f$L_2@f$ norm convergence rate
1785is much smaller than our theoretical expectations
1786because the linear solver becomes the limiting factor due
1787to round-off. Of course the @f$L_2@f$ error is also very small already in
1791<a name="TestresultsoniQsub2subiwithigamma1i"></a><h3>Test results on <i>Q<sub>2</sub></i> with <i>γ = 1</i> </h3>
1794For comparison with the results above, let us now also consider the
1795case where we simply choose @f$\gamma=1@f$:
1797<table align="center" class="doxtable">
1799 <th>Number of refinements </th><th> @f$\|u-u_h\|_{L_2}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|_{H^1}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|^\circ_{H^2}@f$ </th><th> Conv. rates </th>
1802 <td> 2 </td><td> 7.350e-02 </td><td> </td><td> 7.323e-01 </td><td> </td><td> 10.343 </td><td> </td>
1805 <td> 3 </td><td> 6.798e-03 </td><td> 3.43 </td><td> 1.716e-01 </td><td> 2.09 </td><td>4.836 </td><td> 1.09 </td>
1808 <td> 4 </td><td> 9.669e-04 </td><td> 2.81 </td><td> 6.436e-02 </td><td> 1.41 </td><td> 3.590 </td><td> 0.430 </td>
1811 <td> 5 </td><td> 1.755e-04 </td><td> 2.46 </td><td> 2.831e-02 </td><td> 1.18 </td><td>3.144 </td><td> 0.19 </td>
1814Although @f$L_2@f$ norm convergence rates of @f$u@f$ more or less
1815follows the theoretical expectations,
1816the @f$H^1@f$-seminorm and @f$H^2@f$-seminorm do not seem to converge as expected.
1817Comparing results from @f$\gamma = 1@f$ and @f$\gamma = p(p+1)@f$, it is clear that
1818@f$\gamma = p(p+1)@f$ is a better penalty.
1819Given that @f$\gamma=1@f$ is already too small for @f$Q_2@f$ elements, it may not be surprising that if one repeated the
1820experiment with a @f$Q_3@f$ element, the results are even more disappointing: One again only obtains convergence
1821rates of 2, 1, zero -- i.e., no better than for the @f$Q_2@f$ element (although the errors are smaller in magnitude).
1822Maybe surprisingly, however, one obtains more or less the expected convergence orders when using @f$Q_4@f$
1823elements. Regardless, this uncertainty suggests that @f$\gamma=1@f$ is at best a risky choice, and at worst an
1824unreliable one and that we should choose @f$\gamma@f$ larger.
1827<a name="TestresultsoniQsub2subiwithigamma2i"></a><h3>Test results on <i>Q<sub>2</sub></i> with <i>γ = 2</i> </h3>
1830Since @f$\gamma=1@f$ is clearly too small, one might conjecture that
1831@f$\gamma=2@f$ might actually work better. Here is what one obtains in
1834<table align="center" class="doxtable">
1836 <th>Number of refinements </th><th> @f$\|u-u_h\|_{L_2}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|_{H^1}@f$ </th><th> Conv. rates </th><th> @f$|u-u_h|^\circ_{H^2}@f$ </th><th> Conv. rates </th>
1839 <td> 2 </td><td> 4.133e-02 </td><td> </td><td> 2.517e-01 </td><td> </td><td> 3.056 </td><td> </td>
1842 <td> 3 </td><td> 6.500e-03 </td><td>2.66 </td><td> 5.916e-02 </td><td> 2.08 </td><td>1.444 </td><td> 1.08 </td>
1845 <td> 4 </td><td> 6.780e-04 </td><td> 3.26 </td><td> 1.203e-02 </td><td> 2.296 </td><td> 6.151e-01 </td><td> 1.231 </td>
1848 <td> 5 </td><td> 1.622e-04 </td><td> 2.06 </td><td> 2.448e-03 </td><td> 2.297 </td><td> 2.618e-01 </td><td> 1.232 </td>
1851In this case, the convergence rates more or less follow the
1852theoretical expectations, but, compared to the results from @f$\gamma =
1853p(p+1)@f$, are more variable.
1854Again, we could repeat this kind of experiment for @f$Q_3@f$ and @f$Q_4@f$ elements. In both cases, we will find that we
1855obtain roughly the expected convergence rates. Of more interest may then be to compare the absolute
1856size of the errors. While in the table above, for the @f$Q_2@f$ case, the errors on the finest grid are comparable between
1857the @f$\gamma=p(p+1)@f$ and @f$\gamma=2@f$ case, for @f$Q_3@f$ the errors are substantially larger for @f$\gamma=2@f$ than for
1858@f$\gamma=p(p+1)@f$. The same is true for the @f$Q_4@f$ case.
1861<a name="Conclusionsforthechoiceofthepenaltyparameter"></a><h3> Conclusions for the choice of the penalty parameter </h3>
1864The conclusions for which of the "reasonable" choices one should use for the penalty parameter
1865is that @f$\gamma=p(p+1)@f$ yields the expected results. It is, consequently, what the code
1866uses as currently written.
1869<a name="Possibilitiesforextensions"></a><h3> Possibilities for extensions </h3>
1872There are a number of obvious extensions to this program that would
1875- The program uses a square domain and a uniform mesh. Real problems
1876 don't come
this way, and one should verify convergence also on
1877 domains with other shapes and, in particular, curved boundaries. One
1878 may also be interested in resolving areas of less regularity by
1879 using adaptive mesh refinement.
1881- From a more theoretical perspective, the convergence results above
1882 only used the
"broken" @f$H^2@f$ seminorm @f$|\cdot|^\circ_{H^2}@f$ instead
1883 of the
"equivalent" norm @f$|\cdot|_h@f$. This is good enough to
1884 convince ourselves that the program isn
't fundamentally
1885 broken. However, it might be interesting to measure the error in the
1886 actual norm for which we have theoretical results. Implementing this
1887 addition should not be overly difficult using, for example, the
1888 FEInterfaceValues class combined with MeshWorker::mesh_loop() in the
1889 same spirit as we used for the assembly of the linear system.
1892<a name="Derivationforthesimplysupportedplates"></a> <h4> Derivation for the simply supported plates </h4>
1895 Similar to the "clamped" boundary condition addressed in the implementation,
1896 we will derive the @f$C^0@f$ IP finite element scheme for simply supported plates:
1898 \Delta^2 u(\mathbf x) &= f(\mathbf x)
1899 \qquad \qquad &&\forall \mathbf x \in \Omega,
1900 u(\mathbf x) &= g(\mathbf x) \qquad \qquad
1901 &&\forall \mathbf x \in \partial\Omega, \\
1902 \Delta u(\mathbf x) &= h(\mathbf x) \qquad \qquad
1903 &&\forall \mathbf x \in \partial\Omega.
1905 We multiply the biharmonic equation by the test function @f$v_h@f$ and integrate over @f$ K @f$ and get:
1907 \int_K v_h (\Delta^2 u_h)
1908 &= \int_K (D^2 v_h) : (D^2 u_h)
1909 + \int_{\partial K} v_h \frac{\partial (\Delta u_h)}{\partial \mathbf{n}}
1910 -\int_{\partial K} (\nabla v_h) \cdot (\frac{\partial \nabla u_h}{\partial \mathbf{n}}).
1913 Summing up over all cells @f$K \in \mathbb{T}@f$,since normal directions of @f$\Delta u_h@f$ are pointing at
1914 opposite directions on each interior edge shared by two cells and @f$v_h = 0@f$ on @f$\partial \Omega@f$,
1916 \sum_{K \in \mathbb{T}} \int_{\partial K} v_h \frac{\partial (\Delta u_h)}{\partial \mathbf{n}} = 0,
1918 and by the definition of jump over cell interfaces,
1920 -\sum_{K \in \mathbb{T}} \int_{\partial K} (\nabla v_h) \cdot (\frac{\partial \nabla u_h}{\partial \mathbf{n}}) = -\sum_{e \in \mathbb{F}} \int_{e} \jump{\frac{\partial v_h}{\partial \mathbf{n}}} (\frac{\partial^2 u_h}{\partial \mathbf{n^2}}).
1922 We separate interior faces and boundary faces of the domain,
1924 -\sum_{K \in \mathbb{T}} \int_{\partial K} (\nabla v_h) \cdot (\frac{\partial \nabla u_h}{\partial \mathbf{n}}) = -\sum_{e \in \mathbb{F}^i} \int_{e} \jump{\frac{\partial v_h}{\partial \mathbf{n}}} (\frac{\partial^2 u_h}{\partial \mathbf{n^2}})
1925 - \sum_{e \in \partial \Omega} \int_{e} \jump{\frac{\partial v_h}{\partial \mathbf{n}}} h,
1927 where @f$\mathbb{F}^i@f$ is the set of interior faces.
1930 \sum_{K \in \mathbb{T}} \int_K (D^2 v_h) : (D^2 u_h) \ dx - \sum_{e \in \mathbb{F}^i} \int_{e} \jump{\frac{\partial v_h}{\partial \mathbf{n}}} (\frac{\partial^2 u_h}{\partial \mathbf{n^2}}) \ ds
1931 = \sum_{K \in \mathbb{T}}\int_{K} v_h f \ dx + \sum_{e\subset\partial\Omega} \int_{e} \jump{\frac{\partial v_h}{\partial \mathbf{n}}} h \ ds.
1934 In order to symmetrize and stabilize the discrete problem,
1935 we add symmetrization and stabilization term.
1936 We finally get the @f$C^0@f$ IP finite element scheme for the biharmonic equation:
1937 find @f$u_h@f$ such that @f$u_h =g@f$ on @f$\partial \Omega@f$ and
1939 \mathcal{A}(v_h,u_h)&=\mathcal{F}(v_h) \quad \text{holds for all test functions } v_h,
1943 \mathcal{A}(v_h,u_h):=&\sum_{K \in \mathbb{T}}\int_K D^2v_h:D^2u_h \ dx
1946 -\sum_{e \in \mathbb{F}^i} \int_{e}
1947 \jump{\frac{\partial v_h}{\partial \mathbf n}}
1948 \average{\frac{\partial^2 u_h}{\partial \mathbf n^2}} \ ds
1949 -\sum_{e \in \mathbb{F}^i} \int_{e}
1950 \average{\frac{\partial^2 v_h}{\partial \mathbf n^2}}
1951 \jump{\frac{\partial u_h}{\partial \mathbf n}} \ ds
1953 &+ \sum_{e \in \mathbb{F}^i}
1956 \jump{\frac{\partial v_h}{\partial \mathbf n}}
1957 \jump{\frac{\partial u_h}{\partial \mathbf n}} \ ds,
1961 \mathcal{F}(v_h)&:=\sum_{K \in \mathbb{T}}\int_{K} v_h f \ dx
1963 \sum_{e\subset\partial\Omega}
1964 \int_e \jump{\frac{\partial v_h}{\partial \mathbf n}} h \ ds.
1966 The implementation of this boundary case is similar to the "clamped" version
1967 except that `boundary_worker` is no longer needed for system assembling
1968 and the right hand side is changed according to the formulation.
1971<a name="PlainProg"></a>
1972<h1> The plain program</h1>
1973@include "step-47.cc"
virtual RangeNumberType value(const Point< dim > &p, const unsigned int component=0) const
__global__ void set(Number *val, const Number s, const size_type N)
void make_hanging_node_constraints(const DoFHandler< dim, spacedim > &dof_handler, AffineConstraints< number > &constraints)
void make_flux_sparsity_pattern(const DoFHandler< dim, spacedim > &dof_handler, SparsityPatternType &sparsity_pattern)
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
double norm(const FEValuesBase< dim > &fe, const ArrayView< const std::vector< Tensor< 1, dim > > > &Du)
void run(const Iterator &begin, const typename identity< Iterator >::type &end, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length, const unsigned int chunk_size)
int(&) functions(const void *v1, const void *v2)
::VectorizedArray< Number, width > cos(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > pow(const ::VectorizedArray< Number, width > &, const Number p)
const ::parallel::distributed::Triangulation< dim, spacedim > * triangulation