diff --git a/tests/weak_forms/wf_common_tests/step-navier_stokes-beltrami.h b/tests/weak_forms/wf_common_tests/step-navier_stokes-beltrami.h new file mode 100644 index 00000000..a4e45c6e --- /dev/null +++ b/tests/weak_forms/wf_common_tests/step-navier_stokes-beltrami.h @@ -0,0 +1,2166 @@ +/* $Id: NavierStokes-Beltrami.cc 2008-04-15 10:54:52CET martinkr $ */ +/* Author: Martin Kronbichler, Uppsala University, 2008 */ +/* $Id: NavierStokes-Beltrami.cc 2008-04-15 10:54:52CET martinkr $ */ +/* Version: $Name$ */ +/* */ +/* Copyright (C) 2008 by the author */ +/* */ +/* This file is subject to QPL and may not be distributed */ +/* without copyright and license information. Please refer */ +/* to the file deal.II/doc/license.html for the text and */ +/* further information on this license. */ + +// @sect3{The Navier-Stokes problem} + +// This program implements an algorithm for the +// incompressible Navier-Stokes equations solving +// the whole system at once, i.e., without any +// projection equation. This approach is also called +// "monolithic" in the literature. +// +// The program is build mainly on step-20 for the +// solution of the mixed Laplace problem and the +// step-22 tutorial program for the stationary +// Stokes problem - with the relevant changes for +// Navier-Stokes, though. The program compares the +// solution of the Navier-Stokes equations with the +// exact velocities and pressures for the so-called +// 3D Beltrami flow, a laminar flow field with an +// analytic solution expression. The Beltrami +// parameters $a$ and $d$ are set to $a = \pi/4$ and +// $d = a \sqrt{2}$, respectively. For details on +// the Beltrami flow, see, e.g., V. Gravemeier, The +// Variational Multiscale Method for Laminar and +// Turbulent Flow, Arch. Comput. Meth. Engng. +// 13(2):249-324, section 5.1. The 2D version of +// this flow is referred to as Taylor flow in the +// literature. + +// @sect4{Technical remarks on this file} + +// You compile and run this program by just +// typing make run in the +// terminal window of the directory where +// this file and the appropriate +// Makefile sits. +// It is assumed that +// you have a fully compiled deal.II program +// on your computer (obtained by first +// performing the ./configure +// in the main deal.II directory and then +// running make all (what usually +// takes some hour). +// The directory of this +// file is supposed to sit two levels above +// the deal.II main directory, i.e., +// ../../ refers to deal's main +// directory and ../../lib/ is +// the directory of the deal.II library +// files. + +// @sect3{Include files} + +// In the beginning, we have to include the relevant +// deal.II libraries that contain the main FEM info. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Next, import all dealii names +// into global namespace. +using namespace dealii; + +// @sect3{The NavierStokesProblem class template} +template +class NavierStokesProblem +{ + // This class is basically an extension of the step-20 and + // step-21 tutorial programs of deal.II. The + // constructor of the Navier-Stokes class is + // hence build up in the same way as there. + + // @sect4{Member functions} + + // The member functions that are called from + // outside are the constructor itself and + // the run function. +public: + NavierStokesProblem(); + void + run(); + + // The private part is only + // accessible to member functions of + // NavierStokesProblem. + // The first few functions do, in order, + // first generate the grid and dof + // structure, assemble the linear system + // and right hand side (where the first + // of these functions acutally only distributes + // the work to possibly several threads + // that run the next function in parallel). + // The solve function solves + // the linear system to a given right hand + // sides (including a few options + // for the preconditioner). + // The last functions calculate the + // L2 error, write the information to + // files and print the computing + // times of individual code aspects. +private: + void + make_grid_and_dofs(); + + void + assemble_system(); + + void + assemble_system_interval( + const typename DoFHandler::active_cell_iterator &beginc, + const typename DoFHandler::active_cell_iterator &endc); + + unsigned int + solve(); + + void + compute_errors() const; + + void + output_results(const unsigned int timestep_number) const; + + void + print_computing_times() const; + + // @sect4{Problem variables} + + // The first four declared variables + // regard the triangulation object + // and the finite element discretization. + // Then define the variable to contain + // information on the degrees of + // freedom and (possibly) constraints + // for adaptive grid + // refinement as well as for the pressure + // that might need to be + // fixed by a zero mean constraint. + const unsigned int degree; + FESystem fe; + Triangulation triangulation; + const unsigned int n_global_refinements; + + DoFHandler dof_handler; + ConstraintMatrix hanging_node_and_pressure_constraints; + + // In contrast to the step-20 tutorial + // program, we do not use block vectors + // and matrices here, but build up the + // system at once. A more efficient + // implementation may require changes + // at this point, though. + // + // As opposed to the linear and time + // independent systems in step-20 and + // step-22, we need some vectors to + // store old solution values from + // time stepping. + SparsityPattern sparsity_pattern; + SparseMatrix system_matrix; + + Vector solution_update; + Vector solution, solution_old, solution_old_scaled; + Vector system_rhs, exact; + + // We set the variables for the time integration, + // i.e., time, final time, time step, previous time + // step (for BDF-2), time step number and a weight + // that is used in assembly for the contribution of + // the time derivative. + // + // The variable nu is used to define + // the fluid kinematic viscosity, the only parameter + // in the incompressible Navier-Stokes equations, + // where density is constant. + // + // The subsequent parameters are used to control how + // many nonlinear iterations we want to perform at + // most, the tolerance level at which we want to + // stop that iteration, and how often we write + // results to an output file. + // + // The last parameter is a switch to disable/enable + // the stabilization. There are a few options that + // can be set, but it is recommended to either use + // the setting 0 (which uses plain finite elements + // without additional stabilization) or setting 5, + // which uses all stabilization options, which are + // SUPG and LSIC for velocity test functions, and + // PSPG for the pressure test function. + // More details: + //
    + //
  • 0: no stabilization at all + //
  • 1: PSPG stabilization only + //
  • 2: LSIC only + //
  • 3: PSPG and LSIC + //
  • 4: SUPG and LSIC + //
  • 5: SUPG, PSPG, and LSIC
+ const enum TimeStepping { EulerBackw, BDF2 } time_stepping; + double time; + const double time_final; + double time_step, time_step_old, time_step_weight; + int time_step_number; + + const double nu; + + const unsigned int max_nl_iteration; + const double tol_nl_iteration; + + const unsigned int output_timestep_skip; + + const unsigned int stabilization; + + // Assembly and linear solver options. + // + // The variable pressure_constraint + // determines whether to constraint the pressure to + // zero mean value on the boundary or to set it the + // standard Dirichlet conditions onto it. + // + // The variable assembly_type specifies + // whether the assembly should be done in parallel + // (with as many threads as there are CPUs detected + // on the system) or in serial. + // + // The variable solver_type specifies + // the linear solver to be used. One can use + // iterative solvers or direct solve with UMFPACK, + // or one can disable the solve process (e.g. when + // testing something else). + // + // Last, the variable + // preconditioner_type specifies the + // preconditioning to be used for the iterative + // solver. + const bool pressure_constraint; + const enum AssemblyType { Parallel, Serial } assembly_type; + const enum SolverType { None, GMRES, BiCGStab, Direct } solver_type; + const enum PreconditionerType { Id, SSOR, ILU, Vanka } preconditioner_type; + + // Since we want to do the assembly in parallel, we + // have to include a variable that locks the system + // matrix and rhs while one process is writing the + // entries. + Threads::ThreadMutex assembler_lock; + + // We shall use a vector that measures the times in + // individual steps for the solution, namely setting + // up the system like making the grid, assembling + // the linear systems, manipulating the variables + // during the steps and solving the linear systems. + Vector comptimes; + Vector compcounter; +}; + + + +// @sect3{Equation data} + +// The task is now to define - in analogy to the +// step-20 and step-22 tutorial programs, the right +// hand side of the problem (i.e. the forcing term +// f in the momentum equation of the Navier-Stokes +// system), boundary values for both the velocity +// and the pressure, and a function that describes +// both velocity and pressure in the exact solution. +// The functions are of dimension dim +// in the velocity and one for the pressure. +// +// @sect4{Right hand side} +template +class RightHandSide : public Function +{ +public: + RightHandSide(const unsigned int n_components = dim, const double time = 0.) + : Function(n_components, time) + {} + + virtual void + vector_value(const Point &p, Vector &values) const; +}; + +template +void +RightHandSide::vector_value(const Point &p, + Vector & values) const +{ + Assert(values.size() == dim, ExcDimensionMismatch(values.size(), dim)); + + // double time = this->get_time (); + + + for (unsigned int i = 0; i < dim; ++i) + values(i) = 0. + 0 * p[i]; +} + + + +// @sect4{Exact Solution} + +// Before specifying the initial condition and the +// boundary conditions, we implement the exact +// solution. From this, we shall get the necessary +// boundary and initial data afterwards. The +// function implemented here is the so-called Taylor +// flow in 2D (see Kim and Moin, JCP 59, pp. 308-323 +// (1985)) and the Beltrami flow in 3D (Ethier and +// Steiman, Int. J. Num. Meth. Fluids 19, 369-375, +// 1994). +template +class ExactSolution : public Function +{ +public: + ExactSolution(const unsigned int n_components = dim + 1, + const double time = 0., + const double viscosity = 1.) + : Function(n_components, time) + , nu(viscosity) + {} + + virtual void + vector_value(const Point &p, Vector &values) const; + +private: + const double nu; +}; + +template +void +ExactSolution::vector_value(const Point &p, + Vector & values) const +{ + Assert(values.size() == dim + 1, + ExcDimensionMismatch(values.size(), dim + 1)); + + const double time = this->get_time(); + + const double a = 0.25 * deal_II_numbers::PI; + const double d = std::sqrt(2) * a; + + switch (dim) + { + case 3: + values(0) = -a * + (std::exp(a * p[0]) * std::sin(a * p[1] + d * p[2]) + + std::exp(a * p[2]) * std::cos(a * p[0] + d * p[1])) * + std::exp(-nu * d * d * time); + values(1) = -a * + (std::exp(a * p[1]) * std::sin(a * p[2] + d * p[0]) + + std::exp(a * p[0]) * std::cos(a * p[1] + d * p[2])) * + std::exp(-nu * d * d * time); + values(2) = -a * + (std::exp(a * p[2]) * std::sin(a * p[0] + d * p[1]) + + std::exp(a * p[1]) * std::cos(a * p[2] + d * p[0])) * + std::exp(-nu * d * d * time); + values(3) = + -a * a * 0.5 * + (std::exp(2 * a * p[0]) + std::exp(2 * a * p[1]) + + std::exp(2 * a * p[2]) + + 2 * std::sin(a * p[0] + d * p[1]) * std::cos(a * p[2] + d * p[0]) * + std::exp(a * (p[1] + p[2])) + + 2 * std::sin(a * p[1] + d * p[2]) * std::cos(a * p[0] + d * p[1]) * + std::exp(a * (p[2] + p[0])) + + 2 * std::sin(a * p[2] + d * p[0]) * std::cos(a * p[1] + d * p[2]) * + std::exp(a * (p[0] + p[1]))) * + std::exp(-2 * nu * d * d * time); + break; + case 2: + values(0) = -a * std::cos(a * p[0]) * std::sin(a * p[1]) * + std::exp(-nu * d * d * time); + values(1) = a * std::sin(a * p[0]) * std::cos(a * p[1]) * + std::exp(-nu * d * d * time); + values(2) = -a * a * 0.25 * + (std::cos(2 * a * p[0]) + std::cos(2 * a * p[1])) * + std::exp(-2 * nu * d * d * time); + break; + default: + Assert(false, ExcNotImplemented()); + } +} + + + +// @sect4{Initial values at time 0} +// +// Note that we only refer to the exact solution at +// this point (and we'll do so for the boundary +// condition as well). This has the advantage that +// changes would be needed only at one point in the +// code. +template +class InitialValues : public Function +{ +public: + InitialValues(const unsigned int n_components = dim + 1, + const double time = 0., + const double viscosity = 1.) + : Function(n_components, time) + , nu(viscosity) + {} + + virtual void + vector_value(const Point &p, Vector &values) const; + +private: + const double nu; +}; + +template +void +InitialValues::vector_value(const Point &p, + Vector & values) const +{ + ExactSolution(dim + 1, this->get_time(), nu).vector_value(p, values); +} + + + +// @sect4{Boundary values} +template +class BoundaryValues : public Function +{ +public: + BoundaryValues(const unsigned int n_components = dim + 1, + const double time = 0., + const double viscosity = 1.) + : Function(n_components, time) + , nu(viscosity) + {} + + virtual void + vector_value(const Point &p, Vector &values) const; + +private: + const double nu; +}; + + +template +void +BoundaryValues::vector_value(const Point &p, + Vector & values) const +{ + ExactSolution(dim + 1, this->get_time(), nu).vector_value(p, values); +} + + + +// @sect3{Calculation of NS-relevant basis functions} + +// This function calculates the required basis +// functions for the assembly process, including +// stabilization terms as given in Y. Bazilevs, +// V.M. Calo, J.A. Cottrell, T.J.R. Hughes, +// A. Reali, G. Scovazzi: Variational multiscale +// residual-based turbulence modeling for large eddy +// simulation of incompressible flows; +// Comput. Methods Appl. Mech. Engrg. 197 +// (2007), 173--201. These parameters are based on +// local Green's functions representing the +// Navier--Stokes differential operator (and its +// inverse). +// +// The reason why we do this in an extra function +// has mainly cosmetic reasons, since this code is +// relatively lengthy. +template +inline void +get_fe(const FEValuesBase & fe_values, + const SolutionVector & solution, + const SolutionVector & solution_old_scaled, + const std::vector & local_dof_indices, + const double nu, + const std::vector> & rhs_values, + const double time_step_weight, + const unsigned int stabilization, + const double h, + std::vector> & phi_u, + std::vector> & phi_u_weight, + std::vector>> &grad_phi_u, + std::vector> & gradT_phi_u, + std::vector> & div_phi_u_p, + std::vector> & residual_phi, + std::vector>> &stab_grad_phi, + std::vector> & func_rhs, + std::vector> & stab_rhs) +{ + const FEValuesExtractors::Vector velocities(0); + + const unsigned int dofs_per_cell = fe_values.get_fe().dofs_per_cell; + const unsigned int n_q_points = fe_values.get_JxW_values().size(); + + const double nu_sqrt = std::sqrt(nu); + + // We now do something terribly complicated: + // we want to extract the degree of the + // velocity interpolation, but do not want + // to use that as an input parameter. + // Hence, we extract the FE from + // FEValuesBase, use the velocity + // block and get the name. The result will be + // something like FE_Q<2>(3) (for dim=2, + // degree = 3). Then we do some C++ tricks + // to extract the ninth character and finally + // convert it to an integer. + std::string element_name = fe_values.get_fe().base_element(0).get_name(); + const unsigned int degree = atoi(&(element_name[8])); + + const double constant_inverse_estimate = + (degree == 1) ? 24. : (244. + std::sqrt(9136.)) / 3.; + + for (unsigned int q = 0; q < n_q_points; ++q) + { + // First calculate some square roots + // of numbers we are going to use. + const double weight = fe_values.JxW(q); + const double weight_sqrt = std::sqrt(weight); + + // @sect4{Evaluate solution at old time steps} + // + // Now we evaluate the velocity at + // the old time step, the current one + // and calculate the current residual. + // This evaluation is similar to what + // is done in the function + // fe_values.get_function_values (and + // the corresponding derivative + // functions), but we do it manually + // here since we're going to need a few + // function evaluations and not + // everything that is included by + // these functions. + // + // The first thing to do is to + // create some temporary variables + // that will hold these function + // evaluations. Then we proceed + // by the actual procedure of the + // function evaluations, which + // is basically just a weighted sum over + // all basis functions or its derivatives, + // respectively. + Tensor<1, dim> velocity; + Tensor<1, dim> velocity_olds; + Tensor<2, dim> velocity_grad; + Tensor<1, dim> velocity_lapl; + Tensor<1, dim> pressure_grad; + double velocity_div = 0.; + + for (unsigned int k = 0; k < dofs_per_cell; ++k) + { + const unsigned int component_k = + fe_values.get_fe().system_to_component_index(k).first; + const double sol_k = solution(local_dof_indices[k]); + + if (component_k < dim) + { + velocity[component_k] += fe_values.shape_value(k, q) * sol_k; + velocity_olds[component_k] += + fe_values.shape_value(k, q) * + solution_old_scaled(local_dof_indices[k]); + velocity_grad[component_k] += fe_values.shape_grad(k, q) * sol_k; + + double tmp = 0.; + for (unsigned int d = 0; d < dim; ++d) + tmp += fe_values.shape_hessian(k, q)[d][d]; + velocity_lapl[component_k] += tmp * sol_k; + + velocity_div += fe_values.shape_grad(k, q)[component_k] * sol_k; + } + else if (component_k == dim) + pressure_grad += fe_values.shape_grad(k, q) * sol_k; + } + + // Now we reshape the right hand side + // function as a tensor (which will + // be used further down). + Tensor<1, dim> rhs; + for (unsigned int d = 0; d < dim; ++d) + rhs[d] = rhs_values[q](d); + + // @sect4{Calculation of stabilization parameters} + // + // The next step is to calculate the + // stabilization parameters for SUPG/PSPG/LISC + // stabilization, following the + // definitions in Bazilevs et al. + // These calculations involve the + // inverse of the Jacobian matrix of + // the transformation from unit to + // real cell and, at their heart, + // are based on an asymtotic + // expansion of the Green's function + // representation of the inverse of + // the Navier-Stokes velocity + // operator. + double tau_supg, tau_lsic; + { + Tensor<1, dim> ones; + for (unsigned int d = 0; d < dim; ++d) + ones[d] = 1.; + + const Tensor<2, dim> g_matrix = + fe_values.inverse_jacobian(q) * + transpose(fe_values.inverse_jacobian(q)); + const Tensor<1, dim> g_vector = + transpose(fe_values.inverse_jacobian(q)) * ones; + + double uGu = 0., GG = 0., gg = 0.; + for (unsigned int d = 0; d < dim; d++) + { + gg += g_vector[d] * g_vector[d]; + for (unsigned int e = 0; e < dim; e++) + { + uGu += velocity[d] * g_matrix[d][e] * velocity[e]; + GG += g_matrix[d][e] * g_matrix[d][e]; + } + } + + tau_supg = + 1. / std::sqrt(4. * time_step_weight * time_step_weight + uGu + + constant_inverse_estimate * nu * nu * GG); + + if (tau_supg > 1e-8 && gg > 1e-8) + tau_lsic = 1. / (tau_supg * gg); + else + tau_lsic = 1.; + } + const double tau_supg_sqrt = std::sqrt(tau_supg); + const double tau_lsic_sqrt = std::sqrt(tau_lsic); + + + // From the evaluated functions, we can construct + // the residual of the current solution function. + Tensor<1, dim> residual; + if (stabilization > 3) + { + residual = (velocity * time_step_weight - velocity_olds + + velocity * velocity_grad - rhs) + + pressure_grad - nu * velocity_lapl; + residual *= tau_supg * weight_sqrt; + } + + // @sect4{Generation of NS basis functions} + // + // Now comes the actual loop over all cell dofs that + // constructs the basis functions that are going to + // be used in the calculations. Note that we already + // incorporate most of the products at this points, + // since this saves us from having to do that at the + // inner loops of assembly where it would be more + // expensive. + // + // Next comes a short description of the information + // that the whole bunch of target arrays are filled + // with. All numbers are multiplied by the square + // root of the integration weight (JxW), since all + // terms appear in products. $\phi$ denotes basis + // functions for velocity and $\psi$ for pressure. + // + //
    + //
  • phi_u: represents the velocity + // test function $\phi$ + // + //
  • phi_u_weight: + // $\phi / dt + u \cdot \nabla \phi + + // (\nabla \cdot u) \phi $ + // + //
  • grad_phi_u: + // $ \sqrt{\nu} \nabla \phi $ + // + //
  • gradT_phi_u: + // $ \sqrt{\nu} (\nabla \phi)^T $ + // + //
  • div_phi_u_p has the velocity part + // $ \sqrt{\tau_{LSIC}} \nabla \cdot \phi$ + // and the pressure part + // $ \psi / \sqrt{\tau_{LSIC}} $ + // + //
  • residual_phi contains the + // residual part in the velocity dofs, i.e. + // $ (-\nu \Delta \phi + u \cdot \nabla \phi + \phi/dt) + // \sqrt{\tau_{SUPG}}$ + // + //
  • stab_grad_phi stabilization + // (SUPG type) test function, for velocity as + // $ (u \cdot \nabla \phi - res_u \tau_{SUPG} \phi + // + u \cdot (\nabla \phi)^T) \sqrt{\tau_{SUPG}} $ + // (with $res_u$ denoting the residual in the + // Navier-Stokes equations using the current + // velocity) + // and pressure as + // $ \sqrt{\tau_{SUPG}}\nabla \psi $ + // + //
+ for (unsigned int k = 0; k < dofs_per_cell; ++k) + { + const unsigned int component_k = + fe_values.get_fe().system_to_component_index(k).first; + + if (component_k < dim) + { + const double div_phi_u_k = + fe_values.shape_grad(k, q)[component_k] * weight_sqrt; + const double u_grad_phi_k = + velocity * fe_values.shape_grad(k, q) * weight_sqrt; + + phi_u[k][q] = fe_values.shape_value(k, q) * weight_sqrt; + phi_u_weight[k][q] = phi_u[k][q] * time_step_weight; + phi_u_weight[k][q] += + u_grad_phi_k + + weight_sqrt * velocity_div * fe_values.shape_value(k, q); + + grad_phi_u[k][q] = + fe_values.shape_grad(k, q) * (weight_sqrt * nu_sqrt); + gradT_phi_u[k][q] = div_phi_u_k * nu_sqrt; + + div_phi_u_p[k][q] = div_phi_u_k * tau_lsic_sqrt; + + residual_phi[k][q] = 0; + stab_grad_phi[k][q] = 0; + if (stabilization > 3 || stabilization % 2 != 0) + { + for (unsigned int l = 0; l < dim; ++l) + residual_phi[k][q] -= fe_values.shape_hessian(k, q)[l][l]; + residual_phi[k][q] *= nu * weight_sqrt; + residual_phi[k][q] += + u_grad_phi_k + phi_u[k][q] * time_step_weight; + residual_phi[k][q] *= tau_supg_sqrt; + + if (stabilization > 3) + { + stab_grad_phi[k][q][component_k] += + u_grad_phi_k - residual * fe_values.shape_grad(k, q); + for (unsigned int d = 0; d < dim; ++d) + stab_grad_phi[k][q][d] += + fe_values.shape_grad(k, q) * velocity * weight_sqrt; + stab_grad_phi[k][q] *= tau_supg_sqrt; + } + } + } + else if (component_k == dim) + { + div_phi_u_p[k][q] = + fe_values.shape_value(k, q) * weight_sqrt / tau_lsic_sqrt; + if (stabilization % 2 != 0) + stab_grad_phi[k][q] = + fe_values.shape_grad(k, q) * weight_sqrt * tau_supg_sqrt; + } + } + + // Finally, construct the right hand side + // function values, where the arrays are + // given as follows: + //
    + //
  • func_rhs contains + // $u_{old}/dt + f_{rhs}$ + //
  • stab_rhs contains + // $(u_{old}/dt + f_{rhs}) \sqrt{\tau_{SUPG}}$ + //
+ func_rhs[q] = (rhs + velocity_olds) * (weight_sqrt); + stab_rhs[q] = func_rhs[q] * tau_supg_sqrt; + } +} + + + +// @sect3{NavierStokesProblem class implementation} + +// @sect4{NavierStokesProblem::NavierStokesProblem} + +// In the constructor of this class, we first store +// the value that was passed in concerning the +// degree of the finite elements we shall use (a +// degree of one, for example, means to use Q2 and +// Q1 elements for velocity and pressure, resp.), +// and then construct the vector valued element +// belonging to the space X_h, described in the +// introduction of step-20. We set the desired time +// stepping scheme, the time interval, viscosity, +// output options and the stabilization. +template +NavierStokesProblem::NavierStokesProblem() + : degree(1) + , fe(FE_Q(degree + 1), dim, FE_Q(degree), 1) + , dof_handler(triangulation) + , n_global_refinements(5) + , time_stepping(BDF2) + , time(0.) + , time_final(0.5) + , time_step(0.04) + , nu(.1) + , output_timestep_skip(1) + , max_nl_iteration(10) + , tol_nl_iteration(1e-5) + , stabilization(0) + , pressure_constraint(false) + , assembly_type(Serial) + , solver_type(GMRES) + , preconditioner_type(ILU) +{} + + + +// @sect4{NavierStokesProblem::make_grid_and_dofs} +template +void +NavierStokesProblem::make_grid_and_dofs() +{ + // This next function starts out with functions + // calls that create and refine a mesh, and then + // associate degrees of freedom with it. Since our + // program includes the measurement of computing + // times of important steps of the program, the very + // first command in this function starts the timer. + // + // Next, we generate a (-1,1)^dim cube and + // refine it n_global_refinements + // times, and associate the finite element + // with it. + Timer computing_timer; + + GridGenerator::hyper_cube(triangulation, -1, 1); + /*GridGenerator::hyper_rectangle (triangulation, Point(-1,-1), + Point(1,1));*/ + triangulation.refine_global(n_global_refinements); + + dof_handler.distribute_dofs(fe); + + // The degrees of freedom are renumbered using + // King reordering from the boost library + // in case we use an ILU preconditioner. The + // deal.II documentation of the DoFRenumbering + // class compares several choices of + // renumbering strategies, where King ordering + // turns out to be the best choice for + // the stationary Stokes equations. The discretized + // time-dependent Navier-Stokes equations result + // in a similar system of equations, so it should + // perform well in this case, too. However, + // we need to take care regarding the high + // memory consumption of the boost + // functions, so we switch to a simpler + // Cuthill_McKee algorithm (from the deal + // library) in case we have a lot of + // degrees of freedom. + if (preconditioner_type == ILU && solver_type == GMRES) + { + if (dof_handler.n_dofs() < 600000) + DoFRenumbering::boost::king_ordering(dof_handler); + else + DoFRenumbering::Cuthill_McKee(dof_handler); + } + + // The next step is to construct a mean value + // constraint on the pressure. This is only + // necessary when there are no boundary conditions + // set on the pressure, which is the case when + // Dirichlet conditions on the velocity are set on + // the whole domain boundary. + // + // Hence, we build a constraint that we set the + // first pressure variable to the sum of all the + // others on the boundary in analogy to what is done + // in the step-11 tutorial program. We choose to + // enforce only the mean pressure on the boundary + // because this is much (!!) faster in deal.II - + // note that the standard constraint on all pressure + // dofs, which would be another possibility from a + // mathematical point of view, causes the + // pressure-pressure matrix to be full! We + // emphasize that the current algorithm assumes a + // grid that is refined uniformly. For non-uniform + // grids, additional work is necessary! + // + // The actual work done here first clears old + // contents in the constraints and then proceeds by + // extracting the pressure degrees of freedom, which + // are in the end of the degrees of freedom list due + // to the component-wise numbering. This + // construction is similar to what is done in the + // deal.II tutorial step-11. + Timer computing_timer2; + hanging_node_and_pressure_constraints.clear(); + + if (pressure_constraint) + { + std::vector pressure_dofs(dof_handler.n_dofs(), false); + std::vector pressure_mask(dim + 1, false); + pressure_mask[dim] = true; + DoFTools::extract_boundary_dofs(static_cast &>( + dof_handler), + pressure_mask, + pressure_dofs); + + const unsigned int first_pressure_dof = std::distance( + pressure_dofs.begin(), + std::find(pressure_dofs.begin(), pressure_dofs.end(), true)); + + // We want to check for hanging nodes as well when + // adaptive grid refinement is used. Additionally, + // we now impose the zero mean on the pressure by + // setting the first variable in the pressure to the + // sum of all the others. First we add a constraint + // on the first pressure dof, and then we create + // entries to all other pressure dofs on the + // boundary. + // + // Then, finally, the constraint matrix for hanging + // nodes and pressure constraints is set up. + hanging_node_and_pressure_constraints.add_line(first_pressure_dof); + + for (unsigned int i = first_pressure_dof + 1; i < dof_handler.n_dofs(); + ++i) + if (pressure_dofs[i] == true) + { + hanging_node_and_pressure_constraints.add_entry(first_pressure_dof, + i, + -1); + } + } + + DoFTools::make_hanging_node_constraints( + dof_handler, hanging_node_and_pressure_constraints); + hanging_node_and_pressure_constraints.close(); + comptimes(4) += computing_timer2(); + computing_timer2.reset(); + + // In order to figure out the sizes of the + // respective blocks, we call the + // DoFTools::count_dofs_per_block + // function that counts how many shape functions are + // non-zero for a particular vector component. With + // this done, we write some information about the + // dofs and some equation info to the screen. Note + // that the decision on which stabilization to use + // is rather involved. As mentioned in the + // declaration of the class members, a value of 0 + // and 5 for stabilization are the two reasonable + // choices for no stabilization or full + // stabilization. + { + std::vector block_component(dim + 1, 0); + block_component[dim] = 1; + std::vector dofs_per_block(2); + DoFTools::count_dofs_per_block(dof_handler, + dofs_per_block, + block_component); + + std::cout << std::endl + << " Calculation of laminar " << dim << "D Taylor/Beltrami flow." + << std::endl + << " Number of active cells: " << triangulation.n_active_cells() + << "." << std::endl + << " Number of degrees of freedom: " << dof_handler.n_dofs() + << " (" << dofs_per_block[0] << " + " << dofs_per_block[1] << ")." + << std::endl + << " Enabled stabilizations SUPG/PSPG/LSIC: "; + std::cout.precision(2); + const bool tau_supg = (stabilization > 3) ? true : false; + const bool tau_pspg = (stabilization % 2 == 0) ? false : true; + const bool tau_lsic = (stabilization > 1) ? true : false; + std::cout << tau_supg << " / " << tau_pspg << " / " << tau_lsic << "." + << std::endl; + } + + // The next step is to generate the actual sparsity + // pattern. The idea is to rely on the class + // CompressedSparsityPattern, which directly builds + // up the sparsity pattern of the matrix without any + // intermediate condensing. See the documentation of + // step-22 and step-31 for more details on this + // process. With that generation completed, the + // system matrix can be derived from the sparsity + // pattern. + CompressedSparsityPattern csp(dof_handler.n_dofs(), dof_handler.n_dofs()); + DoFTools::make_sparsity_pattern(dof_handler, + csp, + hanging_node_and_pressure_constraints, + false); + + sparsity_pattern.copy_from(csp); + + system_matrix.reinit(sparsity_pattern); + + // Here, one can have a look at the newly generated + // sparsity pattern. + /*std::ofstream out ("sparsity_pattern.gpl"); + sparsity_pattern.print_gnuplot (out);*/ + + // Eventually, we have to resize the solution and + // right hand side vectors to the number of degrees + // of freedom in the problem. + solution_update.reinit(dof_handler.n_dofs()); + solution.reinit(dof_handler.n_dofs()); + solution_old.reinit(dof_handler.n_dofs()); + solution_old_scaled.reinit(dof_handler.n_dofs()); + exact.reinit(dof_handler.n_dofs()); + system_rhs.reinit(dof_handler.n_dofs()); + + // Now we are done for this function, so we add the + // time to the timer variable. + comptimes(0) += computing_timer(); +} + + + +// @sect4{NavierStokesProblem::assemble_system} +template +void +NavierStokesProblem::assemble_system() +{ + // This function assembles the stiffness matrix in + // the Navier-Stokes equations. Since we want to do + // the assembly in parallel, this function is merely + // a wrapper that distributes all work to the + // function + // NavierStokesProblem::assemble_system_interval + // as soon as a start and end cell for the + // individual chunks are determined. + // + // In the lines of the deal.II tutorial programs, we + // first clear the matrix and right hand sides from + // previous time steps, and then enter the + // respective option of serial or parallel assembly + // using a switch command. + Timer computing_timer; + + system_matrix = 0; + system_rhs = 0; + + switch (assembly_type) + { + case Parallel: + { + // Set the actual number of processors to be used in + // assembly (parallel), and then proceed by + // generating a vector that contains start and end + // cell of the chunks. Once this is done, each chunk + // is send out to a processor, which can all work on + // their chunks independently. In the end, when all + // work is done, the threads need to be joined + // again. + const unsigned int n_threads = multithread_info.n_cpus; + Threads::ThreadGroup<> threads; + + typedef + typename DoFHandler::active_cell_iterator active_cell_iterator; + std::vector> + thread_ranges = Threads::split_range( + dof_handler.begin_active(), dof_handler.end(), n_threads); + + for (unsigned int thread = 0; thread < n_threads; ++thread) + threads += Threads::spawn( + *this, &NavierStokesProblem::assemble_system_interval)( + thread_ranges[thread].first, thread_ranges[thread].second); + + threads.join_all(); + break; + } + case Serial: + { + // Serial assembly just runs the process from the + // beginning to the end. + NavierStokesProblem::assemble_system_interval( + dof_handler.begin_active(), dof_handler.end()); + break; + } + default: + { + } + } + + // When the matrix has been generated, we set the + // constraints for hanging nodes (in the case of an + // adaptively refined grid), see tutorial step-8. + // + // Then, we implement the Dirichlet boundary + // conditions. Nodes that are subject to Dirichlet + // boundary conditions get their node values + // imposed. The process how this is done is quite + // simple. First, the diagonal element in the matrix + // corresponding to the current degree of freedom + // (fixed by Dirichlet b.c.) will get a nonzero + // value (the one that is already there or some + // other value that is equally large as other + // diagonal entries). By Gaussian elimination, all + // nonzeros in this column are eliminated (giving + // the resp. boundary influence to the right hand + // side system_rhs), and then even all + // entries in the row corresponding to that degree + // of freedom are deleted. See the deal.II + // documentation on the boundary interpolation in + // the VectorTools class and + // MatrixTools::apply_boundary_values + // for details. + // + // We build a component mask so that we can impose + // Dirichlet boundary conditions only on the + // velocity but not on the pressure. The pressure + // component will simply be disabled in the + // bool vector in case the constraint + // is set. For the Beltrami flow considered in this + // program, this is actually not the case, so there + // will still be Dirichlet conditions on pressure. + Timer computing_timer2; + hanging_node_and_pressure_constraints.condense(system_matrix); + hanging_node_and_pressure_constraints.condense(system_rhs); + + + std::map boundary_values; + std::vector velocity_mask(dim + 1, true); + + if (pressure_constraint) + velocity_mask[dim] = false; + + // Now we write the respective boundary values to + // the variable boundary_values and + // then impose these values to the global matrix and + // right hand side. + // + // Then we are done and record the computing times. + VectorTools::interpolate_boundary_values(dof_handler, + 0, + BoundaryValues(dim + 1, + time, + nu), + boundary_values, + velocity_mask); + MatrixTools::apply_boundary_values(boundary_values, + system_matrix, + solution_update, + system_rhs); + comptimes(5) += computing_timer2(); + + comptimes(1) += computing_timer(); + compcounter(1) += 1; +} + + +// @sect4{NavierStokesProblem::assemble_system_interval} +template +void +NavierStokesProblem::assemble_system_interval( + const typename DoFHandler::active_cell_iterator &beginc, + const typename DoFHandler::active_cell_iterator &endc) +{ + // The function that actually + // assembles the linear system is + // similar to the one in step-22 + // of the deal.II tutorial. + // We allocate some variables + // and build the structure + // for the cell stiffness matrix + // local_matrix and the + // local right hand side vector + // local_rhs. + // As usual, local_dof_indices + // contains the information on + // the position of the cell + // degrees of freedom in the + // global system. + QGauss quadrature_formula(degree + 2); + + FEValues fe_values(fe, + quadrature_formula, + update_values | update_gradients | update_hessians | + update_inverse_jacobians | + update_quadrature_points | update_JxW_values); + + const unsigned int dofs_per_cell = fe.dofs_per_cell; + const unsigned int n_q_points = quadrature_formula.size(); + + FullMatrix local_matrix(dofs_per_cell, dofs_per_cell); + Vector local_rhs(dofs_per_cell); + + std::vector local_dof_indices(dofs_per_cell); + + // We declare the objects that contain + // the information on the right hand + // side of the Navier-Stokes equations and + // the boudary values on both velocity and pressure. + // We need both a representation for the continuous + // functions as well as their values + // at the quadrature points in the cells and + // on the faces, respectively. + const RightHandSide right_hand_side(dim, time); + std::vector> rhs_values(n_q_points, Vector(dim)); + + // Vectors to hold the evaluations of + // the FE basis functions and derived + // quantities in the Navier-Stokes + // equations assembly. The names should + // mostly be self-explaining. The terms + // usually include also weights from + // the quadrature formula, so one needs + // to be careful when applying the + // terms in other orders as the ones + // used in the code. + // + // Next follow similar arrays for + // the evalutions of the right hand + // side at the quadrature points, + // i.e. forcing function and information + // from the old time step. + std::vector> phi_u(dofs_per_cell, + std::vector(n_q_points)); + std::vector> phi_u_weight( + dofs_per_cell, std::vector(n_q_points)); + std::vector>> grad_phi_u( + dofs_per_cell, std::vector>(n_q_points)); + std::vector> gradT_phi_u(dofs_per_cell, + std::vector(n_q_points)); + std::vector> div_phi_u_p(dofs_per_cell, + std::vector(n_q_points)); + std::vector> residual_phi( + dofs_per_cell, std::vector(n_q_points)); + std::vector>> stab_grad_phi( + dofs_per_cell, std::vector>(n_q_points)); + + std::vector> func_rhs(n_q_points); + std::vector> stab_rhs(n_q_points); + + // With all this in place, we can + // go on with the loop over all + // cells and add the local contributions. + // + // The first thing to do is to + // evaluate the FE basis functions + // at the quadrature + // points of the cell, as well as + // derivatives and the other + // quantities specified above. + // Moreover, we need to reset + // the local matrices and + // right hand side before + // filling them with new information + // from the current cell. + typename DoFHandler::active_cell_iterator cell; + for (cell = beginc; cell != endc; ++cell) + { + fe_values.reinit(cell); + local_matrix = 0; + local_rhs = 0; + cell->get_dof_indices(local_dof_indices); + const double h = cell->diameter(); + + // Evalulate right hand side function, + // density and viscosity at the + // integration points. Then, we use + // the finite element basis + // functions to get the interpolated + // velocity value at the quadrature + // points. We do this for both + // the current velocity and + // the (weighted) old velocity. + right_hand_side.vector_value_list(fe_values.get_quadrature_points(), + rhs_values); + + // For building the total matrix, + // start by translating the general + // basis functions into the relevant + // information for the Navier-Stokes + // system. + get_fe(fe_values, + solution, + solution_old_scaled, + local_dof_indices, + nu, + rhs_values, + time_step_weight, + stabilization, + h, + phi_u, + phi_u_weight, + grad_phi_u, + gradT_phi_u, + div_phi_u_p, + residual_phi, + stab_grad_phi, + func_rhs, + stab_rhs); + + // Loop over the cell dofs in + // order to fill the local matrix + // with information. Note that we + // loop first over the dofs and + // only at the innermost position + // over the quadrature points. This + // accelerates the code for this + // example, since we need less + // if statements in + // order to determine which terms + // we need to calculate for the + // current result. + // + // We also include different assembly + // options, depending on whether we + // have stabilization turned on + // or off. This gives us a code that + // is more difficult to read, but it + // avoids a performance penalty we'd + // face otherwise when no stabilization + // would be used. + for (unsigned int i = 0; i < dofs_per_cell; ++i) + { + const unsigned int component_i = + fe.system_to_component_index(i).first; + if (component_i < dim && stabilization < 2) + { + for (unsigned int j = 0; j < dofs_per_cell; ++j) + { + const unsigned int component_j = + fe.system_to_component_index(j).first; + + if (component_j < dim && component_i == component_j) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + phi_u[i][q] * phi_u_weight[j][q] + + grad_phi_u[i][q] * grad_phi_u[j][q] + + gradT_phi_u[i][q] * gradT_phi_u[j][q]; + + else if (component_j < dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + gradT_phi_u[i][q] * gradT_phi_u[j][q]; + + else if (component_j == dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) -= + div_phi_u_p[i][q] * div_phi_u_p[j][q]; + } + + for (unsigned int q = 0; q < n_q_points; ++q) + local_rhs(i) += phi_u[i][q] * func_rhs[q][component_i]; + } /* end case for velocity dofs w/o stabilization */ + else if (component_i < dim) + { + for (unsigned int j = 0; j < dofs_per_cell; ++j) + { + const unsigned int component_j = + fe.system_to_component_index(j).first; + + if (component_j < dim && component_j == component_i) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + phi_u[i][q] * phi_u_weight[j][q] + + grad_phi_u[i][q] * grad_phi_u[j][q] + + gradT_phi_u[i][q] * gradT_phi_u[j][q] + + div_phi_u_p[i][q] * div_phi_u_p[j][q] + + stab_grad_phi[i][q][component_j] * residual_phi[j][q]; + + else if (component_j < dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + gradT_phi_u[i][q] * gradT_phi_u[j][q] + + div_phi_u_p[i][q] * div_phi_u_p[j][q] + + stab_grad_phi[i][q][component_j] * residual_phi[j][q]; + + else if (component_j == dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + -div_phi_u_p[i][q] * div_phi_u_p[j][q] + + stab_grad_phi[i][q] * stab_grad_phi[j][q]; + } + + for (unsigned int q = 0; q < n_q_points; ++q) + local_rhs(i) += phi_u[i][q] * func_rhs[q][component_i] + + stab_grad_phi[i][q] * stab_rhs[q]; + } /* end case for velocity dofs w/ stabilization */ + else if (component_i == dim && stabilization % 2 == 0) + { + for (unsigned int j = 0; j < dofs_per_cell; ++j) + { + const unsigned int component_j = + fe.system_to_component_index(j).first; + if (component_j < dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + div_phi_u_p[i][q] * div_phi_u_p[j][q]; + } + } /* end case for pressure dofs w/o stabilization */ + else if (component_i == dim) + { + for (unsigned int j = 0; j < dofs_per_cell; ++j) + { + const unsigned int component_j = + fe.system_to_component_index(j).first; + if (component_j < dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + div_phi_u_p[i][q] * div_phi_u_p[j][q] + + stab_grad_phi[i][q][component_j] * residual_phi[j][q]; + else if (component_j == dim) + for (unsigned int q = 0; q < n_q_points; ++q) + local_matrix(i, j) += + stab_grad_phi[i][q] * stab_grad_phi[j][q]; + } + + for (unsigned int q = 0; q < n_q_points; ++q) + local_rhs(i) += stab_grad_phi[i][q] * stab_rhs[q]; + } /* end case for pressure dofs w/ stabilization */ + } /* end loop over i in dofs_per_cell */ + + // The final stage in the loop over all cells is to + // insert the local contributions into the global + // stiffness matrix system_matrix and + // the global right hand side + // system_rhs. Since there may be + // several processes running in parallel, we have to + // aquire permission to write into the global matrix + // by the assembler_lock variable (in + // order to not access the global matrix by two + // processes simulaneously and possibly lose data). + assembler_lock.acquire(); + hanging_node_and_pressure_constraints.distribute_local_to_global( + local_matrix, local_dof_indices, system_matrix); + hanging_node_and_pressure_constraints.distribute_local_to_global( + local_rhs, local_dof_indices, system_rhs); + assembler_lock.release(); + } +} + + + +// @sect4{NavierStokesProblem::solve} +template +unsigned int +NavierStokesProblem::solve() +{ + unsigned int n_iterations = 0; + Timer computing_timer; + switch (solver_type) + { + // For the iterative solvers, we + // choose to use a unified interface + // to access the solver. We first + // extract the solver information + // and then call an object of type + // SolverSelector, which automatically + // assigns the correct solver. + // In case of GMRES, we also want + // to use more than the standard + // of 30 basis vector, so we need + // to interfere once again at this + // point. This is done by the function + // set_data. + case GMRES: + case BiCGStab: + { + std::string solver_string; + if (solver_type == GMRES) + solver_string = "gmres"; + else + solver_string = "bicgstab"; + + SolverControl solver_control(system_matrix.m(), + system_rhs.l2_norm() * 1e-10); + GrowingVectorMemory> mem; + SolverSelector> solver_selector(solver_string, + solver_control, + mem); + if (solver_type == GMRES) + solver_selector.set_data( + SolverGMRES>::AdditionalData(120)); + + switch (preconditioner_type) + { + case SSOR: /* SSOR preconditioner */ + { + Timer computing_timer2; + PreconditionSSOR<> preconditioner; + preconditioner.initialize(system_matrix, 1.33); + comptimes(6) += computing_timer2(); + + solver_selector.solve(system_matrix, + solution_update, + system_rhs, + preconditioner); + break; + } + case ILU: /* Sparse ILU preconditioner */ + { + Timer computing_timer2; + SparseILU preconditioner; + SparseILU::AdditionalData ilu_data; + preconditioner.initialize(system_matrix, ilu_data); + comptimes(6) += computing_timer2(); + + solver_selector.solve(system_matrix, + solution_update, + system_rhs, + preconditioner); + break; + } + case Vanka: /* Vanka preconditioner: expensive, bad quality */ + { + Timer computing_timer2; + std::vector signature(3, false); + signature[dim] = true; + std::vector selected_dofs(dof_handler.n_dofs(), false); + DoFTools::extract_dofs(dof_handler, signature, selected_dofs); + SparseVanka preconditioner(system_matrix, + selected_dofs); + comptimes(6) += computing_timer2(); + + solver_selector.solve(system_matrix, + solution_update, + system_rhs, + preconditioner); + break; + } + default: /* no preconditioner */ + solver_selector.solve(system_matrix, + solution_update, + system_rhs, + PreconditionIdentity()); + } + + n_iterations = solver_control.last_step(); + break; + } + case Direct: /* direct solve */ + { + Timer computing_timer2; + SparseDirectUMFPACK A_direct; + A_direct.initialize(system_matrix); + comptimes(6) += computing_timer2(); + A_direct.vmult(solution_update, system_rhs); + n_iterations = 1; + break; + } + default: + { + Assert(false, ExcNotImplemented()); + } + } + + comptimes(2) += computing_timer(); + compcounter(2) += 1; + return n_iterations; +} + + +// @sect4{NavierStokesProblem::compute_errors} +template +void +NavierStokesProblem::compute_errors() const +{ + // After we have dealt with the + // linear solver and preconditioners, + // we continue with the + // implementation of our main + // class. In particular, the next + // task is to compute the errors in + // our numerical solution, in both + // the pressures as well as + // velocities, in analogy to + // step-20. + // + // To compute errors in the solution, + // we have already introduced the + // VectorTools::integrate_difference + // function in step-7 and + // step-11. However, there we only + // dealt with scalar solutions, + // whereas here we have a + // vector-valued solution with + // components that even denote + // different quantities and may have + // different orders of convergence, + // a case that is quite usual for + // mixed finite element programs. + // What we therefore + // have to do is to `mask' the + // components that we are interested + // in. This is easily done: the + // VectorTools::integrate_difference + // function takes as its last + // argument a pointer to a weight + // function (the parameter defaults + // to the null pointer, meaning unit + // weights). What we simply have to + // do is to pass a function object + // that equals one in the components + // we are interested in, and zero in + // the other ones. For example, to + // compute the pressure error, we + // should pass a function that + // represents the constant vector + // with a unit value in component + // dim, whereas for the velocity + // the constant vector should be one + // in the first dim components, + // and zero in the location of the + // pressure. + // + // In deal.II, the + // ComponentSelectFunction does + // exactly this: it wants to know how + // many vector components the + // function it is to represent should + // have (in our case this would be + // dim+1, for the joint + // velocity-pressure space) and which + // individual or range of components + // should be equal to one. We + // therefore define two such masks at + // the beginning of the function, + // following by an object + // representing the exact solution + // and a vector in which we will + // store the cellwise errors as + // computed by + // integrate_difference: + const ComponentSelectFunction pressure_mask(dim, dim + 1); + const ComponentSelectFunction velocity_mask(std::make_pair(0, dim), + dim + 1); + + Vector cellwise_errors(triangulation.n_active_cells()); + + // As already discussed in step-7, + // we have to realize that it is + // impossible to integrate the + // errors exactly. All we can do is + // approximate this integral using + // quadrature. This actually + // presents a slight twist here: if + // we naively chose an object of + // type QGauss@(degree+1) + // as one may be inclined to do + // (this is what we used for + // integrating the linear system), + // one realizes that the error is + // very small and does not follow + // the expected convergence curves + // at all. What is happening is + // that for the mixed finite + // elements used here, the Gauss + // points happen to be + // superconvergence points in which + // the pointwise error is much + // smaller (and converges with + // higher order) than anywhere + // else. These are therefore not + // particularly good points for + // ingration. To avoid this + // problem, we simply use a + // trapezoidal rule and iterate it + // degree+2 times in each + // coordinate direction (again as + // explained in step-7): + QTrapez<1> q_trapez; + QIterated quadrature(q_trapez, degree + 3); + + // With this, we can then let the + // library compute the errors and + // output them to the screen: + VectorTools::integrate_difference(dof_handler, + solution, + ExactSolution(dim + 1, time, nu), + cellwise_errors, + quadrature, + VectorTools::L2_norm, + &pressure_mask); + const double p_l2_error = cellwise_errors.l2_norm(); + + VectorTools::integrate_difference(dof_handler, + solution, + ExactSolution(dim + 1, time, nu), + cellwise_errors, + quadrature, + VectorTools::L2_norm, + &velocity_mask); + const double u_l2_error = cellwise_errors.l2_norm(); + + std::cout.precision(4); + std::cout << " L2-Errors: ||e_p||_L2 = " << p_l2_error + << ", ||e_u||_L2 = " << u_l2_error << std::endl; +} + + +// @sect4{NavierStokesProblem::output_results} +template +void +NavierStokesProblem::output_results( + const unsigned int timestep_number) const +{ + // The last interesting function is the one + // in which we generate graphical + // output. Everything here looks obvious + // and familiar. Note how we construct + // unique names for all the solution + // variables at the beginning, like we did + // in step-8 and other programs later + // on. The only thing worth mentioning is + // that for higher order elements, in seems + // inappropriate to only show a single + // bilinear quadrilateral per cell in the + // graphical output. We therefore generate + // patches of size (degree+1)x(degree+1) to + // capture the full information content of + // the solution. See the step-7 tutorial + // program for more information on this. + // + // Some of the code in this function is + // re-used from step-22, especially + // regarding to output format of a + // vtk file. + std::vector solution_names(dim, "velocity"); + solution_names.push_back("p"); + + DataOut data_out; + + data_out.attach_dof_handler(dof_handler); + + std::vector + data_component_interpretation( + dim + 1, DataComponentInterpretation::component_is_scalar); + for (unsigned int i = 0; i < dim; ++i) + data_component_interpretation[i] = + DataComponentInterpretation::component_is_part_of_vector; + + // First write out the numerical + // solution. + { + data_out.add_data_vector(solution, + solution_names, + DataOut::type_dof_data, + data_component_interpretation); + + data_out.build_patches(degree + 1); + + std::ostringstream filename; + filename << "numerical-" << Utilities::int_to_string(timestep_number, 3) + << ".vtk"; + std::ofstream output(filename.str().c_str()); + data_out.write_vtk(output); + } + + // Now, we write out the error + // between the exact solution + // and the numerical solution, + // which sometimes can be interesting + // to study. + { + data_out.add_data_vector(solution_update, + solution_names, + DataOut::type_dof_data, + data_component_interpretation); + + data_out.build_patches(degree + 1); + + std::ostringstream filename; + filename << "difference-" << Utilities::int_to_string(timestep_number, 3) + << ".vtk"; + std::ofstream output(filename.str().c_str()); + data_out.write_vtk(output); + } +} + + +// @sect4{NavierStokesProblem::print_computing_times} +template +void +NavierStokesProblem::print_computing_times() const +{ + // This function prints the various computing + // times to the screen. To keep it short, + // it generates a (nice) table that includes + // assembly times and times for the solution + // of the linear systems. + const double total_time = + comptimes(0) + comptimes(1) + comptimes(2) + comptimes(3); + std::cout << "\n\n+-------------------------------------+--------------" + << "+---------+\n" + << "| Computing timer (CPU times) |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << total_time << " s | 100% |\n"; + std::cout << "+-------------------------------------+--------------" + << "+---------+\n" + << "| Setup system |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(0) << " s | "; + std::cout.precision(2); + std::cout.width(6); + std::cout << comptimes(0) / total_time * 100 + << "% |\n| -> thereof pressure constraint |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(4) << " s | "; + std::cout.width(6); + std::cout.precision(1); + std::cout << comptimes(4) / total_time * 100 << "% |\n| Assembly of "; + std::cout.width(7); + std::cout << compcounter(1) << " matrices |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(1) << " s | "; + std::cout.width(6); + std::cout.precision(2); + std::cout << comptimes(1) / total_time * 100 + << "% |\n| -> thereof bc, pressure constr. |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(5) << " s | "; + std::cout.width(6); + std::cout.precision(2); + std::cout << comptimes(5) / total_time * 100 + << "% |\n| average time per assembly: |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(1) / compcounter(1) + << " s | --- |\n| Solving "; + std::cout.width(7); + std::cout << compcounter(2) << " linear systems |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(2) << " s | "; + std::cout.width(6); + std::cout.precision(2); + std::cout << comptimes(2) / total_time * 100 + << "% |\n| -> thereof preconditioner build |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(6) << " s | "; + std::cout.width(6); + std::cout.precision(2); + std::cout << comptimes(6) / total_time * 100 + << "% |\n| average time per solve: |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(2) / compcounter(2) + << " s | --- |\n| Manipulations, output, error cal |"; + std::cout.width(10); + std::cout.precision(3); + std::cout << comptimes(3) << " s | "; + std::cout.width(6); + std::cout.precision(2); + std::cout << comptimes(3) / total_time * 100 + << "% |\n+-------------------------------------+" + << "--------------+---------+\n" + << std::endl; +} + + +// @sect4{NavierStokesProblem::run} +template +void +NavierStokesProblem::run() +{ + // This is the final function of our + // main class. In this function, the actual + // time integration and nonlinear iteration + // is performed. Of course, we start by + // by building up the grid and set the + // initial values, and then proceed with the + // time loop. During the calculations, + // some information is written to file and + // $L_2$-errors are computed. + + comptimes.reinit(7); + compcounter.reinit(4); + + Timer computing_timer; + + make_grid_and_dofs(); + + { + VectorTools::interpolate(dof_handler, + InitialValues(dim + 1, time, nu), + solution); + VectorTools::interpolate(dof_handler, + InitialValues(dim + 1, time, nu), + exact); + } + + output_results(0); + + // Some variables for the time integration + time_step_number = 0; + bool done = false; + const double time_step_actual_size = time_step; + + // @sect5{Time loop} + while (!done) + { + // We possibly use BDF-2 as time integration, + // so in the first step, we have to do something + // in a different way (we need two old solution + // values for BDF-2, but we only have the + // initial value). We use the backward Euler + // with a very small time step then. Of course + // we have to account for this modification + // in the next step and 'reset' the actual time + // step size. This will be done once the + // work for the first time step is completed. + if (time_stepping == BDF2 && time_step_number == 0) + { + time_step = + std::min(0.25 * time_step_actual_size, + 4. * time_step_actual_size * time_step_actual_size); + } + + // Check whether we can already reach the final time. + // If so, adjust the time step so that we hit the + // target time exactly and set the variable + // done to true, which will end + // the while body once we're + // through to the bottom. + if (time + time_step > time_final - 1e-1 * time_step) + { + time_step = time_final - time; + done = true; + } + + // Now that we have determined the correct time step, + // we advance in time and increase the step counter. + // Additionally, we update the values from old time + // steps. In case of BDF2 integration, we take the + // values from two previous time steps, weight them + // accordingly and save it in + // solution_old_scaled. In case of + // backward Euler time integration, this is just the + // previous solution divided by the time step size. + // Additionally, we calculate an initial guess for + // the nonlinear iteration that is based on an + // extrapolation of the old values. Again, this is + // somewhat more involved for BDF-2 than implicit + // Euler. However, the quality of the initial + // guess for the nonlinear iteration is + // considerably improved from this + // extrapolation than a naive use of the old + // solution value. + time += time_step; + time_step_number += 1; + if (time_stepping == BDF2 && time_step_number > 1) + { + // Calculate extrapolated solution + // (= initial guess for nonlinear + // iteration) at time n+1 + solution_update = solution; + solution_update.sadd((time_step + time_step_old) / time_step_old, + -time_step / time_step_old, + solution_old); + + // Calculate scaled old solutions at n and n-1 + solution_old_scaled = solution; + solution_old_scaled.sadd( + (time_step + time_step_old) / (time_step * time_step_old), + -time_step / (time_step_old * (time_step + time_step_old)), + solution_old); + + // Calculate scaling for time n+1 + time_step_weight = (2. * time_step + time_step_old) / + (time_step * (time_step + time_step_old)); + } + else + { + solution_old_scaled = solution; + solution_old_scaled /= time_step; + solution_update = solution; + time_step_weight = 1. / time_step; + } + + // Now we got all necessary information from + // the old time step, and are able to update the + // solution vectors for the next step. + solution_old = solution; + solution = solution_update; + + + // Print some information about + // the current time step to screen. + std::cout << std::endl << "Time step #" << time_step_number << ", "; + std::cout.precision(3); + std::cout << "advancing to t = " << time << " " + << "(dt = " << time_step; + std::cout << "). " << std::endl; + + + // Now we prepare temporary variables for + // the nonlinear iteration. + // solution_norm will be used to + // measure the initial size of the defect, and + // iteration_count counts the nonlinear + // iterations. + double solution_norm = solution.l2_norm(); + unsigned int iteration_count = 0; + std::cout << " Nonlinear iteration [nl error / lin. its]: " + << std::flush; + + + // @sect5{Nonlinear iteration} + // + // We start the nonlinear iteration with + // the assembly of the current matrix + // system, which is then solved. + // Hanging node constraints are, if present, + // distributed after the solution process. + do + { + iteration_count += 1; + + assemble_system(); + + const unsigned int n_iterations = solve(); + + hanging_node_and_pressure_constraints.distribute(solution_update); + + // We want to compute the difference between the + // new solution and the one from the previous + // iteration. We save the difference in the + // variable system_rhs, which is not longer used + // at this point anyway. Moreover, we use + // a scaling when writing the difference between + // the old and the new solution to the screen. + system_rhs = solution_update; + system_rhs.add(-1., solution); + + solution = solution_update; + + std::cout << '['; + std::cout.precision(2); + std::cout << system_rhs.l2_norm() / solution_norm << " / "; + std::cout << n_iterations << "] " << std::flush; + } + // We have to iterate as long as the relative error is + // larger than the prescribed tolerance and the maximum + // number of allowed iterations is not exceeded. + while (system_rhs.l2_norm() > tol_nl_iteration * solution_norm && + iteration_count < max_nl_iteration); + + std::cout << std::endl; + + // Now we are done with the nonlinear + // iteration and finalize the time + // step. First, we get the exact + // solution at the current time, which + // will then be used in the calculation + // of the absolute L2-errors in + // the velocity and the pressure. + { + VectorTools::interpolate(dof_handler, + ExactSolution(dim + 1, time, nu), + exact); + } + + compute_errors(); + + // Now we 'reset' to the actual time step size in case + // we're doing the first time step in BDF-2. + time_step_old = time_step; + if (time_stepping == BDF2 && time_step_number == 1) + { + time_step = time_step_actual_size; + } + + // We check whether we are at + // a time step where to save + // the current solution to a file. + if (time_step_number % output_timestep_skip == 0) + { + solution_update -= exact; + output_results(time_step_number / output_timestep_skip); + } + } /* End of the time loop */ + + comptimes(3) = computing_timer() - comptimes(0) - comptimes(1) - comptimes(2); + + print_computing_times(); +} + + + +// // @sect3{The main function} + +// // The main function we stole from the tutorial +// // program step-20 (apart from the changed +// // class names, of course). It only creates +// // the variables and functions for the +// // NavierStokesProblem in 2d or 3d (set +// // the respective number to <2> +// // or <3>!) and +// // then runs it. Exceptions are to be catched +// // and, as far as identifiable, categorized. +// int +// main() +// { +// try +// { +// deallog.depth_console(0); + +// NavierStokesProblem<2> navier_stokes_problem; +// navier_stokes_problem.run(); +// } +// catch (std::exception &exc) +// { +// std::cerr << std::endl +// << std::endl +// << "----------------------------------------------------" +// << std::endl; +// std::cerr << "Exception on processing: " << std::endl +// << exc.what() << std::endl +// << "Aborting!" << std::endl +// << "----------------------------------------------------" +// << std::endl; + +// return 1; +// } +// catch (...) +// { +// std::cerr << std::endl +// << std::endl +// << "----------------------------------------------------" +// << std::endl; +// std::cerr << "Unknown exception!" << std::endl +// << "Aborting!" << std::endl +// << "----------------------------------------------------" +// << std::endl; +// return 1; +// } + +// return 0; +// }