1498 *
constexpr double kappa = 1
e-6;
1499 *
constexpr double reference_density = 3300;
1501 *
constexpr double expansion_coefficient = 2
e-5;
1502 *
constexpr double specific_heat = 1250;
1503 *
constexpr double radiogenic_heating = 7.4e-12;
1506 *
constexpr double R0 = 6371000. - 2890000.;
1507 *
constexpr double R1 = 6371000. - 35000.;
1509 *
constexpr double T0 = 4000 + 273;
1510 *
constexpr double T1 = 700 + 273;
1515 * The next
set of definitions are
for functions that encode the density
1518 * they compute) are discussed in the introduction:
1521 *
double density(
const double temperature)
1524 *
reference_density *
1525 *
(1 - expansion_coefficient * (temperature - reference_temperature)));
1529 *
template <
int dim>
1532 *
const double r = p.
norm();
1533 *
return -(1.245e-6 * r + 7.714e13 / r / r) * p / r;
1538 *
template <
int dim>
1539 *
class TemperatureInitialValues :
public Function<dim>
1542 *
TemperatureInitialValues()
1547 *
const unsigned int component = 0)
const override;
1555 *
template <
int dim>
1556 *
double TemperatureInitialValues<dim>::value(
const Point<dim> &p,
1557 *
const unsigned int)
const
1559 *
const double r = p.norm();
1560 *
const double h = R1 - R0;
1562 *
const double s = (r - R0) / h;
1567 *
const double phi = std::atan2(p[0], p[1]);
1568 *
const double tau = s + 0.2 * s * (1 - s) *
std::sin(6 * phi) * q;
1570 *
return T0 * (1.0 - tau) + T1 * tau;
1574 *
template <
int dim>
1576 *
TemperatureInitialValues<dim>::vector_value(
const Point<dim> &p,
1579 *
for (
unsigned int c = 0; c < this->n_components; ++c)
1580 *
values(c) = TemperatureInitialValues<dim>::value(p, c);
1586 * As mentioned in the introduction we need to rescale the pressure to
1587 * avoid the relative ill-conditioning of the momentum and mass
1588 * conservation equations. The scaling factor is @f$\frac{\eta}{
L}@f$ where
1589 * @f$L@f$ was a typical length
scale. By experimenting it turns out that a
1590 * good length
scale is the
diameter of plumes, which is around 10 km:
1593 *
constexpr double pressure_scaling = eta / 10000;
1597 * The
final number in
this namespace is a
constant that denotes the
1598 * number of seconds per (average, tropical) year. We use
this only when
1599 * generating screen output: internally, all computations of
this program
1600 * happen in SI units (kilogram, meter, seconds) but writing geological
1601 * times in seconds yields
numbers that
one can
't relate to reality, and
1602 * so we convert to years using the factor defined here:
1605 * const double year_in_seconds = 60 * 60 * 24 * 365.2425;
1607 * } // namespace EquationData
1614 * <a name="step_32-PreconditioningtheStokessystem"></a>
1615 * <h3>Preconditioning the Stokes system</h3>
1619 * This namespace implements the preconditioner. As discussed in the
1620 * introduction, this preconditioner differs in a number of key portions
1621 * from the one used in @ref step_31 "step-31". Specifically, it is a right preconditioner,
1622 * implementing the matrix
1624 * \left(\begin{array}{cc}A^{-1} & A^{-1}B^TS^{-1}
1626 * \end{array}\right)
1628 * where the two inverse matrix operations
1629 * are approximated by linear solvers or, if the right flag is given to the
1630 * constructor of this class, by a single AMG V-cycle for the velocity
1631 * block. The three code blocks of the <code>vmult</code> function implement
1632 * the multiplications with the three blocks of this preconditioner matrix
1633 * and should be self explanatory if you have read through @ref step_31 "step-31" or the
1634 * discussion of composing solvers in @ref step_20 "step-20".
1637 * namespace LinearSolvers
1639 * template <class PreconditionerTypeA, class PreconditionerTypeMp>
1640 * class BlockSchurPreconditioner : public EnableObserverPointer
1643 * BlockSchurPreconditioner(const TrilinosWrappers::BlockSparseMatrix &S,
1644 * const TrilinosWrappers::BlockSparseMatrix &Spre,
1645 * const PreconditionerTypeMp &Mppreconditioner,
1646 * const PreconditionerTypeA &Apreconditioner,
1647 * const bool do_solve_A)
1648 * : stokes_matrix(&S)
1649 * , stokes_preconditioner_matrix(&Spre)
1650 * , mp_preconditioner(Mppreconditioner)
1651 * , a_preconditioner(Apreconditioner)
1652 * , do_solve_A(do_solve_A)
1655 * void vmult(TrilinosWrappers::MPI::BlockVector &dst,
1656 * const TrilinosWrappers::MPI::BlockVector &src) const
1658 * TrilinosWrappers::MPI::Vector utmp(src.block(0));
1661 * SolverControl solver_control(5000, 1e-6 * src.block(1).l2_norm());
1663 * SolverCG<TrilinosWrappers::MPI::Vector> solver(solver_control);
1665 * solver.solve(stokes_preconditioner_matrix->block(1, 1),
1668 * mp_preconditioner);
1670 * dst.block(1) *= -1.0;
1674 * stokes_matrix->block(0, 1).vmult(utmp, dst.block(1));
1676 * utmp.add(src.block(0));
1679 * if (do_solve_A == true)
1681 * SolverControl solver_control(5000, utmp.l2_norm() * 1e-2);
1682 * SolverCG<TrilinosWrappers::MPI::Vector> solver(solver_control);
1683 * solver.solve(stokes_matrix->block(0, 0),
1686 * a_preconditioner);
1689 * a_preconditioner.vmult(dst.block(0), utmp);
1693 * const ObserverPointer<const TrilinosWrappers::BlockSparseMatrix>
1695 * const ObserverPointer<const TrilinosWrappers::BlockSparseMatrix>
1696 * stokes_preconditioner_matrix;
1697 * const PreconditionerTypeMp &mp_preconditioner;
1698 * const PreconditionerTypeA &a_preconditioner;
1699 * const bool do_solve_A;
1701 * } // namespace LinearSolvers
1708 * <a name="step_32-Definitionofassemblydatastructures"></a>
1709 * <h3>Definition of assembly data structures</h3>
1713 * As described in the introduction, we will use the WorkStream mechanism
1714 * discussed in the @ref threads topic to parallelize operations among the
1715 * processors of a single machine. The WorkStream class requires that data
1716 * is passed around in two kinds of data structures, one for scratch data
1717 * and one to pass data from the assembly function to the function that
1718 * copies local contributions into global objects.
1722 * The following namespace (and the two sub-namespaces) contains a
1723 * collection of data structures that serve this purpose, one pair for each
1724 * of the four operations discussed in the introduction that we will want to
1725 * parallelize. Each assembly routine gets two sets of data: a Scratch array
1726 * that collects all the classes and arrays that are used for the
1727 * calculation of the cell contribution, and a CopyData array that keeps
1728 * local matrices and vectors which will be written into the global
1729 * matrix. Whereas CopyData is a container for the final data that is
1730 * written into the global matrices and vector (and, thus, absolutely
1731 * necessary), the Scratch arrays are merely there for performance reasons
1732 * — it would be much more expensive to set up a FEValues object on
1733 * each cell, than creating it only once and updating some derivative data.
1737 * @ref step_31 "step-31" had four assembly routines: One for the preconditioner matrix of
1738 * the Stokes system, one for the Stokes matrix and right hand side, one for
1739 * the temperature matrices and one for the right hand side of the
1740 * temperature equation. We here organize the scratch arrays and CopyData
1741 * objects for each of those four assembly components using a
1742 * <code>struct</code> environment (since we consider these as temporary
1743 * objects we pass around, rather than classes that implement functionality
1744 * of their own, though this is a more subjective point of view to
1745 * distinguish between <code>struct</code>s and <code>class</code>es).
1749 * Regarding the Scratch objects, each struct is equipped with a constructor
1750 * that creates an @ref FEValues object using the @ref FiniteElement,
1751 * Quadrature, @ref Mapping (which describes the interpolation of curved
1752 * boundaries), and @ref UpdateFlags instances. Moreover, we manually
1753 * implement a copy constructor (since the FEValues class is not copyable by
1754 * itself), and provide some additional vector fields that are used to hold
1755 * intermediate data during the computation of local contributions.
1759 * Let us start with the scratch arrays and, specifically, the one used for
1760 * assembly of the Stokes preconditioner:
1763 * namespace Assembly
1767 * template <int dim>
1768 * struct StokesPreconditioner
1770 * StokesPreconditioner(const FiniteElement<dim> &stokes_fe,
1771 * const Quadrature<dim> &stokes_quadrature,
1772 * const Mapping<dim> &mapping,
1773 * const UpdateFlags update_flags);
1775 * StokesPreconditioner(const StokesPreconditioner &data);
1778 * FEValues<dim> stokes_fe_values;
1780 * std::vector<Tensor<2, dim>> grad_phi_u;
1781 * std::vector<double> phi_p;
1784 * template <int dim>
1785 * StokesPreconditioner<dim>::StokesPreconditioner(
1786 * const FiniteElement<dim> &stokes_fe,
1787 * const Quadrature<dim> &stokes_quadrature,
1788 * const Mapping<dim> &mapping,
1789 * const UpdateFlags update_flags)
1790 * : stokes_fe_values(mapping, stokes_fe, stokes_quadrature, update_flags)
1791 * , grad_phi_u(stokes_fe.n_dofs_per_cell())
1792 * , phi_p(stokes_fe.n_dofs_per_cell())
1797 * template <int dim>
1798 * StokesPreconditioner<dim>::StokesPreconditioner(
1799 * const StokesPreconditioner &scratch)
1800 * : stokes_fe_values(scratch.stokes_fe_values.get_mapping(),
1801 * scratch.stokes_fe_values.get_fe(),
1802 * scratch.stokes_fe_values.get_quadrature(),
1803 * scratch.stokes_fe_values.get_update_flags())
1804 * , grad_phi_u(scratch.grad_phi_u)
1805 * , phi_p(scratch.phi_p)
1812 * The next one is the scratch object used for the assembly of the full
1813 * Stokes system. Observe that we derive the StokesSystem scratch class
1814 * from the StokesPreconditioner class above. We do this because all the
1815 * objects that are necessary for the assembly of the preconditioner are
1816 * also needed for the actual matrix system and right hand side, plus
1817 * some extra data. This makes the program more compact. Note also that
1818 * the assembly of the Stokes system and the temperature right hand side
1819 * further down requires data from temperature and velocity,
1820 * respectively, so we actually need two FEValues objects for those two
1824 * template <int dim>
1825 * struct StokesSystem : public StokesPreconditioner<dim>
1827 * StokesSystem(const FiniteElement<dim> &stokes_fe,
1828 * const Mapping<dim> &mapping,
1829 * const Quadrature<dim> &stokes_quadrature,
1830 * const UpdateFlags stokes_update_flags,
1831 * const FiniteElement<dim> &temperature_fe,
1832 * const UpdateFlags temperature_update_flags);
1834 * StokesSystem(const StokesSystem<dim> &data);
1837 * FEValues<dim> temperature_fe_values;
1839 * std::vector<Tensor<1, dim>> phi_u;
1840 * std::vector<SymmetricTensor<2, dim>> grads_phi_u;
1841 * std::vector<double> div_phi_u;
1843 * std::vector<double> old_temperature_values;
1847 * template <int dim>
1848 * StokesSystem<dim>::StokesSystem(
1849 * const FiniteElement<dim> &stokes_fe,
1850 * const Mapping<dim> &mapping,
1851 * const Quadrature<dim> &stokes_quadrature,
1852 * const UpdateFlags stokes_update_flags,
1853 * const FiniteElement<dim> &temperature_fe,
1854 * const UpdateFlags temperature_update_flags)
1855 * : StokesPreconditioner<dim>(stokes_fe,
1856 * stokes_quadrature,
1858 * stokes_update_flags)
1859 * , temperature_fe_values(mapping,
1861 * stokes_quadrature,
1862 * temperature_update_flags)
1863 * , phi_u(stokes_fe.n_dofs_per_cell())
1864 * , grads_phi_u(stokes_fe.n_dofs_per_cell())
1865 * , div_phi_u(stokes_fe.n_dofs_per_cell())
1866 * , old_temperature_values(stokes_quadrature.size())
1870 * template <int dim>
1871 * StokesSystem<dim>::StokesSystem(const StokesSystem<dim> &scratch)
1872 * : StokesPreconditioner<dim>(scratch)
1873 * , temperature_fe_values(
1874 * scratch.temperature_fe_values.get_mapping(),
1875 * scratch.temperature_fe_values.get_fe(),
1876 * scratch.temperature_fe_values.get_quadrature(),
1877 * scratch.temperature_fe_values.get_update_flags())
1878 * , phi_u(scratch.phi_u)
1879 * , grads_phi_u(scratch.grads_phi_u)
1880 * , div_phi_u(scratch.div_phi_u)
1881 * , old_temperature_values(scratch.old_temperature_values)
1887 * After defining the objects used in the assembly of the Stokes system,
1888 * we do the same for the assembly of the matrices necessary for the
1889 * temperature system. The general structure is very similar:
1892 * template <int dim>
1893 * struct TemperatureMatrix
1895 * TemperatureMatrix(const FiniteElement<dim> &temperature_fe,
1896 * const Mapping<dim> &mapping,
1897 * const Quadrature<dim> &temperature_quadrature);
1899 * TemperatureMatrix(const TemperatureMatrix &data);
1902 * FEValues<dim> temperature_fe_values;
1904 * std::vector<double> phi_T;
1905 * std::vector<Tensor<1, dim>> grad_phi_T;
1909 * template <int dim>
1910 * TemperatureMatrix<dim>::TemperatureMatrix(
1911 * const FiniteElement<dim> &temperature_fe,
1912 * const Mapping<dim> &mapping,
1913 * const Quadrature<dim> &temperature_quadrature)
1914 * : temperature_fe_values(mapping,
1916 * temperature_quadrature,
1917 * update_values | update_gradients |
1918 * update_JxW_values)
1919 * , phi_T(temperature_fe.n_dofs_per_cell())
1920 * , grad_phi_T(temperature_fe.n_dofs_per_cell())
1924 * template <int dim>
1925 * TemperatureMatrix<dim>::TemperatureMatrix(
1926 * const TemperatureMatrix &scratch)
1927 * : temperature_fe_values(
1928 * scratch.temperature_fe_values.get_mapping(),
1929 * scratch.temperature_fe_values.get_fe(),
1930 * scratch.temperature_fe_values.get_quadrature(),
1931 * scratch.temperature_fe_values.get_update_flags())
1932 * , phi_T(scratch.phi_T)
1933 * , grad_phi_T(scratch.grad_phi_T)
1939 * The final scratch object is used in the assembly of the right hand
1940 * side of the temperature system. This object is significantly larger
1941 * than the ones above because a lot more quantities enter the
1942 * computation of the right hand side of the temperature equation. In
1943 * particular, the temperature values and gradients of the previous two
1944 * time steps need to be evaluated at the quadrature points, as well as
1945 * the velocities and the strain rates (i.e. the symmetric gradients of
1946 * the velocity) that enter the right hand side as friction heating
1947 * terms. Despite the number of terms, the following should be rather
1951 * template <int dim>
1952 * struct TemperatureRHS
1954 * TemperatureRHS(const FiniteElement<dim> &temperature_fe,
1955 * const FiniteElement<dim> &stokes_fe,
1956 * const Mapping<dim> &mapping,
1957 * const Quadrature<dim> &quadrature);
1959 * TemperatureRHS(const TemperatureRHS &data);
1962 * FEValues<dim> temperature_fe_values;
1963 * FEValues<dim> stokes_fe_values;
1965 * std::vector<double> phi_T;
1966 * std::vector<Tensor<1, dim>> grad_phi_T;
1968 * std::vector<Tensor<1, dim>> old_velocity_values;
1969 * std::vector<Tensor<1, dim>> old_old_velocity_values;
1971 * std::vector<SymmetricTensor<2, dim>> old_strain_rates;
1972 * std::vector<SymmetricTensor<2, dim>> old_old_strain_rates;
1974 * std::vector<double> old_temperature_values;
1975 * std::vector<double> old_old_temperature_values;
1976 * std::vector<Tensor<1, dim>> old_temperature_grads;
1977 * std::vector<Tensor<1, dim>> old_old_temperature_grads;
1978 * std::vector<double> old_temperature_laplacians;
1979 * std::vector<double> old_old_temperature_laplacians;
1983 * template <int dim>
1984 * TemperatureRHS<dim>::TemperatureRHS(
1985 * const FiniteElement<dim> &temperature_fe,
1986 * const FiniteElement<dim> &stokes_fe,
1987 * const Mapping<dim> &mapping,
1988 * const Quadrature<dim> &quadrature)
1989 * : temperature_fe_values(mapping,
1992 * update_values | update_gradients |
1993 * update_hessians | update_quadrature_points |
1994 * update_JxW_values)
1995 * , stokes_fe_values(mapping,
1998 * update_values | update_gradients)
1999 * , phi_T(temperature_fe.n_dofs_per_cell())
2000 * , grad_phi_T(temperature_fe.n_dofs_per_cell())
2003 * old_velocity_values(quadrature.size())
2004 * , old_old_velocity_values(quadrature.size())
2005 * , old_strain_rates(quadrature.size())
2006 * , old_old_strain_rates(quadrature.size())
2009 * old_temperature_values(quadrature.size())
2010 * , old_old_temperature_values(quadrature.size())
2011 * , old_temperature_grads(quadrature.size())
2012 * , old_old_temperature_grads(quadrature.size())
2013 * , old_temperature_laplacians(quadrature.size())
2014 * , old_old_temperature_laplacians(quadrature.size())
2018 * template <int dim>
2019 * TemperatureRHS<dim>::TemperatureRHS(const TemperatureRHS &scratch)
2020 * : temperature_fe_values(
2021 * scratch.temperature_fe_values.get_mapping(),
2022 * scratch.temperature_fe_values.get_fe(),
2023 * scratch.temperature_fe_values.get_quadrature(),
2024 * scratch.temperature_fe_values.get_update_flags())
2025 * , stokes_fe_values(scratch.stokes_fe_values.get_mapping(),
2026 * scratch.stokes_fe_values.get_fe(),
2027 * scratch.stokes_fe_values.get_quadrature(),
2028 * scratch.stokes_fe_values.get_update_flags())
2029 * , phi_T(scratch.phi_T)
2030 * , grad_phi_T(scratch.grad_phi_T)
2033 * old_velocity_values(scratch.old_velocity_values)
2034 * , old_old_velocity_values(scratch.old_old_velocity_values)
2035 * , old_strain_rates(scratch.old_strain_rates)
2036 * , old_old_strain_rates(scratch.old_old_strain_rates)
2039 * old_temperature_values(scratch.old_temperature_values)
2040 * , old_old_temperature_values(scratch.old_old_temperature_values)
2041 * , old_temperature_grads(scratch.old_temperature_grads)
2042 * , old_old_temperature_grads(scratch.old_old_temperature_grads)
2043 * , old_temperature_laplacians(scratch.old_temperature_laplacians)
2044 * , old_old_temperature_laplacians(scratch.old_old_temperature_laplacians)
2046 * } // namespace Scratch
2051 * The CopyData objects are even simpler than the Scratch objects as all
2052 * they have to do is to store the results of local computations until
2053 * they can be copied into the global matrix or vector objects. These
2054 * structures therefore only need to provide a constructor, a copy
2055 * operation, and some arrays for local matrix, local vectors and the
2056 * relation between local and global degrees of freedom (a.k.a.
2057 * <code>local_dof_indices</code>). Again, we have one such structure for
2058 * each of the four operations we will parallelize using the WorkStream
2062 * namespace CopyData
2064 * template <int dim>
2065 * struct StokesPreconditioner
2067 * StokesPreconditioner(const FiniteElement<dim> &stokes_fe);
2068 * StokesPreconditioner(const StokesPreconditioner &data);
2069 * StokesPreconditioner &operator=(const StokesPreconditioner &) = default;
2071 * FullMatrix<double> local_matrix;
2072 * std::vector<types::global_dof_index> local_dof_indices;
2075 * template <int dim>
2076 * StokesPreconditioner<dim>::StokesPreconditioner(
2077 * const FiniteElement<dim> &stokes_fe)
2078 * : local_matrix(stokes_fe.n_dofs_per_cell(), stokes_fe.n_dofs_per_cell())
2079 * , local_dof_indices(stokes_fe.n_dofs_per_cell())
2082 * template <int dim>
2083 * StokesPreconditioner<dim>::StokesPreconditioner(
2084 * const StokesPreconditioner &data)
2085 * : local_matrix(data.local_matrix)
2086 * , local_dof_indices(data.local_dof_indices)
2091 * template <int dim>
2092 * struct StokesSystem : public StokesPreconditioner<dim>
2094 * StokesSystem(const FiniteElement<dim> &stokes_fe);
2096 * Vector<double> local_rhs;
2099 * template <int dim>
2100 * StokesSystem<dim>::StokesSystem(const FiniteElement<dim> &stokes_fe)
2101 * : StokesPreconditioner<dim>(stokes_fe)
2102 * , local_rhs(stokes_fe.n_dofs_per_cell())
2107 * template <int dim>
2108 * struct TemperatureMatrix
2110 * TemperatureMatrix(const FiniteElement<dim> &temperature_fe);
2112 * FullMatrix<double> local_mass_matrix;
2113 * FullMatrix<double> local_stiffness_matrix;
2114 * std::vector<types::global_dof_index> local_dof_indices;
2117 * template <int dim>
2118 * TemperatureMatrix<dim>::TemperatureMatrix(
2119 * const FiniteElement<dim> &temperature_fe)
2120 * : local_mass_matrix(temperature_fe.n_dofs_per_cell(),
2121 * temperature_fe.n_dofs_per_cell())
2122 * , local_stiffness_matrix(temperature_fe.n_dofs_per_cell(),
2123 * temperature_fe.n_dofs_per_cell())
2124 * , local_dof_indices(temperature_fe.n_dofs_per_cell())
2129 * template <int dim>
2130 * struct TemperatureRHS
2132 * TemperatureRHS(const FiniteElement<dim> &temperature_fe);
2134 * Vector<double> local_rhs;
2135 * std::vector<types::global_dof_index> local_dof_indices;
2136 * FullMatrix<double> matrix_for_bc;
2139 * template <int dim>
2140 * TemperatureRHS<dim>::TemperatureRHS(
2141 * const FiniteElement<dim> &temperature_fe)
2142 * : local_rhs(temperature_fe.n_dofs_per_cell())
2143 * , local_dof_indices(temperature_fe.n_dofs_per_cell())
2144 * , matrix_for_bc(temperature_fe.n_dofs_per_cell(),
2145 * temperature_fe.n_dofs_per_cell())
2147 * } // namespace CopyData
2148 * } // namespace Assembly
2155 * <a name="step_32-ThecodeBoussinesqFlowProblemcodeclasstemplate"></a>
2156 * <h3>The <code>BoussinesqFlowProblem</code> class template</h3>
2160 * This is the declaration of the main class. It is very similar to @ref step_31 "step-31"
2161 * but there are a number differences we will comment on below.
2165 * The top of the class is essentially the same as in @ref step_31 "step-31", listing the
2166 * public methods and a set of private functions that do the heavy
2167 * lifting. Compared to @ref step_31 "step-31" there are only two additions to this
2168 * section: the function <code>get_cfl_number()</code> that computes the
2169 * maximum CFL number over all cells which we then compute the global time
2170 * step from, and the function <code>get_entropy_variation()</code> that is
2171 * used in the computation of the entropy stabilization. It is akin to the
2172 * <code>get_extrapolated_temperature_range()</code> we have used in @ref step_31 "step-31"
2173 * for this purpose, but works on the entropy instead of the temperature
2177 * template <int dim>
2178 * class BoussinesqFlowProblem
2181 * struct Parameters;
2182 * BoussinesqFlowProblem(Parameters ¶meters);
2186 * void setup_dofs();
2187 * void assemble_stokes_preconditioner();
2188 * void build_stokes_preconditioner();
2189 * void assemble_stokes_system();
2190 * void assemble_temperature_matrix();
2191 * void assemble_temperature_system(const double maximal_velocity);
2192 * double get_maximal_velocity() const;
2193 * double get_cfl_number() const;
2194 * double get_entropy_variation(const double average_temperature) const;
2195 * std::pair<double, double> get_extrapolated_temperature_range() const;
2197 * void output_results();
2198 * void refine_mesh(const unsigned int max_grid_level);
2200 * double compute_viscosity(
2201 * const std::vector<double> &old_temperature,
2202 * const std::vector<double> &old_old_temperature,
2203 * const std::vector<Tensor<1, dim>> &old_temperature_grads,
2204 * const std::vector<Tensor<1, dim>> &old_old_temperature_grads,
2205 * const std::vector<double> &old_temperature_laplacians,
2206 * const std::vector<double> &old_old_temperature_laplacians,
2207 * const std::vector<Tensor<1, dim>> &old_velocity_values,
2208 * const std::vector<Tensor<1, dim>> &old_old_velocity_values,
2209 * const std::vector<SymmetricTensor<2, dim>> &old_strain_rates,
2210 * const std::vector<SymmetricTensor<2, dim>> &old_old_strain_rates,
2211 * const double global_u_infty,
2212 * const double global_T_variation,
2213 * const double average_temperature,
2214 * const double global_entropy_variation,
2215 * const double cell_diameter) const;
2220 * The first significant new component is the definition of a struct for
2221 * the parameters according to the discussion in the introduction. This
2222 * structure is initialized by reading from a parameter file during
2223 * construction of this object.
2228 * Parameters(const std::string ¶meter_filename);
2230 * static void declare_parameters(ParameterHandler &prm);
2231 * void parse_parameters(ParameterHandler &prm);
2235 * unsigned int initial_global_refinement;
2236 * unsigned int initial_adaptive_refinement;
2238 * bool generate_graphical_output;
2239 * unsigned int graphical_output_interval;
2241 * unsigned int adaptive_refinement_interval;
2243 * double stabilization_alpha;
2244 * double stabilization_c_R;
2245 * double stabilization_beta;
2247 * unsigned int stokes_velocity_degree;
2248 * bool use_locally_conservative_discretization;
2250 * unsigned int temperature_degree;
2254 * Parameters ¶meters;
2258 * The <code>pcout</code> (for <i>%parallel <code>std::cout</code></i>)
2259 * object is used to simplify writing output: each MPI process can use
2260 * this to generate output as usual, but since each of these processes
2261 * will (hopefully) produce the same output it will just be replicated
2262 * many times over; with the ConditionalOStream class, only the output
2263 * generated by one MPI process will actually be printed to screen,
2264 * whereas the output by all the other threads will simply be forgotten.
2267 * ConditionalOStream pcout;
2271 * The following member variables will then again be similar to those in
2272 * @ref step_31 "step-31" (and to other tutorial programs). As mentioned in the
2273 * introduction, we fully distribute computations, so we will have to use
2274 * the parallel::distributed::Triangulation class (see @ref step_40 "step-40") but the
2275 * remainder of these variables is rather standard with two exceptions:
2279 * - The <code>mapping</code> variable is used to denote a higher-order
2280 * polynomial mapping. As mentioned in the introduction, we use this
2281 * mapping when forming integrals through quadrature for all cells.
2285 * - In a bit of naming confusion, you will notice below that some of the
2286 * variables from namespace TrilinosWrappers are taken from namespace
2287 * TrilinosWrappers::MPI (such as the right hand side vectors) whereas
2288 * others are not (such as the various matrices). This is due to legacy
2289 * reasons. We will frequently have to query velocities
2290 * and temperatures at arbitrary quadrature points; consequently, rather
2291 * than importing ghost information of a vector whenever we need access
2292 * to degrees of freedom that are relevant locally but owned by another
2293 * processor, we solve linear systems in %parallel but then immediately
2294 * initialize a vector including ghost entries of the solution for further
2295 * processing. The various <code>*_solution</code> vectors are therefore
2296 * filled immediately after solving their respective linear system in
2297 * %parallel and will always contain values for all
2298 * @ref GlossLocallyRelevantDof "locally relevant degrees of freedom";
2299 * the fully distributed vectors that we obtain from the solution process
2300 * and that only ever contain the
2301 * @ref GlossLocallyOwnedDof "locally owned degrees of freedom" are
2302 * destroyed immediately after the solution process and after we have
2303 * copied the relevant values into the member variable vectors.
2306 * parallel::distributed::Triangulation<dim> triangulation;
2307 * double global_Omega_diameter;
2309 * const MappingQ<dim> mapping;
2311 * const FESystem<dim> stokes_fe;
2312 * DoFHandler<dim> stokes_dof_handler;
2313 * AffineConstraints<double> stokes_constraints;
2315 * TrilinosWrappers::BlockSparseMatrix stokes_matrix;
2316 * TrilinosWrappers::BlockSparseMatrix stokes_preconditioner_matrix;
2318 * TrilinosWrappers::MPI::BlockVector stokes_solution;
2319 * TrilinosWrappers::MPI::BlockVector old_stokes_solution;
2320 * TrilinosWrappers::MPI::BlockVector stokes_rhs;
2323 * const FE_Q<dim> temperature_fe;
2324 * DoFHandler<dim> temperature_dof_handler;
2325 * AffineConstraints<double> temperature_constraints;
2327 * TrilinosWrappers::SparseMatrix temperature_mass_matrix;
2328 * TrilinosWrappers::SparseMatrix temperature_stiffness_matrix;
2329 * TrilinosWrappers::SparseMatrix temperature_matrix;
2331 * TrilinosWrappers::MPI::Vector temperature_solution;
2332 * TrilinosWrappers::MPI::Vector old_temperature_solution;
2333 * TrilinosWrappers::MPI::Vector old_old_temperature_solution;
2334 * TrilinosWrappers::MPI::Vector temperature_rhs;
2338 * double old_time_step;
2339 * unsigned int timestep_number;
2341 * std::shared_ptr<TrilinosWrappers::PreconditionAMG> Amg_preconditioner;
2342 * std::shared_ptr<TrilinosWrappers::PreconditionJacobi> Mp_preconditioner;
2343 * std::shared_ptr<TrilinosWrappers::PreconditionJacobi> T_preconditioner;
2345 * bool rebuild_stokes_matrix;
2346 * bool rebuild_stokes_preconditioner;
2347 * bool rebuild_temperature_matrices;
2348 * bool rebuild_temperature_preconditioner;
2352 * The next member variable, <code>computing_timer</code> is used to
2353 * conveniently account for compute time spent in certain "sections" of
2354 * the code that are repeatedly entered. For example, we will enter (and
2355 * leave) sections for Stokes matrix assembly and would like to accumulate
2356 * the run time spent in this section over all time steps. Every so many
2357 * time steps as well as at the end of the program (through the destructor
2358 * of the TimerOutput class) we will then produce a nice summary of the
2359 * times spent in the different sections into which we categorize the
2360 * run-time of this program.
2363 * TimerOutput computing_timer;
2367 * After these member variables we have a number of auxiliary functions
2368 * that have been broken out of the ones listed above. Specifically, there
2369 * are first three functions that we call from <code>setup_dofs</code> and
2370 * then the ones that do the assembling of linear systems:
2373 * void setup_stokes_matrix(
2374 * const std::vector<IndexSet> &stokes_partitioning,
2375 * const std::vector<IndexSet> &stokes_relevant_partitioning);
2376 * void setup_stokes_preconditioner(
2377 * const std::vector<IndexSet> &stokes_partitioning,
2378 * const std::vector<IndexSet> &stokes_relevant_partitioning);
2379 * void setup_temperature_matrices(
2380 * const IndexSet &temperature_partitioning,
2381 * const IndexSet &temperature_relevant_partitioning);
2386 * Following the @ref MTWorkStream "task-based parallelization" paradigm,
2387 * we split all the assembly routines into two parts: a first part that
2388 * can do all the calculations on a certain cell without taking care of
2389 * other threads, and a second part (which is writing the local data into
2390 * the global matrices and vectors) which can be entered by only one
2391 * thread at a time. In order to implement that, we provide functions for
2392 * each of those two steps for all the four assembly routines that we use
2393 * in this program. The following eight functions do exactly this:
2396 * void local_assemble_stokes_preconditioner(
2397 * const typename DoFHandler<dim>::active_cell_iterator &cell,
2398 * Assembly::Scratch::StokesPreconditioner<dim> &scratch,
2399 * Assembly::CopyData::StokesPreconditioner<dim> &data);
2401 * void copy_local_to_global_stokes_preconditioner(
2402 * const Assembly::CopyData::StokesPreconditioner<dim> &data);
2405 * void local_assemble_stokes_system(
2406 * const typename DoFHandler<dim>::active_cell_iterator &cell,
2407 * Assembly::Scratch::StokesSystem<dim> &scratch,
2408 * Assembly::CopyData::StokesSystem<dim> &data);
2410 * void copy_local_to_global_stokes_system(
2411 * const Assembly::CopyData::StokesSystem<dim> &data);
2414 * void local_assemble_temperature_matrix(
2415 * const typename DoFHandler<dim>::active_cell_iterator &cell,
2416 * Assembly::Scratch::TemperatureMatrix<dim> &scratch,
2417 * Assembly::CopyData::TemperatureMatrix<dim> &data);
2419 * void copy_local_to_global_temperature_matrix(
2420 * const Assembly::CopyData::TemperatureMatrix<dim> &data);
2424 * void local_assemble_temperature_rhs(
2425 * const std::pair<double, double> global_T_range,
2426 * const double global_max_velocity,
2427 * const double global_entropy_variation,
2428 * const typename DoFHandler<dim>::active_cell_iterator &cell,
2429 * Assembly::Scratch::TemperatureRHS<dim> &scratch,
2430 * Assembly::CopyData::TemperatureRHS<dim> &data);
2432 * void copy_local_to_global_temperature_rhs(
2433 * const Assembly::CopyData::TemperatureRHS<dim> &data);
2437 * Finally, we forward declare a member class that we will define later on
2438 * and that will be used to compute a number of quantities from our
2439 * solution vectors that we'd like to put into the output files
for
2443 *
class Postprocessor;
2450 * <a name=
"step_32-BoussinesqFlowProblemclassimplementation"></a>
2451 * <h3>BoussinesqFlowProblem
class implementation</h3>
2456 * <a name=
"step_32-BoussinesqFlowProblemParameters"></a>
2457 * <h4>BoussinesqFlowProblem::Parameters</h4>
2461 * Here comes the definition of the parameters
for the Stokes problem. We
2462 * allow to
set the
end time
for the simulation, the
level of refinements
2463 * (both global and adaptive, which in the
sum specify what maximum
level
2464 * the cells are allowed to have), and the interval between refinements in
2465 * the time stepping.
2469 * Then, we let the user specify constants
for the stabilization parameters
2470 * (as discussed in the introduction), the polynomial degree
for the Stokes
2471 * velocity space, whether to use the locally conservative discretization
2472 * based on
FE_DGP elements
for the pressure or not (
FE_Q elements
for
2473 * pressure), and the polynomial degree
for the
temperature interpolation.
2477 * The constructor checks
for a
valid input file (
if not, a file with
2478 *
default parameters
for the quantities is written), and eventually parses
2482 *
template <
int dim>
2483 *
BoussinesqFlowProblem<dim>::Parameters::Parameters(
2484 *
const std::string ¶meter_filename)
2486 *
, initial_global_refinement(2)
2487 *
, initial_adaptive_refinement(2)
2488 *
, adaptive_refinement_interval(10)
2489 *
, stabilization_alpha(2)
2490 *
, stabilization_c_R(0.11)
2491 *
, stabilization_beta(0.078)
2492 *
, stokes_velocity_degree(2)
2493 *
, use_locally_conservative_discretization(
true)
2494 *
, temperature_degree(2)
2497 *
BoussinesqFlowProblem<dim>::Parameters::declare_parameters(prm);
2499 *
std::ifstream parameter_file(parameter_filename);
2501 *
if (!parameter_file)
2503 *
parameter_file.close();
2505 *
std::ofstream parameter_out(parameter_filename);
2511 *
"Input parameter file <" + parameter_filename +
2512 *
"> not found. Creating a template file of the same name."));
2515 *
prm.parse_input(parameter_file);
2516 *
parse_parameters(prm);
2523 * Next we have a function that declares the parameters that we expect in
2524 * the input file, together with their
data types,
default values and a
2528 *
template <
int dim>
2529 *
void BoussinesqFlowProblem<dim>::Parameters::declare_parameters(
2532 *
prm.declare_entry(
"End time",
2535 *
"The end time of the simulation in years.");
2536 *
prm.declare_entry(
"Initial global refinement",
2539 *
"The number of global refinement steps performed on "
2540 *
"the initial coarse mesh, before the problem is first "
2542 *
prm.declare_entry(
"Initial adaptive refinement",
2545 *
"The number of adaptive refinement steps performed after "
2546 *
"initial global refinement.");
2547 *
prm.declare_entry(
"Time steps between mesh refinement",
2550 *
"The number of time steps after which the mesh is to be "
2551 *
"adapted based on computed error indicators.");
2552 *
prm.declare_entry(
"Generate graphical output",
2555 *
"Whether graphical output is to be generated or not. "
2556 *
"You may not want to get graphical output if the number "
2557 *
"of processors is large.");
2558 *
prm.declare_entry(
"Time steps between graphical output",
2561 *
"The number of time steps between each generation of "
2562 *
"graphical output files.");
2564 *
prm.enter_subsection(
"Stabilization parameters");
2566 *
prm.declare_entry(
"alpha",
2569 *
"The exponent in the entropy viscosity stabilization.");
2570 *
prm.declare_entry(
"c_R",
2573 *
"The c_R factor in the entropy viscosity "
2574 *
"stabilization.");
2575 *
prm.declare_entry(
"beta",
2578 *
"The beta factor in the artificial viscosity "
2579 *
"stabilization. An appropriate value for 2d is 0.052 "
2580 *
"and 0.078 for 3d.");
2582 *
prm.leave_subsection();
2584 *
prm.enter_subsection(
"Discretization");
2586 *
prm.declare_entry(
2587 *
"Stokes velocity polynomial degree",
2590 *
"The polynomial degree to use for the velocity variables "
2591 *
"in the Stokes system.");
2592 *
prm.declare_entry(
2593 *
"Temperature polynomial degree",
2596 *
"The polynomial degree to use for the temperature variable.");
2597 *
prm.declare_entry(
2598 *
"Use locally conservative discretization",
2601 *
"Whether to use a Stokes discretization that is locally "
2602 *
"conservative at the expense of a larger number of degrees "
2603 *
"of freedom, or to go with a cheaper discretization "
2604 *
"that does not locally conserve mass (although it is "
2605 *
"globally conservative.");
2607 *
prm.leave_subsection();
2614 * And then we need a function that reads the contents of the
2616 * results into variables that store the
values of the parameters we have
2617 * previously declared:
2620 *
template <
int dim>
2621 *
void BoussinesqFlowProblem<dim>::Parameters::parse_parameters(
2624 *
end_time = prm.get_double(
"End time");
2625 *
initial_global_refinement = prm.get_integer(
"Initial global refinement");
2626 *
initial_adaptive_refinement =
2627 *
prm.get_integer(
"Initial adaptive refinement");
2629 *
adaptive_refinement_interval =
2630 *
prm.get_integer(
"Time steps between mesh refinement");
2632 *
generate_graphical_output = prm.get_bool(
"Generate graphical output");
2633 *
graphical_output_interval =
2634 *
prm.get_integer(
"Time steps between graphical output");
2636 *
prm.enter_subsection(
"Stabilization parameters");
2638 *
stabilization_alpha = prm.get_double(
"alpha");
2639 *
stabilization_c_R = prm.get_double(
"c_R");
2640 *
stabilization_beta = prm.get_double(
"beta");
2642 *
prm.leave_subsection();
2644 *
prm.enter_subsection(
"Discretization");
2646 *
stokes_velocity_degree =
2647 *
prm.get_integer(
"Stokes velocity polynomial degree");
2648 *
temperature_degree = prm.get_integer(
"Temperature polynomial degree");
2649 *
use_locally_conservative_discretization =
2650 *
prm.get_bool(
"Use locally conservative discretization");
2652 *
prm.leave_subsection();
2660 * <a name=
"step_32-BoussinesqFlowProblemBoussinesqFlowProblem"></a>
2661 * <h4>BoussinesqFlowProblem::BoussinesqFlowProblem</h4>
2665 * The constructor of the problem is very similar to the constructor in
2666 * @ref step_31
"step-31". What is different is the %
parallel communication: Trilinos uses
2667 * a message passing interface (
MPI)
for data distribution. When entering
2668 * the BoussinesqFlowProblem
class, we have to decide how the parallelization
2669 * is to be done. We choose a rather simple strategy and let all processors
2670 * that are running the program work together, specified by the communicator
2671 * <code>MPI_COMM_WORLD</code>. Next, we create the output stream (as we
2672 * already did in @ref step_18
"step-18") that only generates output on the
first MPI
2673 * process and is completely forgetful on all others. The implementation of
2674 *
this idea is to
check the process number when <code>pcout</code> gets a
2675 *
true argument, and it uses the <code>std::cout</code> stream
for
2676 * output. If we are
one processor five,
for instance, then we will give a
2677 * <code>
false</code> argument to <code>pcout</code>, which means that the
2678 * output of that processor will not be printed. With the exception of the
2679 * mapping object (
for which we use polynomials of degree 4) all but the
2680 *
final member variable are exactly the same as in @ref step_31
"step-31".
2684 * This
final object, the
TimerOutput object, is then told to restrict
2685 * output to the <code>pcout</code> stream (processor 0), and then we
2686 * specify that we want to get a summary table at the
end of the program
2687 * which shows us wallclock times (as opposed to CPU times). We will
2688 * manually also request intermediate summaries every so many time steps in
2689 * the <code>
run()</code> function below.
2692 *
template <
int dim>
2693 *
BoussinesqFlowProblem<dim>::BoussinesqFlowProblem(Parameters ¶meters_)
2694 *
: parameters(parameters_)
2698 *
triangulation(MPI_COMM_WORLD,
2704 *
global_Omega_diameter(0.)
2710 *
stokes_fe(
FE_Q<dim>(parameters.stokes_velocity_degree) ^ dim,
2711 *
(parameters.use_locally_conservative_discretization ?
2713 *
FE_DGP<dim>(parameters.stokes_velocity_degree - 1)) :
2715 *
FE_Q<dim>(parameters.stokes_velocity_degree - 1))))
2718 *
stokes_dof_handler(triangulation)
2721 *
temperature_fe(parameters.temperature_degree)
2722 *
, temperature_dof_handler(triangulation)
2726 *
, old_time_step(0)
2727 *
, timestep_number(0)
2728 *
, rebuild_stokes_matrix(true)
2729 *
, rebuild_stokes_preconditioner(true)
2730 *
, rebuild_temperature_matrices(true)
2731 *
, rebuild_temperature_preconditioner(true)
2734 *
computing_timer(MPI_COMM_WORLD,
2745 * <a name=
"step_32-TheBoussinesqFlowProblemhelperfunctions"></a>
2746 * <h4>The BoussinesqFlowProblem helper
functions</h4>
2748 * <a name=
"step_32-BoussinesqFlowProblemget_maximal_velocity"></a>
2749 * <h5>BoussinesqFlowProblem::get_maximal_velocity</h5>
2753 * Except
for two small details, the function to compute the global maximum
2754 * of the velocity is the same as in @ref step_31
"step-31". The
first detail is actually
2755 * common to all
functions that implement loops over all cells in the
2756 * triangulation: When operating in %
parallel, each processor can only work
2757 * on a chunk of cells since each processor only has a certain part of the
2758 * entire triangulation. This chunk of cells that we want to work on is
2759 * identified via a so-called <code>
subdomain_id</code>, as we also did in
2760 * @ref step_18
"step-18". All we need to change is hence to perform the cell-related
2761 * operations only on cells that are owned by the current process (as
2762 * opposed to ghost or artificial cells), i.
e.
for which the subdomain id
2763 * equals the number of the process ID. Since this is a commonly used
2764 * operation, there is a shortcut
for this operation: we can ask whether the
2765 * cell is owned by the current processor using
2766 * <code>cell-@>is_locally_owned()</code>.
2770 * The
second difference is the way we calculate the maximum
value. Before,
2771 * we could simply have a <code>double</code> variable that we checked
2772 * against on each quadrature
point for each cell. Now, we have to be a bit
2773 * more careful since each processor only operates on a subset of
2774 * cells. What we do is to
first let each processor calculate the maximum
2775 * among its cells, and then do a global communication operation
2777 * all the maximum
values of the individual processors.
MPI provides such a
2778 * call, but it
's even simpler to use the respective function in namespace
2779 * Utilities::MPI using the MPI communicator object since that will do the
2780 * right thing even if we work without MPI and on a single machine only. The
2781 * call to <code>Utilities::MPI::max</code> needs two arguments, namely the
2782 * local maximum (input) and the MPI communicator, which is MPI_COMM_WORLD
2786 * template <int dim>
2787 * double BoussinesqFlowProblem<dim>::get_maximal_velocity() const
2789 * const QIterated<dim> quadrature_formula(QTrapezoid<1>(),
2790 * parameters.stokes_velocity_degree);
2791 * const unsigned int n_q_points = quadrature_formula.size();
2793 * FEValues<dim> fe_values(mapping,
2795 * quadrature_formula,
2797 * std::vector<Tensor<1, dim>> velocity_values(n_q_points);
2799 * const FEValuesExtractors::Vector velocities(0);
2801 * double max_local_velocity = 0;
2803 * for (const auto &cell : stokes_dof_handler.active_cell_iterators())
2804 * if (cell->is_locally_owned())
2806 * fe_values.reinit(cell);
2807 * fe_values[velocities].get_function_values(stokes_solution,
2810 * for (unsigned int q = 0; q < n_q_points; ++q)
2811 * max_local_velocity =
2812 * std::max(max_local_velocity, velocity_values[q].norm());
2815 * return Utilities::MPI::max(max_local_velocity, MPI_COMM_WORLD);
2822 * <a name="step_32-BoussinesqFlowProblemget_cfl_number"></a>
2823 * <h5>BoussinesqFlowProblem::get_cfl_number</h5>
2827 * The next function does something similar, but we now compute the CFL
2828 * number, i.e., maximal velocity on a cell divided by the cell
2829 * diameter. This number is necessary to determine the time step size, as we
2830 * use a semi-explicit time stepping scheme for the temperature equation
2831 * (see @ref step_31 "step-31" for a discussion). We compute it in the same way as above:
2832 * Compute the local maximum over all locally owned cells, then exchange it
2833 * via MPI to find the global maximum.
2836 * template <int dim>
2837 * double BoussinesqFlowProblem<dim>::get_cfl_number() const
2839 * const QIterated<dim> quadrature_formula(QTrapezoid<1>(),
2840 * parameters.stokes_velocity_degree);
2841 * const unsigned int n_q_points = quadrature_formula.size();
2843 * FEValues<dim> fe_values(mapping,
2845 * quadrature_formula,
2847 * std::vector<Tensor<1, dim>> velocity_values(n_q_points);
2849 * const FEValuesExtractors::Vector velocities(0);
2851 * double max_local_cfl = 0;
2853 * for (const auto &cell : stokes_dof_handler.active_cell_iterators())
2854 * if (cell->is_locally_owned())
2856 * fe_values.reinit(cell);
2857 * fe_values[velocities].get_function_values(stokes_solution,
2860 * double max_local_velocity = 1e-10;
2861 * for (unsigned int q = 0; q < n_q_points; ++q)
2862 * max_local_velocity =
2863 * std::max(max_local_velocity, velocity_values[q].norm());
2865 * std::max(max_local_cfl, max_local_velocity / cell->diameter());
2868 * return Utilities::MPI::max(max_local_cfl, MPI_COMM_WORLD);
2875 * <a name="step_32-BoussinesqFlowProblemget_entropy_variation"></a>
2876 * <h5>BoussinesqFlowProblem::get_entropy_variation</h5>
2880 * Next comes the computation of the global entropy variation
2881 * @f$\|E(T)-\bar{E}(T)\|_\infty@f$ where the entropy @f$E@f$ is defined as
2882 * discussed in the introduction. This is needed for the evaluation of the
2883 * stabilization in the temperature equation as explained in the
2884 * introduction. The entropy variation is actually only needed if we use
2885 * @f$\alpha=2@f$ as a power in the residual computation. The infinity norm is
2886 * computed by the maxima over quadrature points, as usual in discrete
2891 * In order to compute this quantity, we first have to find the
2892 * space-average @f$\bar{E}(T)@f$ and then evaluate the maximum. However, that
2893 * means that we would need to perform two loops. We can avoid the overhead
2894 * by noting that @f$\|E(T)-\bar{E}(T)\|_\infty =
2895 * \max\big(E_{\textrm{max}}(T)-\bar{E}(T),
2896 * \bar{E}(T)-E_{\textrm{min}}(T)\big)@f$, i.e., the maximum out of the
2897 * deviation from the average entropy in positive and negative
2898 * directions. The four quantities we need for the latter formula (maximum
2899 * entropy, minimum entropy, average entropy, area) can all be evaluated in
2900 * the same loop over all cells, so we choose this simpler variant.
2903 * template <int dim>
2904 * double BoussinesqFlowProblem<dim>::get_entropy_variation(
2905 * const double average_temperature) const
2907 * if (parameters.stabilization_alpha != 2)
2910 * const QGauss<dim> quadrature_formula(parameters.temperature_degree + 1);
2911 * const unsigned int n_q_points = quadrature_formula.size();
2913 * FEValues<dim> fe_values(temperature_fe,
2914 * quadrature_formula,
2915 * update_values | update_JxW_values);
2916 * std::vector<double> old_temperature_values(n_q_points);
2917 * std::vector<double> old_old_temperature_values(n_q_points);
2921 * In the two functions above we computed the maximum of numbers that were
2922 * all non-negative, so we knew that zero was certainly a lower bound. On
2923 * the other hand, here we need to find the maximum deviation from the
2924 * average value, i.e., we will need to know the maximal and minimal
2925 * values of the entropy for which we don't a priori know the
sign.
2929 * To compute it, we can therefore start with the largest and smallest
2930 * possible
values we can store in a double precision number: The minimum
2931 * is initialized with a bigger and the maximum with a smaller number than
2932 * any
one that is going to appear. We are then guaranteed that these
2934 * processor does not own any cells, in the communication step at the
2935 * latest. The following
loop then computes the minimum and maximum local
2936 * entropy as well as keeps track of the area/
volume of the part of the
2937 * domain we locally own and the integral over the entropy on it:
2940 *
double min_entropy =
std::numeric_limits<double>::
max(),
2941 *
max_entropy =
std::numeric_limits<double>::lowest(), area = 0,
2942 *
entropy_integrated = 0;
2944 *
for (
const auto &cell : temperature_dof_handler.active_cell_iterators())
2945 *
if (cell->is_locally_owned())
2947 *
fe_values.
reinit(cell);
2948 *
fe_values.get_function_values(old_temperature_solution,
2949 *
old_temperature_values);
2950 *
fe_values.get_function_values(old_old_temperature_solution,
2951 *
old_old_temperature_values);
2952 *
for (
unsigned int q = 0; q < n_q_points; ++q)
2955 *
(old_temperature_values[q] + old_old_temperature_values[q]) / 2;
2956 *
const double entropy =
2957 *
((T - average_temperature) * (T - average_temperature));
2959 *
min_entropy =
std::min(min_entropy, entropy);
2960 *
max_entropy =
std::max(max_entropy, entropy);
2961 *
area += fe_values.JxW(q);
2962 *
entropy_integrated += fe_values.JxW(q) * entropy;
2968 * Now we only need to exchange
data between processors: we need to
sum
2969 * the two integrals (<code>area</code>, <code>entropy_integrated</code>),
2970 * and get the extrema
for maximum and minimum. We could
do this through
2971 * four different
data exchanges, but we can it with two:
2973 *
values that are all to be summed up. And we can also utilize the
2975 * the minimal entropies equals forming the
negative of the maximum over
2976 * the
negative of the minimal entropies;
this maximum can then be
2977 * combined with forming the maximum over the maximal entropies.
2980 *
const double local_sums[2] = {entropy_integrated, area},
2981 *
local_maxima[2] = {-min_entropy, max_entropy};
2982 *
double global_sums[2], global_maxima[2];
2989 * Having computed everything
this way, we can then compute the average
2990 * entropy and find the @f$L^\infty@f$
norm by taking the larger of the
2991 * deviation of the maximum or minimum from the average:
2994 *
const double average_entropy = global_sums[0] / global_sums[1];
2995 *
const double entropy_diff =
std::max(global_maxima[1] - average_entropy,
2996 *
average_entropy - (-global_maxima[0]));
2997 *
return entropy_diff;
3005 * <a name=
"step_32-BoussinesqFlowProblemget_extrapolated_temperature_range"></a>
3006 * <h5>BoussinesqFlowProblem::get_extrapolated_temperature_range</h5>
3010 * The next function computes the minimal and maximal
value of the
3011 * extrapolated
temperature over the entire domain. Again,
this is only a
3012 * slightly modified version of the respective function in @ref step_31
"step-31". As in
3013 * the function above, we collect local minima and maxima and then compute
3014 * the global extrema
using the same trick as above.
3018 * As already discussed in @ref step_31
"step-31", the function needs to distinguish
3019 * between the
first and all following time steps because it uses a higher
3020 * order
temperature extrapolation scheme when at least two previous time
3021 * steps are available.
3024 *
template <
int dim>
3025 *
std::pair<double, double>
3026 *
BoussinesqFlowProblem<dim>::get_extrapolated_temperature_range() const
3029 *
parameters.temperature_degree);
3030 *
const unsigned int n_q_points = quadrature_formula.size();
3034 *
quadrature_formula,
3036 *
std::vector<double> old_temperature_values(n_q_points);
3037 *
std::vector<double> old_old_temperature_values(n_q_points);
3039 *
double min_local_temperature = std::numeric_limits<double>::max(),
3040 *
max_local_temperature = std::numeric_limits<double>::lowest();
3042 *
if (timestep_number != 0)
3044 *
for (
const auto &cell : temperature_dof_handler.active_cell_iterators())
3045 *
if (cell->is_locally_owned())
3047 *
fe_values.
reinit(cell);
3048 *
fe_values.get_function_values(old_temperature_solution,
3049 *
old_temperature_values);
3050 *
fe_values.get_function_values(old_old_temperature_solution,
3051 *
old_old_temperature_values);
3053 *
for (
unsigned int q = 0; q < n_q_points; ++q)
3056 *
(1. + time_step / old_time_step) *
3057 *
old_temperature_values[q] -
3058 *
time_step / old_time_step * old_old_temperature_values[q];
3060 *
min_local_temperature =
3061 *
std::min(min_local_temperature, temperature);
3062 *
max_local_temperature =
3063 *
std::max(max_local_temperature, temperature);
3069 *
for (
const auto &cell : temperature_dof_handler.active_cell_iterators())
3070 *
if (cell->is_locally_owned())
3072 *
fe_values.
reinit(cell);
3073 *
fe_values.get_function_values(old_temperature_solution,
3074 *
old_temperature_values);
3076 *
for (
unsigned int q = 0; q < n_q_points; ++q)
3078 *
const double temperature = old_temperature_values[q];
3080 *
min_local_temperature =
3081 *
std::min(min_local_temperature, temperature);
3082 *
max_local_temperature =
3083 *
std::max(max_local_temperature, temperature);
3088 *
double local_extrema[2] = {-min_local_temperature, max_local_temperature};
3089 *
double global_extrema[2];
3092 *
return std::make_pair(-global_extrema[0], global_extrema[1]);
3099 * <a name=
"step_32-BoussinesqFlowProblemcompute_viscosity"></a>
3100 * <h5>BoussinesqFlowProblem::compute_viscosity</h5>
3104 * The function that calculates the viscosity is purely local and so needs
3105 * no communication at all. It is mostly the same as in @ref step_31
"step-31" but with an
3106 * updated formulation of the viscosity
if @f$\alpha=2@f$ is chosen:
3109 *
template <
int dim>
3110 *
double BoussinesqFlowProblem<dim>::compute_viscosity(
3111 *
const std::vector<double> &old_temperature,
3112 *
const std::vector<double> &old_old_temperature,
3115 *
const std::vector<double> &old_temperature_laplacians,
3116 *
const std::vector<double> &old_old_temperature_laplacians,
3121 *
const double global_u_infty,
3122 *
const double global_T_variation,
3123 *
const double average_temperature,
3124 *
const double global_entropy_variation,
3125 *
const double cell_diameter)
const
3127 *
if (global_u_infty == 0)
3128 *
return 5
e-3 * cell_diameter;
3130 *
const unsigned int n_q_points = old_temperature.size();
3132 *
double max_residual = 0;
3133 *
double max_velocity = 0;
3135 *
for (
unsigned int q = 0; q < n_q_points; ++q)
3138 *
(old_velocity_values[q] + old_old_velocity_values[q]) / 2;
3141 *
(old_strain_rates[q] + old_old_strain_rates[q]) / 2;
3143 *
const double T = (old_temperature[q] + old_old_temperature[q]) / 2;
3144 *
const double dT_dt =
3145 *
(old_temperature[q] - old_old_temperature[q]) / old_time_step;
3146 *
const double u_grad_T =
3147 *
u * (old_temperature_grads[q] + old_old_temperature_grads[q]) / 2;
3149 *
const double kappa_Delta_T =
3150 *
EquationData::kappa *
3151 *
(old_temperature_laplacians[q] + old_old_temperature_laplacians[q]) /
3153 *
const double gamma =
3154 *
((EquationData::radiogenic_heating * EquationData::density(T) +
3155 *
2 * EquationData::eta * strain_rate * strain_rate) /
3156 *
(EquationData::density(T) * EquationData::specific_heat));
3158 *
double residual =
std::abs(dT_dt + u_grad_T - kappa_Delta_T - gamma);
3159 *
if (parameters.stabilization_alpha == 2)
3160 *
residual *=
std::abs(T - average_temperature);
3162 *
max_residual =
std::max(residual, max_residual);
3166 *
const double max_viscosity =
3167 *
(parameters.stabilization_beta * max_velocity * cell_diameter);
3168 *
if (timestep_number == 0)
3169 *
return max_viscosity;
3172 *
Assert(old_time_step > 0, ExcInternalError());
3174 *
double entropy_viscosity;
3175 *
if (parameters.stabilization_alpha == 2)
3176 *
entropy_viscosity =
3177 *
(parameters.stabilization_c_R * cell_diameter * cell_diameter *
3178 *
max_residual / global_entropy_variation);
3180 *
entropy_viscosity =
3181 *
(parameters.stabilization_c_R * cell_diameter *
3182 *
global_Omega_diameter * max_velocity * max_residual /
3183 *
(global_u_infty * global_T_variation));
3185 *
return std::min(max_viscosity, entropy_viscosity);
3194 * <a name=
"step_32-TheBoussinesqFlowProblemsetupfunctions"></a>
3195 * <h4>The BoussinesqFlowProblem setup
functions</h4>
3201 * mostly the same as in @ref step_31
"step-31", but it has been broken out into three
3202 *
functions of their own
for simplicity.
3206 * The
main functional difference between the code here and that in @ref step_31
"step-31"
3207 * is that the matrices we want to
set up are distributed across multiple
3208 * processors. Since we still want to build up the sparsity pattern
first
3209 *
for efficiency reasons, we could
continue to build the <i>entire</i>
3211 * @ref step_31
"step-31". However, that would be inefficient: every processor would build
3212 * the same sparsity pattern, but only initialize a small part of the
matrix
3213 *
using it. It also violates the principle that every processor should only
3214 * work on those cells it owns (and,
if necessary the layer of ghost cells
3220 * which is (obviously) a wrapper around a sparsity pattern
object provided
3221 * by Trilinos. The advantage is that the Trilinos sparsity pattern
class
3222 * can communicate across multiple processors:
if this processor fills in
3223 * all the
nonzero entries that result from the cells it owns, and every
3224 * other processor does so as well, then at the
end after some
MPI
3225 * communication initiated by the <code>
compress()</code> call, we will have
3226 * the globally assembled sparsity pattern available with which the global
3227 *
matrix can be initialized.
3231 * There is
one important aspect when initializing Trilinos sparsity
3232 * patterns in
parallel: In addition to specifying the locally owned rows
3233 * and columns of the matrices via the @p stokes_partitioning
index set, we
3234 * also supply information about all the rows we are possibly going to write
3235 * into when assembling on a certain processor. The
set of locally relevant
3236 * rows contains all such rows (possibly also a few unnecessary ones, but it
3237 * is difficult to find the exact row indices before actually getting
3238 * indices on all cells and resolving constraints). This additional
3239 * information allows to exactly determine the structure
for the
3240 * off-processor
data found during assembly. While Trilinos matrices are
3241 * able to collect
this information on the fly as well (when initializing
3242 * them from some other reinit method), it is less efficient and leads to
3243 * problems when assembling matrices with multiple threads. In
this program,
3244 * we pessimistically assume that only
one processor at a time can write
3245 * into the
matrix while assembly (whereas the computation is
parallel),
3246 * which is fine
for Trilinos matrices. In practice,
one can
do better by
3247 * hinting
WorkStream at cells that
do not share vertices, allowing
for
3248 * parallelism among those cells (see the graph coloring algorithms and
3249 *
WorkStream with colored iterators argument). However, that only works
3250 * when only
one MPI processor is present because Trilinos
' internal data
3251 * structures for accumulating off-processor data on the fly are not thread
3252 * safe. With the initialization presented here, there is no such problem
3253 * and one could safely introduce graph coloring for this algorithm.
3257 * The only other change we need to make is to tell the
3258 * DoFTools::make_sparsity_pattern() function that it is only supposed to
3259 * work on a subset of cells, namely the ones whose
3260 * <code>subdomain_id</code> equals the number of the current processor, and
3261 * to ignore all other cells.
3265 * This strategy is replicated across all three of the following functions.
3269 * Note that Trilinos matrices store the information contained in the
3270 * sparsity patterns, so we can safely release the <code>sp</code> variable
3271 * once the matrix has been given the sparsity structure.
3274 * template <int dim>
3275 * void BoussinesqFlowProblem<dim>::setup_stokes_matrix(
3276 * const std::vector<IndexSet> &stokes_partitioning,
3277 * const std::vector<IndexSet> &stokes_relevant_partitioning)
3279 * stokes_matrix.clear();
3281 * Table<2, DoFTools::Coupling> coupling(dim + 1, dim + 1);
3282 * for (unsigned int c = 0; c < dim + 1; ++c)
3283 * for (unsigned int d = 0; d < dim + 1; ++d)
3284 * if (!((c == dim) && (d == dim)))
3285 * coupling[c][d] = DoFTools::always;
3287 * coupling[c][d] = DoFTools::none;
3289 * BlockDynamicSparsityPattern dsp(stokes_relevant_partitioning);
3291 * DoFTools::make_sparsity_pattern(stokes_dof_handler,
3294 * stokes_constraints,
3296 * Utilities::MPI::this_mpi_process(
3299 * const IndexSet stokes_locally_relevant =
3300 * DoFTools::extract_locally_relevant_dofs(stokes_dof_handler);
3302 * SparsityTools::distribute_sparsity_pattern(
3304 * stokes_dof_handler.locally_owned_dofs(),
3306 * stokes_locally_relevant);
3308 * stokes_matrix.reinit(stokes_partitioning, dsp, MPI_COMM_WORLD);
3313 * template <int dim>
3314 * void BoussinesqFlowProblem<dim>::setup_stokes_preconditioner(
3315 * const std::vector<IndexSet> &stokes_partitioning,
3316 * const std::vector<IndexSet> &stokes_relevant_partitioning)
3318 * Amg_preconditioner.reset();
3319 * Mp_preconditioner.reset();
3321 * stokes_preconditioner_matrix.clear();
3323 * Table<2, DoFTools::Coupling> coupling(dim + 1, dim + 1);
3324 * for (unsigned int c = 0; c < dim + 1; ++c)
3325 * for (unsigned int d = 0; d < dim + 1; ++d)
3327 * coupling[c][d] = DoFTools::always;
3329 * coupling[c][d] = DoFTools::none;
3331 * BlockDynamicSparsityPattern dsp(stokes_relevant_partitioning);
3333 * DoFTools::make_sparsity_pattern(stokes_dof_handler,
3336 * stokes_constraints,
3338 * Utilities::MPI::this_mpi_process(
3341 * const IndexSet stokes_locally_relevant =
3342 * DoFTools::extract_locally_relevant_dofs(stokes_dof_handler);
3344 * SparsityTools::distribute_sparsity_pattern(
3346 * stokes_dof_handler.locally_owned_dofs(),
3348 * stokes_locally_relevant);
3350 * stokes_preconditioner_matrix.reinit(stokes_partitioning,
3356 * template <int dim>
3357 * void BoussinesqFlowProblem<dim>::setup_temperature_matrices(
3358 * const IndexSet &temperature_partitioner,
3359 * const IndexSet &temperature_relevant_partitioner)
3361 * T_preconditioner.reset();
3362 * temperature_mass_matrix.clear();
3363 * temperature_stiffness_matrix.clear();
3364 * temperature_matrix.clear();
3366 * DynamicSparsityPattern dsp(temperature_relevant_partitioner);
3368 * DoFTools::make_sparsity_pattern(temperature_dof_handler,
3370 * temperature_constraints,
3372 * Utilities::MPI::this_mpi_process(
3374 * SparsityTools::distribute_sparsity_pattern(
3376 * temperature_partitioner,
3378 * temperature_relevant_partitioner);
3380 * TrilinosWrappers::SparsityPattern sp;
3381 * sp.reinit(temperature_partitioner,
3382 * temperature_partitioner,
3387 * temperature_matrix.reinit(sp);
3388 * temperature_mass_matrix.reinit(sp);
3389 * temperature_stiffness_matrix.reinit(sp);
3396 * The remainder of the setup function (after splitting out the three
3397 * functions above) mostly has to deal with the things we need to do for
3398 * parallelization across processors. Because setting all of this up is a
3399 * significant compute time expense of the program, we put everything we do
3400 * here into a timer group so that we can get summary information about the
3401 * fraction of time spent in this part of the program at its end.
3405 * At the top as usual we enumerate degrees of freedom and sort them by
3406 * component/block, followed by writing their numbers to the screen from
3407 * processor zero. The DoFHandler::distributed_dofs() function, when applied
3408 * to a parallel::distributed::Triangulation object, sorts degrees of
3409 * freedom in such a way that all degrees of freedom associated with
3410 * subdomain zero come before all those associated with subdomain one,
3411 * etc. For the Stokes part, this entails, however, that velocities and
3412 * pressures become intermixed, but this is trivially solved by sorting
3413 * again by blocks; it is worth noting that this latter operation leaves the
3414 * relative ordering of all velocities and pressures alone, i.e. within the
3415 * velocity block we will still have all those associated with subdomain
3416 * zero before all velocities associated with subdomain one, etc. This is
3417 * important since we store each of the blocks of this matrix distributed
3418 * across all processors and want this to be done in such a way that each
3419 * processor stores that part of the matrix that is roughly equal to the
3420 * degrees of freedom located on those cells that it will actually work on.
3424 * When printing the numbers of degrees of freedom, note that these numbers
3425 * are going to be large if we use many processors. Consequently, we let the
3426 * stream put a comma separator in between every three digits. The state of
3427 * the stream, using the locale, is saved from before to after this
3428 * operation. While slightly opaque, the code works because the default
3429 * locale (which we get using the constructor call
3430 * <code>std::locale("")</code>) implies printing numbers with a comma
3431 * separator for every third digit (i.e., thousands, millions, billions).
3435 * In this function as well as many below, we measure how much time
3436 * we spend here and collect that in a section called "Setup dof
3437 * systems" across function invocations. This is done using an
3438 * TimerOutput::Scope object that gets a timer going in the section
3439 * with above name of the `computing_timer` object upon construction
3440 * of the local variable; the timer is stopped again when the
3441 * destructor of the `timing_section` variable is called. This, of
3442 * course, happens either at the end of the function, or if we leave
3443 * the function through a `return` statement or when an exception is
3444 * thrown somewhere -- in other words, whenever we leave this
3445 * function in any way. The use of such "scope" objects therefore
3446 * makes sure that we do not have to manually add code that tells
3447 * the timer to stop at every location where this function may be
3451 * template <int dim>
3452 * void BoussinesqFlowProblem<dim>::setup_dofs()
3454 * TimerOutput::Scope timing_section(computing_timer, "Setup dof systems");
3456 * stokes_dof_handler.distribute_dofs(stokes_fe);
3458 * std::vector<unsigned int> stokes_sub_blocks(dim + 1, 0);
3459 * stokes_sub_blocks[dim] = 1;
3460 * DoFRenumbering::component_wise(stokes_dof_handler, stokes_sub_blocks);
3462 * temperature_dof_handler.distribute_dofs(temperature_fe);
3464 * const std::vector<types::global_dof_index> stokes_dofs_per_block =
3465 * DoFTools::count_dofs_per_fe_block(stokes_dof_handler, stokes_sub_blocks);
3467 * const types::global_dof_index n_u = stokes_dofs_per_block[0],
3468 * n_p = stokes_dofs_per_block[1],
3469 * n_T = temperature_dof_handler.n_dofs();
3471 * std::locale s = pcout.get_stream().getloc();
3472 * pcout.get_stream().imbue(std::locale(""));
3473 * pcout << "Number of active cells: " << triangulation.n_global_active_cells()
3474 * << " (on " << triangulation.n_levels() << " levels)" << std::endl
3475 * << "Number of degrees of freedom: " << n_u + n_p + n_T << " (" << n_u
3476 * << '+
' << n_p << '+
' << n_T << ')
' << std::endl
3478 * pcout.get_stream().imbue(s);
3483 * After this, we have to set up the various partitioners (of type
3484 * <code>IndexSet</code>, see the introduction) that describe which parts
3485 * of each matrix or vector will be stored where, then call the functions
3486 * that actually set up the matrices, and at the end also resize the
3487 * various vectors we keep around in this program.
3493 * const IndexSet &stokes_locally_owned_index_set =
3494 * stokes_dof_handler.locally_owned_dofs();
3495 * const IndexSet stokes_locally_relevant_set =
3496 * DoFTools::extract_locally_relevant_dofs(stokes_dof_handler);
3498 * std::vector<IndexSet> stokes_partitioning;
3499 * stokes_partitioning.push_back(
3500 * stokes_locally_owned_index_set.get_view(0, n_u));
3501 * stokes_partitioning.push_back(
3502 * stokes_locally_owned_index_set.get_view(n_u, n_u + n_p));
3504 * std::vector<IndexSet> stokes_relevant_partitioning;
3505 * stokes_relevant_partitioning.push_back(
3506 * stokes_locally_relevant_set.get_view(0, n_u));
3507 * stokes_relevant_partitioning.push_back(
3508 * stokes_locally_relevant_set.get_view(n_u, n_u + n_p));
3510 * const IndexSet temperature_partitioning =
3511 * temperature_dof_handler.locally_owned_dofs();
3512 * const IndexSet temperature_relevant_partitioning =
3513 * DoFTools::extract_locally_relevant_dofs(temperature_dof_handler);
3517 * Following this, we can compute constraints for the solution vectors,
3518 * including hanging node constraints and homogeneous and inhomogeneous
3519 * boundary values for the Stokes and temperature fields. Note that as for
3520 * everything else, the constraint objects can not hold <i>all</i>
3521 * constraints on every processor. Rather, each processor needs to store
3522 * only those that are actually necessary for correctness given that it
3523 * only assembles linear systems on cells it owns. As discussed in the
3524 * @ref distributed_paper "this paper", the set of constraints we need to
3525 * know about is exactly the set of constraints on all locally relevant
3526 * degrees of freedom, so this is what we use to initialize the constraint
3531 * stokes_constraints.clear();
3532 * stokes_constraints.reinit(stokes_locally_owned_index_set,
3533 * stokes_locally_relevant_set);
3535 * DoFTools::make_hanging_node_constraints(stokes_dof_handler,
3536 * stokes_constraints);
3538 * const FEValuesExtractors::Vector velocity_components(0);
3539 * VectorTools::interpolate_boundary_values(
3540 * stokes_dof_handler,
3542 * Functions::ZeroFunction<dim>(dim + 1),
3543 * stokes_constraints,
3544 * stokes_fe.component_mask(velocity_components));
3546 * std::set<types::boundary_id> no_normal_flux_boundaries;
3547 * no_normal_flux_boundaries.insert(1);
3548 * VectorTools::compute_no_normal_flux_constraints(stokes_dof_handler,
3550 * no_normal_flux_boundaries,
3551 * stokes_constraints,
3553 * stokes_constraints.close();
3556 * temperature_constraints.clear();
3557 * temperature_constraints.reinit(temperature_partitioning,
3558 * temperature_relevant_partitioning);
3560 * DoFTools::make_hanging_node_constraints(temperature_dof_handler,
3561 * temperature_constraints);
3562 * VectorTools::interpolate_boundary_values(
3563 * temperature_dof_handler,
3565 * EquationData::TemperatureInitialValues<dim>(),
3566 * temperature_constraints);
3567 * VectorTools::interpolate_boundary_values(
3568 * temperature_dof_handler,
3570 * EquationData::TemperatureInitialValues<dim>(),
3571 * temperature_constraints);
3572 * temperature_constraints.close();
3577 * All this done, we can then initialize the various matrix and vector
3578 * objects to their proper sizes. At the end, we also record that all
3579 * matrices and preconditioners have to be re-computed at the beginning of
3580 * the next time step. Note how we initialize the vectors for the Stokes
3581 * and temperature right hand sides: These are writable vectors (last
3582 * boolean argument set to @p true) that have the correct one-to-one
3583 * partitioning of locally owned elements but are still given the relevant
3584 * partitioning for means of figuring out the vector entries that are
3585 * going to be set right away. As for matrices, this allows for writing
3586 * local contributions into the vector with multiple threads (always
3587 * assuming that the same vector entry is not accessed by multiple threads
3588 * at the same time). The other vectors only allow for read access of
3589 * individual elements, including ghosts, but are not suitable for
3593 * setup_stokes_matrix(stokes_partitioning, stokes_relevant_partitioning);
3594 * setup_stokes_preconditioner(stokes_partitioning,
3595 * stokes_relevant_partitioning);
3596 * setup_temperature_matrices(temperature_partitioning,
3597 * temperature_relevant_partitioning);
3599 * stokes_rhs.reinit(stokes_partitioning,
3600 * stokes_relevant_partitioning,
3603 * stokes_solution.reinit(stokes_relevant_partitioning, MPI_COMM_WORLD);
3604 * old_stokes_solution.reinit(stokes_solution);
3606 * temperature_rhs.reinit(temperature_partitioning,
3607 * temperature_relevant_partitioning,
3610 * temperature_solution.reinit(temperature_relevant_partitioning,
3612 * old_temperature_solution.reinit(temperature_solution);
3613 * old_old_temperature_solution.reinit(temperature_solution);
3615 * rebuild_stokes_matrix = true;
3616 * rebuild_stokes_preconditioner = true;
3617 * rebuild_temperature_matrices = true;
3618 * rebuild_temperature_preconditioner = true;
3626 * <a name="step_32-TheBoussinesqFlowProblemassemblyfunctions"></a>
3627 * <h4>The BoussinesqFlowProblem assembly functions</h4>
3631 * Following the discussion in the introduction and in the @ref threads
3632 * topic, we split the assembly functions into different parts:
3636 * <ul> <li> The local calculations of matrices and right hand sides, given
3637 * a certain cell as input (these functions are named
3638 * <code>local_assemble_*</code> below). The resulting function is, in other
3639 * words, essentially the body of the loop over all cells in @ref step_31 "step-31". Note,
3640 * however, that these functions store the result from the local
3641 * calculations in variables of classes from the CopyData namespace.
3645 * <li>These objects are then given to the second step which writes the
3646 * local data into the global data structures (these functions are named
3647 * <code>copy_local_to_global_*</code> below). These functions are pretty
3652 * <li>These two subfunctions are then used in the respective assembly
3653 * routine (called <code>assemble_*</code> below), where a WorkStream object
3654 * is set up and runs over all the cells that belong to the processor's
3660 * <a name=
"step_32-Stokespreconditionerassembly"></a>
3661 * <h5>Stokes preconditioner assembly</h5>
3665 * Let us start with the functions that builds the Stokes
3666 * preconditioner. The
first two of these are pretty trivial, given the
3667 * discussion above. Note in particular that the
main point in
using the
3668 * scratch
data object is that we want to avoid allocating any objects on
3669 * the free space each time we visit a
new cell. As a consequence, the
3670 * assembly function below only has automatic local variables, and
3671 * everything
else is accessed through the scratch
data object, which is
3672 * allocated only once before we start the loop over all cells:
3675 *
template <
int dim>
3676 *
void BoussinesqFlowProblem<dim>::local_assemble_stokes_preconditioner(
3678 *
Assembly::Scratch::StokesPreconditioner<dim> &scratch,
3679 *
Assembly::CopyData::StokesPreconditioner<dim> &
data)
3681 *
const unsigned
int dofs_per_cell = stokes_fe.n_dofs_per_cell();
3682 *
const unsigned int n_q_points =
3683 *
scratch.stokes_fe_values.n_quadrature_points;
3688 *
scratch.stokes_fe_values.reinit(cell);
3689 *
cell->get_dof_indices(
data.local_dof_indices);
3691 *
data.local_matrix = 0;
3693 *
for (
unsigned int q = 0; q < n_q_points; ++q)
3695 *
for (
unsigned int k = 0; k < dofs_per_cell; ++k)
3697 *
scratch.grad_phi_u[k] =
3698 *
scratch.stokes_fe_values[velocities].gradient(k, q);
3699 *
scratch.phi_p[k] = scratch.stokes_fe_values[pressure].value(k, q);
3702 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
3703 *
for (
unsigned int j = 0; j < dofs_per_cell; ++j)
3704 *
data.local_matrix(i, j) +=
3705 *
(EquationData::eta *
3706 *
scalar_product(scratch.grad_phi_u[i], scratch.grad_phi_u[j]) +
3707 *
(1. / EquationData::eta) * EquationData::pressure_scaling *
3708 *
EquationData::pressure_scaling *
3709 *
(scratch.phi_p[i] * scratch.phi_p[j])) *
3710 *
scratch.stokes_fe_values.JxW(q);
3716 *
template <
int dim>
3717 *
void BoussinesqFlowProblem<dim>::copy_local_to_global_stokes_preconditioner(
3718 *
const Assembly::CopyData::StokesPreconditioner<dim> &
data)
3720 *
stokes_constraints.distribute_local_to_global(
data.local_matrix,
3721 *
data.local_dof_indices,
3722 *
stokes_preconditioner_matrix);
3728 * Now
for the function that actually puts things together,
using the
3730 * enumerate the cells it is supposed to work on. Typically,
one would use
3732 * actually only want the subset of cells that in fact are owned by the
3734 * play: you give it a range of cells and it provides an
iterator that only
3735 * iterates over that subset of cells that satisfy a certain predicate (a
3736 * predicate is a function of one argument that either returns true or
3737 * false). The predicate we use here is
IteratorFilters::LocallyOwnedCell,
3738 * i.e., it returns true exactly if the cell is owned by the current
3739 * processor. The resulting
iterator range is then exactly what we need.
3743 * With this obstacle out of the way, we call the
WorkStream::run
3744 * function with this
set of cells, scratch and copy objects, and
3745 * with pointers to two functions: the local assembly and
3746 * copy-local-to-global function. These functions need to have very
3747 * specific signatures: three arguments in the
first and one
3748 * argument in the latter case (see the documentation of the
3749 *
WorkStream::run function
for the meaning of these arguments).
3750 * Note how we use a lambda functions to
3751 * create a function
object that satisfies this requirement. It uses
3752 * function arguments
for the local assembly function that specify
3753 * cell, scratch
data, and copy
data, as well as function argument
3754 *
for the copy function that expects the
3755 *
data to be written into the global matrix (also see the discussion in
3756 * @ref step_13 "step-13"'s <code>assemble_linear_system()</code> function). On the other
3757 * hand, the implicit zeroth argument of member functions (namely
3758 * the <code>this</code>
pointer of the
object on which that member
3759 * function is to operate on) is <i>bound</i> to the
3760 * <code>this</code>
pointer of the current function and is captured. The
3761 *
WorkStream::run function, as a consequence, does not need to know
3762 * anything about the
object these functions work on.
3766 * When the
WorkStream is executed, it will create several local assembly
3767 * routines of the
first kind
for several cells and let some available
3768 * processors work on them. The function that needs to be synchronized,
3769 * i.e., the write operation into the global matrix, however, is executed by
3770 * only one thread at a time in the prescribed order. Of course, this only
3771 * holds
for the parallelization on a single
MPI process. Different
MPI
3772 * processes will have their own
WorkStream objects and do that work
3773 * completely independently (and in different memory spaces). In a
3774 * distributed calculation, some
data will accumulate at degrees of freedom
3775 * that are not owned by the respective processor. It would be inefficient
3776 * to send
data around every time we encounter such a dof. What happens
3777 * instead is that the Trilinos sparse matrix will keep that
data and send
3778 * it to the owner at the
end of assembly, by calling the
3779 * <code>compress()</code> command.
3782 *
template <
int dim>
3783 *
void BoussinesqFlowProblem<dim>::assemble_stokes_preconditioner()
3785 *
stokes_preconditioner_matrix = 0;
3787 *
const QGauss<dim> quadrature_formula(parameters.stokes_velocity_degree + 1);
3789 *
using CellFilter =
3794 *
Assembly::Scratch::StokesPreconditioner<dim> &scratch,
3795 *
Assembly::CopyData::StokesPreconditioner<dim> &
data) {
3796 *
this->local_assemble_stokes_preconditioner(cell, scratch,
data);
3800 *
[
this](
const Assembly::CopyData::StokesPreconditioner<dim> &
data) {
3801 *
this->copy_local_to_global_stokes_preconditioner(
data);
3805 *
stokes_dof_handler.begin_active()),
3807 *
stokes_dof_handler.end()),
3810 *
Assembly::Scratch::StokesPreconditioner<dim>(
3812 *
quadrature_formula,
3815 *
Assembly::CopyData::StokesPreconditioner<dim>(stokes_fe));
3824 * The
final function in
this block initiates assembly of the Stokes
3825 * preconditioner
matrix and then in fact builds the Stokes
3826 * preconditioner. It is mostly the same as in the
serial case. The only
3827 * difference to @ref step_31
"step-31" is that we use a Jacobi preconditioner
for the
3828 * pressure mass
matrix instead of IC, as discussed in the introduction.
3831 *
template <
int dim>
3832 *
void BoussinesqFlowProblem<dim>::build_stokes_preconditioner()
3834 *
if (rebuild_stokes_preconditioner ==
false)
3838 *
" Build Stokes preconditioner");
3839 *
pcout <<
" Rebuilding Stokes preconditioner..." << std::flush;
3841 *
assemble_stokes_preconditioner();
3844 *
const std::vector<std::vector<bool>> constant_modes =
3846 *
stokes_dof_handler, stokes_fe.component_mask(velocity_components));
3848 *
Mp_preconditioner =
3849 *
std::make_shared<TrilinosWrappers::PreconditionJacobi>();
3850 *
Amg_preconditioner = std::make_shared<TrilinosWrappers::PreconditionAMG>();
3854 *
Amg_data.elliptic =
true;
3855 *
#ifdef DEAL_II_TRILINOS_WITH_EPETRA
3856 *
Amg_data.higher_order_elements =
true;
3858 *
Amg_data.smoother_sweeps = 2;
3859 *
Amg_data.aggregation_threshold = 0.02;
3861 *
Mp_preconditioner->initialize(stokes_preconditioner_matrix.block(1, 1));
3862 *
Amg_preconditioner->initialize(stokes_preconditioner_matrix.block(0, 0),
3865 *
rebuild_stokes_preconditioner =
false;
3867 *
pcout << std::endl;
3874 * <a name=
"step_32-Stokessystemassembly"></a>
3875 * <h5>Stokes system assembly</h5>
3879 * The next three
functions implement the assembly of the Stokes system,
3880 * again
split up into a part performing local calculations,
one for writing
3881 * the local
data into the global
matrix and vector, and
one for actually
3882 * running the
loop over all cells with the help of the
WorkStream
3883 *
class. Note that the assembly of the Stokes
matrix needs only to be done
3884 * in
case we have changed the mesh. Otherwise, just the
3885 * (
temperature-dependent) right hand side needs to be calculated
3886 * here. Since we are working with distributed matrices and vectors, we have
3888 * the assembly in order to send non-local
data to the owner process.
3891 *
template <
int dim>
3892 *
void BoussinesqFlowProblem<dim>::local_assemble_stokes_system(
3894 *
Assembly::Scratch::StokesSystem<dim> &scratch,
3895 *
Assembly::CopyData::StokesSystem<dim> &
data)
3897 *
const unsigned int dofs_per_cell =
3898 *
scratch.stokes_fe_values.get_fe().n_dofs_per_cell();
3899 *
const unsigned int n_q_points =
3900 *
scratch.stokes_fe_values.n_quadrature_points;
3905 *
scratch.stokes_fe_values.reinit(cell);
3908 *
cell->as_dof_handler_iterator(temperature_dof_handler);
3909 *
scratch.temperature_fe_values.reinit(temperature_cell);
3911 *
if (rebuild_stokes_matrix)
3912 *
data.local_matrix = 0;
3913 *
data.local_rhs = 0;
3915 *
scratch.temperature_fe_values.get_function_values(
3916 *
old_temperature_solution, scratch.old_temperature_values);
3918 *
for (
unsigned int q = 0; q < n_q_points; ++q)
3920 *
const double old_temperature = scratch.old_temperature_values[q];
3922 *
for (
unsigned int k = 0; k < dofs_per_cell; ++k)
3924 *
scratch.phi_u[k] = scratch.stokes_fe_values[velocities].value(k, q);
3925 *
if (rebuild_stokes_matrix)
3927 *
scratch.grads_phi_u[k] =
3928 *
scratch.stokes_fe_values[velocities].symmetric_gradient(k, q);
3929 *
scratch.div_phi_u[k] =
3930 *
scratch.stokes_fe_values[velocities].divergence(k, q);
3931 *
scratch.phi_p[k] =
3932 *
scratch.stokes_fe_values[pressure].value(k, q);
3936 *
if (rebuild_stokes_matrix ==
true)
3937 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
3938 *
for (
unsigned int j = 0; j < dofs_per_cell; ++j)
3939 *
data.local_matrix(i, j) +=
3940 *
(EquationData::eta * 2 *
3941 *
(scratch.grads_phi_u[i] * scratch.grads_phi_u[j]) -
3942 *
(EquationData::pressure_scaling * scratch.div_phi_u[i] *
3943 *
scratch.phi_p[j]) -
3944 *
(EquationData::pressure_scaling * scratch.phi_p[i] *
3945 *
scratch.div_phi_u[j])) *
3946 *
scratch.stokes_fe_values.JxW(q);
3949 *
scratch.stokes_fe_values.quadrature_point(q));
3951 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
3952 *
data.local_rhs(i) += (EquationData::density(old_temperature) *
3953 *
gravity * scratch.phi_u[i]) *
3954 *
scratch.stokes_fe_values.JxW(q);
3957 *
cell->get_dof_indices(
data.local_dof_indices);
3962 *
template <
int dim>
3963 *
void BoussinesqFlowProblem<dim>::copy_local_to_global_stokes_system(
3964 *
const Assembly::CopyData::StokesSystem<dim> &
data)
3966 *
if (rebuild_stokes_matrix ==
true)
3967 *
stokes_constraints.distribute_local_to_global(
data.local_matrix,
3969 *
data.local_dof_indices,
3973 *
stokes_constraints.distribute_local_to_global(
data.local_rhs,
3974 *
data.local_dof_indices,
3980 *
template <
int dim>
3981 *
void BoussinesqFlowProblem<dim>::assemble_stokes_system()
3984 *
" Assemble Stokes system");
3986 *
if (rebuild_stokes_matrix ==
true)
3987 *
stokes_matrix = 0;
3991 *
const QGauss<dim> quadrature_formula(parameters.stokes_velocity_degree + 1);
3993 *
using CellFilter =
3998 *
stokes_dof_handler.begin_active()),
4001 *
Assembly::Scratch::StokesSystem<dim> &scratch,
4002 *
Assembly::CopyData::StokesSystem<dim> &
data) {
4003 * this->local_assemble_stokes_system(cell, scratch, data);
4005 *
[
this](
const Assembly::CopyData::StokesSystem<dim> &
data) {
4006 * this->copy_local_to_global_stokes_system(data);
4008 *
Assembly::Scratch::StokesSystem<dim>(
4011 *
quadrature_formula,
4016 *
Assembly::CopyData::StokesSystem<dim>(stokes_fe));
4018 *
if (rebuild_stokes_matrix ==
true)
4022 *
rebuild_stokes_matrix =
false;
4024 *
pcout << std::endl;
4031 * <a name=
"step_32-Temperaturematrixassembly"></a>
4032 * <h5>Temperature
matrix assembly</h5>
4036 * The task to be performed by the next three
functions is to calculate a
4038 * combined in order to yield the semi-implicit time stepping
matrix that
4039 * consists of the mass
matrix plus a time step-dependent weight factor
4040 * times the Laplace
matrix. This function is again essentially the body of
4041 * the
loop over all cells from @ref step_31
"step-31".
4045 * The two following
functions perform similar services as the ones above.
4048 *
template <
int dim>
4049 *
void BoussinesqFlowProblem<dim>::local_assemble_temperature_matrix(
4051 *
Assembly::Scratch::TemperatureMatrix<dim> &scratch,
4052 *
Assembly::CopyData::TemperatureMatrix<dim> &
data)
4054 *
const unsigned int dofs_per_cell =
4055 *
scratch.temperature_fe_values.get_fe().n_dofs_per_cell();
4056 *
const unsigned int n_q_points =
4057 *
scratch.temperature_fe_values.n_quadrature_points;
4059 *
scratch.temperature_fe_values.reinit(cell);
4060 *
cell->get_dof_indices(
data.local_dof_indices);
4062 *
data.local_mass_matrix = 0;
4063 *
data.local_stiffness_matrix = 0;
4065 *
for (
unsigned int q = 0; q < n_q_points; ++q)
4067 *
for (
unsigned int k = 0; k < dofs_per_cell; ++k)
4069 *
scratch.grad_phi_T[k] =
4070 *
scratch.temperature_fe_values.shape_grad(k, q);
4071 *
scratch.phi_T[k] = scratch.temperature_fe_values.shape_value(k, q);
4074 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
4075 *
for (
unsigned int j = 0; j < dofs_per_cell; ++j)
4077 *
data.local_mass_matrix(i, j) +=
4078 *
(scratch.phi_T[i] * scratch.phi_T[j] *
4079 *
scratch.temperature_fe_values.JxW(q));
4080 *
data.local_stiffness_matrix(i, j) +=
4081 *
(EquationData::kappa * scratch.grad_phi_T[i] *
4082 *
scratch.grad_phi_T[j] * scratch.temperature_fe_values.JxW(q));
4089 *
template <
int dim>
4090 *
void BoussinesqFlowProblem<dim>::copy_local_to_global_temperature_matrix(
4091 *
const Assembly::CopyData::TemperatureMatrix<dim> &
data)
4093 *
temperature_constraints.distribute_local_to_global(
data.local_mass_matrix,
4094 *
data.local_dof_indices,
4095 *
temperature_mass_matrix);
4096 *
temperature_constraints.distribute_local_to_global(
4097 *
data.local_stiffness_matrix,
4098 *
data.local_dof_indices,
4099 *
temperature_stiffness_matrix);
4103 *
template <
int dim>
4104 *
void BoussinesqFlowProblem<dim>::assemble_temperature_matrix()
4106 *
if (rebuild_temperature_matrices ==
false)
4110 *
" Assemble temperature matrices");
4111 *
temperature_mass_matrix = 0;
4112 *
temperature_stiffness_matrix = 0;
4114 *
const QGauss<dim> quadrature_formula(parameters.temperature_degree + 2);
4116 *
using CellFilter =
4121 *
temperature_dof_handler.begin_active()),
4123 *
temperature_dof_handler.end()),
4125 *
Assembly::Scratch::TemperatureMatrix<dim> &scratch,
4126 *
Assembly::CopyData::TemperatureMatrix<dim> &
data) {
4127 * this->local_assemble_temperature_matrix(cell, scratch, data);
4129 *
[
this](
const Assembly::CopyData::TemperatureMatrix<dim> &
data) {
4130 * this->copy_local_to_global_temperature_matrix(data);
4132 *
Assembly::Scratch::TemperatureMatrix<dim>(temperature_fe,
4134 *
quadrature_formula),
4135 *
Assembly::CopyData::TemperatureMatrix<dim>(temperature_fe));
4140 *
rebuild_temperature_matrices =
false;
4141 *
rebuild_temperature_preconditioner =
true;
4148 * <a name=
"step_32-Temperaturerighthandsideassembly"></a>
4149 * <h5>Temperature right hand side assembly</h5>
4153 * This is the last assembly function. It calculates the right hand side of
4154 * the
temperature system, which includes the convection and the
4155 * stabilization terms. It includes a lot of evaluations of old solutions at
4156 * the quadrature points (which are necessary
for calculating the artificial
4157 * viscosity of stabilization), but is otherwise similar to the other
4158 * assembly
functions. Notice, once again, how we resolve the dilemma of
4159 * having inhomogeneous boundary conditions, by just making a right hand
4160 * side at
this point (compare the comments
for the <code>
project()</code>
4161 * function above): We create some
matrix columns with exactly the
values
4162 * that would be entered
for the
temperature @ref GlossStiffnessMatrix
"stiffness matrix", in case we
4163 * have inhomogeneously constrained dofs. That will account
for the correct
4164 * balance of the right hand side vector with the
matrix system of
4168 *
template <
int dim>
4169 *
void BoussinesqFlowProblem<dim>::local_assemble_temperature_rhs(
4170 *
const std::pair<double, double> global_T_range,
4171 *
const double global_max_velocity,
4172 *
const double global_entropy_variation,
4174 *
Assembly::Scratch::TemperatureRHS<dim> &scratch,
4175 *
Assembly::CopyData::TemperatureRHS<dim> &
data)
4177 *
const bool use_bdf2_scheme = (timestep_number != 0);
4179 *
const unsigned int dofs_per_cell =
4180 *
scratch.temperature_fe_values.get_fe().n_dofs_per_cell();
4181 *
const unsigned int n_q_points =
4182 *
scratch.temperature_fe_values.n_quadrature_points;
4186 *
data.local_rhs = 0;
4187 *
data.matrix_for_bc = 0;
4188 *
cell->get_dof_indices(
data.local_dof_indices);
4190 *
scratch.temperature_fe_values.
reinit(cell);
4193 *
cell->as_dof_handler_iterator(stokes_dof_handler);
4194 *
scratch.stokes_fe_values.
reinit(stokes_cell);
4196 *
scratch.temperature_fe_values.get_function_values(
4197 *
old_temperature_solution, scratch.old_temperature_values);
4198 *
scratch.temperature_fe_values.get_function_values(
4199 *
old_old_temperature_solution, scratch.old_old_temperature_values);
4201 *
scratch.temperature_fe_values.get_function_gradients(
4202 *
old_temperature_solution, scratch.old_temperature_grads);
4203 *
scratch.temperature_fe_values.get_function_gradients(
4204 *
old_old_temperature_solution, scratch.old_old_temperature_grads);
4206 *
scratch.temperature_fe_values.get_function_laplacians(
4207 *
old_temperature_solution, scratch.old_temperature_laplacians);
4208 *
scratch.temperature_fe_values.get_function_laplacians(
4209 *
old_old_temperature_solution, scratch.old_old_temperature_laplacians);
4211 *
scratch.stokes_fe_values[velocities].get_function_values(
4212 *
stokes_solution, scratch.old_velocity_values);
4213 *
scratch.stokes_fe_values[velocities].get_function_values(
4214 *
old_stokes_solution, scratch.old_old_velocity_values);
4215 *
scratch.stokes_fe_values[velocities].get_function_symmetric_gradients(
4216 *
stokes_solution, scratch.old_strain_rates);
4217 *
scratch.stokes_fe_values[velocities].get_function_symmetric_gradients(
4218 *
old_stokes_solution, scratch.old_old_strain_rates);
4221 *
compute_viscosity(scratch.old_temperature_values,
4222 *
scratch.old_old_temperature_values,
4223 *
scratch.old_temperature_grads,
4224 *
scratch.old_old_temperature_grads,
4225 *
scratch.old_temperature_laplacians,
4226 *
scratch.old_old_temperature_laplacians,
4227 *
scratch.old_velocity_values,
4228 *
scratch.old_old_velocity_values,
4229 *
scratch.old_strain_rates,
4230 *
scratch.old_old_strain_rates,
4231 *
global_max_velocity,
4232 *
global_T_range.second - global_T_range.first,
4233 *
0.5 * (global_T_range.second + global_T_range.first),
4234 *
global_entropy_variation,
4235 *
cell->diameter());
4237 *
for (
unsigned int q = 0; q < n_q_points; ++q)
4239 *
for (
unsigned int k = 0; k < dofs_per_cell; ++k)
4241 *
scratch.phi_T[k] = scratch.temperature_fe_values.shape_value(k, q);
4242 *
scratch.grad_phi_T[k] =
4243 *
scratch.temperature_fe_values.shape_grad(k, q);
4247 *
const double T_term_for_rhs =
4248 *
(use_bdf2_scheme ?
4249 *
(scratch.old_temperature_values[q] *
4250 *
(1 + time_step / old_time_step) -
4251 *
scratch.old_old_temperature_values[q] * (time_step * time_step) /
4252 *
(old_time_step * (time_step + old_time_step))) :
4253 *
scratch.old_temperature_values[q]);
4255 *
const double ext_T =
4256 *
(use_bdf2_scheme ? (scratch.old_temperature_values[q] *
4257 *
(1 + time_step / old_time_step) -
4258 *
scratch.old_old_temperature_values[q] *
4259 *
time_step / old_time_step) :
4260 *
scratch.old_temperature_values[q]);
4263 *
(use_bdf2_scheme ? (scratch.old_temperature_grads[q] *
4264 *
(1 + time_step / old_time_step) -
4265 *
scratch.old_old_temperature_grads[q] * time_step /
4267 *
scratch.old_temperature_grads[q]);
4270 *
(use_bdf2_scheme ?
4271 *
(scratch.old_velocity_values[q] * (1 + time_step / old_time_step) -
4272 *
scratch.old_old_velocity_values[q] * time_step / old_time_step) :
4273 *
scratch.old_velocity_values[q]);
4276 *
(use_bdf2_scheme ?
4277 *
(scratch.old_strain_rates[q] * (1 + time_step / old_time_step) -
4278 *
scratch.old_old_strain_rates[q] * time_step / old_time_step) :
4279 *
scratch.old_strain_rates[q]);
4281 *
const double gamma =
4282 *
((EquationData::radiogenic_heating * EquationData::density(ext_T) +
4283 *
2 * EquationData::eta * extrapolated_strain_rate *
4284 *
extrapolated_strain_rate) /
4285 *
(EquationData::density(ext_T) * EquationData::specific_heat));
4287 *
for (
unsigned int i = 0; i < dofs_per_cell; ++i)
4289 *
data.local_rhs(i) +=
4290 *
(T_term_for_rhs * scratch.phi_T[i] -
4291 *
time_step * extrapolated_u * ext_grad_T * scratch.phi_T[i] -
4292 *
time_step * nu * ext_grad_T * scratch.grad_phi_T[i] +
4293 *
time_step * gamma * scratch.phi_T[i]) *
4294 *
scratch.temperature_fe_values.JxW(q);
4296 *
if (temperature_constraints.is_inhomogeneously_constrained(
4297 *
data.local_dof_indices[i]))
4299 *
for (
unsigned int j = 0; j < dofs_per_cell; ++j)
4300 *
data.matrix_for_bc(j, i) +=
4301 *
(scratch.phi_T[i] * scratch.phi_T[j] *
4302 *
(use_bdf2_scheme ? ((2 * time_step + old_time_step) /
4303 *
(time_step + old_time_step)) :
4305 *
scratch.grad_phi_T[i] * scratch.grad_phi_T[j] *
4306 *
EquationData::kappa * time_step) *
4307 *
scratch.temperature_fe_values.JxW(q);
4314 *
template <
int dim>
4315 *
void BoussinesqFlowProblem<dim>::copy_local_to_global_temperature_rhs(
4316 *
const Assembly::CopyData::TemperatureRHS<dim> &
data)
4318 *
temperature_constraints.distribute_local_to_global(
data.local_rhs,
4319 *
data.local_dof_indices,
4321 *
data.matrix_for_bc);
4328 * In the function that runs the
WorkStream for actually calculating the
4329 * right hand side, we also generate the
final matrix. As mentioned above,
4330 * it is a
sum of the mass
matrix and the Laplace
matrix, times some time
4331 * step-dependent weight. This weight is specified by the BDF-2 time
4332 * integration scheme, see the introduction in @ref step_31
"step-31". What is
new in
this
4333 * tutorial program (in addition to the use of
MPI parallelization and the
4335 * preconditioner as well. The reason is that the setup of the Jacobi
4336 * preconditioner takes a noticeable time compared to the solver because we
4337 * usually only need between 10 and 20 iterations
for solving the
4338 *
temperature system (
this might sound strange, as Jacobi really only
4339 * consists of a diagonal, but in Trilinos it is derived from more general
4340 * framework
for point relaxation preconditioners which is a bit
4341 * inefficient). Hence, it is more efficient to precompute the
4342 * preconditioner, even though the
matrix entries may slightly change
4343 * because the time step might change. This is not too big a problem because
4344 * we
remesh every few time steps (and regenerate the preconditioner then).
4347 *
template <
int dim>
4348 *
void BoussinesqFlowProblem<dim>::assemble_temperature_system(
4349 *
const double maximal_velocity)
4351 *
const bool use_bdf2_scheme = (timestep_number != 0);
4353 *
if (use_bdf2_scheme ==
true)
4355 *
temperature_matrix.copy_from(temperature_mass_matrix);
4356 *
temperature_matrix *=
4357 *
(2 * time_step + old_time_step) / (time_step + old_time_step);
4358 *
temperature_matrix.add(time_step, temperature_stiffness_matrix);
4362 *
temperature_matrix.copy_from(temperature_mass_matrix);
4363 *
temperature_matrix.add(time_step, temperature_stiffness_matrix);
4366 *
if (rebuild_temperature_preconditioner ==
true)
4368 *
T_preconditioner =
4369 *
std::make_shared<TrilinosWrappers::PreconditionJacobi>();
4370 *
T_preconditioner->initialize(temperature_matrix);
4371 *
rebuild_temperature_preconditioner =
false;
4376 * The next part is computing the right hand side vectors. To
do so, we
4377 *
first compute the average
temperature @f$T_m@f$ that we use
for evaluating
4378 * the artificial viscosity stabilization through the residual @f$E(T) =
4379 * (
T-T_m)^2@f$. We
do this by defining the midpoint between maximum and
4380 * minimum temperature as average temperature in the definition of the
4381 * entropy viscosity. An alternative would be to use the integral average,
4382 * but the results are not very sensitive to
this choice. The rest then
4383 * only
requires calling
WorkStream::run again, binding the arguments to
4384 * the <code>local_assemble_temperature_rhs</code> function that are the
4385 * same in every call to the correct values:
4388 *
temperature_rhs = 0;
4390 *
const QGauss<dim> quadrature_formula(parameters.temperature_degree + 2);
4391 *
const std::pair<double, double> global_T_range =
4392 *
get_extrapolated_temperature_range();
4394 *
const double average_temperature =
4395 *
0.5 * (global_T_range.first + global_T_range.second);
4396 *
const double global_entropy_variation =
4397 *
get_entropy_variation(average_temperature);
4399 *
using CellFilter =
4403 *
[
this, global_T_range, maximal_velocity, global_entropy_variation](
4405 *
Assembly::Scratch::TemperatureRHS<dim> &scratch,
4406 *
Assembly::CopyData::TemperatureRHS<dim> &
data) {
4407 *
this->local_assemble_temperature_rhs(global_T_range,
4409 *
global_entropy_variation,
4415 *
auto copier = [
this](
const Assembly::CopyData::TemperatureRHS<dim> &
data) {
4416 *
this->copy_local_to_global_temperature_rhs(
data);
4420 *
temperature_dof_handler.begin_active()),
4422 *
temperature_dof_handler.end()),
4425 *
Assembly::Scratch::TemperatureRHS<dim>(
4426 *
temperature_fe, stokes_fe, mapping, quadrature_formula),
4427 *
Assembly::CopyData::TemperatureRHS<dim>(temperature_fe));
4437 * <a name=
"step_32-BoussinesqFlowProblemsolve"></a>
4438 * <h4>BoussinesqFlowProblem::solve</h4>
4442 * This function solves the linear systems in each time step of the
4443 * Boussinesq problem. First, we work on the Stokes system and then on the
4444 *
temperature system. In essence, it does the same things as the respective
4445 * function in @ref step_31
"step-31". However, there are a few changes here.
4449 * The
first change is related to the way we store our solution: we keep the
4450 * vectors with locally owned degrees of freedom plus ghost nodes on each
4451 *
MPI node. When we enter a solver which is supposed to perform
4452 *
matrix-vector products with a distributed
matrix,
this is not the
4453 * appropriate form, though. There, we will want to have the solution vector
4454 * to be distributed in the same way as the
matrix, i.e. without any
4455 * ghosts. So what we
do first is to generate a distributed vector called
4456 * <code>distributed_stokes_solution</code> and put only the locally owned
4457 * dofs into that, which is neatly done by the <code>
operator=</code> of the
4462 * Next, we
scale the pressure solution (or rather, the initial guess)
for
4463 * the solver so that it matches with the length scales in the matrices, as
4464 * discussed in the introduction. We also immediately
scale the pressure
4465 * solution back to the correct units after the solution is completed. We
4466 * also need to
set the pressure
values at hanging nodes to
zero. This we
4467 * also did in @ref step_31
"step-31" in order not to disturb the Schur complement by some
4468 * vector entries that actually are irrelevant during the solve stage. As a
4469 * difference to @ref step_31
"step-31", here we
do it only
for the locally owned pressure
4470 * dofs. After solving
for the Stokes solution, each processor copies the
4471 * distributed solution back into the solution vector that also includes
4476 * The third and most obvious change is that we have two variants
for the
4477 * Stokes solver:
A fast solver that sometimes breaks down, and a robust
4478 * solver that is slower. This is what we already discussed in the
4479 * introduction. Here is how we realize it: First, we perform 30 iterations
4480 * with the fast solver based on the simple preconditioner based on the AMG
4481 *
V-cycle instead of an
approximate solve (
this is indicated by the
4482 * <code>false</code> argument to the
4483 * <code>LinearSolvers::BlockSchurPreconditioner</code>
object). If we
4484 * converge, everything is fine. If we
do not converge, the solver control
4486 *
this would
abort the program because we don
't catch them in our usual
4487 * <code>solve()</code> functions. This is certainly not what we want to
4488 * happen here. Rather, we want to switch to the strong solver and continue
4489 * the solution process with whatever vector we got so far. Hence, we catch
4490 * the exception with the C++ try/catch mechanism. We then simply go through
4491 * the same solver sequence again in the <code>catch</code> clause, this
4492 * time passing the @p true flag to the preconditioner for the strong
4493 * solver, signaling an approximate CG solve.
4496 * template <int dim>
4497 * void BoussinesqFlowProblem<dim>::solve()
4500 * TimerOutput::Scope timer_section(computing_timer,
4501 * " Solve Stokes system");
4503 * pcout << " Solving Stokes system... " << std::flush;
4505 * TrilinosWrappers::MPI::BlockVector distributed_stokes_solution(
4507 * distributed_stokes_solution = stokes_solution;
4509 * distributed_stokes_solution.block(1) /= EquationData::pressure_scaling;
4511 * const unsigned int
4512 * start = (distributed_stokes_solution.block(0).size() +
4513 * distributed_stokes_solution.block(1).local_range().first),
4514 * end = (distributed_stokes_solution.block(0).size() +
4515 * distributed_stokes_solution.block(1).local_range().second);
4516 * for (unsigned int i = start; i < end; ++i)
4517 * if (stokes_constraints.is_constrained(i))
4518 * distributed_stokes_solution(i) = 0;
4521 * PrimitiveVectorMemory<TrilinosWrappers::MPI::BlockVector> mem;
4523 * unsigned int n_iterations = 0;
4524 * const double solver_tolerance = 1e-8 * stokes_rhs.l2_norm();
4525 * SolverControl solver_control(30, solver_tolerance);
4529 * const LinearSolvers::BlockSchurPreconditioner<
4530 * TrilinosWrappers::PreconditionAMG,
4531 * TrilinosWrappers::PreconditionJacobi>
4532 * preconditioner(stokes_matrix,
4533 * stokes_preconditioner_matrix,
4534 * *Mp_preconditioner,
4535 * *Amg_preconditioner,
4538 * SolverFGMRES<TrilinosWrappers::MPI::BlockVector> solver(
4541 * SolverFGMRES<TrilinosWrappers::MPI::BlockVector>::AdditionalData(
4543 * solver.solve(stokes_matrix,
4544 * distributed_stokes_solution,
4548 * n_iterations = solver_control.last_step();
4551 * catch (SolverControl::NoConvergence &)
4553 * const LinearSolvers::BlockSchurPreconditioner<
4554 * TrilinosWrappers::PreconditionAMG,
4555 * TrilinosWrappers::PreconditionJacobi>
4556 * preconditioner(stokes_matrix,
4557 * stokes_preconditioner_matrix,
4558 * *Mp_preconditioner,
4559 * *Amg_preconditioner,
4562 * SolverControl solver_control_refined(stokes_matrix.m(),
4563 * solver_tolerance);
4564 * SolverFGMRES<TrilinosWrappers::MPI::BlockVector> solver(
4565 * solver_control_refined,
4567 * SolverFGMRES<TrilinosWrappers::MPI::BlockVector>::AdditionalData(
4569 * solver.solve(stokes_matrix,
4570 * distributed_stokes_solution,
4575 * (solver_control.last_step() + solver_control_refined.last_step());
4579 * stokes_constraints.distribute(distributed_stokes_solution);
4581 * distributed_stokes_solution.block(1) *= EquationData::pressure_scaling;
4583 * stokes_solution = distributed_stokes_solution;
4584 * pcout << n_iterations << " iterations." << std::endl;
4590 * Now let's turn to the
temperature part: First, we compute the time step
4591 *
size. We found that we need smaller time steps
for 3
d than
for 2
d for
4592 * the shell geometry. This is because the cells are more distorted in
4593 * that
case (it is the smallest edge length that determines the CFL
4594 * number). Instead of computing the time step from maximum velocity and
4595 * minimal mesh
size as in @ref step_31
"step-31", we compute local CFL
numbers, i.e., on
4596 * each cell we compute the maximum velocity times the mesh
size, and
4597 * compute the maximum of them. Hence, we need to choose the factor in
4598 * front of the time step slightly smaller. (We later re-considered
this
4599 * approach towards time stepping. If you
're curious about this, you may
4600 * want to read the time stepping section in @cite HDGB17 .)
4604 * After temperature right hand side assembly, we solve the linear
4605 * system for temperature (with fully distributed vectors without
4606 * ghost elements and using the solution from the last timestep as
4607 * our initial guess for the iterative solver), apply constraints,
4608 * and copy the vector back to one with ghosts.
4612 * In the end, we extract the temperature range similarly to @ref step_31 "step-31" to
4613 * produce some output (for example in order to help us choose the
4614 * stabilization constants, as discussed in the introduction). The only
4615 * difference is that we need to exchange maxima over all processors.
4619 * TimerOutput::Scope timer_section(computing_timer,
4620 * " Assemble temperature rhs");
4622 * old_time_step = time_step;
4624 * const double scaling = (dim == 3 ? 0.25 : 1.0);
4625 * time_step = (scaling / (2.1 * dim * std::sqrt(1. * dim)) /
4626 * (parameters.temperature_degree * get_cfl_number()));
4628 * const double maximal_velocity = get_maximal_velocity();
4629 * pcout << " Maximal velocity: "
4630 * << maximal_velocity * EquationData::year_in_seconds * 100
4631 * << " cm/year" << std::endl;
4633 * << "Time step: " << time_step / EquationData::year_in_seconds
4634 * << " years" << std::endl;
4636 * assemble_temperature_system(maximal_velocity);
4640 * TimerOutput::Scope timer_section(computing_timer,
4641 * " Solve temperature system");
4643 * SolverControl solver_control(temperature_matrix.m(),
4644 * 1e-12 * temperature_rhs.l2_norm());
4645 * SolverCG<TrilinosWrappers::MPI::Vector> cg(solver_control);
4647 * TrilinosWrappers::MPI::Vector distributed_temperature_solution(
4649 * distributed_temperature_solution = old_temperature_solution;
4651 * cg.solve(temperature_matrix,
4652 * distributed_temperature_solution,
4654 * *T_preconditioner);
4656 * temperature_constraints.distribute(distributed_temperature_solution);
4657 * temperature_solution = distributed_temperature_solution;
4659 * pcout << " " << solver_control.last_step()
4660 * << " CG iterations for temperature" << std::endl;
4662 * double temperature[2] = {std::numeric_limits<double>::max(),
4663 * std::numeric_limits<double>::lowest()};
4664 * double global_temperature[2];
4666 * for (unsigned int i =
4667 * distributed_temperature_solution.local_range().first;
4668 * i < distributed_temperature_solution.local_range().second;
4672 * std::min<double>(temperature[0],
4673 * distributed_temperature_solution(i));
4675 * std::max<double>(temperature[1],
4676 * distributed_temperature_solution(i));
4679 * temperature[0] *= -1.0;
4680 * Utilities::MPI::max(temperature, MPI_COMM_WORLD, global_temperature);
4681 * global_temperature[0] *= -1.0;
4683 * pcout << " Temperature range: " << global_temperature[0] << ' '
4684 * << global_temperature[1] << std::endl;
4692 * <a name="step_32-BoussinesqFlowProblemoutput_results"></a>
4693 * <h4>BoussinesqFlowProblem::output_results</h4>
4697 * Next comes the function that generates the output. The quantities to
4698 * output could be introduced manually like we did in @ref step_31 "step-31". An
4699 * alternative is to hand this task over to a class PostProcessor that
4700 * inherits from the class DataPostprocessor, which can be attached to
4701 * DataOut. This allows us to output derived quantities from the solution,
4702 * like the friction heating included in this example. It overloads the
4703 * virtual function DataPostprocessor::evaluate_vector_field(),
4704 * which is then internally called from DataOut::build_patches(). We have to
4705 * give it values of the numerical solution, its derivatives, normals to the
4706 * cell, the actual evaluation points and any additional quantities. This
4707 * follows the same procedure as discussed in @ref step_29 "step-29" and other programs.
4710 * template <int dim>
4711 * class BoussinesqFlowProblem<dim>::Postprocessor
4712 * : public DataPostprocessor<dim>
4715 * Postprocessor(const unsigned int partition, const double minimal_pressure);
4717 * virtual void evaluate_vector_field(
4718 * const DataPostprocessorInputs::Vector<dim> &inputs,
4719 * std::vector<Vector<double>> &computed_quantities) const override;
4721 * virtual std::vector<std::string> get_names() const override;
4723 * virtual std::vector<
4724 * DataComponentInterpretation::DataComponentInterpretation>
4725 * get_data_component_interpretation() const override;
4727 * virtual UpdateFlags get_needed_update_flags() const override;
4730 * const unsigned int partition;
4731 * const double minimal_pressure;
4735 * template <int dim>
4736 * BoussinesqFlowProblem<dim>::Postprocessor::Postprocessor(
4737 * const unsigned int partition,
4738 * const double minimal_pressure)
4739 * : partition(partition)
4740 * , minimal_pressure(minimal_pressure)
4746 * Here we define the names for the variables we want to output. These are
4747 * the actual solution values for velocity, pressure, and temperature, as
4748 * well as the friction heating and to each cell the number of the processor
4749 * that owns it. This allows us to visualize the partitioning of the domain
4750 * among the processors. Except for the velocity, which is vector-valued,
4751 * all other quantities are scalar.
4754 * template <int dim>
4755 * std::vector<std::string>
4756 * BoussinesqFlowProblem<dim>::Postprocessor::get_names() const
4758 * std::vector<std::string> solution_names(dim, "velocity");
4759 * solution_names.emplace_back("p");
4760 * solution_names.emplace_back("T");
4761 * solution_names.emplace_back("friction_heating");
4762 * solution_names.emplace_back("partition");
4764 * return solution_names;
4768 * template <int dim>
4769 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
4770 * BoussinesqFlowProblem<dim>::Postprocessor::get_data_component_interpretation()
4773 * std::vector<DataComponentInterpretation::DataComponentInterpretation>
4774 * interpretation(dim,
4775 * DataComponentInterpretation::component_is_part_of_vector);
4777 * interpretation.push_back(DataComponentInterpretation::component_is_scalar);
4778 * interpretation.push_back(DataComponentInterpretation::component_is_scalar);
4779 * interpretation.push_back(DataComponentInterpretation::component_is_scalar);
4780 * interpretation.push_back(DataComponentInterpretation::component_is_scalar);
4782 * return interpretation;
4786 * template <int dim>
4788 * BoussinesqFlowProblem<dim>::Postprocessor::get_needed_update_flags() const
4790 * return update_values | update_gradients | update_quadrature_points;
4796 * Now we implement the function that computes the derived quantities. As we
4797 * also did for the output, we rescale the velocity from its SI units to
4798 * something more readable, namely cm/year. Next, the pressure is scaled to
4799 * be between 0 and the maximum pressure. This makes it more easily
4800 * comparable -- in essence making all pressure variables positive or
4801 * zero. Temperature is taken as is, and the friction heating is computed as
4802 * @f$2 \eta \varepsilon(\mathbf{u}) \cdot \varepsilon(\mathbf{u})@f$.
4806 * The quantities we output here are more for illustration, rather than for
4807 * actual scientific value. We come back to this briefly in the results
4808 * section of this program and explain what one may in fact be interested in.
4811 * template <int dim>
4812 * void BoussinesqFlowProblem<dim>::Postprocessor::evaluate_vector_field(
4813 * const DataPostprocessorInputs::Vector<dim> &inputs,
4814 * std::vector<Vector<double>> &computed_quantities) const
4816 * const unsigned int n_evaluation_points = inputs.solution_values.size();
4817 * Assert(inputs.solution_gradients.size() == n_evaluation_points,
4818 * ExcInternalError());
4819 * Assert(computed_quantities.size() == n_evaluation_points,
4820 * ExcInternalError());
4821 * Assert(inputs.solution_values[0].size() == dim + 2, ExcInternalError());
4823 * for (unsigned int p = 0; p < n_evaluation_points; ++p)
4825 * for (unsigned int d = 0; d < dim; ++d)
4826 * computed_quantities[p](d) = (inputs.solution_values[p](d) *
4827 * EquationData::year_in_seconds * 100);
4829 * const double pressure =
4830 * (inputs.solution_values[p](dim) - minimal_pressure);
4831 * computed_quantities[p](dim) = pressure;
4833 * const double temperature = inputs.solution_values[p](dim + 1);
4834 * computed_quantities[p](dim + 1) = temperature;
4836 * Tensor<2, dim> grad_u;
4837 * for (unsigned int d = 0; d < dim; ++d)
4838 * grad_u[d] = inputs.solution_gradients[p][d];
4839 * const SymmetricTensor<2, dim> strain_rate = symmetrize(grad_u);
4840 * computed_quantities[p](dim + 2) =
4841 * 2 * EquationData::eta * strain_rate * strain_rate;
4843 * computed_quantities[p](dim + 3) = partition;
4850 * The <code>output_results()</code> function has a similar task to the one
4851 * in @ref step_31 "step-31". However, here we are going to demonstrate a different
4852 * technique on how to merge output from different DoFHandler objects. The
4853 * way we're going to achieve
this recombination is to create a joint
4854 *
DoFHandler that collects both components, the Stokes solution and the
4855 *
temperature solution. This can be nicely done by combining the finite
4856 * elements from the two systems to form
one FESystem, and let
this
4857 * collective system define a
new DoFHandler object. To be sure that
4858 * everything was done correctly, we perform a sanity
check that ensures
4859 * that we got all the dofs from both Stokes and
temperature even in the
4860 * combined system. We then combine the
data vectors. Unfortunately, there
4861 * is no straight-forward relation that tells us how to sort Stokes and
4862 *
temperature vector into the joint vector. The way we can get around
this
4863 * trouble is to rely on the information collected in the
FESystem. For each
4864 * dof on a cell, the joint finite element knows to which equation
component
4866 * information we need! So we step through all cells (with iterators into
4867 * all three DoFHandlers moving in sync), and for each joint cell dof, we
4868 * read out that component using the FiniteElement::system_to_base_index
4869 * function (see there for a description of what the various parts of its
4870 * return value contain). We also need to keep track whether we're on a
4871 * Stokes dof or a temperature dof, which is contained in
4872 * joint_fe.system_to_base_index(i).first.first. Eventually, the dof_indices
4873 *
data structures on either of the three systems tell us how the relation
4874 * between global vector and local dofs looks like on the present cell,
4875 * which concludes
this tedious work. We make sure that each processor only
4876 * works on the subdomain it owns locally (and not on ghost or artificial
4877 * cells) when building the joint solution vector. The same will then have
4883 * What we
end up with is a
set of patches that we can write
using the
4885 * have to pay attention that what each processor writes is really only its
4886 * own part of the domain, i.e. we will want to write each processor
's
4887 * contribution into a separate file. This we do by adding an additional
4888 * number to the filename when we write the solution. This is not really
4889 * new, we did it similarly in @ref step_40 "step-40". Note that we write in the compressed
4890 * format @p .vtu instead of plain vtk files, which saves quite some
4895 * All the rest of the work is done in the PostProcessor class.
4898 * template <int dim>
4899 * void BoussinesqFlowProblem<dim>::output_results()
4901 * TimerOutput::Scope timer_section(computing_timer, "Postprocessing");
4903 * const FESystem<dim> joint_fe(stokes_fe, 1, temperature_fe, 1);
4905 * DoFHandler<dim> joint_dof_handler(triangulation);
4906 * joint_dof_handler.distribute_dofs(joint_fe);
4907 * Assert(joint_dof_handler.n_dofs() ==
4908 * stokes_dof_handler.n_dofs() + temperature_dof_handler.n_dofs(),
4909 * ExcInternalError());
4911 * TrilinosWrappers::MPI::Vector joint_solution;
4912 * joint_solution.reinit(joint_dof_handler.locally_owned_dofs(),
4916 * std::vector<types::global_dof_index> local_joint_dof_indices(
4917 * joint_fe.n_dofs_per_cell());
4918 * std::vector<types::global_dof_index> local_stokes_dof_indices(
4919 * stokes_fe.n_dofs_per_cell());
4920 * std::vector<types::global_dof_index> local_temperature_dof_indices(
4921 * temperature_fe.n_dofs_per_cell());
4923 * typename DoFHandler<dim>::active_cell_iterator
4924 * joint_cell = joint_dof_handler.begin_active(),
4925 * joint_endc = joint_dof_handler.end(),
4926 * stokes_cell = stokes_dof_handler.begin_active(),
4927 * temperature_cell = temperature_dof_handler.begin_active();
4928 * for (; joint_cell != joint_endc;
4929 * ++joint_cell, ++stokes_cell, ++temperature_cell)
4930 * if (joint_cell->is_locally_owned())
4932 * joint_cell->get_dof_indices(local_joint_dof_indices);
4933 * stokes_cell->get_dof_indices(local_stokes_dof_indices);
4934 * temperature_cell->get_dof_indices(local_temperature_dof_indices);
4936 * for (unsigned int i = 0; i < joint_fe.n_dofs_per_cell(); ++i)
4937 * if (joint_fe.system_to_base_index(i).first.first == 0)
4939 * Assert(joint_fe.system_to_base_index(i).second <
4940 * local_stokes_dof_indices.size(),
4941 * ExcInternalError());
4943 * joint_solution(local_joint_dof_indices[i]) = stokes_solution(
4944 * local_stokes_dof_indices[joint_fe.system_to_base_index(i)
4949 * Assert(joint_fe.system_to_base_index(i).first.first == 1,
4950 * ExcInternalError());
4951 * Assert(joint_fe.system_to_base_index(i).second <
4952 * local_temperature_dof_indices.size(),
4953 * ExcInternalError());
4954 * joint_solution(local_joint_dof_indices[i]) =
4955 * temperature_solution(
4956 * local_temperature_dof_indices
4957 * [joint_fe.system_to_base_index(i).second]);
4962 * joint_solution.compress(VectorOperation::insert);
4964 * const IndexSet locally_relevant_joint_dofs =
4965 * DoFTools::extract_locally_relevant_dofs(joint_dof_handler);
4966 * TrilinosWrappers::MPI::Vector locally_relevant_joint_solution;
4967 * locally_relevant_joint_solution.reinit(locally_relevant_joint_dofs,
4969 * locally_relevant_joint_solution = joint_solution;
4971 * Postprocessor postprocessor(Utilities::MPI::this_mpi_process(
4973 * stokes_solution.block(1).min());
4975 * DataOut<dim> data_out;
4976 * data_out.attach_dof_handler(joint_dof_handler);
4977 * data_out.add_data_vector(locally_relevant_joint_solution, postprocessor);
4978 * data_out.build_patches();
4980 * static int out_index = 0;
4981 * data_out.write_vtu_with_pvtu_record(
4982 * "./", "solution", out_index, MPI_COMM_WORLD, 5);
4992 * <a name="step_32-BoussinesqFlowProblemrefine_mesh"></a>
4993 * <h4>BoussinesqFlowProblem::refine_mesh</h4>
4997 * This function isn't really
new either. Since the <code>setup_dofs</code>
4998 * function that we call in the middle has its own timer section, we
split
4999 * timing
this function into two sections. It will also allow us to easily
5000 * identify which of the two is more expensive.
5004 * One thing of note, however, is that we only want to compute error
5005 * indicators on the locally owned subdomain. In order to achieve
this, we
5007 * function. Note that the vector
for error estimates is resized to the
5008 * number of active cells present on the current process, which is less than
5009 * the total number of active cells on all processors (but more than the
5010 * number of locally owned active cells); each processor only has a few
5011 * coarse cells around the locally owned ones, as also explained in @ref step_40
"step-40".
5015 * The local error estimates are then handed to a %
parallel version of
5017 * also @ref step_40
"step-40") which looks at the errors and finds the cells that need
5018 * refinement by comparing the error
values across processors. As in
5019 * @ref step_31
"step-31", we want to limit the maximum grid
level. So in
case some cells
5020 * have been marked that are already at the finest
level, we simply clear
5024 *
template <
int dim>
5026 *
BoussinesqFlowProblem<dim>::refine_mesh(
const unsigned int max_grid_level)
5029 *
temperature_dof_handler);
5031 *
stokes_dof_handler);
5035 *
"Refine mesh structure, part 1");
5037 *
Vector<float> estimated_error_per_cell(triangulation.n_active_cells());
5040 *
temperature_dof_handler,
5043 *
temperature_solution,
5044 *
estimated_error_per_cell,
5048 *
triangulation.locally_owned_subdomain());
5051 *
triangulation, estimated_error_per_cell, 0.3, 0.1);
5053 *
if (triangulation.n_levels() > max_grid_level)
5055 *
triangulation.begin_active(max_grid_level);
5056 *
cell != triangulation.end();
5058 *
cell->clear_refine_flag();
5062 * With all flags marked as necessary, we can then tell the
5064 * the next, which they will
do when notified by
5065 *
Triangulation as part of the @p execute_coarsening_and_refinement() call.
5066 * The syntax is similar to the non-%
parallel solution transfer (with the
5067 * exception that here a
pointer to the vector entries is enough). The
5068 * remainder of the function further down below is then concerned with
5069 * setting up the
data structures again after mesh refinement and
5070 * restoring the solution vectors on the new mesh.
5074 *
&temperature_solution, &old_temperature_solution};
5075 *
const std::vector<const TrilinosWrappers::MPI::BlockVector *> x_stokes = {
5076 *
&stokes_solution, &old_stokes_solution};
5078 *
triangulation.prepare_coarsening_and_refinement();
5080 *
temperature_trans.prepare_for_coarsening_and_refinement(x_temperature);
5081 *
stokes_trans.prepare_for_coarsening_and_refinement(x_stokes);
5083 *
triangulation.execute_coarsening_and_refinement();
5090 *
"Refine mesh structure, part 2");
5096 *
std::vector<TrilinosWrappers::MPI::Vector *> tmp = {&distributed_temp1,
5097 *
&distributed_temp2};
5098 *
temperature_trans.interpolate(tmp);
5102 * enforce constraints to make the interpolated solution conforming on
5106 *
temperature_constraints.distribute(distributed_temp1);
5107 *
temperature_constraints.distribute(distributed_temp2);
5109 *
temperature_solution = distributed_temp1;
5110 *
old_temperature_solution = distributed_temp2;
5117 *
std::vector<TrilinosWrappers::MPI::BlockVector *> stokes_tmp = {
5118 *
&distributed_stokes, &old_distributed_stokes};
5120 *
stokes_trans.interpolate(stokes_tmp);
5124 * enforce constraints to make the interpolated solution conforming on
5128 *
stokes_constraints.distribute(distributed_stokes);
5129 *
stokes_constraints.distribute(old_distributed_stokes);
5131 *
stokes_solution = distributed_stokes;
5132 *
old_stokes_solution = old_distributed_stokes;
5142 * <a name=
"step_32-BoussinesqFlowProblemrun"></a>
5143 * <h4>BoussinesqFlowProblem::run</h4>
5147 * This is the
final and controlling function in
this class. It, in fact,
5148 * runs the entire rest of the program and is, once more, very similar to
5149 * @ref step_31
"step-31". The only substantial difference is that we use a different mesh
5153 *
template <
int dim>
5154 *
void BoussinesqFlowProblem<dim>::run()
5160 *
(dim == 3) ? 96 : 12,
5165 *
triangulation.refine_global(parameters.initial_global_refinement);
5169 *
unsigned int pre_refinement_step = 0;
5171 *
start_time_iteration:
5175 *
temperature_dof_handler.locally_owned_dofs());
5179 * standard finite elements via deal.II
's own native MatrixFree framework:
5180 * since we use standard Lagrange elements of moderate order this function
5184 * VectorTools::project(temperature_dof_handler,
5185 * temperature_constraints,
5186 * QGauss<dim>(parameters.temperature_degree + 2),
5187 * EquationData::TemperatureInitialValues<dim>(),
5191 * Having so computed the current temperature field, let us set the member
5192 * variable that holds the temperature nodes. Strictly speaking, we really
5193 * only need to set <code>old_temperature_solution</code> since the first
5194 * thing we will do is to compute the Stokes solution that only requires
5196 * come from not initializing the other vectors as well (especially since
5197 * it
's a relatively cheap operation and we only have to do it once at the
5198 * beginning of the program) if we ever want to extend our numerical
5199 * method or physical model, and so we initialize
5200 * <code>old_temperature_solution</code> and
5201 * <code>old_old_temperature_solution</code> as well. The assignment makes
5202 * sure that the vectors on the left hand side (which where initialized to
5203 * contain ghost elements as well) also get the correct ghost elements. In
5204 * other words, the assignment here requires communication between
5208 * temperature_solution = solution;
5209 * old_temperature_solution = solution;
5210 * old_old_temperature_solution = solution;
5213 * timestep_number = 0;
5214 * time_step = old_time_step = 0;
5220 * pcout << "Timestep " << timestep_number
5221 * << ": t=" << time / EquationData::year_in_seconds << " years"
5224 * assemble_stokes_system();
5225 * build_stokes_preconditioner();
5226 * assemble_temperature_matrix();
5230 * pcout << std::endl;
5232 * if ((timestep_number == 0) &&
5233 * (pre_refinement_step < parameters.initial_adaptive_refinement))
5235 * refine_mesh(parameters.initial_global_refinement +
5236 * parameters.initial_adaptive_refinement);
5237 * ++pre_refinement_step;
5238 * goto start_time_iteration;
5240 * else if ((timestep_number > 0) &&
5241 * (timestep_number % parameters.adaptive_refinement_interval ==
5243 * refine_mesh(parameters.initial_global_refinement +
5244 * parameters.initial_adaptive_refinement);
5246 * if ((parameters.generate_graphical_output == true) &&
5247 * (timestep_number % parameters.graphical_output_interval == 0))
5252 * In order to speed up linear solvers, we extrapolate the solutions
5253 * from the old time levels to the new one. This gives a very good
5254 * initial guess, cutting the number of iterations needed in solvers
5255 * by more than one half. We do not need to extrapolate in the last
5256 * iteration, so if we reached the final time, we stop here.
5260 * As the last thing during a time step (before actually bumping up
5261 * the number of the time step), we check whether the current time
5262 * step number is divisible by 100, and if so we let the computing
5263 * timer print a summary of CPU times spent so far.
5266 * if (time > parameters.end_time * EquationData::year_in_seconds)
5269 * TrilinosWrappers::MPI::BlockVector old_old_stokes_solution;
5270 * old_old_stokes_solution = old_stokes_solution;
5271 * old_stokes_solution = stokes_solution;
5272 * old_old_temperature_solution = old_temperature_solution;
5273 * old_temperature_solution = temperature_solution;
5274 * if (old_time_step > 0)
5278 * Trilinos sadd does not like ghost vectors even as input. Copy
5279 * into distributed vectors for now:
5283 * TrilinosWrappers::MPI::BlockVector distr_solution(stokes_rhs);
5284 * distr_solution = stokes_solution;
5285 * TrilinosWrappers::MPI::BlockVector distr_old_solution(stokes_rhs);
5286 * distr_old_solution = old_old_stokes_solution;
5287 * distr_solution.sadd(1. + time_step / old_time_step,
5288 * -time_step / old_time_step,
5289 * distr_old_solution);
5290 * stokes_solution = distr_solution;
5293 * TrilinosWrappers::MPI::Vector distr_solution(temperature_rhs);
5294 * distr_solution = temperature_solution;
5295 * TrilinosWrappers::MPI::Vector distr_old_solution(temperature_rhs);
5296 * distr_old_solution = old_old_temperature_solution;
5297 * distr_solution.sadd(1. + time_step / old_time_step,
5298 * -time_step / old_time_step,
5299 * distr_old_solution);
5300 * temperature_solution = distr_solution;
5304 * if ((timestep_number > 0) && (timestep_number % 100 == 0))
5305 * computing_timer.print_summary();
5307 * time += time_step;
5308 * ++timestep_number;
5314 * If we are generating graphical output, do so also for the last time
5315 * step unless we had just done so before we left the do-while loop
5318 * if ((parameters.generate_graphical_output == true) &&
5319 * !((timestep_number - 1) % parameters.graphical_output_interval == 0))
5322 * } // namespace Step32
5329 * <a name="step_32-Thecodemaincodefunction"></a>
5330 * <h3>The <code>main</code> function</h3>
5334 * The main function is short as usual and very similar to the one in
5335 * @ref step_31 "step-31". Since we use a parameter file which is specified as an argument in
5336 * the command line, we have to read it in here and pass it on to the
5337 * Parameters class for parsing. If no filename is given in the command line,
5338 * we simply use the <code>step-32.prm</code> file which is distributed
5339 * together with the program.
5343 * Because 3d computations are simply very slow unless you throw a lot of
5344 * processors at them, the program defaults to 2d. You can get the 3d version
5345 * by changing the constant dimension below to 3.
5348 * int main(int argc, char *argv[])
5352 * using namespace Step32;
5353 * using namespace dealii;
5355 * Utilities::MPI::MPI_InitFinalize mpi_initialization(
5356 * argc, argv, numbers::invalid_unsigned_int);
5358 * std::string parameter_filename;
5360 * parameter_filename = argv[1];
5362 * parameter_filename = "step-32.prm";
5364 * const int dim = 2;
5365 * BoussinesqFlowProblem<dim>::Parameters parameters(parameter_filename);
5366 * BoussinesqFlowProblem<dim> flow_problem(parameters);
5367 * flow_problem.run();
5369 * catch (std::exception &exc)
5371 * std::cerr << std::endl
5373 * << "----------------------------------------------------"
5375 * std::cerr << "Exception on processing: " << std::endl
5376 * << exc.what() << std::endl
5377 * << "Aborting!" << std::endl
5378 * << "----------------------------------------------------"
5385 * std::cerr << std::endl
5387 * << "----------------------------------------------------"
5389 * std::cerr << "Unknown exception!" << std::endl
5390 * << "Aborting!" << std::endl
5391 * << "----------------------------------------------------"
5399@anchor step_32-ResultsSection
5400<a name="step_32-Results"></a><h1>Results</h1>
5403When run, the program simulates convection in 3d in much the same way
5404as @ref step_31 "step-31" did, though with an entirely different testcase.
5407<a name="step_32-Comparisonofresultswithstep31"></a><h3>Comparison of results with step-31</h3>
5410Before we go to this testcase, however, let us show a few results from a
5411slightly earlier version of this program that was solving exactly the
5412testcase we used in @ref step_31 "step-31", just that we now solve it in parallel and with
5413much higher resolution. We show these results mainly for comparison.
5415Here are two images that show this higher resolution if we choose a 3d
5416computation in <code>main()</code> and if we set
5417<code>initial_refinement=3</code> and
5418<code>n_pre_refinement_steps=4</code>. At the time steps shown, the
5419meshes had around 72,000 and 236,000 cells, for a total of 2,680,000
5420and 8,250,000 degrees of freedom, respectively, more than an order of
5421magnitude more than we had available in @ref step_31 "step-31":
5423<table align="center" class="doxtable">
5426 <img src="https://dealii.org/images/steps/developer/step-32.3d.cube.0.png" alt="">
5431 <img src="https://dealii.org/images/steps/developer/step-32.3d.cube.1.png" alt="">
5436The computation was done on a subset of 50 processors of the Brazos
5437cluster at Texas A&M University.
5440<a name="step_32-Resultsfora2dcircularshelltestcase"></a><h3>Results for a 2d circular shell testcase</h3>
5443Next, we will run @ref step_32 "step-32" with the parameter file in the directory with one
5444change: we increase the final time to 1e9. Here we are using 16 processors. The
5445command to launch is (note that @ref step_32 "step-32".prm is the default):
5449\$ mpirun -np 16 ./step-32
5453Note that running a job on a cluster typically requires going through a job
5454scheduler, which we won't discuss here. The output will look roughly like
5459\$ mpirun -np 16 ./step-32
5460Number of active cells: 12,288 (on 6 levels)
5461Number of degrees of freedom: 186,624 (99,840+36,864+49,920)
5463Timestep 0: t=0 years
5465 Rebuilding Stokes preconditioner...
5466 Solving Stokes system... 41 iterations.
5467 Maximal velocity: 60.4935 cm/year
5468 Time step: 18166.9 years
5470 Temperature range: 973 4273.16
5472Number of active cells: 15,921 (on 7 levels)
5473Number of degrees of freedom: 252,723 (136,640+47,763+68,320)
5475Timestep 0: t=0 years
5477 Rebuilding Stokes preconditioner...
5478 Solving Stokes system... 50 iterations.
5479 Maximal velocity: 60.3223 cm/year
5480 Time step: 10557.6 years
5482 Temperature range: 973 4273.16
5484Number of active cells: 19,926 (on 8 levels)
5485Number of degrees of freedom: 321,246 (174,312+59,778+87,156)
5487Timestep 0: t=0 years
5489 Rebuilding Stokes preconditioner...
5490 Solving Stokes system... 50 iterations.
5491 Maximal velocity: 57.8396 cm/year
5492 Time step: 5453.78 years
5494 Temperature range: 973 4273.16
5496Timestep 1: t=5453.78 years
5498 Solving Stokes system... 49 iterations.
5499 Maximal velocity: 59.0231 cm/year
5500 Time step: 5345.86 years
5502 Temperature range: 973 4273.16
5504Timestep 2: t=10799.6 years
5506 Solving Stokes system... 24 iterations.
5507 Maximal velocity: 60.2139 cm/year
5508 Time step: 5241.51 years
5510 Temperature range: 973 4273.16
5514Timestep 100: t=272151 years
5516 Solving Stokes system... 21 iterations.
5517 Maximal velocity: 161.546 cm/year
5518 Time step: 1672.96 years
5520 Temperature range: 973 4282.57
5522Number of active cells: 56,085 (on 8 levels)
5523Number of degrees of freedom: 903,408 (490,102+168,255+245,051)
5527+---------------------------------------------+------------+------------+
5528| Total wallclock time elapsed since start | 115s | |
5530| Section | no. calls | wall time | % of total |
5531+---------------------------------+-----------+------------+------------+
5532| Assemble Stokes system | 103 | 2.82s | 2.5% |
5533| Assemble
temperature matrices | 12 | 0.452s | 0.39% |
5535| Build Stokes preconditioner | 12 | 2.09s | 1.8% |
5536| Solve Stokes system | 103 | 90.4s | 79% |
5538| Postprocessing | 3 | 0.532s | 0.46% |
5539| Refine mesh structure, part 1 | 12 | 0.93s | 0.81% |
5540| Refine mesh structure, part 2 | 12 | 0.384s | 0.33% |
5541| Setup dof systems | 13 | 2.96s | 2.6% |
5542+---------------------------------+-----------+------------+------------+
5546+---------------------------------------------+------------+------------+
5547| Total wallclock time elapsed since start | 9.14e+04s | |
5549| Section | no. calls | wall time | % of total |
5550+---------------------------------+-----------+------------+------------+
5551| Assemble Stokes system | 47045 | 2.05e+03s | 2.2% |
5552| Assemble
temperature matrices | 4707 | 310s | 0.34% |
5553| Assemble
temperature rhs | 47045 | 8.7e+03s | 9.5% |
5554| Build Stokes preconditioner | 4707 | 1.48e+03s | 1.6% |
5555| Solve Stokes system | 47045 | 7.34e+04s | 80% |
5556| Solve
temperature system | 47045 | 1.46e+03s | 1.6% |
5557| Postprocessing | 1883 | 222s | 0.24% |
5558| Refine mesh structure, part 1 | 4706 | 641s | 0.7% |
5559| Refine mesh structure, part 2 | 4706 | 259s | 0.28% |
5560| Setup dof systems | 4707 | 1.86e+03s | 2% |
5561+---------------------------------+-----------+------------+------------+
5565The simulation terminates when the time reaches the 1 billion years
5566selected in the input file. You can
extrapolate from this how long a
5567simulation would take
for a different final time (the time step
size
5568ultimately settles on somewhere around 20,000 years, so computing
for
5569two billion years will take 100,000 time steps, give or take 20%). As
5570can be seen here, we spend most of the compute time in assembling
5571linear systems and — above all — in solving Stokes
5575To demonstrate the output we show the output from every 1250th time step here:
5579 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-000.png" alt=
"">
5582 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-050.png" alt=
"">
5585 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-100.png" alt=
"">
5590 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-150.png" alt=
"">
5593 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-200.png" alt=
"">
5596 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-250.png" alt=
"">
5601 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-300.png" alt=
"">
5604 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-350.png" alt=
"">
5607 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-400.png" alt=
"">
5612 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-450.png" alt=
"">
5615 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-500.png" alt=
"">
5618 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-550.png" alt=
"">
5623 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-time-600.png" alt=
"">
5626 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-cells.png" alt=
"">
5629 <img src=
"https://dealii.org/images/steps/developer/step-32-2d-partition.png" alt=
"">
5634The last two images show the grid as well as the partitioning of the mesh
for
5635the same computation with 16 subdomains and 16 processors. The full dynamics of
5636this simulation are really only visible by looking at an animation,
for example
5638href=
"https://dealii.org/images/steps/developer/step-32-2d-temperature.webm">shown
5639on
this site</a>. This image is well worth watching due to its artistic quality
5640and entrancing depiction of the evolution of the magma plumes.
5642If you watch the movie, you
'll see that the convection pattern goes
5643through several stages: First, it gets rid of the instable temperature
5644layering with the hot material overlain by the dense cold
5645material. After this great driver is removed and we have a sort of
5646stable situation, a few blobs start to separate from the hot boundary
5647layer at the inner ring and rise up, with a few cold fingers also
5648dropping down from the outer boundary layer. During this phase, the solution
5649remains mostly symmetric, reflecting the 12-fold symmetry of the
5650original mesh. In a final phase, the fluid enters vigorous chaotic
5651stirring in which all symmetries are lost. This is a pattern that then
5652continues to dominate flow.
5654These different phases can also be identified if we look at the
5655maximal velocity as a function of time in the simulation:
5657<img src="https://dealii.org/images/steps/developer/step-32.2d.t_vs_vmax.png" alt="">
5659Here, the velocity (shown in centimeters per year) becomes very large,
5660to the order of several meters per year) at the beginning when the
5661temperature layering is instable. It then calms down to relatively
5662small values before picking up again in the chaotic stirring
5663regime. There, it remains in the range of 10-40 centimeters per year,
5664quite within the physically expected region.
5667<a name="step_32-Resultsfora3dsphericalshelltestcase"></a><h3>Results for a 3d spherical shell testcase</h3>
56703d computations are very expensive computationally. Furthermore, as
5671seen above, interesting behavior only starts after quite a long time
5672requiring more CPU hours than is available on a typical
5673cluster. Consequently, rather than showing a complete simulation here,
5674let us simply show a couple of pictures we have obtained using the
5675successor to this program, called <i>ASPECT</i> (short for <i>Advanced
5676%Solver for Problems in Earth's ConvecTion</i>), that is being
5677developed independently of deal.II and that already incorporates some
5678of the extensions discussed below. The following two pictures show
5679isocontours of the temperature and the partition of the domain (along
5680with the mesh) onto 512 processors:
5683<img src=
"https://dealii.org/images/steps/developer/step-32.3d-sphere.solution.png" alt=
"">
5685<img src=
"https://dealii.org/images/steps/developer/step-32.3d-sphere.partition.png" alt=
"">
5689<a name=
"step-32-extensions"></a>
5690<a name=
"step_32-Possibilitiesforextensions"></a><h3>Possibilities
for extensions</h3>
5693There are many directions in which
this program could be extended. As
5694mentioned at the
end of the introduction, most of these are under active
5695development in the <i>ASPECT</i> (
short for <i>Advanced %Solver
for Problems
5696in Earth
's ConvecTion</i>) code at the time this tutorial program is being
5697finished. Specifically, the following are certainly topics that one should
5698address to make the program more useful:
5701 <li> <b>Adiabatic heating/cooling:</b>
5702 The temperature field we get in our simulations after a while
5703 is mostly constant with boundary layers at the inner and outer
5704 boundary, and streamers of cold and hot material mixing
5705 everything. Yet, this doesn't match our expectation that things
5706 closer to the earth core should be hotter than closer to the
5707 surface. The reason is that the energy equation we have used does
5708 not include a term that describes adiabatic cooling and heating:
5709 rock, like gas, heats up as you
compress it. Consequently, material
5710 that rises up cools adiabatically, and cold material that sinks down
5711 heats adiabatically. The correct
temperature equation would
5712 therefore look somewhat like
this:
5716 \nabla \cdot \kappa \nabla
T &=& \
gamma + \tau\frac{Dp}{Dt},
5718 or, expanding the advected derivative @f$\frac{D}{Dt} =
5719 \frac{\partial}{\partial t} + \mathbf u \cdot \nabla@f$:
5721 \frac{\partial
T}{\partial t}
5723 {\mathbf u} \cdot \nabla
T
5725 \nabla \cdot \kappa \nabla
T &=& \
gamma +
5726 \tau\left\{\frac{\partial
5727 p}{\partial t} + \mathbf u \cdot \nabla p \right\}.
5729 In other words, as pressure increases in a rock
volume
5730 (@f$\frac{Dp}{Dt}>0@f$) we get an additional heat source, and vice
5733 The time derivative of the pressure is a bit awkward to
5734 implement. If necessary, one could approximate
using the fact
5735 outlined in the introduction that the pressure can be decomposed
5736 into a dynamic component due to temperature differences and the
5737 resulting flow, and a
static component that results solely from the
5738 static pressure of the overlying rock. Since the latter is much
5739 bigger, one may approximate @f$p\approx p_{\text{
static}}=-\rho_{\text{ref}}
5740 [1+\beta T_{\text{ref}}] \varphi@f$, and consequently
5741 @f$\frac{Dp}{Dt} \approx \left\{- \mathbf u \cdot \nabla \rho_{\text{ref}}
5742 [1+\beta T_{\text{ref}}]\varphi\right\} = \rho_{\text{ref}}
5743 [1+\beta T_{\text{ref}}] \mathbf u \cdot \mathbf g@f$.
5744 In other words,
if the fluid is moving in the direction of gravity
5745 (downward) it will be compressed and because in that
case @f$\mathbf u
5746 \cdot \mathbf g > 0@f$ we get a
positive heat source. Conversely, the
5747 fluid will cool down
if it moves against the direction of gravity.
5749<li> <
b>Compressibility:</
b>
5750 As already hinted at in the
temperature model above,
5751 mantle rocks are not incompressible. Rather, given the enormous pressures in
5752 the earth mantle (at the core-mantle boundary, the pressure is approximately
5753 140 GPa, equivalent to 1,400,000 times atmospheric pressure), rock actually
5754 does
compress to something around 1.5 times the density it would have
5755 at surface pressure. Modeling
this presents any number of
5756 difficulties. Primarily, the mass conservation equation is no longer
5757 @f$\textrm{div}\;\mathbf u=0@f$ but should read
5758 @f$\textrm{div}(\rho\mathbf u)=0@f$ where the density @f$\rho@f$ is now no longer
5759 spatially constant but depends on temperature and pressure. A consequence is
5760 that the model is now no longer linear; a linearized version of the Stokes
5761 equation is also no longer
symmetric requiring us to rethink preconditioners
5762 and, possibly, even the discretization. We won
't go into detail here as to
5763 how this can be resolved.
5765<li> <b>Nonlinear material models:</b> As already hinted at in various places,
5766 material parameters such as the density, the viscosity, and the various
5767 thermal parameters are not constant throughout the earth mantle. Rather,
5768 they nonlinearly depend on the pressure and temperature, and in the case of
5769 the viscosity on the strain rate @f$\varepsilon(\mathbf u)@f$. For complicated
5770 models, the only way to solve such models accurately may be to actually
5771 iterate this dependence out in each time step, rather than simply freezing
5772 coefficients at values extrapolated from the previous time step(s).
5774<li> <b>Checkpoint/restart:</b> Running this program in 2d on a number of
5775 processors allows solving realistic models in a day or two. However, in 3d,
5776 compute times are so large that one runs into two typical problems: (i) On
5777 most compute clusters, the queuing system limits run times for individual
5778 jobs are to 2 or 3 days; (ii) losing the results of a computation due to
5779 hardware failures, misconfigurations, or power outages is a shame when
5780 running on hundreds of processors for a couple of days. Both of these
5781 problems can be addressed by periodically saving the state of the program
5782 and, if necessary, restarting the program at this point. This technique is
5783 commonly called <i>checkpoint/restart</i> and it requires that the entire
5784 state of the program is written to a permanent storage location (e.g. a hard
5785 drive). Given the complexity of the data structures of this program, this is
5786 not entirely trivial (it may also involve writing gigabytes or more of
5787 data), but it can be made easier by realizing that one can save the state
5788 between two time steps where it essentially only consists of the mesh and
5789 solution vectors; during restart one would then first re-enumerate degrees
5790 of freedom in the same way as done before and then re-assemble
5791 matrices. Nevertheless, given the distributed nature of the data structures
5792 involved here, saving and restoring the state of a program is not
5793 trivial. An additional complexity is introduced by the fact that one may
5794 want to change the number of processors between runs, for example because
5795 one may wish to continue computing on a mesh that is finer than the one used
5796 to precompute a starting temperature field at an intermediate time.
5798<li> <b>Predictive postprocessing:</b> The point of computations like this is
5799 not simply to solve the equations. Rather, it is typically the exploration
5800 of different physical models and their comparison with things that we can
5801 measure at the earth surface, in order to find which models are realistic
5802 and which are contradicted by reality. To this end, we need to compute
5803 quantities from our solution vectors that are related to what we can
5804 observe. Among these are, for example, heatfluxes at the surface of the
5805 earth, as well as seismic velocities throughout the mantle as these affect
5806 earthquake waves that are recorded by seismographs.
5808<li> <b>Better refinement criteria:</b> As can be seen above for the
58093d case, the mesh in 3d is primarily refined along the inner
5810boundary. This is because the boundary layer there is stronger than
5811any other transition in the domain, leading us to refine there almost
5812exclusively and basically not at all following the plumes. One
5813certainly needs better refinement criteria to track the parts of the
5814solution we are really interested in better than the criterion used
5815here, namely the KellyErrorEstimator applied to the temperature, is
5820There are many other ways to extend the current program. However, rather than
5821discussing them here, let us point to the much larger open
5822source code ASPECT (see https://aspect.geodynamics.org/ ) that constitutes the
5823further development of @ref step_32 "step-32" and that already includes many such possible
5827<a name="step_32-PlainProg"></a>
5828<h1> The plain program</h1>
5829@include "step-32.cc"
* * for(const auto &cell :triangulation.active_cell_iterators())
* * int main(int argc, char **argv)
* x_component_mask set(0, true)
* * * struct InterferenceTaperTransform *
virtual void build_patches(const unsigned int n_subdivisions=0)
void reinit(const Triangulation< dim, spacedim > &tria)
active_cell_iterator begin_active(const unsigned int level=0) const
virtual RangeNumberType value(const Point< dim > &p, const unsigned int component=0) const
virtual void vector_value(const Point< dim > &p, Vector< RangeNumberType > &values) const
static void estimate(const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof, const Quadrature< dim - 1 > &quadrature, const std::map< types::boundary_id, const Function< spacedim, Number > * > &neumann_bc, const ReadVector< Number > &solution, Vector< float > &error, const ComponentMask &component_mask={}, const Function< spacedim > *coefficients=nullptr, const unsigned int n_threads=numbers::invalid_unsigned_int, const types::subdomain_id subdomain_id=numbers::invalid_subdomain_id, const types::material_id material_id=numbers::invalid_material_id, const Strategy strategy=cell_diameter_over_24)
numbers::NumberTraits< Number >::real_type norm() const
#define Assert(cond, exc)
#define AssertThrow(cond, exc)
typename ActiveSelector::active_cell_iterator active_cell_iterator
void loop(IteratorType begin, std_cxx20::type_identity_t< IteratorType > end, DOFINFO &dinfo, INFOBOX &info, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &)> &cell_worker, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &)> &boundary_worker, const std::function< void(std_cxx20::type_identity_t< DOFINFO > &, std_cxx20::type_identity_t< DOFINFO > &, typename INFOBOX::CellInfo &, typename INFOBOX::CellInfo &)> &face_worker, AssemblerType &assembler, const LoopControl &lctrl=LoopControl())
@ update_values
Shape function values.
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.
@ update_quadrature_points
Transformed quadrature points.
std::vector< index_type > data
std::vector< value_type > split(const typename ::Triangulation< dim, spacedim >::cell_iterator &parent, const value_type parent_value)
void approximate(const SynchronousIterators< std::tuple< typename DoFHandler< dim, spacedim >::active_cell_iterator, Vector< float >::iterator > > &cell, const Mapping< dim, spacedim > &mapping, const DoFHandler< dim, spacedim > &dof_handler, const InputVector &solution, const unsigned int component)
Expression sign(const Expression &x)
void hyper_shell(Triangulation< dim, spacedim > &tria, const Point< spacedim > ¢er, const double inner_radius, const double outer_radius, const unsigned int n_cells=0, 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)
@ valid
Iterator points to a valid object.
@ matrix
Contents is actually a matrix.
@ symmetric
Matrix is symmetric.
constexpr types::blas_int zero
constexpr types::blas_int one
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 > b(const Tensor< 2, dim, Number > &F)
SymmetricTensor< 2, dim, Number > d(const Tensor< 2, dim, Number > &F, const Tensor< 2, dim, Number > &dF_dt)
* * * ScaleZFunction< dim, Number, components >::ScaleZFunction * component(component)
* * if(update_pressure &update_flags) * compute_pressure(constitutive_request
* * * RotationFunction< dim, Number >::RotationFunction Number(dim)
* * * ThermoPlasticMaterial< dim, ViscoplasticYieldLaw, Number > ThermoPlasticMaterial * * * * * reference_temperature(293.15)
* const Number temperature
* * * * std::vector< Number > ThermoPlasticMaterial< dim, ViscoplasticYieldLaw, Number >::get_state_parameters const
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)
T max(const T &t, const MPI_Comm mpi_communicator)
unsigned int this_mpi_process(const MPI_Comm mpi_communicator)
std::string compress(const std::string &input)
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 run(const std::vector< std::vector< Iterator > > &colored_iterators, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length=2 *MultithreadInfo::n_threads(), const unsigned int chunk_size=8)
void abort(const ExceptionBase &exc) noexcept
bool check(const ConstraintKinds kind_in, const unsigned int dim)
long double gamma(const unsigned int n)
int(&) functions(const void *v1, const void *v2)
void reinit(MatrixBlock< MatrixType > &v, const BlockSparsityPattern &p)
void refine_and_coarsen_fixed_fraction(::Triangulation< dim, spacedim > &tria, const ::Vector< Number > &criteria, const double top_fraction_of_error, const double bottom_fraction_of_error, const VectorTools::NormType norm_type=VectorTools::L1_norm)
::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 > cos(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sin(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > sqrt(const ::VectorizedArray< Number, width > &)
::VectorizedArray< Number, width > abs(const ::VectorizedArray< Number, width > &)
unsigned int subdomain_id
std::vector< std::vector< bool > > constant_modes