From: Johannes Heinz <43043310+jh66637@users.noreply.github.com>
Date: Fri, 2 Feb 2024 15:32:21 +0000 (+0100)
Subject: Add tutorial on Nitsche-type mortaring
X-Git-Tag: v9.6.0-rc1~398^2
X-Git-Url: https://gitweb.dealii.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F16299%2Fhead;p=dealii.git
Add tutorial on Nitsche-type mortaring
---
diff --git a/doc/doxygen/references.bib b/doc/doxygen/references.bib
index b82b2a5d7b..85c081c077 100644
--- a/doc/doxygen/references.bib
+++ b/doc/doxygen/references.bib
@@ -1856,6 +1856,71 @@
}
+%-------------------------------------------------------------------------------
+% Step 89
+%-------------------------------------------------------------------------------
+
+@article{hochbruck2014efficient,
+ author = {Marlis Hochbruck and Tomislav Pa{\v{z}}ur and Andreas Schulz and Ekkachai Thawinan and Christian Wieners},
+ journal = {{ZAMM} - Journal of Applied Mathematics and Mechanics / Zeitschrift für Angewandte Mathematik und Mechanik},
+ title = {Efficient time integration for discontinuous {G}alerkin approximations of linear wave equations},
+ year = {2014},
+ month = {sep},
+ number = {3},
+ pages = {237--259},
+ volume = {95},
+ doi = {10.1002/zamm.201300306},
+ publisher = {Wiley},
+}
+
+@article{arnold2002unified,
+ author = {Arnold, D. and Brezzi, F. and Cockburn, B. and Marini, L.},
+ journal = {SIAM Journal on Numerical Analysis},
+ title = {Unified analysis of discontinuous {Galerkin} methods for elliptic problems},
+ year = {2002},
+ number = {5},
+ pages = {1749-1779},
+ volume = {39},
+ doi = {10.1137/S0036142901384162},
+ eprint = {https://doi.org/10.1137/S0036142901384162},
+}
+
+@article{nguyen2011high,
+ author = {N.C. Nguyen and J. Peraire and B. Cockburn},
+ journal = {Journal of Computational Physics},
+ title = {High-order implicit hybridizable discontinuous {G}alerkin methods for acoustics and elastodynamics},
+ year = {2011},
+ issn = {0021-9991},
+ number = {10},
+ pages = {3695 - 3718},
+ volume = {230},
+ doi = {https://doi.org/10.1016/j.jcp.2011.01.035},
+}
+
+@article{bangerth2010adaptive,
+ author = {W. Bangerth and M. Geiger and R. Rannacher},
+ journal = {Computational Methods in Applied Mathematics},
+ title = {Adaptive {G}alerkin finite element methods for the wave equation},
+ year = {2010},
+ number = {1},
+ pages = {3--48},
+ volume = {10},
+ doi = {10.2478/cmam-2010-0001},
+ publisher = {Walter de Gruyter {GmbH}},
+}
+
+@article{duerrwaechter2021an,
+ author = {Jakob Dürrwächter and Marius Kurz and Patrick Kopper and Daniel Kempf and Claus-Dieter Munz and Andrea Beck},
+ journal = {Computers \& Fluids},
+ title = {An efficient sliding mesh interface method for high-order discontinuous {G}alerkin schemes},
+ year = {2021},
+ month = {mar},
+ pages = {104825},
+ volume = {217},
+ doi = {10.1016/j.compfluid.2020.104825},
+ publisher = {Elsevier {BV}},
+}
+
%-------------------------------------------------------------------------------
% References used elsewhere
diff --git a/doc/news/changes/major/20231127HeinzBergbauerFederMunch b/doc/news/changes/major/20231127HeinzBergbauerFederMunch
new file mode 100644
index 0000000000..f8f70b4659
--- /dev/null
+++ b/doc/news/changes/major/20231127HeinzBergbauerFederMunch
@@ -0,0 +1,6 @@
+New: The new tutorial step-89 presents the use of FERemoteEvaluation during matrix-free operator
+evaluation for non-matching and Chimera methods. The acoustic conservation equations are solved
+using Nitsche-type mortaring and point-to-point interpolation to demonstrate that
+a simple point-to-point interpolation approach is sometimes not sufficient.
+
+(Johannes Heinz, Marco Feder, Peter Munch, 2023/11/27)
diff --git a/examples/step-89/CMakeLists.txt b/examples/step-89/CMakeLists.txt
new file mode 100644
index 0000000000..9795f5d7a3
--- /dev/null
+++ b/examples/step-89/CMakeLists.txt
@@ -0,0 +1,55 @@
+##
+# CMake script
+##
+
+# Set the name of the project and target:
+set(TARGET "step-89")
+
+# Declare all source files the target consists of. Here, this is only
+# the one step-X.cc file, but as you expand your project you may wish
+# to add other source files as well. If your project becomes much larger,
+# you may want to either replace the following statement by something like
+# file(GLOB_RECURSE TARGET_SRC "source/*.cc")
+# file(GLOB_RECURSE TARGET_INC "include/*.h")
+# set(TARGET_SRC ${TARGET_SRC} ${TARGET_INC})
+# or switch altogether to the large project CMakeLists.txt file discussed
+# in the "CMake in user projects" page accessible from the "User info"
+# page of the documentation.
+set(TARGET_SRC
+ ${TARGET}.cc
+ )
+
+# Usually, you will not need to modify anything beyond this point...
+
+cmake_minimum_required(VERSION 3.13.4)
+
+find_package(deal.II 9.6.0
+ HINTS ${deal.II_DIR} ${DEAL_II_DIR} ../ ../../ $ENV{DEAL_II_DIR}
+ )
+if(NOT ${deal.II_FOUND})
+ message(FATAL_ERROR "\n"
+ "*** Could not locate a (sufficiently recent) version of deal.II. ***\n\n"
+ "You may want to either pass a flag -DDEAL_II_DIR=/path/to/deal.II to cmake\n"
+ "or set an environment variable \"DEAL_II_DIR\" that contains this path."
+ )
+endif()
+
+#
+# Are all dependencies fulfilled?
+#
+if(NOT DEAL_II_WITH_MPI) # keep in one line
+ message(FATAL_ERROR "
+Error! This tutorial requires a deal.II library that was configured with the following option:
+ DEAL_II_WITH_MPI = ON
+However, the deal.II library found at ${DEAL_II_PATH} was configured with these options:
+ DEAL_II_WITH_MPI = ${DEAL_II_WITH_MPI}
+This conflicts with the requirements."
+ )
+endif()
+
+
+deal_ii_initialize_cached_variables()
+set(CLEAN_UP_FILES *.log *.gmv *.gnuplot *.gpl *.eps *.pov *.ucd *.d2 *.vtu *.pvtu)
+project(${TARGET})
+deal_ii_invoke_autopilot()
+-
diff --git a/examples/step-89/doc/builds-on b/examples/step-89/doc/builds-on
new file mode 100644
index 0000000000..809da9b9ad
--- /dev/null
+++ b/examples/step-89/doc/builds-on
@@ -0,0 +1 @@
+step-67 step-87
diff --git a/examples/step-89/doc/intro.dox b/examples/step-89/doc/intro.dox
new file mode 100644
index 0000000000..666eef93e2
--- /dev/null
+++ b/examples/step-89/doc/intro.dox
@@ -0,0 +1,221 @@
+
+
+
+This program was contributed by Johannes Heinz, Maximilian Bergbauer, Marco Feder, and Peter Munch.
+Many ideas presented here are the result of common code development with
+Niklas Fehn, Luca Heltai, Martin Kronbichler,
+and Magdalena Schreter-Fleischhacker.
+
+This tutorial is loosely based on the publication
+"High-order non-conforming discontinuous Galerkin methods for the acoustic conservation equations"
+by Johannes Heinz, Peter Munch and Manfred Kaltenbacher @cite heinz2022high.
+
+Johannes Heinz was supported by the European Unionâs Framework Programme for Research
+and Innovation Horizon 2020 (2014-2020) under the Marie SkÅodowská--Curie Grant
+Agreement No. [812719].
+
+
+@dealiiTutorialDOI{10.5281/zenodo.10033975,https://zenodo.org/badge/DOI/10.5281/zenodo.10033975.svg}
+
+
Introduction
+
+This tutorial presents one way how to apply non-matching and/or Chimera methods
+within matrix-free loops in deal.II.
+We are following @cite heinz2022high to show that in some cases a simple point-to-point
+interpolation is not sufficient. As a remedy, Nitsche-type mortaring is used to suppress
+artificial modes observed for the acoustic conservation equations @cite heinz2022high.
+
+%Acoustic conservation equations
+
+Acoustic conservation equations are used to describe linear wave propagation.
+The set of equations consists of the conservation of mass and momentum
+@f[
+ \frac{\partial \, p}{\partial \, t} + \rho c^2 \nabla\cdot \mathbf{u} = 0,\\
+ \frac{\partial \, \mathbf{u}}{\partial \, t} + \frac{1}{\rho}\nabla p = \mathbf{0}.
+@f]
+Here, $p$ is the acoustic pressure, $\mathbf{u}$ the acoustic particle velocity, $c$ the
+speed of sound, and $\rho$ the mean density of the fluid in which waves are propagating.
+For the discretization we make use of discontinuous Galerkin (DG) methods. DG methods are
+especially attractive for the acoustic conservation equations due to their low numerical
+dispersion errors. More importantly for this tutorial, DG methods natively extend to non-matching
+Nitsche-type methods @cite arnold2002unified. I.e., numerical fluxes are not only used on inner
+element faces but also as non-matching coupling conditions.
+
+The discretized equations read
+@f[
+ \int_{\Omega} q_h\frac{\partial \, p_h}{\partial \, t} +\int_{\Omega} q_h \rho c^2 \nabla\cdot\mathbf{u}_h +\int_{\partial\Omega} q_h\mathbf{n}\cdot\rho c^2(\mathbf{u}^*_h-\mathbf{u}_h)=0,\\
+ \int_{\Omega} \mathbf{w}_h\cdot\frac{\partial \,\mathbf{u}_h}{\partial \, t} +\int_{\Omega} \mathbf{w}_h\cdot \frac{1}{\rho} \nabla p_h +\int_{\partial\Omega} \mathbf{w}_h \cdot\mathbf{n} \frac{1}{\rho}(p^*_h-p_h)=\mathbf{0},
+@f]
+where $\mathbf{w}_h$ and $q_h$ are test functions. The numerical fluxes are
+defined as follows @cite hochbruck2014efficient
+@f[
+ p_h^*=p_h-\frac{\tau^-}{\tau^-+\tau^+}[p_h]+\frac{\tau^-\tau^+}{\tau^-+\tau^+}\jump{\mathbf{u}_h},\\
+ \mathbf{u}_h^*=\mathbf{u}_h-\frac{\gamma^-}{\gamma^-+\gamma^+}[\mathbf{u}_h]+\frac{\gamma^-\gamma^+}{\gamma^-+\gamma^+}\jump{p_h},
+@f]
+with the penalty parameters $\tau=\frac{\rho c}{2}$ and $\gamma=\frac{1}{2\rho c}$.
+$[a] = a^- - a^+ $ denotes the jump of an arbitrary quantity $a$
+over element faces (face between elements $K^-$ and $K^+$) and
+$\jump{a} = a^- \mathbf{n}^- + a^+ \mathbf{n}^+$.
+For homogeneous materials, the fluxes reduce to standard Lax--Friedrichs fluxes
+($\gamma^-=\gamma^+$ and $\tau^-=\tau^+$)
+@f[
+ p_h^*=\average{p_h}+\tau\jump{\mathbf{u}_h},\\
+ \mathbf{u}_h^*=\average{\mathbf{u}_h}+\gamma\jump{p_h}.
+@f]
+$\average{a}=\frac{a^- + a^+}{2}$ denots the averaging operator.
+
+%Non-matching discretizations
+
+Non-matching discretizations can be used to connect mesh regions with different element sizes
+without the need for a transition region. Therefore, they are highly desirable in multiphysics
+applications. One example is a plate that radiates sound. The plate needs a much finer
+discretization than the surrounding air. In purely acoustic simulations, different materials
+require different element sizes to resolve the same wave because the speed of sound is directly
+proportional to the wavelength (we will simulate this example later on).
+
+Considering sliding rotating interfaces @cite duerrwaechter2021an also requires the ability to
+handle non-matching discretizations: A cylindrical mesh is embedded in a surrounding mesh with a
+cylindrical hole. Every time step, the cylinder rotates while the outer mesh remains at the
+same position. In this situation it is hardly possible to construct a conforming mesh in every
+time step.
+
+Besides this, non-matching methods can be extended to Chimera methods (elements overlap).
+Chimera methods can help to reduce the pressure on mesh generation tools since different regions
+of a mesh (that may overlap) can be considered independently.
+
+Different methods exist to treat non-matching interfaces. Within this tutorial, we concentrate on
+two methods: Point-to-point interpolation and Nitsche-type mortaring.
+
+%Point-to-point interpolation
+
+@image html https://www.dealii.org/images/steps/developer/step_89_intro_point_to_point.svg "" width=25%
+
+Point-to-point interpolation is a naive approach. The points in which values/gradients are
+queried in the coupling terms are defined by the quadrature points on the element face of
+element $K^-$. As it can be seen from the picture this approach might be subject to aliasing
+in some cases. In the picture, information from element $K_1^+$ is completely neglected.
+
+%Nitsche-type mortaring
+
+@image html https://www.dealii.org/images/steps/developer/step_89_intro_mortaring.svg "" width=25%
+
+Mortaring is the process of computing intersections and is not related to the Mortar method which
+enforces the coupling via Lagrange multipliers. Obtained intersections are also referred to as
+mortars. On each mortar a new integration rule is defined. The integral of the face of element
+$K^-$ is computed on the intersections. This way, the numerical integration is exact as long
+as a sufficient number of integration points is used.
+In this tutorial, the intersections are computed using `CGAL`. Therefore, `deal.II` has
+to be configured with `DEAL_II_WITH_CGAL` for the Nitsche-type mortaring implementation.
+
+%FERemoteEvaluation
+
+FERemoteEvaluation is a wrapper class which provides a similar interface to, e.g., FEEvaluation to
+access values over non-matching interfaces in matrix-free loops. A detailed description on how to setup
+the class and how to use it in actual code is given below on hands-on examples. Within this tutorial we only
+show the usage for non-matching discretizations. Note however, that FERemoteEvaluation can also be used in
+other settings such as volume coupling. Under the hood, Utilities::MPI::RemotePointEvaluation is used to query
+the solution or gradients at quadrature points. A detailed description how this is done can be found in step-87.
+The main difference in the usage of FERemoteEvaluation compared to FEEvaluation is that the interpolated
+values/gradients of the finite element solution at all the quadrature points are precomputed globally before
+the loop that passes through the cells/faces of the mesh (i.e., near the place where the communication takes place)
+instead of performing the interpolation out of the vector on a cell-by-cell basis.
+
+The standard code to evaluate fluxes via FEEvaluation reads:
+@code
+const auto face_function =
+ [&](const auto &data, auto &dst, const auto &src, const auto face_range) {
+
+ FEFaceEvaluation phi_m(data, true);
+ FEFaceEvaluation phi_p(data, false);
+
+ for (unsigned int f = face_range.first; f < face_range.second; ++f)
+ {
+ phi_m.reinit(f);
+ phi_p.reinit(f);
+
+ phi_p.gather_evaluate(src, EvaluationFlags::values); //compute values on face f
+
+ for (unsigned int q = 0; q < phi_m.n_q_points; ++q)
+ phi_m.submit_value(phi_p.get_value(q), q); //access values with phi_p
+
+ phi_m.integrate_scatter(EvaluationFlags::values, dst);
+ }
+ };
+
+matrix_free.template loop({}, face_function, {}, dst, src);
+@endcode
+
+The code to evaluate fluxes via FERemoteEvaluation is shown below.
+For brevity, we assume all boundary faces are somehow connected via non-conforming interfaces for FERemoteEvaluation.
+
+@code
+// Initialize FERemoteEvaluation: Note, that FERemoteEvaluation internally manages
+// the memory to store precomputed values. Therefore, FERemoteEvaluation
+// should be initialized only once to avoid continuous memory
+// allocation/deallocation. At this point, remote_communicator is assumed
+// to be initialized.
+FERemoteEvaluation phi_p_evaluator(remote_communicator);
+
+// Precompute the interpolated values of the finite element solution at all
+// the quadrature points outside the loop, invoking the vector entries and
+// respective basis function at possibly remote MPI processes before communication.
+phi_p_evaluator.gather_evaluate(src, EvaluationFlags::values);
+
+const auto boundary_function =
+ [&](const auto &data, auto &dst, const auto &src, const auto face_range) {
+
+ FEFaceEvaluation phi_m(data, true);
+ // To access the values in a thread safe way each thread has
+ // to create a own accessor object. A small helper function
+ // provides the accessor.
+ auto phi_p = phi_p_evaluator.get_data_accessor();
+
+ for (unsigned int f = face_range.first; f < face_range.second; ++f)
+ {
+ phi_m.reinit(f);
+ phi_p.reinit(f);
+
+ for (unsigned int q = 0; q < phi_m.n_q_points; ++q)
+ phi_m.submit_value(phi_p.get_value(q), q); //access values with phi_p
+
+ phi_m.integrate_scatter(EvaluationFlags::values, dst);
+ }
+ };
+
+matrix_free.template loop({}, {}, boundary_function, dst, src);
+@endcode
+The object @c remote_communicator is of type FERemoteCommunicator and assumed to be correctly initialized in above code snippet.
+FERemoteCommunicator internally manages the update of ghost values over non-matching interfaces and keeps track of the
+mapping between quadrature point index and corresponding values/gradients. The update of the values/gradients happens
+before the actual matrix-free loop. FERemoteCommunicator, as well as FERemoteEvaluation behaves differently for
+the given template parameter @c value_type. If we want to access values at arbitrary points (e.g. in combination with
+@c FEPointEvaluation) @c value_type=Number. If the values are defined at quadrature points of a @c FEEvaluation object
+it is possible to get the values at the quadrature points of batches and @c value_type=VectorizedArray.
+
+Overview
+
+In the following, point-to-point interpolation and Nitsche-type mortaring is implemented.
+
+At first we are considering the test case of a vibrating membrane, see e.g. @cite nguyen2011high.
+Standing waves of length $\lambda=2/M$ are oscillating with a period duration of
+$T=2 / (M \sqrt{dim} c)$. $M$ is the number of modes per meter, i.e. the number of half-waves
+per meter. The corresponding analytical solution reads as
+
+@f[
+ p =\cos(M \sqrt{d} \pi c t)\prod_{i=1}^{d} \sin(M \pi x_i),\\
+ u_i=-\frac{\sin(M \sqrt{d} \pi c t)}{\sqrt{d}\rho c} \cos(M \pi x_i)\prod_{j=1,j\neq i}^{d} \sin(M \pi x_j),
+@f]
+
+For simplicity, we are using homogeneous pressure Dirichlet boundary conditions within this tutorial.
+To be able to do so we have to tailor the domain size as well as the number of modes to conform with
+the homogeneous pressure Dirichlet boundary conditions. Within this tutorial we are using $M=10$ and
+a domain that spans from $(0,0)$ to $(1,1)$.
+
+For the point-to-point interpolation we observe aliasing which can be resolved using Nitsche-type mortaring.
+
+In a more realistic example, we effectively apply the implementations to a test case in which a wave
+is propagating from one fluid into another fluid. The speed of sound in the left part of the domain
+the speed of sound is $c=1$ and in the right part it is $c=3$. Since the wavelength is directly proportional
+to the speed of sound, three times larger elements can be used in the right part of the domain to resolve waves
+up to the same frequency. The test case has been simulated with a different domain and different initial
+conditions, e.g. in @cite bangerth2010adaptive.
diff --git a/examples/step-89/doc/kind b/examples/step-89/doc/kind
new file mode 100644
index 0000000000..c1d9154931
--- /dev/null
+++ b/examples/step-89/doc/kind
@@ -0,0 +1 @@
+techniques
diff --git a/examples/step-89/doc/results.dox b/examples/step-89/doc/results.dox
new file mode 100644
index 0000000000..141fba9a97
--- /dev/null
+++ b/examples/step-89/doc/results.dox
@@ -0,0 +1,80 @@
+Results
+
+Vibrating membrane: Point-to-point interpolation vs. Nitsche-type mortaring
+
+We compare the results of the simulations after the last time step, i.e. at $t=8T$.
+The $y$-component of the velocity field using Nitsche-type mortaring is depicted on the left.
+The same field using point-to-point interpolation is depicted on the right.
+
+
+
+
+ @image html https://www.dealii.org/images/steps/developer/step_89_membrane_test_case_mortaring_velocity_Y.png "" width=60%
+ |
+
+ @image html https://www.dealii.org/images/steps/developer/step_89_membrane_test_case_point_to_point_velocity_Y.png "" width=60%
+ |
+
+
+
+Besides this, the results for the pressure and the velocity in $y$ direction
+are plotted along the horizontal line that spans from (0,0.69) to (1,0.69).
+
+
+
+
+ @image html https://www.dealii.org/images/steps/developer/step_89_membrane_test_case_mortaring_vs_point_to_point_pressure.svg "" width=100%
+ |
+
+ @image html https://www.dealii.org/images/steps/developer/step_89_membrane_test_case_mortaring_vs_point_to_point_velocity_Y.svg "" width=100%
+ |
+
+
+
+While the results of the pressure is similar, $u_y$ differs clearly. At certain
+positions we can see aliasing errors for the point-to-point interpolation.
+For different mesh configurations and/or longer run times, the aliasing effects
+of the point-to-point simulation accumulate and the simulation gets instable.
+To keep the tutorial short we have chosen one mesh that can be used for all
+examples. For a configuration that yields instable results for a wide range of
+polynomial degrees, see @cite heinz2022high.
+
+Wave propagation through in-homogeneous fluid
+
+This is just one example in which non-matching discretizations can be efficiently
+used to reduce the amount of DoFs. The example is nice, since results for a similar
+test case are shown in multiple publications. As before, we slightly adapted the
+test case to be able to use the same mesh for all simulations. The pressure field
+at $t=0.3$ is depicted below.
+
+@image html https://www.dealii.org/images/steps/developer/step_89_inhomogenous_test_case_pressure.png "" width=30%
+
+As expected, we can easily see that the wave length in the right domain is roughly
+three times times the wave length in the left domain. Hence, the wave can be
+resolved with a coarser discretization.
+
+Using the same element size in the whole domain, we can compute a reference result.
+The displayed reference result is obtained by choosing the same subdivision level
+for both sub-domains, i.e. @c subdiv_right = 11. In this particular example the
+reference result uses $92928$ DoFs, while the non-matching result uses $52608$ DoFs.
+The pressure result is plotted along the horizontal line that spans from (0,0.5) to (1,0.5).
+
+@image html https://www.dealii.org/images/steps/developer/step_89_inhomogenous_test_case_conforming_vs_nonmatching.svg "" width=60%
+
+The results, obtained with the non-matching discretization is in good agreement with
+the reference result.
+
+Possibilities for extensions
+
+All the implementations are done with overlapping triangulations in mind. In particular the
+intersections in the mortaring case are constructed such that they are computed correctly
+for overlapping triangulations. For this the intersection requests are of dimension $dim-1$.
+The cells which are intersected with the intersection requests are of dimension $dim$. For the
+simple case of non-conforming interfaces it would be sufficient to compute the intersections
+between $dim-1$ and $dim-1$ entities. Furthermore, the lambda could be adapted, such that cells are
+only marked if they are connected to a certain boundary ID (in this case, e.g. 99) instead of
+marking every cell that is not connected to the opposite boundary ID (in this case, e.g. 98).
+Marking less cells can reduce the setup costs significantly.
+
+Note that for in-homogeneous material in this procedure is questionable, since it is not clear which
+material is present in the overlapping part of the mesh.
diff --git a/examples/step-89/doc/tooltip b/examples/step-89/doc/tooltip
new file mode 100644
index 0000000000..10f51f3381
--- /dev/null
+++ b/examples/step-89/doc/tooltip
@@ -0,0 +1 @@
+Matrix-free operator evaluation for non-matching and Chimera methods with application to acoustic conservation equations.
diff --git a/examples/step-89/step-89.cc b/examples/step-89/step-89.cc
new file mode 100644
index 0000000000..b39ffb885c
--- /dev/null
+++ b/examples/step-89/step-89.cc
@@ -0,0 +1,1796 @@
+/* ---------------------------------------------------------------------
+ *
+ * Copyright (C) 2023 by the deal.II authors
+ *
+ * This file is part of the deal.II library.
+ *
+ * The deal.II library is free software; you can use it, redistribute
+ * it, and/or modify it under the terms of the GNU Lesser General
+ * Public License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * The full text of the license can be found in the file LICENSE.md at
+ * the top level directory of deal.II.
+ *
+ * ---------------------------------------------------------------------
+ *
+ *
+ * Authors: Johannes Heinz, TU Wien, 2023
+ * Maximilian Bergbauer, TUM, 2023
+ * Marco Feder, SISSA, 2023
+ * Peter Munch, University of Augsburg/Uppsala University, 2023
+ */
+
+// @sect3{Include files}
+//
+// The program starts with including all the relevant header files.
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+// The following header file provides the class FERemoteEvaluation, which allows
+// to access values and/or gradients at remote triangulations similar to
+// FEEvaluation.
+#include
+
+// We pack everything that is specific for this program into a namespace
+// of its own.
+namespace Step89
+{
+ using namespace dealii;
+
+ // @sect3{Initial conditions for vibrating membrane}
+ //
+ // Function that provides the initial condition for the vibrating membrane
+ // test case.
+ template
+ class InitialConditionVibratingMembrane : public Function
+ {
+ public:
+ InitialConditionVibratingMembrane(const double modes);
+
+ // Function that the gives the initial pressure (comp 0) and velocity (comp
+ // 1 to 1 + dim).
+ double value(const Point &p, const unsigned int comp) const final;
+
+ // Function that calculates the duration of one oscillation.
+ double get_period_duration(const double speed_of_sound) const;
+
+ private:
+ const double M;
+ };
+
+ template
+ InitialConditionVibratingMembrane::InitialConditionVibratingMembrane(
+ const double modes)
+ : Function(dim + 1, 0.0)
+ , M(modes)
+ {
+ static_assert(dim == 2, "Only implemented for dim==2");
+ }
+
+ template
+ double
+ InitialConditionVibratingMembrane::value(const Point &p,
+ const unsigned int comp) const
+ {
+ if (comp == 0)
+ return std::sin(M * numbers::PI * p[0]) *
+ std::sin(M * numbers::PI * p[1]);
+
+ return 0.0;
+ }
+
+ template
+ double InitialConditionVibratingMembrane::get_period_duration(
+ const double speed_of_sound) const
+ {
+ return 2.0 / (M * std::sqrt(dim) * speed_of_sound);
+ }
+
+ // @sect3{Gauss pulse}
+ //
+ // Function that provides the values of a pressure Gauss pulse.
+ template
+ class GaussPulse : public Function
+ {
+ public:
+ GaussPulse(const double shift_x, const double shift_y);
+
+ // Function that the gives the initial pressure (comp 0) and velocity (comp
+ // 1 to 1 + dim).
+ double value(const Point &p, const unsigned int comp) const final;
+
+ private:
+ const double shift_x;
+ const double shift_y;
+ };
+
+ template
+ GaussPulse::GaussPulse(const double shift_x, const double shift_y)
+ : Function(dim + 1, 0.0)
+ , shift_x(shift_x)
+ , shift_y(shift_y)
+ {
+ static_assert(dim == 2, "Only implemented for dim==2");
+ }
+
+ // Function that the gives the initial pressure (comp 0) and velocity (comp 1
+ // to 1 + dim).
+ template
+ double GaussPulse::value(const Point &p,
+ const unsigned int comp) const
+ {
+ if (comp == 0)
+ return std::exp(-1000.0 * ((std::pow(p[0] - shift_x, 2)) +
+ (std::pow(p[1] - shift_y, 2))));
+
+ return 0.0;
+ }
+
+ // @sect3{Helper functions}
+ //
+ // The following namespace contains free helper functions that are used in the
+ // tutorial.
+ namespace HelperFunctions
+ {
+ // Helper function to check if a boundary ID is related to a non-matching
+ // face. A @c std::set that contains all non-matching boundary IDs is
+ // handed over additionally to the face ID under question. This function
+ // could certainly also be defined inline but this way the code is more easy
+ // to read.
+ bool is_non_matching_face(
+ const std::set &non_matching_face_ids,
+ const types::boundary_id face_id)
+ {
+ return non_matching_face_ids.find(face_id) != non_matching_face_ids.end();
+ }
+
+ // Helper function to set the initial conditions for the vibrating membrane
+ // test case.
+ template
+ void set_initial_condition(MatrixFree matrix_free,
+ const Function &initial_solution,
+ VectorType &dst)
+ {
+ VectorTools::interpolate(*matrix_free.get_mapping_info().mapping,
+ matrix_free.get_dof_handler(),
+ initial_solution,
+ dst);
+ }
+
+ // Helper function to compute the time step size according to the CFL
+ // condition.
+ double
+ compute_dt_cfl(const double hmin, const unsigned int degree, const double c)
+ {
+ return hmin / (std::pow(degree, 1.5) * c);
+ }
+
+ // Helper function that writes vtu output.
+ template
+ void write_vtu(const VectorType &solution,
+ const DoFHandler &dof_handler,
+ const Mapping &mapping,
+ const unsigned int degree,
+ const std::string &name_prefix)
+ {
+ DataOut data_out;
+ DataOutBase::VtkFlags flags;
+ flags.write_higher_order_cells = true;
+ data_out.set_flags(flags);
+
+ std::vector
+ interpretation(
+ dim + 1, DataComponentInterpretation::component_is_part_of_vector);
+ std::vector names(dim + 1, "U");
+
+ interpretation[0] = DataComponentInterpretation::component_is_scalar;
+ names[0] = "P";
+
+ data_out.add_data_vector(dof_handler, solution, names, interpretation);
+
+ data_out.build_patches(mapping, degree, DataOut::curved_inner_cells);
+ data_out.write_vtu_in_parallel(name_prefix + ".vtu",
+ dof_handler.get_communicator());
+ }
+ } // namespace HelperFunctions
+
+ //@sect3{Material access}
+ //
+ // This class stores the information if the fluid is homogeneous
+ // as well as the material properties at every cell.
+ // This class helps to access the correct values without accessing
+ // a large vector of materials in the homogeneous case.
+ template
+ class CellwiseMaterialData
+ {
+ public:
+ template
+ CellwiseMaterialData(
+ const MatrixFree> &matrix_free,
+ const std::map>
+ &material_id_map)
+ // If the map is of size 1, the material is constant in every cell.
+ : homogeneous(material_id_map.size() == 1)
+ {
+ Assert(material_id_map.size() > 0,
+ ExcMessage("No materials given to CellwiseMaterialData"));
+
+ if (homogeneous)
+ {
+ // In the homogeneous case we know the materials in the whole domain.
+ speed_of_sound_homogeneous = material_id_map.begin()->second.first;
+ density_homogeneous = material_id_map.begin()->second.second;
+ }
+ else
+ {
+ // In the in-homogeneous case materials vary between cells. We are
+ // filling a vector with the correct materials, that can be processed
+ // via
+ // @c read_cell_data().
+ const auto n_cell_batches =
+ matrix_free.n_cell_batches() + matrix_free.n_ghost_cell_batches();
+
+ speed_of_sound.resize(n_cell_batches);
+ density.resize(n_cell_batches);
+
+ for (unsigned int cell = 0; cell < n_cell_batches; ++cell)
+ {
+ speed_of_sound[cell] = 1.;
+ density[cell] = 1.;
+ for (unsigned int v = 0;
+ v < matrix_free.n_active_entries_per_cell_batch(cell);
+ ++v)
+ {
+ const auto material_id =
+ matrix_free.get_cell_iterator(cell, v)->material_id();
+
+ speed_of_sound[cell][v] =
+ material_id_map.at(material_id).first;
+ density[cell][v] = material_id_map.at(material_id).second;
+ }
+ }
+ }
+ }
+
+ bool is_homogeneous() const
+ {
+ return homogeneous;
+ }
+
+ const AlignedVector> &get_speed_of_sound() const
+ {
+ Assert(!homogeneous, ExcMessage("Use get_homogeneous_speed_of_sound()"));
+ return speed_of_sound;
+ }
+
+ const AlignedVector> &get_density() const
+ {
+ Assert(!homogeneous, ExcMessage("Use get_homogeneous_density()"));
+ return density;
+ }
+
+ VectorizedArray get_homogeneous_speed_of_sound() const
+ {
+ Assert(homogeneous, ExcMessage("Use get_speed_of_sound()"));
+ return speed_of_sound_homogeneous;
+ }
+
+ VectorizedArray get_homogeneous_density() const
+ {
+ Assert(homogeneous, ExcMessage("Use get_density()"));
+ return density_homogeneous;
+ }
+
+ private:
+ const bool homogeneous;
+
+ // Materials in the in-homogeneous case.
+ AlignedVector> speed_of_sound;
+ AlignedVector> density;
+
+ // Materials in the homogeneous case.
+ VectorizedArray speed_of_sound_homogeneous;
+ VectorizedArray density_homogeneous;
+ };
+
+ // To be able to access the material data in every cell in a thread safe way
+ // @c MaterialEvaluation is used. Similar to @c FEEvaluation, every thread
+ // creates its own instance and thus, there are no race conditions. For
+ // in-homogeneous materials, a @c reinit_cell() or @c reinit_face() function
+ // is used to set the correct material at the current cell batch. In the
+ // homogeneous case the @c _reinit() functions don't have to reset the
+ // materials.
+ template
+ class MaterialEvaluation
+ {
+ public:
+ MaterialEvaluation(
+ const MatrixFree> &matrix_free,
+ const CellwiseMaterialData &material_data)
+ : phi(matrix_free)
+ , phi_face(matrix_free, true)
+ , material_data(material_data)
+ {
+ if (material_data.is_homogeneous())
+ {
+ // Set the material that is used in every cell.
+ speed_of_sound = material_data.get_homogeneous_speed_of_sound();
+ density = material_data.get_homogeneous_density();
+ }
+ }
+
+ bool is_homogeneous() const
+ {
+ return material_data.is_homogeneous();
+ }
+
+ // Update the cell data, given a cell batch index.
+ void reinit_cell(const unsigned int cell)
+ {
+ // In the homogeneous case we do not have to reset the cell data.
+ if (!material_data.is_homogeneous())
+ {
+ // Reinit the FEEvaluation object and set the cell data.
+ phi.reinit(cell);
+ speed_of_sound =
+ phi.read_cell_data(material_data.get_speed_of_sound());
+ density = phi.read_cell_data(material_data.get_density());
+ }
+ }
+
+ // Update the cell data, given a face batch index.
+ void reinit_face(const unsigned int face)
+ {
+ // In the homogeneous case we do not have to reset the cell data.
+ if (!material_data.is_homogeneous())
+ {
+ // Reinit the FEFaceEvaluation object and set the cell data.
+ phi_face.reinit(face);
+ speed_of_sound =
+ phi_face.read_cell_data(material_data.get_speed_of_sound());
+ density = phi_face.read_cell_data(material_data.get_density());
+ }
+ }
+
+ // Return the speed of sound at the current cell batch.
+ VectorizedArray get_speed_of_sound() const
+ {
+ return speed_of_sound;
+ }
+
+ // Return the density at the current cell batch.
+ VectorizedArray get_density() const
+ {
+ return density;
+ }
+
+ private:
+ // Members needed for the in-homogeneous case.
+ FEEvaluation phi;
+ FEFaceEvaluation phi_face;
+
+ // Material defined at every cell.
+ const CellwiseMaterialData &material_data;
+
+ // Materials at current cell.
+ VectorizedArray speed_of_sound;
+ VectorizedArray density;
+ };
+
+
+ //@sect3{Boundary conditions}
+ //
+ // To be able to use the same kernel, for all face integrals we define
+ // a class that returns the needed values at boundaries. In this tutorial
+ // homogeneous pressure Dirichlet boundary conditions are applied via
+ // the mirror principle, i.e. $p_h^+=-p_h^- + 2g$ with $g=0$.
+ template
+ class BCEvaluationP
+ {
+ public:
+ BCEvaluationP(const FEFaceEvaluation &pressure_m)
+ : pressure_m(pressure_m)
+ {}
+
+ typename FEFaceEvaluation::value_type
+ get_value(const unsigned int q) const
+ {
+ return -pressure_m.get_value(q);
+ }
+
+ private:
+ const FEFaceEvaluation &pressure_m;
+ };
+
+ // We don't have to apply boundary conditions for the velocity, i.e.
+ // $\mathbf{u}_h^+=\mathbf{u}_h^-$.
+ template
+ class BCEvaluationU
+ {
+ public:
+ BCEvaluationU(const FEFaceEvaluation &velocity_m)
+ : velocity_m(velocity_m)
+ {}
+
+ typename FEFaceEvaluation::value_type
+ get_value(const unsigned int q) const
+ {
+ return velocity_m.get_value(q);
+ }
+
+ private:
+ const FEFaceEvaluation &velocity_m;
+ };
+
+ //@sect3{Acoustic operator}
+ //
+ // Class that defines the acoustic operator. The class is heavily based on
+ // matrix-free methods. For a better understanding in matrix-free methods
+ // please refer to step-67.
+ template
+ class AcousticOperator
+ {
+ // If the remote evaluators are set up with a VectorizedArray we are
+ // using point-to-point interpolation. Otherwise we make use of
+ // Nitsche-type mortaring.
+ static constexpr bool use_mortaring =
+ std::is_floating_point_v;
+
+ public:
+ // In case of Nitsche-type mortaring, `NonMatching::MappingInfo` has to
+ // be provided in the constructor.
+ AcousticOperator(
+ const MatrixFree &matrix_free,
+ std::shared_ptr> material_data,
+ const std::set &remote_face_ids,
+ std::shared_ptr>
+ pressure_r_eval,
+ std::shared_ptr>
+ velocity_r_eval,
+ std::shared_ptr> c_r_eval,
+ std::shared_ptr> rho_r_eval,
+ std::shared_ptr> nm_info =
+ nullptr)
+ : matrix_free(matrix_free)
+ , material_data(material_data)
+ , remote_face_ids(remote_face_ids)
+ , pressure_r_eval(pressure_r_eval)
+ , velocity_r_eval(velocity_r_eval)
+ , c_r_eval(c_r_eval)
+ , rho_r_eval(rho_r_eval)
+ , nm_mapping_info(nm_info)
+ {
+ if (use_mortaring)
+ Assert(nm_info,
+ ExcMessage(
+ "In case of Nitsche-type mortaring NonMatching::MappingInfo \
+ has to be provided."));
+ }
+
+ // Function to evaluate the acoustic operator.
+ template
+ void evaluate(VectorType &dst, const VectorType &src) const
+ {
+ // Update the precomputed values in corresponding the FERemoteEvaluation
+ // objects. The material parameters do not change and thus, we do
+ // not have to update precomputed values in @c c_r_eval and @c rho_r_eval.
+ pressure_r_eval->gather_evaluate(src, EvaluationFlags::values);
+ velocity_r_eval->gather_evaluate(src, EvaluationFlags::values);
+
+ if constexpr (use_mortaring)
+ {
+ // Perform matrix free loop with Nitsche-type mortaring at
+ // non-matching faces.
+ matrix_free.loop(
+ &AcousticOperator::local_apply_cell,
+ &AcousticOperator::local_apply_face,
+ &AcousticOperator::local_apply_boundary_face_mortaring,
+ this,
+ dst,
+ src,
+ true,
+ MatrixFree::DataAccessOnFaces::values,
+ MatrixFree::DataAccessOnFaces::values);
+ }
+ else
+ {
+ // Perform matrix free loop with point-to-point interpolation at
+ // non-matching faces.
+ matrix_free.loop(
+ &AcousticOperator::local_apply_cell,
+ &AcousticOperator::local_apply_face,
+ &AcousticOperator::local_apply_boundary_face_point_to_point<
+ VectorType>,
+ this,
+ dst,
+ src,
+ true,
+ MatrixFree::DataAccessOnFaces::values,
+ MatrixFree::DataAccessOnFaces::values);
+ }
+ }
+
+ private:
+ // This function evaluates the volume integrals.
+ template
+ void local_apply_cell(
+ const MatrixFree &matrix_free,
+ VectorType &dst,
+ const VectorType &src,
+ const std::pair &cell_range) const
+ {
+ FEEvaluation pressure(matrix_free, 0, 0, 0);
+ FEEvaluation velocity(matrix_free, 0, 0, 1);
+
+ // Class that gives access to the material at each cell
+ MaterialEvaluation material(matrix_free, *material_data);
+
+ for (unsigned int cell = cell_range.first; cell < cell_range.second;
+ ++cell)
+ {
+ velocity.reinit(cell);
+ pressure.reinit(cell);
+
+ pressure.gather_evaluate(src, EvaluationFlags::gradients);
+ velocity.gather_evaluate(src, EvaluationFlags::gradients);
+
+ // Get the materials at the corresponding cell. Since we
+ // introduced @c MaterialEvaluation we can write the code
+ // independent if the material is homogeneous or in-homogeneous.
+ material.reinit_cell(cell);
+ const auto c = material.get_speed_of_sound();
+ const auto rho = material.get_density();
+ for (unsigned int q : pressure.quadrature_point_indices())
+ {
+ pressure.submit_value(rho * c * c * velocity.get_divergence(q),
+ q);
+ velocity.submit_value(1.0 / rho * pressure.get_gradient(q), q);
+ }
+
+ pressure.integrate_scatter(EvaluationFlags::values, dst);
+ velocity.integrate_scatter(EvaluationFlags::values, dst);
+ }
+ }
+
+ // This function evaluates the fluxes at faces between cells with the same
+ // material. If boundary faces are under consideration fluxes into
+ // neighboring faces do not have to be considered which is enforced via
+ // `weight_neighbor=false`. For non-matching faces the fluxes into
+ // neighboring faces are not considered as well. This is because we iterate
+ // over each side of the non-matching face separately (similar to a cell
+ // centric loop).
+ template
+ inline DEAL_II_ALWAYS_INLINE void evaluate_face_kernel(
+ InternalFaceIntegratorPressure &pressure_m,
+ InternalFaceIntegratorVelocity &velocity_m,
+ ExternalFaceIntegratorPressure &pressure_p,
+ ExternalFaceIntegratorVelocity &velocity_p,
+ const typename InternalFaceIntegratorPressure::value_type c,
+ const typename InternalFaceIntegratorPressure::value_type rho) const
+ {
+ // Compute penalty parameters from material parameters.
+ const auto tau = 0.5 * rho * c;
+ const auto gamma = 0.5 / (rho * c);
+
+ for (unsigned int q : pressure_m.quadrature_point_indices())
+ {
+ const auto n = pressure_m.normal_vector(q);
+ const auto pm = pressure_m.get_value(q);
+ const auto um = velocity_m.get_value(q);
+
+ const auto pp = pressure_p.get_value(q);
+ const auto up = velocity_p.get_value(q);
+
+ // Compute homogeneous local Lax-Friedrichs fluxes and submit the
+ // corrsponding values to the integrators.
+ const auto momentum_flux =
+ 0.5 * (pm + pp) + 0.5 * tau * (um - up) * n;
+ velocity_m.submit_value(1.0 / rho * (momentum_flux - pm) * n, q);
+ if constexpr (weight_neighbor)
+ velocity_p.submit_value(1.0 / rho * (momentum_flux - pp) * (-n), q);
+
+ const auto mass_flux = 0.5 * (um + up) + 0.5 * gamma * (pm - pp) * n;
+ pressure_m.submit_value(rho * c * c * (mass_flux - um) * n, q);
+ if constexpr (weight_neighbor)
+ pressure_p.submit_value(rho * c * c * (mass_flux - up) * (-n), q);
+ }
+ }
+
+ // This function evaluates the fluxes at faces between cells with different
+ // materials. This can only happen over non-matching interfaces. Therefore,
+ // it is implicitly known that `weight_neighbor=false` and we can omit the
+ // parameter.
+ template
+ void evaluate_face_kernel_inhomogeneous(
+ InternalFaceIntegratorPressure &pressure_m,
+ InternalFaceIntegratorVelocity &velocity_m,
+ const ExternalFaceIntegratorPressure &pressure_p,
+ const ExternalFaceIntegratorVelocity &velocity_p,
+ const typename InternalFaceIntegratorPressure::value_type c,
+ const typename InternalFaceIntegratorPressure::value_type rho,
+ const MaterialIntegrator &c_r,
+ const MaterialIntegrator &rho_r) const
+ {
+ // Interior material information is constant over quadrature points
+ const auto tau_m = 0.5 * rho * c;
+ const auto gamma_m = 0.5 / (rho * c);
+
+ for (unsigned int q : pressure_m.quadrature_point_indices())
+ {
+ // The material at the neighboring face might vary in every quadrature
+ // point.
+ const auto c_p = c_r.get_value(q);
+ const auto rho_p = rho_r.get_value(q);
+ const auto tau_p = 0.5 * rho_p * c_p;
+ const auto gamma_p = 0.5 / (rho_p * c_p);
+ const auto tau_sum_inv = 1.0 / (tau_m + tau_p);
+ const auto gamma_sum_inv = 1.0 / (gamma_m + gamma_p);
+
+ const auto n = pressure_m.normal_vector(q);
+ const auto pm = pressure_m.get_value(q);
+ const auto um = velocity_m.get_value(q);
+
+ const auto pp = pressure_p.get_value(q);
+ const auto up = velocity_p.get_value(q);
+
+
+ // Compute inhomogeneous fluxes and submit the corresponding values
+ // to the integrators.
+ const auto momentum_flux =
+ pm - tau_m * tau_sum_inv * (pm - pp) +
+ tau_m * tau_p * tau_sum_inv * (um - up) * n;
+ velocity_m.submit_value(1.0 / rho * (momentum_flux - pm) * n, q);
+
+
+ const auto mass_flux =
+ um - gamma_m * gamma_sum_inv * (um - up) +
+ gamma_m * gamma_p * gamma_sum_inv * (pm - pp) * n;
+
+ pressure_m.submit_value(rho * c * c * (mass_flux - um) * n, q);
+ }
+ }
+
+ // This function evaluates the inner face integrals.
+ template
+ void local_apply_face(
+ const MatrixFree &matrix_free,
+ VectorType &dst,
+ const VectorType &src,
+ const std::pair &face_range) const
+ {
+ FEFaceEvaluation pressure_m(
+ matrix_free, true, 0, 0, 0);
+ FEFaceEvaluation pressure_p(
+ matrix_free, false, 0, 0, 0);
+ FEFaceEvaluation velocity_m(
+ matrix_free, true, 0, 0, 1);
+ FEFaceEvaluation velocity_p(
+ matrix_free, false, 0, 0, 1);
+
+ // Class that gives access to the material at each cell
+ MaterialEvaluation material(matrix_free, *material_data);
+
+ for (unsigned int face = face_range.first; face < face_range.second;
+ face++)
+ {
+ velocity_m.reinit(face);
+ velocity_p.reinit(face);
+
+ pressure_m.reinit(face);
+ pressure_p.reinit(face);
+
+ pressure_m.gather_evaluate(src, EvaluationFlags::values);
+ pressure_p.gather_evaluate(src, EvaluationFlags::values);
+
+ velocity_m.gather_evaluate(src, EvaluationFlags::values);
+ velocity_p.gather_evaluate(src, EvaluationFlags::values);
+
+ material.reinit_face(face);
+ evaluate_face_kernel(pressure_m,
+ velocity_m,
+ pressure_p,
+ velocity_p,
+ material.get_speed_of_sound(),
+ material.get_density());
+
+ pressure_m.integrate_scatter(EvaluationFlags::values, dst);
+ pressure_p.integrate_scatter(EvaluationFlags::values, dst);
+ velocity_m.integrate_scatter(EvaluationFlags::values, dst);
+ velocity_p.integrate_scatter(EvaluationFlags::values, dst);
+ }
+ }
+
+
+ //@sect4{Matrix-free boundary function for point-to-point interpolation}
+ //
+ // This function evaluates the boundary face integrals and the
+ // non-matching face integrals using point-to-point interpolation.
+ template
+ void local_apply_boundary_face_point_to_point(
+ const MatrixFree &matrix_free,
+ VectorType &dst,
+ const VectorType &src,
+ const std::pair &face_range) const
+ {
+ // Standard face evaluators.
+ FEFaceEvaluation pressure_m(
+ matrix_free, true, 0, 0, 0);
+ FEFaceEvaluation velocity_m(
+ matrix_free, true, 0, 0, 1);
+
+ // Classes that return the correct BC values.
+ BCEvaluationP pressure_bc(pressure_m);
+ BCEvaluationU velocity_bc(velocity_m);
+
+ // Class that gives access to the material at each cell
+ MaterialEvaluation material(matrix_free, *material_data);
+
+ // Remote evaluators.
+ auto pressure_r = pressure_r_eval->get_data_accessor();
+ auto velocity_r = velocity_r_eval->get_data_accessor();
+ auto c_r = c_r_eval->get_data_accessor();
+ auto rho_r = rho_r_eval->get_data_accessor();
+
+ for (unsigned int face = face_range.first; face < face_range.second;
+ face++)
+ {
+ velocity_m.reinit(face);
+ pressure_m.reinit(face);
+
+ pressure_m.gather_evaluate(src, EvaluationFlags::values);
+ velocity_m.gather_evaluate(src, EvaluationFlags::values);
+
+ if (HelperFunctions::is_non_matching_face(
+ remote_face_ids, matrix_free.get_boundary_id(face)))
+ {
+ // If @c face is non-matching we have to query values via the
+ // FERemoteEvaluaton objects. This is done by passing the
+ // corresponding FERemoteEvaluaton objects to the function that
+ // evaluates the kernel. As mentioned above, each side of the
+ // non-matching interface is traversed separately and we do not
+ // have to consider the neighbor in the kernel. Note, that the
+ // values in the FERemoteEvaluaton objects are already updated at
+ // this point.
+
+ // For point-to-point interpolation we simply use the
+ // corresponding FERemoteEvaluaton objects in combination with the
+ // standard FEFaceEvaluation objects.
+ velocity_r.reinit(face);
+ pressure_r.reinit(face);
+
+ material.reinit_face(face);
+
+ if (material.is_homogeneous())
+ {
+ // If homogeneous material is considered do not use the
+ // inhomogeneous fluxes. While it would be possible
+ // to use the inhomogeneous fluxes they are more expensive to
+ // compute.
+ evaluate_face_kernel(pressure_m,
+ velocity_m,
+ pressure_r,
+ velocity_r,
+ material.get_speed_of_sound(),
+ material.get_density());
+ }
+ else
+ {
+ // If inhomogeneous material is considered use the
+ // in-homogeneous fluxes.
+ c_r.reinit(face);
+ rho_r.reinit(face);
+ evaluate_face_kernel_inhomogeneous(
+ pressure_m,
+ velocity_m,
+ pressure_r,
+ velocity_r,
+ material.get_speed_of_sound(),
+ material.get_density(),
+ c_r,
+ rho_r);
+ }
+ }
+ else
+ {
+ // If @c face is a standard boundary face, evaluate the integral
+ // as usual in the matrix free context. To be able to use the same
+ // kernel as for inner faces we pass the boundary condition
+ // objects to the function that evaluates the kernel. As detailed
+ // above `weight_neighbor=false`.
+ material.reinit_face(face);
+ evaluate_face_kernel(pressure_m,
+ velocity_m,
+ pressure_bc,
+ velocity_bc,
+ material.get_speed_of_sound(),
+ material.get_density());
+ }
+
+ pressure_m.integrate_scatter(EvaluationFlags::values, dst);
+ velocity_m.integrate_scatter(EvaluationFlags::values, dst);
+ }
+ }
+
+ //@sect4{Matrix-free boundary function for Nitsche-type mortaring}
+ //
+ // This function evaluates the boundary face integrals and the
+ // non-matching face integrals using Nitsche-type mortaring.
+ template
+ void local_apply_boundary_face_mortaring(
+ const MatrixFree &matrix_free,
+ VectorType &dst,
+ const VectorType &src,
+ const std::pair &face_range) const
+ {
+ // Standard face evaluators for BCs.
+ FEFaceEvaluation pressure_m(
+ matrix_free, true, 0, 0, 0);
+ FEFaceEvaluation velocity_m(
+ matrix_free, true, 0, 0, 1);
+
+ // For Nitsche-type mortaring we are evaluating the integrals over
+ // intersections. This is why, quadrature points are arbitrarily
+ // distributed on every face. Thus, we can not make use of face batches
+ // and FEFaceEvaluation but have to consider each face individually and
+ // make use of @c FEFacePointEvaluation to evaluate the integrals in the
+ // arbitrarily distributed quadrature points.
+ // Since the setup of FEFacePointEvaluation is more expensive than that of
+ // FEEvaluation we do the setup only once. For this we are using the
+ // helper function @c get_thread_safe_fe_face_point_evaluation_object().
+ FEFacePointEvaluation<1, dim, dim, Number> &pressure_m_mortar =
+ get_thread_safe_fe_face_point_evaluation_object<1>(
+ thread_local_pressure_m_mortar, 0);
+ FEFacePointEvaluation &velocity_m_mortar =
+ get_thread_safe_fe_face_point_evaluation_object(
+ thread_local_velocity_m_mortar, 1);
+
+ BCEvaluationP pressure_bc(pressure_m);
+ BCEvaluationU velocity_bc(velocity_m);
+
+ MaterialEvaluation material(matrix_free, *material_data);
+
+ auto pressure_r_mortar = pressure_r_eval->get_data_accessor();
+ auto velocity_r_mortar = velocity_r_eval->get_data_accessor();
+ auto c_r = c_r_eval->get_data_accessor();
+ auto rho_r = rho_r_eval->get_data_accessor();
+
+ for (unsigned int face = face_range.first; face < face_range.second;
+ ++face)
+ {
+ if (HelperFunctions::is_non_matching_face(
+ remote_face_ids, matrix_free.get_boundary_id(face)))
+ {
+ material.reinit_face(face);
+
+ // First fetch the DoF values with standard FEFaceEvaluation
+ // objects.
+ pressure_m.reinit(face);
+ velocity_m.reinit(face);
+
+ pressure_m.read_dof_values(src);
+ velocity_m.read_dof_values(src);
+
+ // Project the internally stored values into the face DoFs
+ // of the current face.
+ pressure_m.project_to_face(EvaluationFlags::values);
+ velocity_m.project_to_face(EvaluationFlags::values);
+
+ // For mortaring, we have to consider every face from the face
+ // batches separately and have to use the FEFacePointEvaluation
+ // objects to be able to evaluate the integrals with the
+ // arbitrarily distributed quadrature points.
+ for (unsigned int v = 0;
+ v < matrix_free.n_active_entries_per_face_batch(face);
+ ++v)
+ {
+ constexpr unsigned int n_lanes =
+ VectorizedArray::size();
+ velocity_m_mortar.reinit(face * n_lanes + v);
+ pressure_m_mortar.reinit(face * n_lanes + v);
+
+ // Evaluate using FEFacePointEvaluation. As buffer,
+ // simply use the internal buffers from the
+ // FEFaceEvaluation objects.
+ velocity_m_mortar.evaluate_in_face(
+ &velocity_m.get_scratch_data().begin()[0][v],
+ EvaluationFlags::values);
+
+ pressure_m_mortar.evaluate_in_face(
+ &pressure_m.get_scratch_data().begin()[0][v],
+ EvaluationFlags::values);
+
+ velocity_r_mortar.reinit(face * n_lanes + v);
+ pressure_r_mortar.reinit(face * n_lanes + v);
+
+ if (material.is_homogeneous())
+ {
+ // If homogeneous material is considered do not use the
+ // inhomogeneous fluxes. While it would be possible
+ // to use the inhomogeneous fluxes they are more
+ // expensive to compute. Since we are operating on face @c
+ // v we call @c material.get_density()[v].
+ evaluate_face_kernel(
+ pressure_m_mortar,
+ velocity_m_mortar,
+ pressure_r_mortar,
+ velocity_r_mortar,
+ material.get_speed_of_sound()[v],
+ material.get_density()[v]);
+ }
+ else
+ {
+ c_r.reinit(face * n_lanes + v);
+ rho_r.reinit(face * n_lanes + v);
+
+ evaluate_face_kernel_inhomogeneous(
+ pressure_m_mortar,
+ velocity_m_mortar,
+ pressure_r_mortar,
+ velocity_r_mortar,
+ material.get_speed_of_sound()[v],
+ material.get_density()[v],
+ c_r,
+ rho_r);
+ }
+
+ // Integrate using FEFacePointEvaluation. As buffer,
+ // simply use the internal buffers from the
+ // FEFaceEvaluation objects.
+ velocity_m_mortar.integrate_in_face(
+ &velocity_m.get_scratch_data().begin()[0][v],
+ EvaluationFlags::values);
+
+ pressure_m_mortar.integrate_in_face(
+ &pressure_m.get_scratch_data().begin()[0][v],
+ EvaluationFlags::values);
+ }
+
+ // Collect the contributions from the face DoFs to
+ // the internal cell DoFs to be able to use the
+ // member function @c distribute_local_to_global().
+ pressure_m.collect_from_face(EvaluationFlags::values);
+ velocity_m.collect_from_face(EvaluationFlags::values);
+
+ pressure_m.distribute_local_to_global(dst);
+ velocity_m.distribute_local_to_global(dst);
+ }
+ else
+ {
+ // Same as in @c local_apply_boundary_face_point_to_point().
+ velocity_m.reinit(face);
+ pressure_m.reinit(face);
+
+ pressure_m.gather_evaluate(src, EvaluationFlags::values);
+ velocity_m.gather_evaluate(src, EvaluationFlags::values);
+
+ material.reinit_face(face);
+ evaluate_face_kernel(pressure_m,
+ velocity_m,
+ pressure_bc,
+ velocity_bc,
+ material.get_speed_of_sound(),
+ material.get_density());
+
+ pressure_m.integrate_scatter(EvaluationFlags::values, dst);
+ velocity_m.integrate_scatter(EvaluationFlags::values, dst);
+ }
+ }
+ }
+
+ const MatrixFree &matrix_free;
+
+ // CellwiseMaterialData is stored as shared pointer with the same
+ // argumentation.
+ const std::shared_ptr> material_data;
+
+ const std::set remote_face_ids;
+
+ // FERemoteEvaluation objects are strored as shared pointers. This way,
+ // they can also be used for other operators without precomputing the values
+ // multiple times.
+ const std::shared_ptr>
+ pressure_r_eval;
+ const std::shared_ptr>
+ velocity_r_eval;
+
+ const std::shared_ptr>
+ c_r_eval;
+ const std::shared_ptr>
+ rho_r_eval;
+
+ const std::shared_ptr>
+ nm_mapping_info;
+
+ // We store FEFacePointEvaluation objects as members in a thread local
+ // way, since its creation is more expensive compared to FEEvaluation
+ // objects.
+ mutable Threads::ThreadLocalStorage<
+ std::unique_ptr>>
+ thread_local_pressure_m_mortar;
+
+ mutable Threads::ThreadLocalStorage<
+ std::unique_ptr>>
+ thread_local_velocity_m_mortar;
+
+ // Helper function to create and get FEFacePointEvaluation objects in a
+ // thread safe way. On each thread, FEFacePointEvaluation is created if it
+ // has not been created by now. After that, simply return the object
+ // corresponding to the thread under consideration.
+ template
+ FEFacePointEvaluation &
+ get_thread_safe_fe_face_point_evaluation_object(
+ Threads::ThreadLocalStorage<
+ std::unique_ptr>>
+ &fe_face_point_eval_thread_local,
+ unsigned int fist_selected_comp) const
+ {
+ if (fe_face_point_eval_thread_local.get() == nullptr)
+ {
+ fe_face_point_eval_thread_local = std::make_unique<
+ FEFacePointEvaluation>(
+ *nm_mapping_info,
+ matrix_free.get_dof_handler().get_fe(),
+ true,
+ fist_selected_comp);
+ }
+ return *fe_face_point_eval_thread_local.get();
+ }
+ };
+
+ //@sect3{Inverse mass operator}
+ //
+ // Class to apply the inverse mass operator.
+ template
+ class InverseMassOperator
+ {
+ public:
+ InverseMassOperator(const MatrixFree &matrix_free)
+ : matrix_free(matrix_free)
+ {}
+
+ // Function to apply the inverse mass operator.
+ template
+ void apply(VectorType &dst, const VectorType &src) const
+ {
+ dst.zero_out_ghost_values();
+ matrix_free.cell_loop(&InverseMassOperator::local_apply_cell,
+ this,
+ dst,
+ src);
+ }
+
+ private:
+ // Apply the inverse mass operator onto every cell batch.
+ template
+ void local_apply_cell(
+ const MatrixFree &mf,
+ VectorType &dst,
+ const VectorType &src,
+ const std::pair &cell_range) const
+ {
+ FEEvaluation phi(mf);
+ MatrixFreeOperators::CellwiseInverseMassMatrix
+ minv(phi);
+
+ for (unsigned int cell = cell_range.first; cell < cell_range.second;
+ ++cell)
+ {
+ phi.reinit(cell);
+ phi.read_dof_values(src);
+ minv.apply(phi.begin_dof_values(), phi.begin_dof_values());
+ phi.set_dof_values(dst);
+ }
+ }
+
+ const MatrixFree &matrix_free;
+ };
+
+ //@sect3{Runge-Kutta time-stepping}
+ //
+ // This class implements a Runge-Kutta scheme of order 2.
+ template
+ class RungeKutta2
+ {
+ using VectorType = LinearAlgebra::distributed::Vector;
+
+ public:
+ RungeKutta2(
+ const std::shared_ptr>
+ inverse_mass_operator,
+ const std::shared_ptr>
+ acoustic_operator)
+ : inverse_mass_operator(inverse_mass_operator)
+ , acoustic_operator(acoustic_operator)
+ {}
+
+ // Set up and run time loop.
+ void run(const MatrixFree &matrix_free,
+ const double cr,
+ const double end_time,
+ const double speed_of_sound,
+ const Function &initial_condition,
+ const std::string &vtk_prefix)
+ {
+ // Get needed members of matrix free.
+ const auto &dof_handler = matrix_free.get_dof_handler();
+ const auto &mapping = *matrix_free.get_mapping_info().mapping;
+ const auto degree = dof_handler.get_fe().degree;
+
+ // Initialize needed Vectors.
+ VectorType solution;
+ matrix_free.initialize_dof_vector(solution);
+ VectorType solution_temp;
+ matrix_free.initialize_dof_vector(solution_temp);
+
+ // Set the initial condition.
+ HelperFunctions::set_initial_condition(matrix_free,
+ initial_condition,
+ solution);
+
+ // Compute time step size: Compute minimum element edge length.
+ // We assume non-distorted elements, therefore we only compute
+ // the distance between two vertices
+ double h_local_min = std::numeric_limits::max();
+ for (const auto &cell : dof_handler.active_cell_iterators())
+ h_local_min =
+ std::min(h_local_min,
+ (cell->vertex(1) - cell->vertex(0)).norm_square());
+ h_local_min = std::sqrt(h_local_min);
+ const double h_min =
+ Utilities::MPI::min(h_local_min, dof_handler.get_communicator());
+
+ // Compute constant time step size via the CFL condition.
+ const double dt =
+ cr * HelperFunctions::compute_dt_cfl(h_min, degree, speed_of_sound);
+
+ // Perform time integration loop.
+ double time = 0.0;
+ unsigned int timestep = 0;
+ while (time < end_time)
+ {
+ // Write output.
+ HelperFunctions::write_vtu(solution,
+ matrix_free.get_dof_handler(),
+ mapping,
+ degree,
+ "step_89-" + vtk_prefix +
+ std::to_string(timestep));
+
+ // Perform a single time step.
+ std::swap(solution, solution_temp);
+ time += dt;
+ timestep++;
+ perform_time_step(dt, solution, solution_temp);
+ }
+ }
+
+ private:
+ // Perform one Runge-Kutta 2 time step.
+ void
+ perform_time_step(const double dt, VectorType &dst, const VectorType &src)
+ {
+ VectorType k1 = src;
+
+ // First stage.
+ evaluate_stage(k1, src);
+
+ // Second stage.
+ k1.sadd(0.5 * dt, 1.0, src);
+ evaluate_stage(dst, k1);
+ dst.sadd(dt, 1.0, src);
+ }
+
+ // Evaluate a single Runge-Kutta stage.
+ void evaluate_stage(VectorType &dst, const VectorType &src)
+ {
+ // Evaluate the stage
+ acoustic_operator->evaluate(dst, src);
+ dst *= -1.0;
+ inverse_mass_operator->apply(dst, dst);
+ }
+
+ // Needed operators.
+ const std::shared_ptr>
+ inverse_mass_operator;
+ const std::shared_ptr>
+ acoustic_operator;
+ };
+
+
+ // @sect3{Construction of non-matching triangulations}
+ //
+ // This function creates a two dimensional squared triangulation
+ // that spans from (0,0) to (1,1). It consists of two sub-domains.
+ // The left sub-domain spans from (0,0) to (0.525,1). The right
+ // sub-domain spans from (0.525,0) to (1,1). The left sub-domain has
+ // three times smaller elements compared to the right sub-domain.
+ template
+ void build_non_matching_triangulation(
+ Triangulation &tria,
+ std::set &non_matching_faces,
+ const unsigned int refinements)
+ {
+ const double length = 1.0;
+
+ // At non-matching interfaces, we provide different boundary
+ // IDs. These boundary IDs have to differ because later on
+ // RemotePointEvaluation has to search for remote points for
+ // each face, that are defined in the same mesh (since we merge
+ // the mesh) but not on the same side of the non-matching interface.
+ const types::boundary_id non_matching_id_left = 98;
+ const types::boundary_id non_matching_id_right = 99;
+
+ // Provide this information to the caller.
+ non_matching_faces.insert(non_matching_id_left);
+ non_matching_faces.insert(non_matching_id_right);
+
+ // Construct left part of mesh.
+ Triangulation tria_left;
+ const unsigned int subdiv_left = 11;
+ GridGenerator::subdivided_hyper_rectangle(tria_left,
+ {subdiv_left, 2 * subdiv_left},
+ {0.0, 0.0},
+ {0.525 * length, length});
+
+ // The left part of the mesh has the material ID 0.
+ for (const auto &cell : tria_left.active_cell_iterators())
+ cell->set_material_id(0);
+
+ // The right face is non-matching. All other boundary IDs
+ // are set to 0.
+ for (const auto &face : tria_left.active_face_iterators())
+ if (face->at_boundary())
+ {
+ face->set_boundary_id(0);
+ if (face->center()[0] > 0.525 * length - 1e-6)
+ face->set_boundary_id(non_matching_id_left);
+ }
+
+ // Construct right part of mesh.
+ Triangulation tria_right;
+ const unsigned int subdiv_right = 4;
+ GridGenerator::subdivided_hyper_rectangle(tria_right,
+ {subdiv_right, 2 * subdiv_right},
+ {0.525 * length, 0.0},
+ {length, length});
+
+ // The right part of the mesh has the material ID 1.
+ for (const auto &cell : tria_right.active_cell_iterators())
+ cell->set_material_id(1);
+
+ // The left face is non-matching. All other boundary IDs
+ // are set to 0.
+ for (const auto &face : tria_right.active_face_iterators())
+ if (face->at_boundary())
+ {
+ face->set_boundary_id(0);
+ if (face->center()[0] < 0.525 * length + 1e-6)
+ face->set_boundary_id(non_matching_id_right);
+ }
+
+ // Merge triangulations with tolerance 0 to ensure no vertices
+ // are merged, see the documentation of the function
+ // @c merge_triangulations().
+ GridGenerator::merge_triangulations(tria_left,
+ tria_right,
+ tria,
+ /*tolerance*/ 0.,
+ /*copy_manifold_ids*/ false,
+ /*copy_boundary_ids*/ true);
+ tria.refine_global(refinements);
+ }
+
+ // @sect3{Set up and run point-to-point interpolation}
+ //
+ // The main purpose of this function is to fill a
+ // `FERemoteEvaluationCommunicator` object that is needed for point-to-point
+ // interpolation. Additionally, the corresponding remote evaluators are set up
+ // using this remote communicator. Eventually, the operators are handed to the
+ // time integrator that runs the simulation.
+ //
+ template
+ void run_with_point_to_point_interpolation(
+ const MatrixFree &matrix_free,
+ const std::set &non_matching_faces,
+ const std::map> &materials,
+ const double end_time,
+ const Function &initial_condition,
+ const std::string &vtk_prefix)
+ {
+ const auto &dof_handler = matrix_free.get_dof_handler();
+ const auto &tria = dof_handler.get_triangulation();
+
+ // Communication objects know about the communication pattern. I.e.,
+ // they know about the cells and quadrature points that have to be
+ // evaluated at remote faces. This information is given via
+ // RemotePointEvaluation. Additionally, the communication objects
+ // have to be able to match the quadrature points of the remote
+ // points (that provide exterior information) to the quadrature points
+ // defined at the interior cell. In case of point-to-point interpolation
+ // a vector of pairs with face batch Ids and the number of faces in the
+ // batch is needed. @c FERemoteCommunicationObjectEntityBatches
+ // is a container to store this information.
+ //
+ // The information is filled outside of the actual class since in some cases
+ // the information is available from some heuristic and
+ // it is possible to skip some expensive operations. This is for example
+ // the case for sliding rotating interfaces with equally spaced elements on
+ // both sides of the non-matching interface @cite duerrwaechter2021an.
+ //
+ // For the standard case of point to point-to-point interpolation without
+ // any heuristic we make use of the utility function
+ // @c compute_remote_communicator_faces_point_to_point_interpolation().
+ // Please refer to this function to see how to manually set up the
+ // remote communicator from outside.
+
+ std::vector<
+ std::pair()>>>
+ non_matching_faces_marked_vertices;
+
+ for (const auto &nm_face : non_matching_faces)
+ {
+ // Sufficient lambda, that rules out all cells connected to the current
+ // side of the non-matching interface to avoid self intersections.
+ auto marked_vertices = [&]() {
+ // only search points at cells that are not connected to
+ // @c nm_face
+ std::vector mask(tria.n_vertices(), true);
+
+ for (const auto &cell : tria.active_cell_iterators())
+ for (auto const &f : cell->face_indices())
+ if (cell->face(f)->at_boundary() &&
+ cell->face(f)->boundary_id() == nm_face)
+ for (const auto v : cell->vertex_indices())
+ mask[cell->vertex_index(v)] = false;
+
+ return mask;
+ };
+
+ non_matching_faces_marked_vertices.emplace_back(
+ std::make_pair(nm_face, marked_vertices));
+ }
+
+ auto remote_communicator =
+ Utilities::compute_remote_communicator_faces_point_to_point_interpolation(
+ matrix_free, non_matching_faces_marked_vertices);
+
+ // We are using point-to-point interpolation and can therefore
+ // easily access the corresponding data at face batches. This
+ // is why we use a @c VectorizedArray as @c remote_value_type
+ using remote_value_type = VectorizedArray;
+
+ // Set up FERemoteEvaluation object that accesses the pressure
+ // at remote faces.
+ const auto pressure_r =
+ std::make_shared>(
+ remote_communicator, dof_handler, /*first_selected_component*/ 0);
+
+ // Set up FERemoteEvaluation object that accesses the velocity
+ // at remote faces.
+ const auto velocity_r =
+ std::make_shared>(
+ remote_communicator, dof_handler, /*first_selected_component*/ 1);
+
+ // Set up cell-wise material data.
+ const auto material_data =
+ std::make_shared>(matrix_free, materials);
+
+ // If we have an inhomogeneous problem, we have to set up the
+ // material handler that accesses the materials at remote faces.
+ const auto c_r =
+ std::make_shared>(
+ remote_communicator,
+ matrix_free.get_dof_handler().get_triangulation(),
+ /*first_selected_component*/ 0);
+ const auto rho_r =
+ std::make_shared>(
+ remote_communicator,
+ matrix_free.get_dof_handler().get_triangulation(),
+ /*first_selected_component*/ 0);
+
+ if (!material_data->is_homogeneous())
+ {
+ // Initialize and fill DoF vectors that contain the materials.
+ Vector c(
+ matrix_free.get_dof_handler().get_triangulation().n_active_cells());
+ Vector rho(
+ matrix_free.get_dof_handler().get_triangulation().n_active_cells());
+
+ for (const auto &cell : matrix_free.get_dof_handler()
+ .get_triangulation()
+ .active_cell_iterators())
+ {
+ c[cell->active_cell_index()] =
+ materials.at(cell->material_id()).first;
+ rho[cell->active_cell_index()] =
+ materials.at(cell->material_id()).second;
+ }
+
+ // Materials do not change during the simulation, therefore
+ // there is no need to precompute the values after
+ // the first @c gather_evaluate() again.
+ c_r->gather_evaluate(c, EvaluationFlags::values);
+ rho_r->gather_evaluate(rho, EvaluationFlags::values);
+ }
+
+
+ // Set up inverse mass operator.
+ const auto inverse_mass_operator =
+ std::make_shared>(matrix_free);
+
+ // Set up the acoustic operator. Using
+ // `remote_value_type=VectorizedArray` makes the operator use
+ // point-to-point interpolation.
+ const auto acoustic_operator =
+ std::make_shared>(
+ matrix_free,
+ material_data,
+ non_matching_faces,
+ pressure_r,
+ velocity_r,
+ c_r,
+ rho_r);
+
+ // Compute the the maximum speed of sound, needed for the computation of
+ // the time-step size.
+ double speed_of_sound_max = 0.0;
+ for (const auto &mat : materials)
+ speed_of_sound_max = std::max(speed_of_sound_max, mat.second.first);
+
+ // Set up time integrator.
+ RungeKutta2 time_integrator(
+ inverse_mass_operator, acoustic_operator);
+
+ // For considered examples, we found a limiting Courant number of
+ // $\mathrm{Cr}\approx 0.36$ to maintain stability. To ensure, the
+ // error of the temporal discretization is small, we use a considerably
+ // smaller Courant number of $0.2$.
+ time_integrator.run(matrix_free,
+ /*Cr*/ 0.2,
+ end_time,
+ speed_of_sound_max,
+ initial_condition,
+ vtk_prefix);
+ }
+
+ // @sect3{Set up and run Nitsche-type mortaring}
+ //
+ // The main purpose of this function is to fill a
+ // `FERemoteEvaluationCommunicator` object that is needed for Nitsche-type
+ // mortaring. Additionally, the corresponding remote evaluators are set up
+ // using this remote communicator. Eventually, the operators are handed to the
+ // time integrator that runs the simulation.
+ //
+ template
+ void run_with_nitsche_type_mortaring(
+ const MatrixFree &matrix_free,
+ const std::set &non_matching_faces,
+ const std::map> &materials,
+ const double end_time,
+ const Function &initial_condition,
+ const std::string &vtk_prefix)
+ {
+#ifndef DEAL_II_WITH_CGAL
+ (void)matrix_free;
+ (void)non_matching_faces;
+ (void)materials;
+ (void)end_time;
+ (void)initial_condition;
+ (void)vtk_prefix;
+
+ ConditionalOStream pcout(
+ std::cout, (Utilities::MPI::this_mpi_process(MPI_COMM_WORLD) == 0));
+
+ pcout << "In this function, mortars are computed using CGAL. "
+ "Configure deal.II with DEAL_II_WITH_CGAL to run this function.\n";
+
+ return;
+#else
+
+ const auto &dof_handler = matrix_free.get_dof_handler();
+ const auto &tria = dof_handler.get_triangulation();
+ const auto &mapping = *matrix_free.get_mapping_info().mapping;
+ const auto n_quadrature_pnts = matrix_free.get_quadrature().size();
+
+ // In case of Nitsche-type mortaring a vector of pairs with cell iterator
+ // and face number is needed as communication object.
+ // @c FERemoteCommunicationObjectFaces is a container to store this
+ // information.
+ //
+ // For the standard case of Nitsche-type mortaring without
+ // any heuristic we make use of the utility function
+ // @c compute_remote_communicator_faces_nitsche_type_mortaring().
+ // Please refer to this function to see how to manually set up the
+ // remote communicator from outside and how to reinit
+ // NonMatching::MappingInfo.
+
+ std::vector<
+ std::pair()>>>
+ non_matching_faces_marked_vertices;
+
+ for (const auto &nm_face : non_matching_faces)
+ {
+ // Sufficient lambda, that rules out all cells connected to the current
+ // side of the non-matching interface to avoid self intersections.
+ auto marked_vertices = [&]() {
+ // only search points at cells that are not connected to
+ // @c nm_face
+ std::vector mask(tria.n_vertices(), true);
+
+ for (const auto &cell : tria.active_cell_iterators())
+ for (auto const &f : cell->face_indices())
+ if (cell->face(f)->at_boundary() &&
+ cell->face(f)->boundary_id() == nm_face)
+ for (const auto v : cell->vertex_indices())
+ mask[cell->vertex_index(v)] = false;
+
+ return mask;
+ };
+
+ non_matching_faces_marked_vertices.emplace_back(
+ std::make_pair(nm_face, marked_vertices));
+ }
+
+ // Quadrature points are arbitrarily distributed on each non-matching
+ // face. Therefore, we have to make use of FEFacePointEvaluation.
+ // FEFacePointEvaluation needs NonMatching::MappingInfo to work at the
+ // correct quadrature points that are in sync with used FERemoteEvaluation
+ // object. Using
+ // `compute_remote_communicator_faces_nitsche_type_mortaring()` to reinit
+ // NonMatching::MappingInfo ensures this. In the case of mortaring, we have
+ // to use the weights provided by the quadrature rules that are used to set
+ // up NonMatching::MappingInfo. Therefore we set the flag @c
+ // use_global_weights.
+ typename NonMatching::MappingInfo::AdditionalData
+ additional_data;
+ additional_data.use_global_weights = true;
+
+ // Set up NonMatching::MappingInfo with needed update flags and
+ // @c additional_data.
+ auto nm_mapping_info =
+ std::make_shared>(
+ mapping,
+ update_values | update_JxW_values | update_normal_vectors |
+ update_quadrature_points,
+ additional_data);
+
+ auto remote_communicator =
+ Utilities::compute_remote_communicator_faces_nitsche_type_mortaring(
+ matrix_free,
+ non_matching_faces_marked_vertices,
+ n_quadrature_pnts,
+ 0,
+ nm_mapping_info.get());
+
+ // Same as above but since quadrature points are aribtrarily distributed
+ // we have to consider each face in a batch separately and can not make
+ // use of @c VecorizedArray.
+ using remote_value_type = Number;
+
+ const auto pressure_r =
+ std::make_shared>(
+ remote_communicator, dof_handler, /*first_selected_component*/ 0);
+
+ const auto velocity_r =
+ std::make_shared>(
+ remote_communicator, dof_handler, /*first_selected_component*/ 1);
+
+ const auto material_data =
+ std::make_shared>(matrix_free, materials);
+
+ const auto c_r =
+ std::make_shared>(
+ remote_communicator,
+ matrix_free.get_dof_handler().get_triangulation(),
+ /*first_selected_component*/ 0);
+ const auto rho_r =
+ std::make_shared>(
+ remote_communicator,
+ matrix_free.get_dof_handler().get_triangulation(),
+ /*first_selected_component*/ 0);
+
+ if (!material_data->is_homogeneous())
+ {
+ Vector c(
+ matrix_free.get_dof_handler().get_triangulation().n_active_cells());
+ Vector rho(
+ matrix_free.get_dof_handler().get_triangulation().n_active_cells());
+
+ for (const auto &cell : matrix_free.get_dof_handler()
+ .get_triangulation()
+ .active_cell_iterators())
+ {
+ c[cell->active_cell_index()] =
+ materials.at(cell->material_id()).first;
+ rho[cell->active_cell_index()] =
+ materials.at(cell->material_id()).second;
+ }
+
+ c_r->gather_evaluate(c, EvaluationFlags::values);
+ rho_r->gather_evaluate(rho, EvaluationFlags::values);
+ }
+
+ // Set up inverse mass operator.
+ const auto inverse_mass_operator =
+ std::make_shared>(matrix_free);
+
+ // Set up the acoustic operator. Using `remote_value_type=Number`
+ // makes the operator use Nitsche-type mortaring.
+ const auto acoustic_operator =
+ std::make_shared>(
+ matrix_free,
+ material_data,
+ non_matching_faces,
+ pressure_r,
+ velocity_r,
+ c_r,
+ rho_r,
+ nm_mapping_info);
+
+ // Compute the the maximum speed of sound, needed for the computation of
+ // the time-step size.
+ double speed_of_sound_max = 0.0;
+ for (const auto &mat : materials)
+ speed_of_sound_max = std::max(speed_of_sound_max, mat.second.first);
+
+
+ // Set up time integrator.
+ RungeKutta2 time_integrator(
+ inverse_mass_operator, acoustic_operator);
+
+ // Run time loop with Courant number $0.2$.
+ time_integrator.run(matrix_free,
+ /*Cr*/ 0.2,
+ end_time,
+ speed_of_sound_max,
+ initial_condition,
+ vtk_prefix);
+#endif
+ }
+} // namespace Step89
+
+
+// @sect3{main()}
+//
+// Finally, the `main()` function executes the different versions of handling
+// non-matching interfaces.
+int main(int argc, char *argv[])
+{
+ using namespace dealii;
+ constexpr int dim = 2;
+ using Number = double;
+
+ Utilities::MPI::MPI_InitFinalize mpi(argc, argv);
+ std::cout.precision(5);
+ ConditionalOStream pcout(std::cout,
+ (Utilities::MPI::this_mpi_process(MPI_COMM_WORLD) ==
+ 0));
+
+ const unsigned int refinements = 1;
+ const unsigned int degree = 3;
+
+ // Construct non-matching triangulation and fill non-matching boundary IDs.
+
+ // Similar to step-87, the minimum requirement of this tutorial is MPI.
+ // parallel::distributed::Triangulation is used if deal.II is configured
+ // with p4est. Otherwise parallel::shared::Triangulation is used.
+#ifdef DEAL_II_WITH_P4EST
+ parallel::distributed::Triangulation tria(MPI_COMM_WORLD);
+#else
+ parallel::shared::Triangulation tria(MPI_COMM_WORLD);
+#endif
+
+ pcout << "Create non-matching grid..." << std::endl;
+
+ std::set non_matching_faces;
+ Step89::build_non_matching_triangulation(tria,
+ non_matching_faces,
+ refinements);
+
+ pcout << " - Refinement level: " << refinements << std::endl;
+ pcout << " - Number of cells: " << tria.n_cells() << std::endl;
+
+ // Set up MatrixFree.
+
+ pcout << "Create DoFHandler..." << std::endl;
+ DoFHandler dof_handler(tria);
+ dof_handler.distribute_dofs(FESystem(FE_DGQ(degree), dim + 1));
+ pcout << " - Number of DoFs: " << dof_handler.n_dofs() << std::endl;
+
+ AffineConstraints constraints;
+ constraints.close();
+
+ pcout << "Set up MatrixFree..." << std::endl;
+ typename MatrixFree::AdditionalData data;
+ data.mapping_update_flags = update_gradients | update_values;
+ data.mapping_update_flags_inner_faces = update_values;
+ data.mapping_update_flags_boundary_faces =
+ update_quadrature_points | update_values;
+
+ MatrixFree matrix_free;
+ matrix_free.reinit(
+ MappingQ1(), dof_handler, constraints, QGauss(degree + 1), data);
+
+
+ //@sect4{Run vibrating membrane test case}
+ pcout << "Run vibrating membrane test case..." << std::endl;
+ // Vibrating membrane test case:
+ //
+ // Homogeneous pressure DBCs are applied for simplicity. Therefore,
+ // modes can not be chosen arbitrarily.
+ const double modes = 10.0;
+ std::map> homogeneous_material;
+ homogeneous_material[numbers::invalid_material_id] = std::make_pair(1.0, 1.0);
+ const auto initial_solution_membrane =
+ Step89::InitialConditionVibratingMembrane(modes);
+
+ pcout << " - Point-to-point interpolation: " << std::endl;
+ // Run vibrating membrane test case using point-to-point interpolation:
+
+ Step89::run_with_point_to_point_interpolation(
+ matrix_free,
+ non_matching_faces,
+ homogeneous_material,
+ 8.0 * initial_solution_membrane.get_period_duration(
+ homogeneous_material.begin()->second.first),
+ initial_solution_membrane,
+ "vm-p2p");
+
+ pcout << " - Nitsche-type mortaring: " << std::endl;
+ // Run vibrating membrane test case using Nitsche-type mortaring:
+ Step89::run_with_nitsche_type_mortaring(
+ matrix_free,
+ non_matching_faces,
+ homogeneous_material,
+ 8.0 * initial_solution_membrane.get_period_duration(
+ homogeneous_material.begin()->second.first),
+ initial_solution_membrane,
+ "vm-nitsche");
+
+ //@sect4{Run test case with in-homogeneous material}
+ pcout << "Run test case with in-homogeneous material..." << std::endl;
+ // In-homogeneous material test case:
+ //
+ // Run simple test case with in-homogeneous material and Nitsche-type
+ // mortaring:
+ std::map>
+ inhomogeneous_material;
+ inhomogeneous_material[0] = std::make_pair(1.0, 1.0);
+ inhomogeneous_material[1] = std::make_pair(3.0, 1.0);
+ Step89::run_with_nitsche_type_mortaring(matrix_free,
+ non_matching_faces,
+ inhomogeneous_material,
+ /*runtime*/ 0.3,
+ Step89::GaussPulse(0.3, 0.5),
+ "inhomogeneous");
+
+
+ return 0;
+}