SpECTRE Documentation Coverage Report
Current view: top level - Elliptic/DiscontinuousGalerkin - DgOperator.hpp Coverage Total Hit
Commit: 0d1d717af84a81fb1284a20c4013708fe85cc2f0 Lines: 77.8 % 9 7
Test Date: 2026-10-22 17:29:42
Legend: Lines:     hit not hit

            Line data    Source code
       1            0 : // Distributed under the MIT License.
       2              : // See LICENSE.txt for details.
       3              : 
       4              : #pragma once
       5              : 
       6              : #include <cstddef>
       7              : #include <string>
       8              : #include <tuple>
       9              : #include <unordered_map>
      10              : #include <utility>
      11              : 
      12              : #include "DataStructures/DataBox/Tag.hpp"
      13              : #include "DataStructures/DataVector.hpp"
      14              : #include "DataStructures/SliceVariables.hpp"
      15              : #include "DataStructures/TaggedTuple.hpp"
      16              : #include "DataStructures/Tensor/Tensor.hpp"
      17              : #include "DataStructures/Variables.hpp"
      18              : #include "DataStructures/Variables/FrameTransform.hpp"
      19              : #include "Domain/Structure/Direction.hpp"
      20              : #include "Domain/Structure/DirectionMap.hpp"
      21              : #include "Domain/Structure/Element.hpp"
      22              : #include "Domain/Structure/ElementId.hpp"
      23              : #include "Domain/Structure/IndexToSliceAt.hpp"
      24              : #include "Elliptic/Protocols/FirstOrderSystem.hpp"
      25              : #include "Elliptic/Systems/GetFluxesComputer.hpp"
      26              : #include "Elliptic/Systems/GetSourcesComputer.hpp"
      27              : #include "NumericalAlgorithms/DiscontinuousGalerkin/ApplyMassMatrix.hpp"
      28              : #include "NumericalAlgorithms/DiscontinuousGalerkin/Formulation.hpp"
      29              : #include "NumericalAlgorithms/DiscontinuousGalerkin/LiftFlux.hpp"
      30              : #include "NumericalAlgorithms/DiscontinuousGalerkin/LiftFromBoundary.hpp"
      31              : #include "NumericalAlgorithms/DiscontinuousGalerkin/MortarHelpers.hpp"
      32              : #include "NumericalAlgorithms/DiscontinuousGalerkin/NormalDotFlux.hpp"
      33              : #include "NumericalAlgorithms/DiscontinuousGalerkin/ProjectToBoundary.hpp"
      34              : #include "NumericalAlgorithms/DiscontinuousGalerkin/SimpleBoundaryData.hpp"
      35              : #include "NumericalAlgorithms/DiscontinuousGalerkin/SimpleMortarData.hpp"
      36              : #include "NumericalAlgorithms/LinearOperators/Divergence.hpp"
      37              : #include "NumericalAlgorithms/LinearOperators/Divergence.tpp"
      38              : #include "NumericalAlgorithms/LinearOperators/WeakDivergence.hpp"
      39              : #include "NumericalAlgorithms/Spectral/Basis.hpp"
      40              : #include "NumericalAlgorithms/Spectral/Mesh.hpp"
      41              : #include "NumericalAlgorithms/Spectral/Quadrature.hpp"
      42              : #include "NumericalAlgorithms/Spectral/SegmentSize.hpp"
      43              : #include "Utilities/ErrorHandling/Assert.hpp"
      44              : #include "Utilities/Gsl.hpp"
      45              : #include "Utilities/ProtocolHelpers.hpp"
      46              : #include "Utilities/TMPL.hpp"
      47              : 
      48              : /*!
      49              :  * \brief Functionality related to discontinuous Galerkin discretizations of
      50              :  * elliptic equations
      51              :  *
      52              :  * The following is a brief overview of the elliptic DG schemes that are
      53              :  * implemented here. The scheme is described in detail in \cite Fischer2021voj.
      54              :  *
      55              :  * The DG schemes apply to any elliptic PDE that can be formulated in
      56              :  * first-order flux-form, as detailed by
      57              :  * `elliptic::protocols::FirstOrderSystem`.
      58              :  * The DG discretization of equations in this first-order form amounts to
      59              :  * projecting the equations on the set of basis functions that we also use to
      60              :  * represent the fields on the computational grid. The currently implemented DG
      61              :  * operator uses Lagrange interpolating polynomials w.r.t. Legendre-Gauss or
      62              :  * Legendre-Gauss-Lobatto collocation points as basis functions. Skipping all
      63              :  * further details here, the discretization results in a linear equation
      64              :  * \f$A(u)=b\f$ over all grid points and primal variables. Solving the elliptic
      65              :  * equations amounts to numerically inverting the DG operator \f$A\f$, typically
      66              :  * without ever constructing the full matrix but by employing an iterative
      67              :  * linear solver that repeatedly applies the DG operator to "test data". Note
      68              :  * that the DG operator applies directly to the primal variables. Auxiliary
      69              :  * variables are only computed temporarily and don't inflate the size of the
      70              :  * operator. This means the DG operator essentially computes second derivatives
      71              :  * of the primal variables, modified by the fluxes and sources of the system
      72              :  * as well as by DG boundary corrections that couple grid points across element
      73              :  * boundaries.
      74              :  *
      75              :  * \par Boundary corrections:
      76              :  * In this implementation we employ the "internal penalty" DG scheme that
      77              :  * couples grid points across nearest-neighbor elements through the fluxes:
      78              :  *
      79              :  * \f{align}
      80              :  * \label{eq:internal_penalty_auxiliary}
      81              :  * u^* &= \frac{1}{2} \left(u^\mathrm{int} + u^\mathrm{ext}\right) \\
      82              :  * \label{eq:internal_penalty_primal}
      83              :  * (n_i F^i)^* &= \frac{1}{2} n_i \left(
      84              :  * F^i_\mathrm{int} + F^i_\mathrm{ext} \right)
      85              :  * - \sigma n_i F^i(n_j (u^\mathrm{int} - u^\mathrm{ext}))
      86              :  * \f}
      87              :  *
      88              :  * Note that \f$n_i\f$ denotes the face normal on the "interior" side of the
      89              :  * element under consideration. We assume \f$n^\mathrm{ext}_i=-n_i\f$ in the
      90              :  * implementation, i.e. face normals don't depend on the dynamic variables
      91              :  * (which may be discontinuous on element faces). This is the case for the
      92              :  * problems we are expecting to solve, because those will be on fixed background
      93              :  * metrics (e.g. a conformal metric for the XCTS system). Numerically, the face
      94              :  * normals on either side of a mortar may nonetheless be different because the
      95              :  * two faces adjacent to the mortar may resolve them at different resolutions.
      96              :  *
      97              :  * Also note that the numerical fluxes intentionally don't depend on the
      98              :  * auxiliary field values \f$v\f$. This property allows us to communicate data
      99              :  * for both the primal and auxiliary boundary corrections together, instead of
     100              :  * communicating them in two steps. If we were to resort to a two-step
     101              :  * communication we could replace the derivatives in \f$(n_i F^i)^*\f$ with
     102              :  * \f$v\f$, which would result in a generalized "stabilized central flux" that
     103              :  * is slightly less sparse than the internal penalty flux (see e.g.
     104              :  * \cite HesthavenWarburton, section 7.2). We could also choose to ignore the
     105              :  * fluxes in the penalty term, but preliminary tests suggest that this may hurt
     106              :  * convergence.
     107              :  *
     108              :  * For a Poisson system (see `Poisson::FirstOrderSystem`) this numerical flux
     109              :  * reduces to the standard internal penalty flux (see e.g.
     110              :  * \cite HesthavenWarburton, section 7.2, or \cite Arnold2002):
     111              :  *
     112              :  * \f{align}
     113              :  * u^* &= \frac{1}{2} \left(u^\mathrm{int} + u^\mathrm{ext}\right) \\
     114              :  * (n_i F^i)^* &= n_i v_i^* = \frac{1}{2} n_i \left(
     115              :  * \partial_i u^\mathrm{int} + \partial_i u^\mathrm{ext}\right)
     116              :  * - \sigma \left(u^\mathrm{int} - u^\mathrm{ext}\right)
     117              :  * \f}
     118              :  *
     119              :  * where a sum over repeated indices is assumed, since the equation is
     120              :  * formulated on a Euclidean geometry.
     121              :  *
     122              :  * The penalty factor \f$\sigma\f$ is responsible for removing zero eigenmodes
     123              :  * and impacts the conditioning of the linear operator to be solved. See
     124              :  * `elliptic::dg::penalty` for details. For the element size that goes into
     125              :  * computing the penalty we choose
     126              :  * \f$h=\frac{J_\mathrm{volume}}{J_\mathrm{face}}\f$, i.e. the ratio of Jacobi
     127              :  * determinants from logical to inertial coordinates in the element volume and
     128              :  * on the element face, both evaluated on the face (see \cite Vincent2019qpd).
     129              :  * Since both \f$N_\mathrm{points}\f$ and \f$h\f$ can be different on either
     130              :  * side of the element boundary we take the maximum of \f$N_\mathrm{points}\f$
     131              :  * and the pointwise minimum of \f$h\f$ across the element boundary as is done
     132              :  * in \cite Vincent2019qpd. Note that we use the number of points
     133              :  * \f$N_\mathrm{points}\f$ where \cite Vincent2019qpd uses the polynomial degree
     134              :  * \f$N_\mathrm{points} - 1\f$ because we found unstable configurations on
     135              :  * curved meshes when using the polynomial degree. Optimizing the penalty on
     136              :  * curved meshes is subject to further investigation.
     137              :  *
     138              :  * \par Discontinuous fluxes:
     139              :  * The DG operator also supports systems with potentially discontinuous fluxes,
     140              :  * such as elasticity with layered materials. The way to handle the
     141              :  * discontinuous fluxes in the DG scheme is described in \cite Vu2023thn.
     142              :  * Essentially, we evaluate the penalty term in
     143              :  * Eq. $\ref{eq:internal_penalty_primal}$ on both sides of an element boundary
     144              :  * and take the average. The other terms in the numerical flux remain unchanged.
     145              :  */
     146              : namespace elliptic::dg {
     147              : 
     148              : /// Data that is projected to mortars and communicated across element
     149              : /// boundaries
     150              : template <typename PrimalFields, typename PrimalFluxes>
     151            1 : using BoundaryData = ::dg::SimpleBoundaryData<
     152              :     tmpl::append<PrimalFields,
     153              :                  db::wrap_tags_in<::Tags::NormalDotFlux, PrimalFields>>,
     154              :     tmpl::list<>>;
     155              : 
     156              : /// Boundary data on both sides of a mortar.
     157              : ///
     158              : /// \note This is a struct (as opposed to a type alias) so it can be used to
     159              : /// deduce the template parameters
     160              : template <typename TemporalId, typename PrimalFields, typename PrimalFluxes>
     161            1 : struct MortarData
     162              :     : ::dg::SimpleMortarData<TemporalId,
     163              :                              BoundaryData<PrimalFields, PrimalFluxes>,
     164              :                              BoundaryData<PrimalFields, PrimalFluxes>> {};
     165              : 
     166            1 : namespace Tags {
     167              : /// Holds `elliptic::dg::MortarData`, i.e. boundary data on both sides of a
     168              : /// mortar
     169              : template <typename TemporalId, typename PrimalFields, typename PrimalFluxes>
     170            1 : struct MortarData : db::SimpleTag {
     171            0 :   using type = elliptic::dg::MortarData<TemporalId, PrimalFields, PrimalFluxes>;
     172              : };
     173              : }  // namespace Tags
     174              : 
     175              : namespace detail {
     176              : 
     177              : // Mass-conservative restriction: R = M^{-1}_face P^T M_mortar
     178              : //
     179              : // Note that projecting the mortar data times the Jacobian using
     180              : // `Spectral::projection_matrix_child_to_parent(operand_is_massive=false)` is
     181              : // equivalent to this implementation on Gauss grids. However, on Gauss-Lobatto
     182              : // grids we usually diagonally approximate the mass matrix ("mass lumping") but
     183              : // `projection_matrix_child_to_parent(operand_is_massive=false)` uses the full
     184              : // mass matrix. Therefore, the two implementations differ slightly on
     185              : // Gauss-Lobatto grids. Furthermore, note that
     186              : // `projection_matrix_child_to_parent(operand_is_massive=false)` already
     187              : // includes the factors of two that account for the mortar size, so they must be
     188              : // omitted from the mortar Jacobian when using that approach.
     189              : template <typename TagsList, size_t FaceDim>
     190              : Variables<TagsList> mass_conservative_restriction(
     191              :     Variables<TagsList> mortar_vars,
     192              :     [[maybe_unused]] const Mesh<FaceDim>& mortar_mesh,
     193              :     [[maybe_unused]] const ::dg::MortarSize<FaceDim>& mortar_size,
     194              :     [[maybe_unused]] const Scalar<DataVector>& mortar_jacobian,
     195              :     [[maybe_unused]] const Mesh<FaceDim> face_mesh,
     196              :     [[maybe_unused]] const Scalar<DataVector>& face_jacobian) {
     197              :   if constexpr (FaceDim == 0) {
     198              :     return mortar_vars;
     199              :   } else {
     200              :     mortar_vars *= get(mortar_jacobian);
     201              :     ::dg::apply_mass_matrix(make_not_null(&mortar_vars), mortar_mesh);
     202              :     auto face_vars = Spectral::project(
     203              :         mortar_vars, mortar_mesh, face_mesh, mortar_size,
     204              :         make_array<FaceDim>(Spectral::SegmentSize::Full), true);
     205              :     face_vars /= get(face_jacobian);
     206              :     ::dg::apply_inverse_mass_matrix(make_not_null(&face_vars), face_mesh);
     207              :     return face_vars;
     208              :   }
     209              : }
     210              : 
     211              : template <typename System, bool Linearized,
     212              :           typename PrimalFields = typename System::primal_fields,
     213              :           typename PrimalFluxes = typename System::primal_fluxes>
     214              : struct DgOperatorImpl;
     215              : 
     216              : template <typename System, bool Linearized, typename... PrimalFields,
     217              :           typename... PrimalFluxes>
     218              : struct DgOperatorImpl<System, Linearized, tmpl::list<PrimalFields...>,
     219              :                       tmpl::list<PrimalFluxes...>> {
     220              :   static_assert(
     221              :       tt::assert_conforms_to_v<System, elliptic::protocols::FirstOrderSystem>);
     222              : 
     223              :   static constexpr size_t Dim = System::volume_dim;
     224              :   using FluxesComputer = elliptic::get_fluxes_computer<System, Linearized>;
     225              :   using SourcesComputer = elliptic::get_sources_computer<System, Linearized>;
     226              : 
     227              :   struct AllDirections {
     228              :     bool operator()(const Direction<Dim>& /*unused*/) const { return true; }
     229              :   };
     230              : 
     231              :   struct NoDataIsZero {
     232              :     bool operator()(const ElementId<Dim>& /*unused*/) const { return false; }
     233              :   };
     234              : 
     235              :   static constexpr auto full_mortar_size =
     236              :       make_array<Dim - 1>(Spectral::SegmentSize::Full);
     237              : 
     238              :   template <bool AllDataIsZero, typename... DerivVars, typename... PrimalVars,
     239              :             typename... PrimalFluxesVars, typename... PrimalMortarVars,
     240              :             typename... PrimalMortarFluxes, typename TemporalId,
     241              :             typename ApplyBoundaryCondition, typename... FluxesArgs,
     242              :             typename DataIsZero = NoDataIsZero,
     243              :             typename DirectionsPredicate = AllDirections>
     244              :   static void prepare_mortar_data(
     245              :       const gsl::not_null<Variables<tmpl::list<DerivVars...>>*> deriv_vars,
     246              :       const gsl::not_null<Variables<tmpl::list<PrimalFluxesVars...>>*>
     247              :           primal_fluxes,
     248              :       const gsl::not_null<::dg::MortarMap<
     249              :           Dim, MortarData<TemporalId, tmpl::list<PrimalMortarVars...>,
     250              :                           tmpl::list<PrimalMortarFluxes...>>>*>
     251              :           all_mortar_data,
     252              :       const Variables<tmpl::list<PrimalVars...>>& primal_vars,
     253              :       const Element<Dim>& element, const Mesh<Dim>& mesh,
     254              :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     255              :                             Frame::Inertial>& inv_jacobian,
     256              :       const DirectionMap<Dim, tnsr::i<DataVector, Dim>>& face_normals,
     257              :       const ::dg::MortarMap<Dim, Mesh<Dim - 1>>& all_mortar_meshes,
     258              :       const ::dg::MortarMap<Dim, ::dg::MortarSize<Dim - 1>>& all_mortar_sizes,
     259              :       const TemporalId& temporal_id,
     260              :       const ApplyBoundaryCondition& apply_boundary_condition,
     261              :       const std::tuple<FluxesArgs...>& fluxes_args,
     262              :       const DataIsZero& data_is_zero = NoDataIsZero{},
     263              :       const DirectionsPredicate& directions_predicate = AllDirections{}) {
     264              :     static_assert(
     265              :         sizeof...(PrimalVars) == sizeof...(PrimalFields) and
     266              :             sizeof...(PrimalFluxesVars) == sizeof...(PrimalFluxes),
     267              :         "The number of variables must match the number of system fields.");
     268              :     static_assert(
     269              :         (std::is_same_v<typename PrimalVars::type,
     270              :                         typename PrimalFields::type> and
     271              :          ...) and
     272              :             (std::is_same_v<typename PrimalFluxesVars::type,
     273              :                             typename PrimalFluxes::type> and
     274              :              ...),
     275              :         "The variables must have the same tensor types as the system fields.");
     276              : #ifdef SPECTRE_DEBUG
     277              :     for (size_t d = 0; d < Dim; ++d) {
     278              :       ASSERT(mesh.basis(d) == Spectral::Basis::Legendre and
     279              :                  (mesh.quadrature(d) == Spectral::Quadrature::GaussLobatto or
     280              :                   mesh.quadrature(d) == Spectral::Quadrature::Gauss),
     281              :              "The elliptic DG operator is currently only implemented for "
     282              :              "Legendre-Gauss(-Lobatto) grids. Found basis '"
     283              :                  << mesh.basis(d) << "' and quadrature '" << mesh.quadrature(d)
     284              :                  << "' in dimension " << d << ".");
     285              :     }
     286              : #endif  // SPECTRE_DEBUG
     287              :     const auto& element_id = element.id();
     288              :     const bool local_data_is_zero = data_is_zero(element_id);
     289              :     ASSERT(Linearized or not local_data_is_zero,
     290              :            "Only a linear operator can take advantage of the knowledge that "
     291              :            "the operand is zero. Don't return 'true' in 'data_is_zero' unless "
     292              :            "you also set 'Linearized' to 'true'.");
     293              :     const size_t num_points = mesh.number_of_grid_points();
     294              : 
     295              :     // This function and the one below allocate various Variables to compute
     296              :     // intermediate quantities. It could be a performance optimization to reduce
     297              :     // the number of these allocations and/or move some of the memory buffers
     298              :     // into the DataBox to keep them around permanently. The latter should be
     299              :     // informed by profiling.
     300              : 
     301              :     // Compute partial derivatives grad(u) of the system variables, and from
     302              :     // those the fluxes F^i(grad(u)) in the volume. We will take the divergence
     303              :     // of the fluxes in the `apply_operator` function below to compute the full
     304              :     // elliptic equation -div(F) + S = f(x).
     305              :     if (AllDataIsZero or local_data_is_zero) {
     306              :       deriv_vars->initialize(num_points, 0.);
     307              :       primal_fluxes->initialize(num_points, 0.);
     308              :     } else {
     309              :       // Compute partial derivatives of the variables
     310              :       partial_derivatives(deriv_vars, primal_vars, mesh, inv_jacobian);
     311              :       // Compute the fluxes
     312              :       primal_fluxes->initialize(num_points);
     313              :       std::apply(
     314              :           [&primal_fluxes, &primal_vars, &deriv_vars,
     315              :            &element_id](const auto&... expanded_fluxes_args) {
     316              :             if constexpr (FluxesComputer::is_discontinuous) {
     317              :               FluxesComputer::apply(
     318              :                   make_not_null(&get<PrimalFluxesVars>(*primal_fluxes))...,
     319              :                   expanded_fluxes_args..., element_id,
     320              :                   get<PrimalVars>(primal_vars)...,
     321              :                   get<DerivVars>(*deriv_vars)...);
     322              :             } else {
     323              :               (void)element_id;
     324              :               FluxesComputer::apply(
     325              :                   make_not_null(&get<PrimalFluxesVars>(*primal_fluxes))...,
     326              :                   expanded_fluxes_args..., get<PrimalVars>(primal_vars)...,
     327              :                   get<DerivVars>(*deriv_vars)...);
     328              :             }
     329              :           },
     330              :           fluxes_args);
     331              :     }
     332              : 
     333              :     // Populate the mortar data on this element's side of the boundary so it's
     334              :     // ready to be sent to neighbors.
     335              :     for (const auto& direction : [&element]() -> const auto& {
     336              :            if constexpr (AllDataIsZero) {
     337              :              // Skipping internal boundaries for all-zero data because they
     338              :              // won't contribute boundary corrections anyway (data on both sides
     339              :              // of the boundary is the same). For all-zero data we are
     340              :              // interested in external boundaries, to extract inhomogeneous
     341              :              // boundary conditions from a non-linear operator.
     342              :              return element.external_boundaries();
     343              :            } else {
     344              :              (void)element;
     345              :              return Direction<Dim>::all_directions();
     346              :            };
     347              :          }()) {
     348              :       if (not directions_predicate(direction)) {
     349              :         continue;
     350              :       }
     351              :       const bool is_internal = element.neighbors().contains(direction);
     352              :       // Skip directions altogether when both this element and all neighbors in
     353              :       // the direction have zero data. These boundaries won't contribute
     354              :       // corrections, because the data is the same on both sides. External
     355              :       // boundaries also count as zero data here, because they are linearized
     356              :       // (see assert above).
     357              :       if (local_data_is_zero and
     358              :           (not is_internal or
     359              :            alg::all_of(element.neighbors().at(direction), data_is_zero))) {
     360              :         continue;
     361              :       }
     362              :       const auto face_mesh = mesh.slice_away(direction.dimension());
     363              :       const size_t face_num_points = face_mesh.number_of_grid_points();
     364              :       const auto& face_normal = face_normals.at(direction);
     365              :       Variables<tmpl::list<PrimalFluxesVars...>> primal_fluxes_on_face{};
     366              :       BoundaryData<tmpl::list<PrimalMortarVars...>,
     367              :                    tmpl::list<PrimalMortarFluxes...>>
     368              :           boundary_data{};
     369              :       if (AllDataIsZero or local_data_is_zero) {
     370              :         if (is_internal) {
     371              :           // We manufacture zero boundary data directly on the mortars below.
     372              :           // Nothing to do here.
     373              :         } else {
     374              :           boundary_data.field_data.initialize(face_num_points, 0.);
     375              :         }
     376              :       } else {
     377              :         boundary_data.field_data.initialize(face_num_points);
     378              :         primal_fluxes_on_face.initialize(face_num_points);
     379              :         // Project fields to faces
     380              :         // Note: need to convert tags of `Variables` because
     381              :         // `project_contiguous_data_to_boundary` requires that the face and
     382              :         // volume tags are subsets.
     383              :         Variables<
     384              :             tmpl::list<PrimalVars..., ::Tags::NormalDotFlux<PrimalVars>...>>
     385              :             boundary_data_ref{};
     386              :         boundary_data_ref.set_data_ref(boundary_data.field_data.data(),
     387              :                                        boundary_data.field_data.size());
     388              :         ::dg::project_contiguous_data_to_boundary(
     389              :             make_not_null(&boundary_data_ref), primal_vars, mesh, direction);
     390              :         // Compute n_i F^i on faces
     391              :         ::dg::project_contiguous_data_to_boundary(
     392              :             make_not_null(&primal_fluxes_on_face), *primal_fluxes, mesh,
     393              :             direction);
     394              :         EXPAND_PACK_LEFT_TO_RIGHT(normal_dot_flux(
     395              :             make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     396              :                 boundary_data.field_data)),
     397              :             face_normal, get<PrimalFluxesVars>(primal_fluxes_on_face)));
     398              :       }
     399              : 
     400              :       if (is_internal) {
     401              :         if constexpr (not AllDataIsZero) {
     402              :           // Project boundary data on internal faces to mortars
     403              :           for (const auto& neighbor_id : element.neighbors().at(direction)) {
     404              :             if (local_data_is_zero and data_is_zero(neighbor_id)) {
     405              :               continue;
     406              :             }
     407              :             const ::dg::MortarId<Dim> mortar_id{direction, neighbor_id};
     408              :             const auto& mortar_mesh = all_mortar_meshes.at(mortar_id);
     409              :             const auto& mortar_size = all_mortar_sizes.at(mortar_id);
     410              :             if (local_data_is_zero) {
     411              :               // No need to project anything. We just manufacture zero boundary
     412              :               // data on the mortar.
     413              :               BoundaryData<tmpl::list<PrimalMortarVars...>,
     414              :                            tmpl::list<PrimalMortarFluxes...>>
     415              :                   zero_boundary_data{};
     416              :               zero_boundary_data.field_data.initialize(
     417              :                   mortar_mesh.number_of_grid_points(), 0.);
     418              :               (*all_mortar_data)[mortar_id].local_insert(
     419              :                   temporal_id, std::move(zero_boundary_data));
     420              :               continue;
     421              :             }
     422              :             // When no projection is necessary we can safely move the boundary
     423              :             // data from the face as there is only a single neighbor in this
     424              :             // direction
     425              :             auto projected_boundary_data =
     426              :                 Spectral::needs_projection(face_mesh, mortar_mesh, mortar_size)
     427              :                     // NOLINTNEXTLINE
     428              :                     ? boundary_data.project_to_mortar(face_mesh, mortar_mesh,
     429              :                                                       mortar_size)
     430              :                     : std::move(boundary_data);  // NOLINT
     431              :             (*all_mortar_data)[mortar_id].local_insert(
     432              :                 temporal_id, std::move(projected_boundary_data));
     433              :           }
     434              :         }
     435              :       } else {
     436              :         // No need to do projections on external boundaries
     437              :         const ::dg::MortarId<Dim> mortar_id{
     438              :             direction, ElementId<Dim>::external_boundary_id()};
     439              :         (*all_mortar_data)[mortar_id].local_insert(temporal_id, boundary_data);
     440              : 
     441              :         // -------------------------
     442              :         // Apply boundary conditions
     443              :         // -------------------------
     444              :         //
     445              :         // To apply boundary conditions we fill the boundary data with
     446              :         // "exterior" or "ghost" data and set it as remote mortar data, so
     447              :         // external boundaries behave just like internal boundaries when
     448              :         // applying boundary corrections.
     449              :         //
     450              :         // The `apply_boundary_conditions` invocable is expected to impose
     451              :         // boundary conditions by modifying the fields and fluxes that are
     452              :         // passed by reference. Dirichlet-type boundary conditions are imposed
     453              :         // by modifying the fields, and Neumann-type boundary conditions are
     454              :         // imposed by modifying the interior n.F. Note that all data passed to
     455              :         // the boundary conditions is taken from the "interior" side of the
     456              :         // boundary, i.e. with a normal vector that points _out_ of the
     457              :         // computational domain.
     458              :         Variables<tmpl::list<DerivVars...>> deriv_vars_on_boundary{};
     459              :         if (AllDataIsZero or local_data_is_zero) {
     460              :           deriv_vars_on_boundary.initialize(face_num_points, 0.);
     461              :         } else {
     462              :           deriv_vars_on_boundary.initialize(face_num_points);
     463              :           ::dg::project_contiguous_data_to_boundary(
     464              :               make_not_null(&deriv_vars_on_boundary), *deriv_vars, mesh,
     465              :               direction);
     466              :         }
     467              :         apply_boundary_condition(
     468              :             direction,
     469              :             make_not_null(&get<PrimalMortarVars>(boundary_data.field_data))...,
     470              :             make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     471              :                 boundary_data.field_data))...,
     472              :             get<DerivVars>(deriv_vars_on_boundary)...);
     473              : 
     474              :         // Invert the sign of the fluxes to account for the inverted normal on
     475              :         // exterior faces. Also multiply by 2 and add the interior fluxes to
     476              :         // impose the boundary conditions on the _average_ instead of just
     477              :         // setting the fields on the exterior:
     478              :         //   (Dirichlet)  u_D = avg(u) = 1/2 (u_int + u_ext)
     479              :         //                => u_ext = 2 u_D - u_int
     480              :         //   (Neumann)    (n.F)_N = avg(n.F) = 1/2 [(n.F)_int - (n.F)_ext]
     481              :         //                => (n.F)_ext = -2 (n.F)_N + (n.F)_int]
     482              :         const auto impose_on_average = [](const auto exterior_field,
     483              :                                           const auto& interior_field) {
     484              :           for (size_t i = 0; i < interior_field.size(); ++i) {
     485              :             (*exterior_field)[i] *= 2.;
     486              :             (*exterior_field)[i] -= interior_field[i];
     487              :           }
     488              :         };
     489              :         EXPAND_PACK_LEFT_TO_RIGHT(impose_on_average(
     490              :             make_not_null(&get<PrimalMortarVars>(boundary_data.field_data)),
     491              :             get<PrimalMortarVars>(all_mortar_data->at(mortar_id)
     492              :                                       .local_data(temporal_id)
     493              :                                       .field_data)));
     494              :         const auto invert_sign_and_impose_on_average =
     495              :             [](const auto exterior_n_dot_flux,
     496              :                const auto& interior_n_dot_flux) {
     497              :               for (size_t i = 0; i < interior_n_dot_flux.size(); ++i) {
     498              :                 (*exterior_n_dot_flux)[i] *= -2.;
     499              :                 (*exterior_n_dot_flux)[i] += interior_n_dot_flux[i];
     500              :               }
     501              :             };
     502              :         EXPAND_PACK_LEFT_TO_RIGHT(invert_sign_and_impose_on_average(
     503              :             make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     504              :                 boundary_data.field_data)),
     505              :             get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     506              :                 all_mortar_data->at(mortar_id)
     507              :                     .local_data(temporal_id)
     508              :                     .field_data)));
     509              : 
     510              :         // Store the exterior boundary data on the mortar
     511              :         all_mortar_data->at(mortar_id).remote_insert(temporal_id,
     512              :                                                      std::move(boundary_data));
     513              :       }  // if (is_internal)
     514              :     }  // loop directions
     515              :   }
     516              : 
     517              :   // --- This is essentially a break to communicate the mortar data ---
     518              : 
     519              :   template <typename... OperatorTags, typename... PrimalVars,
     520              :             typename... DerivVars, typename... PrimalFluxesVars,
     521              :             typename... PrimalMortarVars, typename... PrimalMortarFluxes,
     522              :             typename TemporalId, typename... FluxesArgs,
     523              :             typename... SourcesArgs, typename... ModifyBoundaryDataArgs,
     524              :             typename DataIsZero = NoDataIsZero,
     525              :             typename DirectionsPredicate = AllDirections>
     526              :   static void apply_operator(
     527              :       const gsl::not_null<Variables<tmpl::list<OperatorTags...>>*>
     528              :           operator_applied_to_vars,
     529              :       const gsl::not_null<::dg::MortarMap<
     530              :           Dim, MortarData<TemporalId, tmpl::list<PrimalMortarVars...>,
     531              :                           tmpl::list<PrimalMortarFluxes...>>>*>
     532              :           all_mortar_data,
     533              :       const Variables<tmpl::list<PrimalVars...>>& primal_vars,
     534              :       const Variables<tmpl::list<DerivVars...>>& deriv_vars,
     535              :       // Taking the primal fluxes computed in the `prepare_mortar_data` function
     536              :       // by const-ref here because other code might use them and so we don't
     537              :       // want to modify them by adding boundary corrections. E.g. linearized
     538              :       // sources use the nonlinear fields and fluxes as background fields.
     539              :       const Variables<tmpl::list<PrimalFluxesVars...>>& primal_fluxes,
     540              :       const Element<Dim>& element, const Mesh<Dim>& mesh,
     541              :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     542              :                             Frame::Inertial>& inv_jacobian,
     543              :       const Scalar<DataVector>& det_inv_jacobian,
     544              :       const Scalar<DataVector>& det_jacobian,
     545              :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     546              :                             Frame::Inertial>& det_times_inv_jacobian,
     547              :       const DirectionMap<Dim, tnsr::i<DataVector, Dim>>& face_normals,
     548              :       const DirectionMap<Dim, tnsr::I<DataVector, Dim>>& face_normal_vectors,
     549              :       const DirectionMap<Dim, Scalar<DataVector>>& face_normal_magnitudes,
     550              :       const DirectionMap<Dim, Scalar<DataVector>>& face_jacobians,
     551              :       const DirectionMap<Dim,
     552              :                          InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     553              :                                          Frame::Inertial>>&
     554              :           face_jacobian_times_inv_jacobians,
     555              :       const ::dg::MortarMap<Dim, Mesh<Dim - 1>>& all_mortar_meshes,
     556              :       const ::dg::MortarMap<Dim, ::dg::MortarSize<Dim - 1>>& all_mortar_sizes,
     557              :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& mortar_jacobians,
     558              :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& penalty_factors,
     559              :       const bool massive, const ::dg::Formulation formulation,
     560              :       const TemporalId& /*temporal_id*/,
     561              :       const DirectionMap<Dim, std::tuple<FluxesArgs...>>& fluxes_args_on_faces,
     562              :       const std::tuple<SourcesArgs...>& sources_args,
     563              :       const std::tuple<ModifyBoundaryDataArgs...>& modify_boundary_data_args,
     564              :       const DataIsZero& data_is_zero = NoDataIsZero{},
     565              :       const DirectionsPredicate& directions_predicate = AllDirections{}) {
     566              :     static_assert(
     567              :         sizeof...(PrimalVars) == sizeof...(PrimalFields) and
     568              :             sizeof...(PrimalFluxesVars) == sizeof...(PrimalFluxes) and
     569              :             sizeof...(PrimalMortarVars) == sizeof...(PrimalFields) and
     570              :             sizeof...(PrimalMortarFluxes) == sizeof...(PrimalFluxes) and
     571              :             sizeof...(OperatorTags) == sizeof...(PrimalFields),
     572              :         "The number of variables must match the number of system fields.");
     573              :     static_assert(
     574              :         (std::is_same_v<typename PrimalVars::type,
     575              :                         typename PrimalFields::type> and
     576              :          ...) and
     577              :             (std::is_same_v<typename PrimalFluxesVars::type,
     578              :                             typename PrimalFluxes::type> and
     579              :              ...) and
     580              :             (std::is_same_v<typename PrimalMortarVars::type,
     581              :                             typename PrimalFields::type> and
     582              :              ...) and
     583              :             (std::is_same_v<typename PrimalMortarFluxes::type,
     584              :                             typename PrimalFluxes::type> and
     585              :              ...) and
     586              :             (std::is_same_v<typename OperatorTags::type,
     587              :                             typename PrimalFields::type> and
     588              :              ...),
     589              :         "The variables must have the same tensor types as the system fields.");
     590              : #ifdef SPECTRE_DEBUG
     591              :     for (size_t d = 0; d < Dim; ++d) {
     592              :       ASSERT(mesh.basis(d) == Spectral::Basis::Legendre and
     593              :                  (mesh.quadrature(d) == Spectral::Quadrature::GaussLobatto or
     594              :                   mesh.quadrature(d) == Spectral::Quadrature::Gauss),
     595              :              "The elliptic DG operator is currently only implemented for "
     596              :              "Legendre-Gauss(-Lobatto) grids. Found basis '"
     597              :                  << mesh.basis(d) << "' and quadrature '" << mesh.quadrature(d)
     598              :                  << "' in dimension " << d << ".");
     599              :     }
     600              : #endif  // SPECTRE_DEBUG
     601              :     const auto& element_id = element.id();
     602              :     const bool local_data_is_zero = data_is_zero(element_id);
     603              :     ASSERT(Linearized or not local_data_is_zero,
     604              :            "Only a linear operator can take advantage of the knowledge that "
     605              :            "the operand is zero. Don't return 'true' in 'data_is_zero' unless "
     606              :            "you also set 'Linearized' to 'true'.");
     607              :     const size_t num_points = mesh.number_of_grid_points();
     608              : 
     609              :     // This function and the one above allocate various Variables to compute
     610              :     // intermediate quantities. It could be a performance optimization to reduce
     611              :     // the number of these allocations and/or move some of the memory buffers
     612              :     // into the DataBox to keep them around permanently. The latter should be
     613              :     // informed by profiling.
     614              : 
     615              :     // Compute volume terms: -div(F) + S
     616              :     if (local_data_is_zero) {
     617              :       operator_applied_to_vars->initialize(num_points, 0.);
     618              :     } else {
     619              :       // "Massive" operators retain the factors from the volume integral:
     620              :       //   \int_volume div(F) \phi_p = w_p det(J)_p div(F)_p
     621              :       // Here, `w` are the quadrature weights (the diagonal logical mass matrix
     622              :       // with mass-lumping) and det(J) is the Jacobian determinant. The
     623              :       // quantities are evaluated at the grid point `p`.
     624              :       if (formulation == ::dg::Formulation::StrongInertial) {
     625              :         // Compute strong divergence:
     626              :         //   div(F) = (J^\hat{i}_i)_p \sum_q (D_\hat{i})_pq (F^i)_q.
     627              :         divergence(operator_applied_to_vars, primal_fluxes, mesh,
     628              :                    massive ? det_times_inv_jacobian : inv_jacobian);
     629              :         // This is the sign flip that makes the operator _minus_ the Laplacian
     630              :         // for a Poisson system
     631              :         *operator_applied_to_vars *= -1.;
     632              :       } else if (formulation == ::dg::Formulation::StrongLogical) {
     633              :         // Strong divergence but with the Jacobian moved into the divergence:
     634              :         //   div(F) = 1/J_p \sum_q (D_\hat{i})_pq J_q (J^\hat{i}_i)_q (F^i)_q.
     635              :         const auto logical_fluxes = transform::first_index_to_different_frame(
     636              :             primal_fluxes, det_times_inv_jacobian);
     637              :         logical_divergence(operator_applied_to_vars, logical_fluxes, mesh);
     638              :         if (massive) {
     639              :           *operator_applied_to_vars *= -1.;
     640              :         } else {
     641              :           *operator_applied_to_vars *= -1. * get(det_inv_jacobian);
     642              :         }
     643              :       } else if (formulation == ::dg::Formulation::WeakInertial) {
     644              :         // Compute weak divergence:
     645              :         //   F^i \partial_i \phi = 1/w_p \sum_q
     646              :         //     (D^T_\hat{i})_pq (w det(J) J^\hat{i}_i F^i)_q
     647              :         weak_divergence(operator_applied_to_vars, primal_fluxes, mesh,
     648              :                         det_times_inv_jacobian);
     649              :         if (not massive) {
     650              :           *operator_applied_to_vars *= get(det_inv_jacobian);
     651              :         }
     652              :       } else {
     653              :         ERROR("Unsupported DG formulation: "
     654              :               << formulation
     655              :               << "\nSupported formulations are: StrongInertial, WeakInertial, "
     656              :                  "StrongLogical.");
     657              :       }
     658              :       if constexpr (not std::is_same_v<SourcesComputer, void>) {
     659              :         Variables<tmpl::list<OperatorTags...>> sources{num_points, 0.};
     660              :         std::apply(
     661              :             [&sources, &primal_vars, &deriv_vars,
     662              :              &primal_fluxes](const auto&... expanded_sources_args) {
     663              :               SourcesComputer::apply(
     664              :                   make_not_null(&get<OperatorTags>(sources))...,
     665              :                   expanded_sources_args..., get<PrimalVars>(primal_vars)...,
     666              :                   get<DerivVars>(deriv_vars)...,
     667              :                   get<PrimalFluxesVars>(primal_fluxes)...);
     668              :             },
     669              :             sources_args);
     670              :         if (massive) {
     671              :           sources *= get(det_jacobian);
     672              :         }
     673              :         *operator_applied_to_vars += sources;
     674              :       }
     675              :     }
     676              :     if (massive) {
     677              :       ::dg::apply_mass_matrix(operator_applied_to_vars, mesh);
     678              :     }
     679              : 
     680              :     // Add boundary corrections
     681              :     // Keeping track if any corrections were applied here, for an optimization
     682              :     // below
     683              :     bool has_any_boundary_corrections = false;
     684              :     Variables<tmpl::list<transform::Tags::TransformedFirstIndex<
     685              :         PrimalFluxesVars, Frame::ElementLogical>...>>
     686              :         lifted_logical_aux_boundary_corrections{num_points, 0.};
     687              :     for (auto& [mortar_id, mortar_data] : *all_mortar_data) {
     688              :       const auto& direction = mortar_id.direction();
     689              :       const auto& neighbor_id = mortar_id.id();
     690              :       const bool is_internal =
     691              :           (neighbor_id != ElementId<Dim>::external_boundary_id());
     692              :       if (not directions_predicate(direction)) {
     693              :         continue;
     694              :       }
     695              :       // When the data on both sides of the mortar is zero then we don't need to
     696              :       // handle this mortar at all.
     697              :       if (local_data_is_zero and
     698              :           (not is_internal or data_is_zero(neighbor_id))) {
     699              :         continue;
     700              :       }
     701              :       has_any_boundary_corrections = true;
     702              : 
     703              :       const auto face_mesh = mesh.slice_away(direction.dimension());
     704              :       auto [local_data, remote_data] = mortar_data.extract();
     705              : 
     706              :       if (is_internal) {
     707              :         if constexpr (Linearized and
     708              :                       not std::is_same_v<typename System::modify_boundary_data,
     709              :                                          void>) {
     710              :           // Apply a linearized modification to received boundary data.
     711              :           // This allows modifications to depend linearly on the variables.
     712              :           std::apply(
     713              :               [&rem_data = remote_data, &loc_data = local_data,
     714              :                m_id = mortar_id](const auto&... args) {
     715              :                 System::modify_boundary_data::apply_linearized(
     716              :                     make_not_null(
     717              :                         &get<PrimalMortarVars>(rem_data.field_data))...,
     718              :                     make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     719              :                         rem_data.field_data))...,
     720              :                     get<PrimalMortarVars>(loc_data.field_data)...,
     721              :                     get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     722              :                         loc_data.field_data)...,
     723              :                     m_id, args...);
     724              :               },
     725              :               modify_boundary_data_args);
     726              :         }
     727              :       }
     728              : 
     729              :       const size_t face_num_points = face_mesh.number_of_grid_points();
     730              :       const auto& face_normal = face_normals.at(direction);
     731              :       const auto& face_normal_vector = face_normal_vectors.at(direction);
     732              :       const auto& fluxes_args_on_face = fluxes_args_on_faces.at(direction);
     733              :       const auto& face_normal_magnitude = face_normal_magnitudes.at(direction);
     734              :       const auto& face_jacobian = face_jacobians.at(direction);
     735              :       const auto& face_jacobian_times_inv_jacobian =
     736              :           face_jacobian_times_inv_jacobians.at(direction);
     737              :       const auto& mortar_mesh =
     738              :           is_internal ? all_mortar_meshes.at(mortar_id) : face_mesh;
     739              :       const auto& mortar_size =
     740              :           is_internal ? all_mortar_sizes.at(mortar_id) : full_mortar_size;
     741              : 
     742              :       // This is the strong auxiliary boundary correction:
     743              :       //   G^i = F^i(n_j (avg(u) - u))
     744              :       // where
     745              :       //   avg(u) - u = -0.5 * (u_int - u_ext)
     746              :       auto avg_vars_on_mortar = Variables<tmpl::list<PrimalMortarVars...>>(
     747              :           local_data.field_data
     748              :               .template extract_subset<tmpl::list<PrimalMortarVars...>>());
     749              :       const auto add_remote_contribution = [](auto& lhs, const auto& rhs) {
     750              :         for (size_t i = 0; i < lhs.size(); ++i) {
     751              :           lhs[i] -= rhs[i];
     752              :         }
     753              :       };
     754              :       EXPAND_PACK_LEFT_TO_RIGHT(add_remote_contribution(
     755              :           get<PrimalMortarVars>(avg_vars_on_mortar),
     756              :           get<PrimalMortarVars>(remote_data.field_data)));
     757              :       avg_vars_on_mortar *= -0.5;
     758              : 
     759              :       // Project from the mortar back down to the face if needed
     760              :       const auto avg_vars_on_face =
     761              :           Spectral::needs_projection(face_mesh, mortar_mesh, mortar_size)
     762              :               ? mass_conservative_restriction(
     763              :                     std::move(avg_vars_on_mortar), mortar_mesh, mortar_size,
     764              :                     mortar_jacobians.at(mortar_id), face_mesh, face_jacobian)
     765              :               : std::move(avg_vars_on_mortar);
     766              : 
     767              :       // Apply fluxes to get G^i
     768              :       Variables<tmpl::list<PrimalFluxesVars...>> auxiliary_boundary_corrections{
     769              :           face_num_points};
     770              :       std::apply(
     771              :           [&auxiliary_boundary_corrections, &face_normal, &face_normal_vector,
     772              :            &avg_vars_on_face,
     773              :            &element_id](const auto&... expanded_fluxes_args_on_face) {
     774              :             if constexpr (FluxesComputer::is_discontinuous) {
     775              :               FluxesComputer::apply(make_not_null(&get<PrimalFluxesVars>(
     776              :                                         auxiliary_boundary_corrections))...,
     777              :                                     expanded_fluxes_args_on_face..., element_id,
     778              :                                     face_normal, face_normal_vector,
     779              :                                     get<PrimalMortarVars>(avg_vars_on_face)...);
     780              :             } else {
     781              :               (void)element_id;
     782              :               FluxesComputer::apply(make_not_null(&get<PrimalFluxesVars>(
     783              :                                         auxiliary_boundary_corrections))...,
     784              :                                     expanded_fluxes_args_on_face...,
     785              :                                     face_normal, face_normal_vector,
     786              :                                     get<PrimalMortarVars>(avg_vars_on_face)...);
     787              :             }
     788              :           },
     789              :           fluxes_args_on_face);
     790              : 
     791              :       // Lifting for the auxiliary boundary correction:
     792              :       //   \int_face G^i \partial_i \phi
     793              :       // We first transform the flux index to the logical frame, apply the
     794              :       // quadrature weights and the Jacobian for the face integral, then take
     795              :       // the logical weak divergence in the volume after lifting (below the loop
     796              :       // over faces).
     797              :       auto logical_aux_boundary_corrections =
     798              :           transform::first_index_to_different_frame(
     799              :               auxiliary_boundary_corrections, face_jacobian_times_inv_jacobian);
     800              :       ::dg::apply_mass_matrix(make_not_null(&logical_aux_boundary_corrections),
     801              :                               face_mesh);
     802              :       if (mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto) {
     803              :         add_slice_to_data(
     804              :             make_not_null(&lifted_logical_aux_boundary_corrections),
     805              :             logical_aux_boundary_corrections, mesh.extents(),
     806              :             direction.dimension(),
     807              :             index_to_slice_at(mesh.extents(), direction));
     808              :       } else {
     809              :         ::dg::lift_boundary_terms_gauss_points(
     810              :             make_not_null(&lifted_logical_aux_boundary_corrections),
     811              :             logical_aux_boundary_corrections, mesh, direction);
     812              :       }
     813              : 
     814              :       // This is the strong primal boundary correction:
     815              :       //   -n.H = -avg(n.F) + n.F + penalty * n.F(n_j jump(u))
     816              :       // Note that the "internal penalty" numerical flux
     817              :       // (as opposed to the LLF flux) uses the raw field derivatives without
     818              :       // boundary corrections in the average, which is why we can communicate
     819              :       // the data so early together with the auxiliary boundary data. In this
     820              :       // case the penalty needs to include a factor N_points^2 / h (see the
     821              :       // `penalty` function).
     822              :       const auto& penalty_factor = penalty_factors.at(mortar_id);
     823              :       // Compute jump on mortar:
     824              :       //   penalty * jump(u) = penalty * (u_int - u_ext)
     825              :       const auto add_remote_jump_contribution =
     826              :           [&penalty_factor](auto& lhs, const auto& rhs) {
     827              :             for (size_t i = 0; i < lhs.size(); ++i) {
     828              :               lhs[i] -= rhs[i];
     829              :               lhs[i] *= get(penalty_factor);
     830              :             }
     831              :           };
     832              :       EXPAND_PACK_LEFT_TO_RIGHT(add_remote_jump_contribution(
     833              :           get<PrimalMortarVars>(local_data.field_data),
     834              :           get<PrimalMortarVars>(remote_data.field_data)));
     835              :       // Compute average on mortar:
     836              :       //   (strong)  -avg(n.F) + n.F = 0.5 * (n.F)_int + 0.5 * (n.F)_ext
     837              :       //   (weak)    -avg(n.F) = -0.5 * (n.F)_int + 0.5 * (n.F)_ext
     838              :       const auto add_avg_contribution = [](auto& lhs, const auto& rhs,
     839              :                                            const double factor) {
     840              :         for (size_t i = 0; i < lhs.size(); ++i) {
     841              :           lhs[i] *= factor;
     842              :           lhs[i] += 0.5 * rhs[i];
     843              :         }
     844              :       };
     845              :       const bool is_strong_formulation =
     846              :           formulation == ::dg::Formulation::StrongInertial or
     847              :           formulation == ::dg::Formulation::StrongLogical;
     848              :       EXPAND_PACK_LEFT_TO_RIGHT(add_avg_contribution(
     849              :           get<::Tags::NormalDotFlux<PrimalMortarVars>>(local_data.field_data),
     850              :           get<::Tags::NormalDotFlux<PrimalMortarVars>>(remote_data.field_data),
     851              :           is_strong_formulation ? 0.5 : -0.5));
     852              : 
     853              :       // Project from the mortar back down to the face if needed, lift and add
     854              :       // to operator. See auxiliary boundary corrections above for details.
     855              :       auto primal_boundary_corrections_on_face =
     856              :           Spectral::needs_projection(face_mesh, mortar_mesh, mortar_size)
     857              :               ? mass_conservative_restriction(
     858              :                     std::move(local_data.field_data), mortar_mesh, mortar_size,
     859              :                     mortar_jacobians.at(mortar_id), face_mesh, face_jacobian)
     860              :               : std::move(local_data.field_data);
     861              : 
     862              :       // Compute fluxes for jump term: n.F(n_j jump(u))
     863              :       // If the fluxes are trivial (just the spatial metric), we can skip this
     864              :       // step because the face normal is normalized.
     865              :       if constexpr (not FluxesComputer::is_trivial) {
     866              :         // We reuse the memory buffer from above for the result.
     867              :         std::apply(
     868              :             [&auxiliary_boundary_corrections, &face_normal, &face_normal_vector,
     869              :              &primal_boundary_corrections_on_face,
     870              :              &element_id](const auto&... expanded_fluxes_args_on_face) {
     871              :               if constexpr (FluxesComputer::is_discontinuous) {
     872              :                 FluxesComputer::apply(
     873              :                     make_not_null(&get<PrimalFluxesVars>(
     874              :                         auxiliary_boundary_corrections))...,
     875              :                     expanded_fluxes_args_on_face..., element_id, face_normal,
     876              :                     face_normal_vector,
     877              :                     get<PrimalMortarVars>(
     878              :                         primal_boundary_corrections_on_face)...);
     879              :               } else {
     880              :                 (void)element_id;
     881              :                 FluxesComputer::apply(
     882              :                     make_not_null(&get<PrimalFluxesVars>(
     883              :                         auxiliary_boundary_corrections))...,
     884              :                     expanded_fluxes_args_on_face..., face_normal,
     885              :                     face_normal_vector,
     886              :                     get<PrimalMortarVars>(
     887              :                         primal_boundary_corrections_on_face)...);
     888              :               }
     889              :             },
     890              :             fluxes_args_on_face);
     891              :         if constexpr (FluxesComputer::is_discontinuous) {
     892              :           if (is_internal) {
     893              :             // For penalty term with discontinuous fluxes: evaluate the fluxes
     894              :             // on the other side of the boundary as well and take average
     895              :             Variables<tmpl::list<PrimalFluxesVars...>> fluxes_other_side{
     896              :                 face_num_points};
     897              :             std::apply(
     898              :                 [&fluxes_other_side, &face_normal, &face_normal_vector,
     899              :                  &primal_boundary_corrections_on_face,
     900              :                  &local_neighbor_id =
     901              :                      neighbor_id](const auto&... expanded_fluxes_args_on_face) {
     902              :                   FluxesComputer::apply(
     903              :                       make_not_null(
     904              :                           &get<PrimalFluxesVars>(fluxes_other_side))...,
     905              :                       expanded_fluxes_args_on_face..., local_neighbor_id,
     906              :                       face_normal, face_normal_vector,
     907              :                       get<PrimalMortarVars>(
     908              :                           primal_boundary_corrections_on_face)...);
     909              :                 },
     910              :                 fluxes_args_on_face);
     911              :             auxiliary_boundary_corrections += fluxes_other_side;
     912              :             auxiliary_boundary_corrections *= 0.5;
     913              :           }
     914              :         }
     915              :         EXPAND_PACK_LEFT_TO_RIGHT(normal_dot_flux(
     916              :             make_not_null(
     917              :                 &get<PrimalMortarVars>(primal_boundary_corrections_on_face)),
     918              :             face_normal,
     919              :             get<PrimalFluxesVars>(auxiliary_boundary_corrections)));
     920              :       }
     921              : 
     922              :       // Add penalty term to average term
     923              :       Variables<tmpl::list<PrimalMortarVars...>> primal_boundary_corrections{};
     924              :       // First half of the memory allocated above is filled with the penalty
     925              :       // term, so just use that memory here.
     926              :       primal_boundary_corrections.set_data_ref(
     927              :           primal_boundary_corrections_on_face.data(), avg_vars_on_face.size());
     928              :       // Second half of the memory is filled with the average term. Add that to
     929              :       // the penalty term.
     930              :       const auto add_avg_term = [](auto& lhs, const auto& rhs) {
     931              :         for (size_t i = 0; i < lhs.size(); ++i) {
     932              :           lhs[i] += rhs[i];
     933              :         }
     934              :       };
     935              :       EXPAND_PACK_LEFT_TO_RIGHT(
     936              :           add_avg_term(get<PrimalMortarVars>(primal_boundary_corrections),
     937              :                        get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     938              :                            primal_boundary_corrections_on_face)));
     939              : 
     940              :       // Lifting for the primal boundary correction:
     941              :       //   \int_face n.H \phi
     942              :       if (massive) {
     943              :         // We apply the quadrature weights and Jacobian for the face integral,
     944              :         // then lift to the volume
     945              :         primal_boundary_corrections *= get(face_jacobian);
     946              :         ::dg::apply_mass_matrix(make_not_null(&primal_boundary_corrections),
     947              :                                 face_mesh);
     948              :         if (mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto) {
     949              :           add_slice_to_data(operator_applied_to_vars,
     950              :                             primal_boundary_corrections, mesh.extents(),
     951              :                             direction.dimension(),
     952              :                             index_to_slice_at(mesh.extents(), direction));
     953              :         } else {
     954              :           ::dg::lift_boundary_terms_gauss_points(operator_applied_to_vars,
     955              :                                                  primal_boundary_corrections,
     956              :                                                  mesh, direction);
     957              :         }
     958              :       } else {
     959              :         // Apply an extra inverse mass matrix to the boundary corrections (with
     960              :         // mass lumping, so it's diagonal).
     961              :         // For Gauss-Lobatto grids this divides out the quadrature weights and
     962              :         // Jacobian on the face, leaving only factors perpendicular to the face.
     963              :         // Those are handled by `dg::lift_flux` (with an extra minus sign since
     964              :         // the function was written for evolution systems).
     965              :         // For Gauss grids the quadrature weights and Jacobians are handled
     966              :         // by `::dg::lift_boundary_terms_gauss_points` (which was also written
     967              :         // for evolution systems, hence the extra minus sign).
     968              :         primal_boundary_corrections *= -1.;
     969              :         if (mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto) {
     970              :           ::dg::lift_flux(make_not_null(&primal_boundary_corrections),
     971              :                           mesh.extents(direction.dimension()),
     972              :                           face_normal_magnitude);
     973              :           add_slice_to_data(operator_applied_to_vars,
     974              :                             primal_boundary_corrections, mesh.extents(),
     975              :                             direction.dimension(),
     976              :                             index_to_slice_at(mesh.extents(), direction));
     977              :         } else {
     978              :           // We already have the `face_jacobian = det(J) * magnitude(n)` here,
     979              :           // so just pass a constant 1 for `magnitude(n)`. This could be
     980              :           // optimized to avoid allocating the vector of ones.
     981              :           ::dg::lift_boundary_terms_gauss_points(
     982              :               operator_applied_to_vars, det_inv_jacobian, mesh, direction,
     983              :               primal_boundary_corrections,
     984              :               Scalar<DataVector>{face_mesh.number_of_grid_points(), 1.},
     985              :               face_jacobian);
     986              :         }
     987              :       }
     988              :     }  // loop over all mortars
     989              : 
     990              :     if (not has_any_boundary_corrections) {
     991              :       // No need to handle auxiliary boundary corrections; return early
     992              :       return;
     993              :     }
     994              : 
     995              :     // Apply weak divergence to lifted auxiliary boundary corrections and add to
     996              :     // operator
     997              :     if (massive) {
     998              :       logical_weak_divergence(operator_applied_to_vars,
     999              :                               lifted_logical_aux_boundary_corrections, mesh,
    1000              :                               true);
    1001              :     } else {
    1002              :       // Possible optimization: eliminate this allocation by building the
    1003              :       // inverse mass matrix into `logical_weak_divergence`
    1004              :       Variables<tmpl::list<OperatorTags...>> massless_aux_boundary_corrections{
    1005              :           num_points};
    1006              :       logical_weak_divergence(make_not_null(&massless_aux_boundary_corrections),
    1007              :                               lifted_logical_aux_boundary_corrections, mesh);
    1008              :       massless_aux_boundary_corrections *= get(det_inv_jacobian);
    1009              :       ::dg::apply_inverse_mass_matrix(
    1010              :           make_not_null(&massless_aux_boundary_corrections), mesh);
    1011              :       *operator_applied_to_vars += massless_aux_boundary_corrections;
    1012              :     }
    1013              :   }
    1014              : 
    1015              :   template <typename... FixedSourcesTags, typename ApplyBoundaryCondition,
    1016              :             typename... FluxesArgs, typename... SourcesArgs,
    1017              :             typename... ModifyBoundaryDataArgs>
    1018              :   static void impose_inhomogeneous_boundary_conditions_on_source(
    1019              :       const gsl::not_null<Variables<tmpl::list<FixedSourcesTags...>>*>
    1020              :           fixed_sources,
    1021              :       const Element<Dim>& element, const Mesh<Dim>& mesh,
    1022              :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
    1023              :                             Frame::Inertial>& inv_jacobian,
    1024              :       const Scalar<DataVector>& det_inv_jacobian,
    1025              :       const Scalar<DataVector>& det_jacobian,
    1026              :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
    1027              :                             Frame::Inertial>& det_times_inv_jacobian,
    1028              :       const DirectionMap<Dim, tnsr::i<DataVector, Dim>>& face_normals,
    1029              :       const DirectionMap<Dim, tnsr::I<DataVector, Dim>>& face_normal_vectors,
    1030              :       const DirectionMap<Dim, Scalar<DataVector>>& face_normal_magnitudes,
    1031              :       const DirectionMap<Dim, Scalar<DataVector>>& face_jacobians,
    1032              :       const DirectionMap<Dim,
    1033              :                          InverseJacobian<DataVector, Dim, Frame::ElementLogical,
    1034              :                                          Frame::Inertial>>&
    1035              :           face_jacobian_times_inv_jacobians,
    1036              :       const ::dg::MortarMap<Dim, Mesh<Dim - 1>>& all_mortar_meshes,
    1037              :       const ::dg::MortarMap<Dim, ::dg::MortarSize<Dim - 1>>& all_mortar_sizes,
    1038              :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& mortar_jacobians,
    1039              :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& penalty_factors,
    1040              :       const bool massive, const ::dg::Formulation formulation,
    1041              :       const ApplyBoundaryCondition& apply_boundary_condition,
    1042              :       const std::tuple<FluxesArgs...>& fluxes_args,
    1043              :       const std::tuple<SourcesArgs...>& sources_args,
    1044              :       const DirectionMap<Dim, std::tuple<FluxesArgs...>>& fluxes_args_on_faces,
    1045              :       const std::tuple<ModifyBoundaryDataArgs...>& modify_boundary_data_args)
    1046              :       // This function adds nothing to the fixed sources if the operator is
    1047              :       // linearized, so it shouldn't be used in that case
    1048              :     requires(not Linearized)
    1049              :   {
    1050              :     // We just feed zero variables through the nonlinear operator to extract the
    1051              :     // constant contribution at external boundaries. Since the variables are
    1052              :     // zero the operator simplifies quite a lot. The simplification is probably
    1053              :     // not very important for performance because this function will only be
    1054              :     // called when solving a linear elliptic system and only once during
    1055              :     // initialization, but we specialize the operator for zero data nonetheless
    1056              :     // just so we can ignore internal boundaries. Only when the system modifies
    1057              :     // boundary data do we need to handle internal boundaries (see below),
    1058              :     // otherwise we can skip them.
    1059              :     const size_t num_points = mesh.number_of_grid_points();
    1060              :     const Variables<tmpl::list<PrimalFields...>> zero_primal_vars{num_points,
    1061              :                                                                   0.};
    1062              :     Variables<tmpl::list<PrimalFluxes...>> primal_fluxes_buffer{num_points};
    1063              :     Variables<tmpl::list<
    1064              :         ::Tags::deriv<PrimalFields, tmpl::size_t<Dim>, Frame::Inertial>...>>
    1065              :         zero_deriv_vars{num_points, 0.};
    1066              :     Variables<tmpl::list<FixedSourcesTags...>> operator_applied_to_zero_vars{
    1067              :         num_points};
    1068              :     // Set up data on mortars
    1069              :     ::dg::MortarMap<Dim, MortarData<size_t, tmpl::list<PrimalFields...>,
    1070              :                                     tmpl::list<PrimalFluxes...>>>
    1071              :         all_mortar_data{};
    1072              :     constexpr size_t temporal_id = std::numeric_limits<size_t>::max();
    1073              :     // Apply the operator to the zero variables, skipping internal boundaries
    1074              :     prepare_mortar_data<true>(
    1075              :         make_not_null(&zero_deriv_vars), make_not_null(&primal_fluxes_buffer),
    1076              :         make_not_null(&all_mortar_data), zero_primal_vars, element, mesh,
    1077              :         inv_jacobian, face_normals, all_mortar_meshes, all_mortar_sizes,
    1078              :         temporal_id, apply_boundary_condition, fluxes_args);
    1079              :     // Modify internal boundary data if needed, e.g. to transform from one
    1080              :     // variable to another when crossing element boundaries. This is a nonlinear
    1081              :     // operation in the sense that feeding zero through the operator is nonzero,
    1082              :     // so we evaluate it here and add the contribution to the fixed sources,
    1083              :     // just like inhomogeneous external boundary conditions.
    1084              :     if constexpr (not std::is_same_v<typename System::modify_boundary_data,
    1085              :                                      void>) {
    1086              :       for (const auto& [direction, neighbors] : element.neighbors()) {
    1087              :         for (const auto& neighbor_id : neighbors) {
    1088              :           const ::dg::MortarId<Dim> mortar_id{direction, neighbor_id};
    1089              :           ASSERT(not all_mortar_data.contains(mortar_id),
    1090              :                  "Mortar data with ID " << mortar_id << " already exists.");
    1091              :           auto& mortar_data = all_mortar_data[mortar_id];
    1092              :           // Manufacture zero mortar data, store as local data, then treat as
    1093              :           // received from neighbor and apply modifications
    1094              :           BoundaryData<tmpl::list<PrimalFields...>, tmpl::list<PrimalFluxes...>>
    1095              :               remote_boundary_data_on_mortar{};
    1096              :           remote_boundary_data_on_mortar.field_data.initialize(
    1097              :               all_mortar_meshes.at(mortar_id).number_of_grid_points(), 0.);
    1098              :           mortar_data.local_insert(temporal_id, remote_boundary_data_on_mortar);
    1099              :           // Modify "received" mortar data
    1100              :           std::apply(
    1101              :               [&remote_boundary_data_on_mortar,
    1102              :                &mortar_id](const auto&... args) {
    1103              :                 System::modify_boundary_data::apply(
    1104              :                     make_not_null(&get<PrimalFields>(
    1105              :                         remote_boundary_data_on_mortar.field_data))...,
    1106              :                     make_not_null(&get<::Tags::NormalDotFlux<PrimalFields>>(
    1107              :                         remote_boundary_data_on_mortar.field_data))...,
    1108              :                     mortar_id, args...);
    1109              :               },
    1110              :               modify_boundary_data_args);
    1111              :           // Insert as remote mortar data
    1112              :           mortar_data.remote_insert(temporal_id,
    1113              :                                     std::move(remote_boundary_data_on_mortar));
    1114              :         }
    1115              :       }
    1116              :     }
    1117              :     apply_operator(
    1118              :         make_not_null(&operator_applied_to_zero_vars),
    1119              :         make_not_null(&all_mortar_data), zero_primal_vars, zero_deriv_vars,
    1120              :         primal_fluxes_buffer, element, mesh, inv_jacobian, det_inv_jacobian,
    1121              :         det_jacobian, det_times_inv_jacobian, face_normals, face_normal_vectors,
    1122              :         face_normal_magnitudes, face_jacobians,
    1123              :         face_jacobian_times_inv_jacobians, all_mortar_meshes, all_mortar_sizes,
    1124              :         mortar_jacobians, penalty_factors, massive, formulation, temporal_id,
    1125              :         fluxes_args_on_faces, sources_args, modify_boundary_data_args);
    1126              :     // Impose the nonlinear (constant) boundary contribution as fixed sources on
    1127              :     // the RHS of the equations
    1128              :     *fixed_sources -= operator_applied_to_zero_vars;
    1129              :   }
    1130              : };
    1131              : 
    1132              : }  // namespace detail
    1133              : 
    1134              : /*!
    1135              :  * \brief Prepare data on mortars so they can be communicated to neighbors
    1136              :  *
    1137              :  * Call this function on all elements and communicate the mortar data, then call
    1138              :  * `elliptic::dg::apply_operator`.
    1139              :  */
    1140              : template <typename System, bool Linearized, typename... Args>
    1141            1 : void prepare_mortar_data(Args&&... args) {
    1142              :   detail::DgOperatorImpl<System, Linearized>::template prepare_mortar_data<
    1143              :       false>(std::forward<Args>(args)...);
    1144              : }
    1145              : 
    1146              : /*!
    1147              :  * \brief Apply the elliptic DG operator
    1148              :  *
    1149              :  * This function applies the elliptic DG operator on an element, assuming all
    1150              :  * data on mortars is already available. Use the
    1151              :  * `elliptic::dg::prepare_mortar_data` function to prepare mortar data on
    1152              :  * neighboring elements, then communicate the data and insert them on the
    1153              :  * "remote" side of the mortars before calling this function.
    1154              :  */
    1155              : template <typename System, bool Linearized, typename... Args>
    1156            1 : void apply_operator(Args&&... args) {
    1157              :   detail::DgOperatorImpl<System, Linearized>::apply_operator(
    1158              :       std::forward<Args>(args)...);
    1159              : }
    1160              : 
    1161              : /*!
    1162              :  * \brief For linear systems, impose inhomogeneous boundary conditions as
    1163              :  * contributions to the fixed sources (i.e. the RHS of the equations).
    1164              :  *
    1165              :  * This function exists because the DG operator must typically be linear, but
    1166              :  * even for linear elliptic equations we typically apply boundary conditions
    1167              :  * with a constant, and therefore nonlinear, contribution. Standard examples are
    1168              :  * inhomogeneous (i.e. non-zero) Dirichlet or Neumann boundary conditions. This
    1169              :  * nonlinear contribution can be added to the fixed sources, leaving only the
    1170              :  * linearized boundary conditions in the DG operator. For standard constant
    1171              :  * Dirichlet or Neumann boundary conditions the linearization is of course just
    1172              :  * zero.
    1173              :  *
    1174              :  * This function essentially feeds zero variables through the nonlinear operator
    1175              :  * and subtracts the result from the fixed sources: `b -= A(x=0)`.
    1176              :  */
    1177              : template <typename System, typename... Args>
    1178            1 : void impose_inhomogeneous_boundary_conditions_on_source(Args&&... args) {
    1179              :   detail::DgOperatorImpl<System, false>::
    1180              :       impose_inhomogeneous_boundary_conditions_on_source(
    1181              :           std::forward<Args>(args)...);
    1182              : }
    1183              : 
    1184              : }  // namespace elliptic::dg
        

Generated by: LCOV version 2.0-1