1270 *
const unsigned int = 0)
const override
1275 *
virtual void vector_value(
const Point<dim> &p,
1278 *
for (
unsigned int c = 0; c < this->n_components; ++c)
1285 *
template <
int dim>
1298 *
ExcMessage(
"Invalid operation for a scalar function."));
1304 *
(dim == 2 ?
Point<dim>(.45, .1) :
Point<dim>(.45, .5, .1)),
1305 *
(dim == 2 ?
Point<dim>(.75, .1) :
Point<dim>(.75, .5, .1))};
1306 *
static const double source_radius = (dim == 2 ? 1. / 32 : 1. / 8);
1315 *
virtual void vector_value(
const Point<dim> &p,
1318 *
for (
unsigned int c = 0; c < this->n_components; ++c)
1329 * <a name=
"Linearsolversandpreconditioners"></a>
1341 *
that here we don't use the Schur complement to solve the Stokes
1342 * equations, though an approximate Schur complement (the mass matrix on the
1343 * pressure space) appears in the preconditioner.
1346 * namespace LinearSolvers
1351 * <a name="ThecodeInverseMatrixcodeclasstemplate"></a>
1352 * <h4>The <code>InverseMatrix</code> class template</h4>
1356 * This class is an interface to calculate the action of an "inverted"
1357 * matrix on a vector (using the <code>vmult</code> operation) in the same
1358 * way as the corresponding class in @ref step_22 "step-22": when the product of an
1359 * object of this class is requested, we solve a linear equation system
1360 * with that matrix using the CG method, accelerated by a preconditioner
1361 * of (templated) class <code>PreconditionerType</code>.
1365 * In a minor deviation from the implementation of the same class in
1366 * @ref step_22 "step-22", we make the <code>vmult</code> function take any
1367 * kind of vector type (it will yield compiler errors, however, if the
1368 * matrix does not allow a matrix-vector product with this kind of
1373 * Secondly, we catch any exceptions that the solver may have thrown. The
1374 * reason is as follows: When debugging a program like this one
1375 * occasionally makes a mistake of passing an indefinite or nonsymmetric
1376 * matrix or preconditioner to the current class. The solver will, in that
1377 * case, not converge and throw a run-time exception. If not caught here
1378 * it will propagate up the call stack and may end up in
1379 * <code>main()</code> where we output an error message that will say that
1380 * the CG solver failed. The question then becomes: Which CG solver? The
1381 * one that inverted the mass matrix? The one that inverted the top left
1382 * block with the Laplace operator? Or a CG solver in one of the several
1383 * other nested places where we use linear solvers in the current code? No
1384 * indication about this is present in a run-time exception because it
1398 *
template <
class MatrixType,
class PreconditionerType>
1403 *
const PreconditionerType &preconditioner);
1406 *
template <
typename VectorType>
1407 *
void vmult(VectorType &dst,
const VectorType &src)
const;
1411 *
const PreconditionerType & preconditioner;
1415 *
template <
class MatrixType,
class PreconditionerType>
1417 *
const MatrixType & m,
1418 *
const PreconditionerType &preconditioner)
1420 *
, preconditioner(preconditioner)
1425 *
template <
class MatrixType,
class PreconditionerType>
1426 *
template <
typename VectorType>
1429 *
const VectorType &src)
const
1431 *
SolverControl solver_control(src.size(), 1e-7 * src.l2_norm());
1438 *
cg.solve(*matrix, dst, src, preconditioner);
1440 *
catch (std::exception &e)
1442 *
Assert(
false, ExcMessage(
e.what()));
1449 * <a name=
"Schurcomplementpreconditioner"></a>
1462 *
Let's have a look at the ideal preconditioner matrix
1463 * @f$P=\left(\begin{array}{cc} A & 0 \\ B & -S \end{array}\right)@f$
1464 * described in the introduction. If we apply this matrix in the solution
1465 * of a linear system, convergence of an iterative GMRES solver will be
1466 * governed by the matrix @f{eqnarray*} P^{-1}\left(\begin{array}{cc} A &
1467 * B^T \\ B & 0 \end{array}\right) = \left(\begin{array}{cc} I & A^{-1}
1468 * B^T \\ 0 & I \end{array}\right), @f} which indeed is very simple. A
1469 * GMRES solver based on exact matrices would converge in one iteration,
1470 * since all eigenvalues are equal (any Krylov method takes at most as
1471 * many iterations as there are distinct eigenvalues). Such a
1472 * preconditioner for the blocked Stokes system has been proposed by
1473 * Silvester and Wathen ("Fast iterative solution of stabilised Stokes
1474 * systems part II. Using general block preconditioners", SIAM
1475 * J. Numer. Anal., 31 (1994), pp. 1352-1367).
1479 * Replacing @f$P@f$ by @f$\tilde{P}@f$ keeps that spirit alive: the product
1480 * @f$P^{-1} A@f$ will still be close to a matrix with eigenvalues 1 with a
1481 * distribution that does not depend on the problem size. This lets us
1482 * hope to be able to get a number of GMRES iterations that is
1483 * problem-size independent.
1487 * The deal.II users who have already gone through the @ref step_20 "step-20" and @ref step_22 "step-22"
1499 * (
using the AMG preconditioner).
1590 *
template <
class PreconditionerTypeA,
class PreconditionerTypeMp>
1597 *
stokes_matrix->block(1, 0).residual(tmp, dst.block(0), src.block(1));
1608 * <a name=
"ThecodeBoussinesqFlowProblemcodeclasstemplate"></a>
1640 *
template <
int dim>
1672 *
const double cell_diameter)
const;
1723 * <a name=
"BoussinesqFlowProblemclassimplementation"></a>
1729 * <a name=
"BoussinesqFlowProblemBoussinesqFlowProblem"></a>
1743 *
and preconditioning:
1746 *
template <
int dim>
1773 * <a name=
"BoussinesqFlowProblemget_maximal_velocity"></a>
1843 *
fe_values.
reinit(cell);
1847 *
for (
unsigned int q = 0;
q < n_q_points; ++
q)
1859 * <a name=
"BoussinesqFlowProblemget_extrapolated_temperature_range"></a>
1893 *
template <
int dim>
1894 *
std::pair<double, double>
1912 *
fe_values.
reinit(cell);
1918 *
for (
unsigned int q = 0;
q < n_q_points; ++
q)
1938 *
fe_values.
reinit(cell);
1942 *
for (
unsigned int q = 0;
q < n_q_points; ++
q)
1960 * <a name=
"BoussinesqFlowProblemcompute_viscosity"></a>
1992 *
template <
int dim>
2005 *
const double cell_diameter)
const
2007 *
constexpr double beta = 0.017 * dim;
2008 *
constexpr double alpha = 1.0;
2011 *
return 5
e-3 * cell_diameter;
2018 *
for (
unsigned int q = 0;
q < n_q_points; ++
q)
2023 *
const double dT_dt =
2033 *
const double residual =
2057 * <a name=
"BoussinesqFlowProblemsetup_dofs"></a>
2075 *
dependent on ILU's, whereas we use AMG here which is not sensitive to the
2076 * DoF numbering. The IC preconditioner for the inversion of the pressure
2077 * mass matrix would of course take advantage of a Cuthill-McKee like
2078 * renumbering, but its costs are low compared to the velocity portion, so
2079 * the additional work does not pay off.
2083 * We then proceed with the generation of the hanging node constraints that
2084 * arise from adaptive grid refinement for both DoFHandler objects. For the
2085 * velocity, we impose no-flux boundary conditions @f$\mathbf{u}\cdot
2086 * \mathbf{n}=0@f$ by adding constraints to the object that already stores the
2087 * hanging node constraints matrix. The second parameter in the function
2088 * describes the first of the velocity components in the total dof vector,
2089 * which is zero here. The variable <code>no_normal_flux_boundaries</code>
2090 * denotes the boundary indicators for which to set the no flux boundary
2091 * conditions; here, this is boundary indicator zero.
2095 * After having done so, we count the number of degrees of freedom in the
2099 * template <int dim>
2100 * void BoussinesqFlowProblem<dim>::setup_dofs()
2102 * std::vector<unsigned int> stokes_sub_blocks(dim + 1, 0);
2103 * stokes_sub_blocks[dim] = 1;
2106 * stokes_dof_handler.distribute_dofs(stokes_fe);
2107 * DoFRenumbering::component_wise(stokes_dof_handler, stokes_sub_blocks);
2109 * stokes_constraints.clear();
2110 * DoFTools::make_hanging_node_constraints(stokes_dof_handler,
2111 * stokes_constraints);
2112 * const std::set<types::boundary_id> no_normal_flux_boundaries = {0};
2113 * VectorTools::compute_no_normal_flux_constraints(stokes_dof_handler,
2115 * no_normal_flux_boundaries,
2116 * stokes_constraints);
2117 * stokes_constraints.close();
2120 * temperature_dof_handler.distribute_dofs(temperature_fe);
2122 * temperature_constraints.clear();
2123 * DoFTools::make_hanging_node_constraints(temperature_dof_handler,
2124 * temperature_constraints);
2125 * temperature_constraints.close();
2128 * const std::vector<types::global_dof_index> stokes_dofs_per_block =
2129 * DoFTools::count_dofs_per_fe_block(stokes_dof_handler, stokes_sub_blocks);
2131 * const types::global_dof_index n_u = stokes_dofs_per_block[0],
2132 * n_p = stokes_dofs_per_block[1],
2133 * n_T = temperature_dof_handler.n_dofs();
2135 * std::cout << "Number of active cells: " << triangulation.n_active_cells()
2136 * << " (on " << triangulation.n_levels() << " levels)" << std::endl
2137 * << "Number of degrees of freedom: " << n_u + n_p + n_T << " ("
2138 * << n_u << '+
' << n_p << '+
' << n_T << ')
' << std::endl
2143 * The next step is to create the sparsity pattern for the Stokes and
2144 * temperature system matrices as well as the preconditioner matrix from
2145 * which we build the Stokes preconditioner. As in @ref step_22 "step-22", we choose to
2146 * create the pattern by
2147 * using the blocked version of DynamicSparsityPattern.
2151 * So, we first release the memory stored in the matrices, then set up an
2152 * object of type BlockDynamicSparsityPattern consisting of
2153 * @f$2\times 2@f$ blocks (for the Stokes system matrix and preconditioner) or
2154 * DynamicSparsityPattern (for the temperature part). We then
2155 * fill these objects with the nonzero pattern, taking into account that
2156 * for the Stokes system matrix, there are no entries in the
2157 * pressure-pressure block (but all velocity vector components couple with
2158 * each other and with the pressure). Similarly, in the Stokes
2159 * preconditioner matrix, only the diagonal blocks are nonzero, since we
2160 * use the vector Laplacian as discussed in the introduction. This
2161 * operator only couples each vector component of the Laplacian with
2162 * itself, but not with the other vector components. (Application of the
2163 * constraints resulting from the no-flux boundary conditions will couple
2164 * vector components at the boundary again, however.)
2168 * When generating the sparsity pattern, we directly apply the constraints
2169 * from hanging nodes and no-flux boundary conditions. This approach was
2170 * already used in @ref step_27 "step-27", but is different from the one in early
2171 * tutorial programs where we first built the original sparsity pattern
2172 * and only then added the entries resulting from constraints. The reason
2173 * for doing so is that later during assembly we are going to distribute
2174 * the constraints immediately when transferring local to global
2175 * dofs. Consequently, there will be no data written at positions of
2176 * constrained degrees of freedom, so we can let the
2177 * DoFTools::make_sparsity_pattern function omit these entries by setting
2178 * the last Boolean flag to <code>false</code>. Once the sparsity pattern
2179 * is ready, we can use it to initialize the Trilinos matrices. Since the
2180 * Trilinos matrices store the sparsity pattern internally, there is no
2181 * need to keep the sparsity pattern around after the initialization of
2185 * stokes_partitioning.resize(2);
2186 * stokes_partitioning[0] = complete_index_set(n_u);
2187 * stokes_partitioning[1] = complete_index_set(n_p);
2189 * stokes_matrix.clear();
2191 * BlockDynamicSparsityPattern dsp(stokes_dofs_per_block,
2192 * stokes_dofs_per_block);
2194 * Table<2, DoFTools::Coupling> coupling(dim + 1, dim + 1);
2196 * for (unsigned int c = 0; c < dim + 1; ++c)
2197 * for (unsigned int d = 0; d < dim + 1; ++d)
2198 * if (!((c == dim) && (d == dim)))
2199 * coupling[c][d] = DoFTools::always;
2201 * coupling[c][d] = DoFTools::none;
2203 * DoFTools::make_sparsity_pattern(
2204 * stokes_dof_handler, coupling, dsp, stokes_constraints, false);
2206 * stokes_matrix.reinit(dsp);
2210 * Amg_preconditioner.reset();
2211 * Mp_preconditioner.reset();
2212 * stokes_preconditioner_matrix.clear();
2214 * BlockDynamicSparsityPattern dsp(stokes_dofs_per_block,
2215 * stokes_dofs_per_block);
2217 * Table<2, DoFTools::Coupling> coupling(dim + 1, dim + 1);
2218 * for (unsigned int c = 0; c < dim + 1; ++c)
2219 * for (unsigned int d = 0; d < dim + 1; ++d)
2221 * coupling[c][d] = DoFTools::always;
2223 * coupling[c][d] = DoFTools::none;
2225 * DoFTools::make_sparsity_pattern(
2226 * stokes_dof_handler, coupling, dsp, stokes_constraints, false);
2228 * stokes_preconditioner_matrix.reinit(dsp);
2233 * The creation of the temperature matrix (or, rather, matrices, since we
2234 * provide a temperature mass matrix and a temperature @ref GlossStiffnessMatrix "stiffness matrix",
2235 * that will be added together for time discretization) follows the
2236 * generation of the Stokes matrix – except that it is much easier
2237 * here since we do not need to take care of any blocks or coupling
2238 * between components. Note how we initialize the three temperature
2239 * matrices: We only use the sparsity pattern for reinitialization of the
2240 * first matrix, whereas we use the previously generated matrix for the
2241 * two remaining reinits. The reason for doing so is that reinitialization
2242 * from an already generated matrix allows Trilinos to reuse the sparsity
2243 * pattern instead of generating a new one for each copy. This saves both
2244 * some time and memory.
2248 * temperature_mass_matrix.clear();
2249 * temperature_stiffness_matrix.clear();
2250 * temperature_matrix.clear();
2252 * DynamicSparsityPattern dsp(n_T, n_T);
2253 * DoFTools::make_sparsity_pattern(temperature_dof_handler,
2255 * temperature_constraints,
2258 * temperature_matrix.reinit(dsp);
2259 * temperature_mass_matrix.reinit(temperature_matrix);
2260 * temperature_stiffness_matrix.reinit(temperature_matrix);
2265 * Lastly, we set the vectors for the Stokes solutions @f$\mathbf u^{n-1}@f$
2266 * and @f$\mathbf u^{n-2}@f$, as well as for the temperatures @f$T^{n}@f$,
2267 * @f$T^{n-1}@f$ and @f$T^{n-2}@f$ (required for time stepping) and all the system
2268 * right hand sides to their correct sizes and block structure:
2271 * IndexSet temperature_partitioning = complete_index_set(n_T);
2272 * stokes_solution.reinit(stokes_partitioning, MPI_COMM_WORLD);
2273 * old_stokes_solution.reinit(stokes_partitioning, MPI_COMM_WORLD);
2274 * stokes_rhs.reinit(stokes_partitioning, MPI_COMM_WORLD);
2276 * temperature_solution.reinit(temperature_partitioning, MPI_COMM_WORLD);
2277 * old_temperature_solution.reinit(temperature_partitioning, MPI_COMM_WORLD);
2278 * old_old_temperature_solution.reinit(temperature_partitioning,
2281 * temperature_rhs.reinit(temperature_partitioning, MPI_COMM_WORLD);
2289 * <a name="BoussinesqFlowProblemassemble_stokes_preconditioner"></a>
2290 * <h4>BoussinesqFlowProblem::assemble_stokes_preconditioner</h4>
2294 * This function assembles the matrix we use for preconditioning the Stokes
2295 * system. What we need are a vector Laplace matrix on the velocity
2296 * components and a mass matrix weighted by @f$\eta^{-1}@f$ on the pressure
2297 * component. We start by generating a quadrature object of appropriate
2298 * order, the FEValues object that can give values and gradients at the
2299 * quadrature points (together with quadrature weights). Next we create data
2300 * structures for the cell matrix and the relation between local and global
2301 * DoFs. The vectors <code>grad_phi_u</code> and <code>phi_p</code> are
2302 * going to hold the values of the basis functions in order to faster build
2303 * up the local matrices, as was already done in @ref step_22 "step-22". Before we start
2304 * the loop over all active cells, we have to specify which components are
2305 * pressure and which are velocity.
2308 * template <int dim>
2309 * void BoussinesqFlowProblem<dim>::assemble_stokes_preconditioner()
2311 * stokes_preconditioner_matrix = 0;
2313 * const QGauss<dim> quadrature_formula(stokes_degree + 2);
2314 * FEValues<dim> stokes_fe_values(stokes_fe,
2315 * quadrature_formula,
2316 * update_JxW_values | update_values |
2317 * update_gradients);
2319 * const unsigned int dofs_per_cell = stokes_fe.n_dofs_per_cell();
2320 * const unsigned int n_q_points = quadrature_formula.size();
2322 * FullMatrix<double> local_matrix(dofs_per_cell, dofs_per_cell);
2323 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
2325 * std::vector<Tensor<2, dim>> grad_phi_u(dofs_per_cell);
2326 * std::vector<double> phi_p(dofs_per_cell);
2328 * const FEValuesExtractors::Vector velocities(0);
2329 * const FEValuesExtractors::Scalar pressure(dim);
2331 * for (const auto &cell : stokes_dof_handler.active_cell_iterators())
2333 * stokes_fe_values.reinit(cell);
2338 * The creation of the local matrix is rather simple. There are only a
2339 * Laplace term (on the velocity) and a mass matrix weighted by
2340 * @f$\eta^{-1}@f$ to be generated, so the creation of the local matrix is
2341 * done in two lines. Once the local matrix is ready (loop over rows
2342 * and columns in the local matrix on each quadrature point), we get
2343 * the local DoF indices and write the local information into the
2344 * global matrix. We do this as in @ref step_27 "step-27", i.e., we directly apply the
2345 * constraints from hanging nodes locally. By doing so, we don't
have
2347 * matrix that will actually be set to zero again later when
2348 * eliminating constraints.
2351 * for (unsigned int q = 0; q < n_q_points; ++q)
2353 * for (unsigned int k = 0; k < dofs_per_cell; ++k)
2355 * grad_phi_u[k] = stokes_fe_values[velocities].gradient(k, q);
2356 * phi_p[k] = stokes_fe_values[pressure].value(k, q);
2359 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
2360 * for (unsigned int j = 0; j < dofs_per_cell; ++j)
2361 * local_matrix(i, j) +=
2362 * (EquationData::eta *
2363 * scalar_product(grad_phi_u[i], grad_phi_u[j]) +
2364 * (1. / EquationData::eta) * phi_p[i] * phi_p[j]) *
2365 * stokes_fe_values.JxW(q);
2368 * cell->get_dof_indices(local_dof_indices);
2369 * stokes_constraints.distribute_local_to_global(
2370 * local_matrix, local_dof_indices, stokes_preconditioner_matrix);
2379 * <a name="BoussinesqFlowProblembuild_stokes_preconditioner"></a>
2380 * <h4>BoussinesqFlowProblem::build_stokes_preconditioner</h4>
2384 * This function generates the inner preconditioners that are going to be
2385 * used for the Schur complement block preconditioner. Since the
2386 * preconditioners need only to be regenerated when the matrices change,
2387 * this function does not have to do anything in case the matrices have not
2388 * changed (i.e., the flag <code>rebuild_stokes_preconditioner</code> has
2389 * the value <code>false</code>). Otherwise its first task is to call
2390 * <code>assemble_stokes_preconditioner</code> to generate the
2391 * preconditioner matrices.
2395 * Next, we set up the preconditioner for the velocity-velocity matrix
2396 * @f$A@f$. As explained in the introduction, we are going to use an AMG
2397 * preconditioner based on a vector Laplace matrix @f$\hat{A}@f$ (which is
2398 * spectrally close to the Stokes matrix @f$A@f$). Usually, the
2399 * TrilinosWrappers::PreconditionAMG class can be seen as a good black-box
2400 * preconditioner which does not need any special knowledge. In this case,
2401 * however, we have to be careful: since we build an AMG for a vector
2402 * problem, we have to tell the preconditioner setup which dofs belong to
2403 * which vector component. We do this using the function
2404 * DoFTools::extract_constant_modes, a function that generates a set of
2405 * <code>dim</code> vectors, where each one has ones in the respective
2406 * component of the vector problem and zeros elsewhere. Hence, these are the
2407 * constant modes on each component, which explains the name of the
2411 * template <int dim>
2412 * void BoussinesqFlowProblem<dim>::build_stokes_preconditioner()
2414 * if (rebuild_stokes_preconditioner == false)
2417 * std::cout << " Rebuilding Stokes preconditioner..." << std::flush;
2419 * assemble_stokes_preconditioner();
2421 * Amg_preconditioner = std::make_shared<TrilinosWrappers::PreconditionAMG>();
2423 * std::vector<std::vector<bool>> constant_modes;
2424 * const FEValuesExtractors::Vector velocity_components(0);
2425 * DoFTools::extract_constant_modes(stokes_dof_handler,
2426 * stokes_fe.component_mask(
2427 * velocity_components),
2429 * TrilinosWrappers::PreconditionAMG::AdditionalData amg_data;
2430 * amg_data.constant_modes = constant_modes;
2434 * Next, we set some more options of the AMG preconditioner. In
2435 * particular, we need to tell the AMG setup that we use quadratic basis
2436 * functions for the velocity matrix (this implies more nonzero elements
2437 * in the matrix, so that a more robust algorithm needs to be chosen
2438 * internally). Moreover, we want to be able to control how the coarsening
2439 * structure is build up. The way the Trilinos smoothed aggregation AMG
2440 * does this is to look which matrix entries are of similar size as the
2441 * diagonal entry in order to algebraically build a coarse-grid
2442 * structure. By setting the parameter <code>aggregation_threshold</code>
2443 * to 0.02, we specify that all entries that are more than two percent of
2444 * size of some diagonal pivots in that row should form one coarse grid
2445 * point. This parameter is rather ad hoc, and some fine-tuning of it can
2446 * influence the performance of the preconditioner. As a rule of thumb,
2447 * larger values of <code>aggregation_threshold</code> will decrease the
2448 * number of iterations, but increase the costs per iteration. A look at
2449 * the Trilinos documentation will provide more information on these
2450 * parameters. With this data set, we then initialize the preconditioner
2451 * with the matrix we want it to apply to.
2455 * Finally, we also initialize the preconditioner for the inversion of the
2456 * pressure mass matrix. This matrix is symmetric and well-behaved, so we
2457 * can chose a simple preconditioner. We stick with an incomplete Cholesky
2458 * (IC) factorization preconditioner, which is designed for symmetric
2459 * matrices. We could have also chosen an SSOR preconditioner with
2460 * relaxation factor around 1.2, but IC is cheaper for our example. We
2461 * wrap the preconditioners into a <code>std::shared_ptr</code>
2462 * pointer, which makes it easier to recreate the preconditioner next time
2463 * around since we do not have to care about destroying the previously
2467 * amg_data.elliptic = true;
2468 * amg_data.higher_order_elements = true;
2469 * amg_data.smoother_sweeps = 2;
2470 * amg_data.aggregation_threshold = 0.02;
2471 * Amg_preconditioner->initialize(stokes_preconditioner_matrix.block(0, 0),
2474 * Mp_preconditioner = std::make_shared<TrilinosWrappers::PreconditionIC>();
2475 * Mp_preconditioner->initialize(stokes_preconditioner_matrix.block(1, 1));
2477 * std::cout << std::endl;
2479 * rebuild_stokes_preconditioner = false;
2487 * <a name="BoussinesqFlowProblemassemble_stokes_system"></a>
2488 * <h4>BoussinesqFlowProblem::assemble_stokes_system</h4>
2492 * The time lag scheme we use for advancing the coupled Stokes-temperature
2493 * system forces us to split up the assembly (and the solution of linear
2494 * systems) into two step. The first one is to create the Stokes system
2495 * matrix and right hand side, and the second is to create matrix and right
2496 * hand sides for the temperature dofs, which depends on the result of the
2497 * linear system for the velocity.
2501 * This function is called at the beginning of each time step. In the first
2502 * time step or if the mesh has changed, indicated by the
2503 * <code>rebuild_stokes_matrix</code>, we need to assemble the Stokes
2525 * use the temperature structures and set an update flag for the basis
2526 * function values which we need for evaluation of the temperature
2527 * solution. The only important part to remember here is that the same
2528 * quadrature formula is used for both FEValues objects to ensure that we
2529 * get matching information when we loop over the quadrature points of the
2534 * The declarations proceed with some shortcuts for array sizes, the
2535 * creation of the local matrix and right hand side as well as the vector
2536 * for the indices of the local dofs compared to the global system.
2539 * template <int dim>
2540 * void BoussinesqFlowProblem<dim>::assemble_stokes_system()
2542 * std::cout << " Assembling..." << std::flush;
2544 * if (rebuild_stokes_matrix == true)
2545 * stokes_matrix = 0;
2549 * const QGauss<dim> quadrature_formula(stokes_degree + 2);
2550 * FEValues<dim> stokes_fe_values(
2552 * quadrature_formula,
2553 * update_values | update_quadrature_points | update_JxW_values |
2554 * (rebuild_stokes_matrix == true ? update_gradients : UpdateFlags(0)));
2556 * FEValues<dim> temperature_fe_values(temperature_fe,
2557 * quadrature_formula,
2560 * const unsigned int dofs_per_cell = stokes_fe.n_dofs_per_cell();
2561 * const unsigned int n_q_points = quadrature_formula.size();
2563 * FullMatrix<double> local_matrix(dofs_per_cell, dofs_per_cell);
2564 * Vector<double> local_rhs(dofs_per_cell);
2566 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
2570 * Next we need a vector that will contain the values of the temperature
2571 * solution at the previous time level at the quadrature points to
2572 * assemble the source term in the right hand side of the momentum
2592 *
std::vector<Tensor<1, dim>>
phi_u(dofs_per_cell);
2593 *
std::vector<SymmetricTensor<2, dim>>
grads_phi_u(dofs_per_cell);
2594 *
std::vector<double>
div_phi_u(dofs_per_cell);
2595 *
std::vector<double>
phi_p(dofs_per_cell);
2607 * in sync. The first statements within the loop are again all very
2608 * familiar, doing the update of the finite element data as specified by
2609 * the update flags, zeroing out the local arrays and getting the values
2610 * of the old solution at the quadrature points. Then we are ready to loop
2611 * over the quadrature points on the cell.
2614 * auto cell = stokes_dof_handler.begin_active();
2615 * const auto endc = stokes_dof_handler.end();
2616 * auto temperature_cell = temperature_dof_handler.begin_active();
2618 * for (; cell != endc; ++cell, ++temperature_cell)
2620 * stokes_fe_values.reinit(cell);
2621 * temperature_fe_values.reinit(temperature_cell);
2626 * temperature_fe_values.get_function_values(old_temperature_solution,
2627 * old_temperature_values);
2629 * for (unsigned int q = 0; q < n_q_points; ++q)
2631 * const double old_temperature = old_temperature_values[q];
2635 * Next we extract the values and gradients of basis functions
2636 * relevant to the terms in the inner products. As shown in
2637 * @ref step_22 "step-22" this helps accelerate assembly.
2641 * Once this is done, we start the loop over the rows and columns
2642 * of the local matrix and feed the matrix with the relevant
2643 * products. The right hand side is filled with the forcing term
2644 * driven by temperature in direction of gravity (which is
2645 * vertical in our example). Note that the right hand side term
2646 * is always generated, whereas the matrix contributions are only
2647 * updated when it is requested by the
2648 * <code>rebuild_matrices</code> flag.
2651 * for (unsigned int k = 0; k < dofs_per_cell; ++k)
2653 * phi_u[k] = stokes_fe_values[velocities].value(k, q);
2654 * if (rebuild_stokes_matrix)
2657 * stokes_fe_values[velocities].symmetric_gradient(k, q);
2659 * stokes_fe_values[velocities].divergence(k, q);
2660 * phi_p[k] = stokes_fe_values[pressure].value(k, q);
2664 * if (rebuild_stokes_matrix)
2665 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
2666 * for (unsigned int j = 0; j < dofs_per_cell; ++j)
2667 * local_matrix(i, j) +=
2668 * (EquationData::eta * 2 * (grads_phi_u[i] * grads_phi_u[j]) -
2669 * div_phi_u[i] * phi_p[j] - phi_p[i] * div_phi_u[j]) *
2670 * stokes_fe_values.JxW(q);
2672 * const Point<dim> gravity =
2673 * -((dim == 2) ? (Point<dim>(0, 1)) : (Point<dim>(0, 0, 1)));
2674 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
2675 * local_rhs(i) += (-EquationData::density * EquationData::beta *
2676 * gravity * phi_u[i] * old_temperature) *
2677 * stokes_fe_values.JxW(q);
2682 * The last step in the loop over all cells is to enter the local
2683 * contributions into the global matrix and vector structures to the
2684 * positions specified in <code>local_dof_indices</code>. Again, we
2685 * let the AffineConstraints class do the insertion of the cell
2686 * matrix elements to the global matrix, which already condenses the
2687 * hanging node constraints.
2690 * cell->get_dof_indices(local_dof_indices);
2692 * if (rebuild_stokes_matrix == true)
2693 * stokes_constraints.distribute_local_to_global(local_matrix,
2695 * local_dof_indices,
2699 * stokes_constraints.distribute_local_to_global(local_rhs,
2700 * local_dof_indices,
2704 * rebuild_stokes_matrix = false;
2706 * std::cout << std::endl;
2714 * <a name="BoussinesqFlowProblemassemble_temperature_matrix"></a>
2715 * <h4>BoussinesqFlowProblem::assemble_temperature_matrix</h4>
2719 * This function assembles the matrix in the temperature equation. The
2720 * temperature matrix consists of two parts, a mass matrix and the time step
2721 * size times a stiffness matrix given by a Laplace term times the amount of
2722 * diffusion. Since the matrix depends on the time step size (which varies
2723 * from one step to another), the temperature matrix needs to be updated
2724 * every time step. We could simply regenerate the matrices in every time
2725 * step, but this is not really efficient since mass and Laplace matrix do
2726 * only change when we change the mesh. Hence, we do this more efficiently
2727 * by generating two separate matrices in this function, one for the mass
2728 * matrix and one for the stiffness (diffusion) matrix. We will then sum up
2729 * the matrix plus the stiffness matrix times the time step size once we
2730 * know the actual time step.
2734 * So the details for this first step are very simple. In case we need to
2735 * rebuild the matrix (i.e., the mesh has changed), we zero the data
2736 * structures, get a quadrature formula and a FEValues object, and create
2737 * local matrices, local dof indices and evaluation structures for the basis
2741 * template <int dim>
2742 * void BoussinesqFlowProblem<dim>::assemble_temperature_matrix()
2744 * if (rebuild_temperature_matrices == false)
2747 * temperature_mass_matrix = 0;
2748 * temperature_stiffness_matrix = 0;
2750 * QGauss<dim> quadrature_formula(temperature_degree + 2);
2751 * FEValues<dim> temperature_fe_values(temperature_fe,
2752 * quadrature_formula,
2753 * update_values | update_gradients |
2754 * update_JxW_values);
2756 * const unsigned int dofs_per_cell = temperature_fe.n_dofs_per_cell();
2757 * const unsigned int n_q_points = quadrature_formula.size();
2759 * FullMatrix<double> local_mass_matrix(dofs_per_cell, dofs_per_cell);
2760 * FullMatrix<double> local_stiffness_matrix(dofs_per_cell, dofs_per_cell);
2762 * std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
2764 * std::vector<double> phi_T(dofs_per_cell);
2765 * std::vector<Tensor<1, dim>> grad_phi_T(dofs_per_cell);
2786 *
for (
unsigned int q = 0;
q < n_q_points; ++
q)
2788 *
for (
unsigned int k = 0;
k < dofs_per_cell; ++
k)
2794 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
2795 *
for (
unsigned int j = 0;
j < dofs_per_cell; ++
j)
2805 *
cell->get_dof_indices(local_dof_indices);
2811 *
local_dof_indices,
2823 * <a name=
"BoussinesqFlowProblemassemble_temperature_system"></a>
2846 *
template <
int dim>
2878 *
const unsigned int dofs_per_cell =
temperature_fe.n_dofs_per_cell();
2883 *
std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
2910 *
std::vector<double>
phi_T(dofs_per_cell);
2911 *
std::vector<Tensor<1, dim>>
grad_phi_T(dofs_per_cell);
2920 *
Now,
let's start the loop over all cells in the triangulation. Again,
2921 * we need two cell iterators that walk in parallel through the cells of
2922 * the two involved DoFHandler objects for the Stokes and temperature
2923 * part. Within the loop, we first set the local rhs to zero, and then get
2924 * the values and derivatives of the old solution functions at the
2925 * quadrature points, since they are going to be needed for the definition
2926 * of the stabilization parameters and as coefficients in the equation,
2927 * respectively. Note that since the temperature has its own DoFHandler
2928 * and FEValues object we get the entire solution at the quadrature point
2929 * (which is the scalar temperature field only anyway) whereas for the
2930 * Stokes part we restrict ourselves to extracting the velocity part (and
2931 * ignoring the pressure part) by using
2932 * <code>stokes_fe_values[velocities].get_function_values</code>.
2935 * auto cell = temperature_dof_handler.begin_active();
2936 * const auto endc = temperature_dof_handler.end();
2937 * auto stokes_cell = stokes_dof_handler.begin_active();
2939 * for (; cell != endc; ++cell, ++stokes_cell)
2943 * temperature_fe_values.reinit(cell);
2944 * stokes_fe_values.reinit(stokes_cell);
2946 * temperature_fe_values.get_function_values(old_temperature_solution,
2947 * old_temperature_values);
2948 * temperature_fe_values.get_function_values(old_old_temperature_solution,
2949 * old_old_temperature_values);
2951 * temperature_fe_values.get_function_gradients(old_temperature_solution,
2952 * old_temperature_grads);
2953 * temperature_fe_values.get_function_gradients(
2954 * old_old_temperature_solution, old_old_temperature_grads);
2956 * temperature_fe_values.get_function_laplacians(
2957 * old_temperature_solution, old_temperature_laplacians);
2958 * temperature_fe_values.get_function_laplacians(
2959 * old_old_temperature_solution, old_old_temperature_laplacians);
2961 * temperature_right_hand_side.value_list(
2962 * temperature_fe_values.get_quadrature_points(), gamma_values);
2964 * stokes_fe_values[velocities].get_function_values(stokes_solution,
2965 * old_velocity_values);
2966 * stokes_fe_values[velocities].get_function_values(
2967 * old_stokes_solution, old_old_velocity_values);
2971 * Next, we calculate the artificial viscosity for stabilization
2972 * according to the discussion in the introduction using the dedicated
2973 * function. With that at hand, we can get into the loop over
2974 * quadrature points and local rhs vector components. The terms here
2975 * are quite lengthy, but their definition follows the time-discrete
2976 * system developed in the introduction of this program. The BDF-2
2977 * scheme needs one more term from the old time step (and involves
2978 * more complicated factors) than the backward Euler scheme that is
2979 * used for the first time step. When all this is done, we distribute
2980 * the local vector into the global one (including hanging node
2985 * compute_viscosity(old_temperature_values,
2986 * old_old_temperature_values,
2987 * old_temperature_grads,
2988 * old_old_temperature_grads,
2989 * old_temperature_laplacians,
2990 * old_old_temperature_laplacians,
2991 * old_velocity_values,
2992 * old_old_velocity_values,
2995 * global_T_range.second - global_T_range.first,
2996 * cell->diameter());
2998 * for (unsigned int q = 0; q < n_q_points; ++q)
3000 * for (unsigned int k = 0; k < dofs_per_cell; ++k)
3002 * grad_phi_T[k] = temperature_fe_values.shape_grad(k, q);
3003 * phi_T[k] = temperature_fe_values.shape_value(k, q);
3006 * const double T_term_for_rhs =
3007 * (use_bdf2_scheme ?
3008 * (old_temperature_values[q] * (1 + time_step / old_time_step) -
3009 * old_old_temperature_values[q] * (time_step * time_step) /
3010 * (old_time_step * (time_step + old_time_step))) :
3011 * old_temperature_values[q]);
3013 * const Tensor<1, dim> ext_grad_T =
3014 * (use_bdf2_scheme ?
3015 * (old_temperature_grads[q] * (1 + time_step / old_time_step) -
3016 * old_old_temperature_grads[q] * time_step / old_time_step) :
3017 * old_temperature_grads[q]);
3019 * const Tensor<1, dim> extrapolated_u =
3020 * (use_bdf2_scheme ?
3021 * (old_velocity_values[q] * (1 + time_step / old_time_step) -
3022 * old_old_velocity_values[q] * time_step / old_time_step) :
3023 * old_velocity_values[q]);
3025 * for (unsigned int i = 0; i < dofs_per_cell; ++i)
3027 * (T_term_for_rhs * phi_T[i] -
3028 * time_step * extrapolated_u * ext_grad_T * phi_T[i] -
3029 * time_step * nu * ext_grad_T * grad_phi_T[i] +
3030 * time_step * gamma_values[q] * phi_T[i]) *
3031 * temperature_fe_values.JxW(q);
3034 * cell->get_dof_indices(local_dof_indices);
3035 * temperature_constraints.distribute_local_to_global(local_rhs,
3036 * local_dof_indices,
3046 * <a name="BoussinesqFlowProblemsolve"></a>
3047 * <h4>BoussinesqFlowProblem::solve</h4>
3051 * This function solves the linear systems of equations. Following the
3052 * introduction, we start with the Stokes system, where we need to generate
3053 * our block Schur preconditioner. Since all the relevant actions are
3054 * implemented in the class <code>BlockSchurPreconditioner</code>, all we
3055 * have to do is to initialize the class appropriately. What we need to pass
3056 * down is an <code>InverseMatrix</code> object for the pressure mass
3057 * matrix, which we set up using the respective class together with the IC
3058 * preconditioner we already generated, and the AMG preconditioner for the
3059 * velocity-velocity matrix. Note that both <code>Mp_preconditioner</code>
3060 * and <code>Amg_preconditioner</code> are only pointers, so we use
3061 * <code>*</code> to pass down the actual preconditioner objects.
3065 * Once the preconditioner is ready, we create a GMRES solver for the block
3066 * system. Since we are working with Trilinos data structures, we have to
3067 * set the respective template argument in the solver. GMRES needs to
3068 * internally store temporary vectors for each iteration (see the discussion
3069 * in the results section of @ref step_22 "step-22") – the more vectors it can use,
3070 * the better it will generally perform. To keep memory demands in check, we
3071 * set the number of vectors to 100. This means that up to 100 solver
3072 * iterations, every temporary vector can be stored. If the solver needs to
3073 * iterate more often to get the specified tolerance, it will work on a
3074 * reduced set of vectors by restarting at every 100 iterations.
3078 * With this all set up, we solve the system and distribute the constraints
3079 * in the Stokes system, i.e., hanging nodes and no-flux boundary condition,
3080 * in order to have the appropriate solution values even at constrained
3081 * dofs. Finally, we write the number of iterations to the screen.
3084 * template <int dim>
3085 * void BoussinesqFlowProblem<dim>::solve()
3087 * std::cout << " Solving..." << std::endl;
3090 * const LinearSolvers::InverseMatrix<TrilinosWrappers::SparseMatrix,
3091 * TrilinosWrappers::PreconditionIC>
3092 * mp_inverse(stokes_preconditioner_matrix.block(1, 1),
3093 * *Mp_preconditioner);
3095 * const LinearSolvers::BlockSchurPreconditioner<
3096 * TrilinosWrappers::PreconditionAMG,
3097 * TrilinosWrappers::PreconditionIC>
3098 * preconditioner(stokes_matrix, mp_inverse, *Amg_preconditioner);
3100 * SolverControl solver_control(stokes_matrix.m(),
3101 * 1e-6 * stokes_rhs.l2_norm());
3103 * SolverGMRES<TrilinosWrappers::MPI::BlockVector> gmres(
3105 * SolverGMRES<TrilinosWrappers::MPI::BlockVector>::AdditionalData(100));
3107 * for (unsigned int i = 0; i < stokes_solution.size(); ++i)
3108 * if (stokes_constraints.is_constrained(i))
3109 * stokes_solution(i) = 0;
3111 * gmres.solve(stokes_matrix, stokes_solution, stokes_rhs, preconditioner);
3113 * stokes_constraints.distribute(stokes_solution);
3115 * std::cout << " " << solver_control.last_step()
3116 * << " GMRES iterations for Stokes subsystem." << std::endl;
3121 * Once we know the Stokes solution, we can determine the new time step
3122 * from the maximal velocity. We have to do this to satisfy the CFL
3123 * condition since convection terms are treated explicitly in the
3124 * temperature equation, as discussed in the introduction. The exact form
3125 * of the formula used here for the time step is discussed in the results
3126 * section of this program.
3130 * There is a snatch here. The formula contains a division by the maximum
3131 * value of the velocity. However, at the start of the computation, we
3132 * have a constant temperature field (we start with a constant
3133 * temperature, and it will be nonconstant only after the first time step
3134 * during which the source acts). Constant temperature means that no
3135 * buoyancy acts, and so the velocity is zero. Dividing by it will not
3136 * likely lead to anything good.
3140 * To avoid the resulting infinite time step, we ask whether the maximal
3141 * velocity is very small (in particular smaller than the values we
3142 * encounter during any of the following time steps) and if so rather than
3143 * dividing by zero we just divide by a small value, resulting in a large
3144 * but finite time step.
3147 * old_time_step = time_step;
3148 * const double maximal_velocity = get_maximal_velocity();
3150 * if (maximal_velocity >= 0.01)
3151 * time_step = 1. / (1.7 * dim * std::sqrt(1. * dim)) / temperature_degree *
3152 * GridTools::minimal_cell_diameter(triangulation) /
3155 * time_step = 1. / (1.7 * dim * std::sqrt(1. * dim)) / temperature_degree *
3156 * GridTools::minimal_cell_diameter(triangulation) / .01;
3159 * << "Time step: " << time_step << std::endl;
3161 * temperature_solution = old_temperature_solution;
3165 * Next we set up the temperature system and the right hand side using the
3166 * function <code>assemble_temperature_system()</code>. Knowing the
3167 * matrix and right hand side of the temperature equation, we set up a
3168 * preconditioner and a solver. The temperature matrix is a mass matrix
3169 * (with eigenvalues around one) plus a Laplace matrix (with eigenvalues
3170 * between zero and @f$ch^{-2}@f$) times a small number proportional to the
3171 * time step @f$k_n@f$. Hence, the resulting symmetric and positive definite
3172 * matrix has eigenvalues in the range @f$[1,1+k_nh^{-2}]@f$ (up to
3173 * constants). This matrix is only moderately ill conditioned even for
3174 * small mesh sizes and we get a reasonably good preconditioner by simple
3175 * means, for example with an incomplete Cholesky decomposition
3176 * preconditioner (IC) as we also use for preconditioning the pressure
3177 * mass matrix solver. As a solver, we choose the conjugate gradient
3178 * method CG. As before, we tell the solver to use Trilinos vectors via
3179 * the template argument <code>TrilinosWrappers::MPI::Vector</code>.
3180 * Finally, we solve, distribute the hanging node constraints and write out
3181 * the number of iterations.
3184 * assemble_temperature_system(maximal_velocity);
3186 * SolverControl solver_control(temperature_matrix.m(),
3187 * 1e-8 * temperature_rhs.l2_norm());
3188 * SolverCG<TrilinosWrappers::MPI::Vector> cg(solver_control);
3190 * TrilinosWrappers::PreconditionIC preconditioner;
3191 * preconditioner.initialize(temperature_matrix);
3193 * cg.solve(temperature_matrix,
3194 * temperature_solution,
3198 * temperature_constraints.distribute(temperature_solution);
3200 * std::cout << " " << solver_control.last_step()
3201 * << " CG iterations for temperature." << std::endl;
3205 * At the end of this function, we step through the vector and read out
3206 * the maximum and minimum temperature value, which we also want to
3207 * output. This will come in handy when determining the correct constant
3208 * in the choice of time step as discuss in the results section of this
3212 * double min_temperature = temperature_solution(0),
3213 * max_temperature = temperature_solution(0);
3214 * for (unsigned int i = 0; i < temperature_solution.size(); ++i)
3217 * std::min<double>(min_temperature, temperature_solution(i));
3219 * std::max<double>(max_temperature, temperature_solution(i));
3222 * std::cout << " Temperature range: " << min_temperature << ' '
3223 * << max_temperature << std::endl;
3232 * <a name="BoussinesqFlowProblemoutput_results"></a>
3233 * <h4>BoussinesqFlowProblem::output_results</h4>
3237 * This function writes the solution to a VTK output file for visualization,
3238 * which is done every tenth time step. This is usually quite a simple task,
3239 * since the deal.II library provides functions that do almost all the job
3240 * for us. There is one new function compared to previous examples: We want
3241 * to visualize both the Stokes solution and the temperature as one data
3242 * set, but we have done all the calculations based on two different
3243 * DoFHandler objects. Luckily, the DataOut class is prepared to deal with
3244 * it. All we have to do is to not attach one single DoFHandler at the
3245 * beginning and then use that for all added vector, but specify the
3246 * DoFHandler to each vector separately. The rest is done as in @ref step_22 "step-22". We
3247 * create solution names (that are going to appear in the visualization
3248 * program for the individual components). The first <code>dim</code>
3249 * components are the vector velocity, and then we have pressure for the
3250 * Stokes part, whereas temperature is scalar. This information is read out
3251 * using the DataComponentInterpretation helper class. Next, we actually
3252 * attach the data vectors with their DoFHandler objects, build patches
3253 * according to the degree of freedom, which are (sub-) elements that
3254 * describe the data for visualization programs. Finally, we open a file
3255 * (that includes the time step number) and write the vtk data into it.
3258 * template <int dim>
3259 * void BoussinesqFlowProblem<dim>::output_results() const
3261 * if (timestep_number % 10 != 0)
3264 * std::vector<std::string> stokes_names(dim, "velocity");
3265 * stokes_names.emplace_back("p");
3266 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
3267 * stokes_component_interpretation(
3268 * dim + 1, DataComponentInterpretation::component_is_scalar);
3269 * for (unsigned int i = 0; i < dim; ++i)
3270 * stokes_component_interpretation[i] =
3271 * DataComponentInterpretation::component_is_part_of_vector;
3273 * DataOut<dim> data_out;
3274 * data_out.add_data_vector(stokes_dof_handler,
3277 * stokes_component_interpretation);
3278 * data_out.add_data_vector(temperature_dof_handler,
3279 * temperature_solution,
3281 * data_out.build_patches(std::min(stokes_degree, temperature_degree));
3283 * std::ofstream output("solution-" +
3284 * Utilities::int_to_string(timestep_number, 4) + ".vtk");
3285 * data_out.write_vtk(output);
3293 * <a name="BoussinesqFlowProblemrefine_mesh"></a>
3294 * <h4>BoussinesqFlowProblem::refine_mesh</h4>
3298 * This function takes care of the adaptive mesh refinement. The three tasks
3299 * this function performs is to first find out which cells to
3300 * refine/coarsen, then to actually do the refinement and eventually
3301 * transfer the solution vectors between the two different grids. The first
3302 * task is simply achieved by using the well-established Kelly error
3303 * estimator on the temperature (it is the temperature we're mainly
3355 *
cell->clear_refine_flag();
3380 *
std::vector<TrilinosWrappers::MPI::Vector>
x_temperature(2);
3475 * initialize time step number
and time step
and start
the time loop.
3517 *
we've remeshed before), and then do the solve. Before going on with
3518 * the next time step, we have to check whether we should first finish
3519 * the pre-refinement steps or if we should remesh (every fifth time
3520 * step), refining up to a level that is consistent with initial
3521 * refinement and pre-refinement steps. Last in the loop is to advance
3522 * the solutions, i.e., to copy the solutions to the next "older" time
3526 * assemble_stokes_system();
3527 * build_stokes_preconditioner();
3528 * assemble_temperature_matrix();
3534 * std::cout << std::endl;
3536 * if ((timestep_number == 0) &&
3537 * (pre_refinement_step < n_pre_refinement_steps))
3539 * refine_mesh(initial_refinement + n_pre_refinement_steps);
3540 * ++pre_refinement_step;
3541 * goto start_time_iteration;
3543 * else if ((timestep_number > 0) && (timestep_number % 5 == 0))
3544 * refine_mesh(initial_refinement + n_pre_refinement_steps);
3546 * time += time_step;
3547 * ++timestep_number;
3549 * old_stokes_solution = stokes_solution;
3550 * old_old_temperature_solution = old_temperature_solution;
3551 * old_temperature_solution = temperature_solution;
3555 * Do all the above until we arrive at time 100.
3558 * while (time <= 100);
3560 * } // namespace Step31
3567 * <a name="Thecodemaincodefunction"></a>
3568 * <h3>The <code>main</code> function</h3>
3572 * The main function looks almost the same as in all other programs.
3576 * There is one difference we have to be careful about. This program uses
3577 * Trilinos and, typically, Trilinos is configured so that it can run in
3592 *
using namespace dealii;
3593 *
using namespace Step31;
3605 *
"This program can only be run in serial, use ./step-31"));
3610 *
catch (std::exception &exc)
3612 *
std::cerr << std::endl
3614 *
<<
"----------------------------------------------------"
3616 *
std::cerr <<
"Exception on processing: " << std::endl
3617 *
<< exc.what() << std::endl
3618 *
<<
"Aborting!" << std::endl
3619 *
<<
"----------------------------------------------------"
3626 *
std::cerr << std::endl
3628 *
<<
"----------------------------------------------------"
3630 *
std::cerr <<
"Unknown exception!" << std::endl
3631 *
<<
"Aborting!" << std::endl
3632 *
<<
"----------------------------------------------------"
3687Number
of degrees
of freedom: 15294 (9398+1197+4699)
3699Number
of degrees
of freedom: 30114 (18518+2337+9259)
3706 Time step: 0.0574449
3714 Time step: 0.0574449
3733 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.00.png" alt=
"">
3736 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.01.png" alt=
"">
3739 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.02.png" alt=
"">
3742 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.03.png" alt=
"">
3747 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.04.png" alt=
"">
3750 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.05.png" alt=
"">
3753 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.06.png" alt=
"">
3756 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.solution.07.png" alt=
"">
3783 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.00.png" alt=
"">
3786 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.01.png" alt=
"">
3789 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.02.png" alt=
"">
3792 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.03.png" alt=
"">
3797 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.04.png" alt=
"">
3800 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.05.png" alt=
"">
3803 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.06.png" alt=
"">
3806 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.2d.grid.07.png" alt=
"">
3834Number
of degrees
of freedom: 12379 (8943+455+2981)
3846Number
of degrees
of freedom: 51497 (37305+1757+12435)
3858Number
of degrees
of freedom: 192425 (139569+6333+46523)
388250, 100, 150, 200, 300, 400, 500, 600, 700,
and 800
yields the
3888 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.00.png" alt=
"">
3891 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.01.png" alt=
"">
3894 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.02.png" alt=
"">
3897 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.03.png" alt=
"">
3902 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.04.png" alt=
"">
3905 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.05.png" alt=
"">
3908 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.06.png" alt=
"">
3911 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.07.png" alt=
"">
3916 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.08.png" alt=
"">
3919 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.09.png" alt=
"">
3922 <
img src=
"https://www.dealii.org/images/steps/developer/step-31.3d.solution.10.png" alt=
"">
3934leads to the crumpled appearance of the isosurfaces. The visualizations shown
3935here were generated using a version of the example which did not enforce the
3936constraints after transferring the mesh.
3940<a name="Numericalexperimentstodetermineoptimalparameters"></a><h3> Numerical experiments to determine optimal parameters </h3>
3943The program as is has three parameters that we don't
have much of a
3971constants @f$c_k@f$ and @f$\beta@f$.
3973Below, we will not discuss the choice of @f$c_R@f$. In the program, we set
3974it to @f$c_R=2^{\frac{4-2\alpha}{d}}@f$. The reason for this value is a
3975bit complicated and has more to do with the history of the program
3976than reasoning: while the correct formula for the global scaling
3977parameter @f$c(\mathbf{u},T)@f$ is shown above, the program (including the
3978version shipped with deal.II 6.2) initially had a bug in that we
3981 \|\mathbf{u}\|_{L^\infty(\Omega)} \ \mathrm{var}(T)
3982 \ \frac{1}{|\mathrm{diam}(\Omega)|^{\alpha-2}}@f$ instead, where
3983we had set the scaling parameter to one. Since we only computed on the
3984unit square/cube where @f$\mathrm{diam}(\Omega)=2^{1/d}@f$, this was
3985entirely equivalent to using the correct formula with
3986@f$c_R=\left(2^{1/d}\right)^{4-2\alpha}=2^{\frac{4-2\alpha}{d}}@f$. Since
3987this value for @f$c_R@f$ appears to work just fine for the current
3988program, we corrected the formula in the program and set @f$c_R@f$ to a
3989value that reproduces exactly the results we had before. We will,
3990however, revisit this issue again in @ref step_32 "step-32".
3992Now, however, back to the discussion of what values of @f$c_k@f$ and
3993@f$\beta@f$ to choose:
3996<a name="Choosingicsubksubiandbeta"></a><h4> Choosing <i>c<sub>k</sub></i> and beta </h4>
3999These two constants are definitely linked in some way. The reason is easy to
4000see: In the case of a pure advection problem,
4001@f$\frac{\partial T}{\partial t} + \mathbf{u}\cdot\nabla T = \gamma@f$, any
4002explicit scheme has to satisfy a CFL condition of the form
4003@f$k\le \min_K \frac{c_k^a h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$. On the other hand,
4004for a pure diffusion problem,
4005@f$\frac{\partial T}{\partial t} + \nu \Delta T = \gamma@f$,
4006explicit schemes need to satisfy a condition
4007@f$k\le \min_K \frac{c_k^d h_K^2}{\nu}@f$. So given the form of @f$\nu@f$ above, an
4008advection diffusion problem like the one we have to solve here will result in
4009a condition of the form
4011k\le \min_K \min \left\{
4012 \frac{c_k^a h_K}{\|\mathbf{u}\|_{L^\infty(K)}},
4013 \frac{c_k^d h_K^2}{\beta \|\mathbf{u}\|_{L^\infty(K)} h_K}\right\}
4015 \min_K \left( \min \left\{
4017 \frac{c_k^d}{\beta}\right\}
4018 \frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}} \right)
4020It follows that we have to face the fact that we might want to choose @f$\beta@f$
4021larger to improve the stability of the numerical scheme (by increasing the
4022amount of artificial diffusion), but we have to pay a price in the form of
4023smaller, and consequently more time steps. In practice, one would therefore
4024like to choose @f$\beta@f$ as small as possible to keep the transport problem
4025sufficiently stabilized while at the same time trying to choose the time step
4026as large as possible to reduce the overall amount of work.
4028The find the right balance, the only way is to do a few computational
4035 \|\mathbf{u}\|_{L^\infty(K)} h_K
4036@f$ to eliminate the effect of the constant @f$c_R@f$ (we know that
4037solutions are stable by using this version of @f$\nu(T)@f$ as an artificial
4038viscosity, but that we can improve things -- i.e. make the solution
4039sharper -- by using the more complicated formula for this artificial
4040viscosity). We then run the program
4041for different values @f$c_k,\beta@f$ and observe maximal and minimal temperatures
4042in the domain. What we expect to see is this: If we choose the time step too
4043big (i.e. choose a @f$c_k@f$ bigger than theoretically allowed) then we will get
4044exponential growth of the temperature. If we choose @f$\beta@f$ too small, then
4045the transport stabilization becomes insufficient and the solution will show
4046significant oscillations but not exponential growth.
4049<a name="ResultsforQsub1subelements"></a><h5>Results for Q<sub>1</sub> elements</h5>
4052Here is what we get for
4053@f$\beta=0.01, \beta=0.1@f$, and @f$\beta=0.5@f$, different choices of @f$c_k@f$, and
4054bilinear elements (<code>temperature_degree=1</code>) in 2d:
4056<table align="center" class="doxtable">
4059 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q1.beta=0.01.png" alt="">
4062 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q1.beta=0.03.png" alt="">
4067 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q1.beta=0.1.png" alt="">
4070 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q1.beta=0.5.png" alt="">
4075The way to interpret these graphs goes like this: for @f$\beta=0.01@f$ and
4076@f$c_k=\frac 12,\frac 14@f$, we see exponential growth or at least large
4077variations, but if we choose
4078@f$k=\frac 18\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$
4079or smaller, then the scheme is
4080stable though a bit wobbly. For more artificial diffusion, we can choose
4081@f$k=\frac 14\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$
4082or smaller for @f$\beta=0.03@f$,
4083@f$k=\frac 13\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$
4084or smaller for @f$\beta=0.1@f$, and again need
4085@f$k=\frac 1{15}\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$
4086for @f$\beta=0.5@f$ (this time because much diffusion requires a small time
4089So how to choose? If we were simply interested in a large time step, then we
4090would go with @f$\beta=0.1@f$ and
4091@f$k=\frac 13\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$.
4097elements to approximate a discontinuous solution. We can therefore see that
4098choosing @f$\beta@f$ too small is bad: too little artificial diffusion leads to
4103On
the other hand,
let's also look at the maximum temperature. Watching the
4104movie of the solution, we see that initially the fluid is at rest. The source
4105keeps heating the same volume of fluid whose temperature increases linearly at
4106the beginning until its buoyancy is able to move it upwards. The hottest part
4107of the fluid is therefore transported away from the solution and fluid taking
4108its place is heated for only a short time before being moved out of the source
4109region, therefore remaining cooler than the initial bubble. If @f$\kappa=0@f$
4110(in the program it is nonzero but very small) then the hottest part of the
4111fluid should be advected along with the flow with its temperature
4125<a name="ResultsforQsub2subelements"></a><h5>Results for Q<sub>2</sub> elements</h5>
4128One can repeat the same sequence of experiments for higher order
4129elements as well. Here are the graphs for bi-quadratic shape functions
4130(<code>temperature_degree=2</code>) for the temperature, while we
4131retain the @f$Q_2/Q_1@f$ stable Taylor-Hood element for the Stokes system:
4133<table align="center" class="doxtable">
4136 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q2.beta=0.01.png" alt="">
4139 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q2.beta=0.03.png" alt="">
4144 <img src="https://www.dealii.org/images/steps/developer/step-31.timestep.q2.beta=0.1.png" alt="">
4149Again, small values of @f$\beta@f$ lead to less diffusion but we have to
4150choose the time step very small to keep things under control. Too
4151large values of @f$\beta@f$ make for more diffusion, but again require
4152small time steps. The best value would appear to be @f$\beta=0.03@f$, as
4153for the @f$Q_1@f$ element, and then we have to choose
4154@f$k=\frac 18\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$ — exactly
4155half the size for the @f$Q_1@f$ element, a fact that may not be surprising
4156if we state the CFL condition as the requirement that the time step be
4157small enough so that the distance transport advects in each time step
4158is no longer than one <i>grid point</i> away (which for @f$Q_1@f$ elements
4159is @f$h_K@f$, but for @f$Q_2@f$ elements is @f$h_K/2@f$). It turns out that @f$\beta@f$
4160needs to be slightly larger for obtaining stable results also late in
4161the simulation at times larger than 60, so we actually choose it as
4162@f$\beta = 0.034@f$ in the code.
4165<a name="Resultsfor3d"></a><h5>Results for 3d</h5>
4168One can repeat these experiments in 3d and find the optimal time step
4169for each value of @f$\beta@f$ and find the best value of @f$\beta@f$. What one
4170finds is that for the same @f$\beta@f$ already used in 2d, the time steps
4171needs to be a bit smaller, by around a factor of 1.2 or so. This is
4172easily explained: the time step restriction is
4173@f$k=\min_K \frac{ch_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$ where @f$h_K@f$ is
4174the <i>diameter</i> of the cell. However, what is really needed is the
4175distance between mesh points, which is @f$\frac{h_K}{\sqrt{d}}@f$. So a
4176more appropriate form would be
4177@f$k=\min_K \frac{ch_K}{\|\mathbf{u}\|_{L^\infty(K)}\sqrt{d}}@f$.
4179The second find is that one needs to choose @f$\beta@f$ slightly bigger
4180(about @f$\beta=0.05@f$ or so). This then again reduces the time step we
4186<a name="Conclusions"></a><h5>Conclusions</h5>
4189Concluding, from the simple computations above, @f$\beta=0.034@f$ appears to be a
4190good choice for the stabilization parameter in 2d, and @f$\beta=0.05@f$ in 3d. In
4191a dimension independent way, we can model this as @f$\beta=0.017d@f$. If one does
4192longer computations (several thousand time steps) on finer meshes, one
4193realizes that the time step size is not quite small enough and that for
4194stability one will have to reduce the above values a bit more (by about a
4195factor of @f$\frac 78@f$).
4197As a consequence, a formula that reconciles 2d, 3d, and variable polynomial
4198degree and takes all factors in account reads as follows:
4201 \frac 1{2 \cdot 1.7} \frac 1{\sqrt{d}}
4204 \frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}
4206 \frac 1{1.7 d\sqrt{d}}
4208 \frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}.
4210In the first form (in the center of the equation), @f$\frac
42111{2 \cdot 1.7}@f$ is a universal constant, @f$\frac 1{\sqrt{d}}@f$
4212is the factor that accounts for the difference between cell diameter
4213and grid point separation,
4214@f$\frac 2d@f$ accounts for the increase in @f$\beta@f$ with space dimension,
4215@f$\frac 1{q_T}@f$ accounts for the distance between grid points for
4216higher order elements, and @f$\frac{h_K}{\|\mathbf{u}\|_{L^\infty(K)}}@f$
4217for the local speed of transport relative to the cell size. This is
4218the formula that we use in the program.
4220As for the question of whether to use @f$Q_1@f$ or @f$Q_2@f$ elements for the
4221temperature, the following considerations may be useful: First,
4222solving the temperature equation is hardly a factor in the overall
4223scheme since almost the entire compute time goes into solving the
4224Stokes system in each time step. Higher order elements for the
4225temperature equation are therefore not a significant drawback. On the
4226other hand, if one compares the size of the over- and undershoots the
4227solution produces due to the discontinuous source description, one
4228notices that for the choice of @f$\beta@f$ and @f$k@f$ as above, the @f$Q_1@f$
4229solution dips down to around @f$-0.47@f$, whereas the @f$Q_2@f$ solution only
4230goes to @f$-0.13@f$ (remember that the exact solution should never become
4231negative at all. This means that the @f$Q_2@f$ solution is significantly
4232more accurate; the program therefore uses these higher order elements,
4233despite the penalty we pay in terms of smaller time steps.
4236<a name="Possibilitiesforextensions"></a><h3> Possibilities for extensions </h3>
4239There are various ways to extend the current program. Of particular interest
4240is, of course, to make it faster and/or increase the resolution of the
4241program, in particular in 3d. This is the topic of the @ref step_32 "step-32"
4242tutorial program which will implement strategies to solve this problem in
4243%parallel on a cluster. It is also the basis of the much larger open
4244source code ASPECT (see https://aspect.geodynamics.org/ ) that can solve realistic
4245problems and that constitutes the further development of @ref step_32 "step-32".
4247Another direction would be to make the fluid flow more realistic. The program
4248was initially written to simulate various cases simulating the convection of
4264<a name=
"PlainProg"></a>
value_type * data() const noexcept
void reinit(value_type *starting_element, const std::size_t n_elements)
void reinit(const TriaIterator< DoFCellAccessor< dim, spacedim, level_dof_access > > &cell)
const std::vector< Point< dim > > & get_unit_support_points() 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, typename InputVector::value_type > * > &neumann_bc, const InputVector &solution, Vector< float > &error, const ComponentMask &component_mask=ComponentMask(), 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)
__global__ void set(Number *val, const Number s, const size_type N)
static ::ExceptionBase & ExcNotImplemented()
#define Assert(cond, exc)
static ::ExceptionBase & ExcMessage(std::string arg1)
#define AssertThrow(cond, exc)
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_hessians
Second derivatives of shape functions.
@ update_values
Shape function values.
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
@ update_quadrature_points
Transformed quadrature points.
IndexSet complete_index_set(const IndexSet::size_type N)
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)
Expression sign(const Expression &x)
void downstream(DoFHandler< dim, spacedim > &dof_handler, const Tensor< 1, spacedim > &direction, const bool dof_wise_renumbering=false)
void hyper_cube(Triangulation< dim, spacedim > &tria, const double left=0., const double right=1., const bool colorize=false)
void refine(Triangulation< dim, spacedim > &tria, const Vector< Number > &criteria, const double threshold, const unsigned int max_to_mark=numbers::invalid_unsigned_int)
void refine_and_coarsen_fixed_fraction(Triangulation< dim, spacedim > &tria, const Vector< Number > &criteria, const double top_fraction, const double bottom_fraction, const unsigned int max_n_cells=std::numeric_limits< unsigned int >::max(), const VectorTools::NormType norm_type=VectorTools::NormType::L1_norm)
@ matrix
Contents is actually a matrix.
double norm(const FEValuesBase< dim > &fe, const ArrayView< const std::vector< Tensor< 1, dim > > > &Du)
Point< spacedim > point(const gp_Pnt &p, const double tolerance=1e-10)
SymmetricTensor< 2, dim, Number > e(const Tensor< 2, dim, Number > &F)
SymmetricTensor< 2, dim, Number > d(const Tensor< 2, dim, Number > &F, const Tensor< 2, dim, Number > &dF_dt)
constexpr ReturnType< rank, T >::value_type & extract(T &t, const ArrayType &indices)
void call(const std::function< RT()> &function, internal::return_value< RT > &ret_val)
std::vector< unsigned int > serial(const std::vector< unsigned int > &targets, const std::function< RequestType(const unsigned int)> &create_request, const std::function< AnswerType(const unsigned int, const RequestType &)> &answer_request, const std::function< void(const unsigned int, const AnswerType &)> &process_answer, const MPI_Comm comm)
T sum(const T &t, const MPI_Comm mpi_communicator)
unsigned int n_mpi_processes(const MPI_Comm mpi_communicator)
void run(const Iterator &begin, const std_cxx20::type_identity_t< Iterator > &end, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length, const unsigned int chunk_size)
void abort(const ExceptionBase &exc) noexcept
unsigned int n_active_cells(const internal::TriangulationImplementation::NumberCache< 1 > &c)
void copy(const T *begin, const T *end, U *dest)
int(&) functions(const void *v1, const void *v2)
void assemble(const MeshWorker::DoFInfoBox< dim, DOFINFO > &dinfo, A *assembler)
void reinit(MatrixBlock< MatrixType > &v, const BlockSparsityPattern &p)
static const unsigned int invalid_unsigned_int
::VectorizedArray< Number, width > min(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > max(const ::VectorizedArray< Number, width > &, const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > pow(const ::VectorizedArray< Number, width > &, const Number p)
::VectorizedArray< Number, width > abs(const ::VectorizedArray< Number, width > &)
const ::parallel::distributed::Triangulation< dim, spacedim > * triangulation
DEAL_II_HOST constexpr Number trace(const SymmetricTensor< 2, dim2, Number > &)