SpECTRE Documentation Coverage Report
Current view: top level - Elliptic/DiscontinuousGalerkin - DgOperator.hpp Hit Total Coverage
Commit: 7b84572e7f3b8c8fb2d105e3d4e12204a244a350 Lines: 7 9 77.8 %
Date: 2025-04-18 14:09:55
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/Tensor/Tensor.hpp"
      16             : #include "DataStructures/Variables.hpp"
      17             : #include "DataStructures/Variables/FrameTransform.hpp"
      18             : #include "Domain/Structure/Direction.hpp"
      19             : #include "Domain/Structure/DirectionMap.hpp"
      20             : #include "Domain/Structure/Element.hpp"
      21             : #include "Domain/Structure/ElementId.hpp"
      22             : #include "Domain/Structure/IndexToSliceAt.hpp"
      23             : #include "Elliptic/Protocols/FirstOrderSystem.hpp"
      24             : #include "Elliptic/Systems/GetFluxesComputer.hpp"
      25             : #include "Elliptic/Systems/GetSourcesComputer.hpp"
      26             : #include "NumericalAlgorithms/DiscontinuousGalerkin/ApplyMassMatrix.hpp"
      27             : #include "NumericalAlgorithms/DiscontinuousGalerkin/Formulation.hpp"
      28             : #include "NumericalAlgorithms/DiscontinuousGalerkin/LiftFlux.hpp"
      29             : #include "NumericalAlgorithms/DiscontinuousGalerkin/LiftFromBoundary.hpp"
      30             : #include "NumericalAlgorithms/DiscontinuousGalerkin/MortarHelpers.hpp"
      31             : #include "NumericalAlgorithms/DiscontinuousGalerkin/NormalDotFlux.hpp"
      32             : #include "NumericalAlgorithms/DiscontinuousGalerkin/ProjectToBoundary.hpp"
      33             : #include "NumericalAlgorithms/DiscontinuousGalerkin/SimpleBoundaryData.hpp"
      34             : #include "NumericalAlgorithms/DiscontinuousGalerkin/SimpleMortarData.hpp"
      35             : #include "NumericalAlgorithms/LinearOperators/Divergence.hpp"
      36             : #include "NumericalAlgorithms/LinearOperators/Divergence.tpp"
      37             : #include "NumericalAlgorithms/LinearOperators/WeakDivergence.hpp"
      38             : #include "NumericalAlgorithms/Spectral/Basis.hpp"
      39             : #include "NumericalAlgorithms/Spectral/Mesh.hpp"
      40             : #include "NumericalAlgorithms/Spectral/Projection.hpp"
      41             : #include "NumericalAlgorithms/Spectral/Quadrature.hpp"
      42             : #include "Utilities/ErrorHandling/Assert.hpp"
      43             : #include "Utilities/Gsl.hpp"
      44             : #include "Utilities/ProtocolHelpers.hpp"
      45             : #include "Utilities/TMPL.hpp"
      46             : #include "Utilities/TaggedTuple.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             :     const auto projection_matrices =
     201             :         Spectral::projection_matrix_child_to_parent(mortar_mesh, face_mesh,
     202             :                                                     mortar_size, true);
     203             :     mortar_vars *= get(mortar_jacobian);
     204             :     ::dg::apply_mass_matrix(make_not_null(&mortar_vars), mortar_mesh);
     205             :     auto face_vars =
     206             :         apply_matrices(projection_matrices, mortar_vars, mortar_mesh.extents());
     207             :     face_vars /= get(face_jacobian);
     208             :     ::dg::apply_inverse_mass_matrix(make_not_null(&face_vars), face_mesh);
     209             :     return face_vars;
     210             :   }
     211             : }
     212             : 
     213             : template <typename System, bool Linearized,
     214             :           typename PrimalFields = typename System::primal_fields,
     215             :           typename PrimalFluxes = typename System::primal_fluxes>
     216             : struct DgOperatorImpl;
     217             : 
     218             : template <typename System, bool Linearized, typename... PrimalFields,
     219             :           typename... PrimalFluxes>
     220             : struct DgOperatorImpl<System, Linearized, tmpl::list<PrimalFields...>,
     221             :                       tmpl::list<PrimalFluxes...>> {
     222             :   static_assert(
     223             :       tt::assert_conforms_to_v<System, elliptic::protocols::FirstOrderSystem>);
     224             : 
     225             :   static constexpr size_t Dim = System::volume_dim;
     226             :   using FluxesComputer = elliptic::get_fluxes_computer<System, Linearized>;
     227             :   using SourcesComputer = elliptic::get_sources_computer<System, Linearized>;
     228             : 
     229             :   struct AllDirections {
     230             :     bool operator()(const Direction<Dim>& /*unused*/) const { return true; }
     231             :   };
     232             : 
     233             :   struct NoDataIsZero {
     234             :     bool operator()(const ElementId<Dim>& /*unused*/) const { return false; }
     235             :   };
     236             : 
     237             :   static constexpr auto full_mortar_size =
     238             :       make_array<Dim - 1>(Spectral::MortarSize::Full);
     239             : 
     240             :   template <bool AllDataIsZero, typename... DerivTags, typename... PrimalVars,
     241             :             typename... PrimalFluxesVars, typename... PrimalMortarVars,
     242             :             typename... PrimalMortarFluxes, typename TemporalId,
     243             :             typename ApplyBoundaryCondition, typename... FluxesArgs,
     244             :             typename DataIsZero = NoDataIsZero,
     245             :             typename DirectionsPredicate = AllDirections>
     246             :   static void prepare_mortar_data(
     247             :       const gsl::not_null<Variables<tmpl::list<DerivTags...>>*> deriv_vars,
     248             :       const gsl::not_null<Variables<tmpl::list<PrimalFluxesVars...>>*>
     249             :           primal_fluxes,
     250             :       const gsl::not_null<::dg::MortarMap<
     251             :           Dim, MortarData<TemporalId, tmpl::list<PrimalMortarVars...>,
     252             :                           tmpl::list<PrimalMortarFluxes...>>>*>
     253             :           all_mortar_data,
     254             :       const Variables<tmpl::list<PrimalVars...>>& primal_vars,
     255             :       const Element<Dim>& element, const Mesh<Dim>& mesh,
     256             :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     257             :                             Frame::Inertial>& inv_jacobian,
     258             :       const DirectionMap<Dim, tnsr::i<DataVector, Dim>>& face_normals,
     259             :       const ::dg::MortarMap<Dim, Mesh<Dim - 1>>& all_mortar_meshes,
     260             :       const ::dg::MortarMap<Dim, ::dg::MortarSize<Dim - 1>>& all_mortar_sizes,
     261             :       const TemporalId& temporal_id,
     262             :       const ApplyBoundaryCondition& apply_boundary_condition,
     263             :       const std::tuple<FluxesArgs...>& fluxes_args,
     264             :       const DataIsZero& data_is_zero = NoDataIsZero{},
     265             :       const DirectionsPredicate& directions_predicate = AllDirections{}) {
     266             :     static_assert(
     267             :         sizeof...(PrimalVars) == sizeof...(PrimalFields) and
     268             :             sizeof...(PrimalFluxesVars) == sizeof...(PrimalFluxes),
     269             :         "The number of variables must match the number of system fields.");
     270             :     static_assert(
     271             :         (std::is_same_v<typename PrimalVars::type,
     272             :                         typename PrimalFields::type> and
     273             :          ...) and
     274             :             (std::is_same_v<typename PrimalFluxesVars::type,
     275             :                             typename PrimalFluxes::type> and
     276             :              ...),
     277             :         "The variables must have the same tensor types as the system fields.");
     278             : #ifdef SPECTRE_DEBUG
     279             :     for (size_t d = 0; d < Dim; ++d) {
     280             :       ASSERT(mesh.basis(d) == Spectral::Basis::Legendre and
     281             :                  (mesh.quadrature(d) == Spectral::Quadrature::GaussLobatto or
     282             :                   mesh.quadrature(d) == Spectral::Quadrature::Gauss),
     283             :              "The elliptic DG operator is currently only implemented for "
     284             :              "Legendre-Gauss(-Lobatto) grids. Found basis '"
     285             :                  << mesh.basis(d) << "' and quadrature '" << mesh.quadrature(d)
     286             :                  << "' in dimension " << d << ".");
     287             :     }
     288             : #endif  // SPECTRE_DEBUG
     289             :     const auto& element_id = element.id();
     290             :     const bool local_data_is_zero = data_is_zero(element_id);
     291             :     ASSERT(Linearized or not local_data_is_zero,
     292             :            "Only a linear operator can take advantage of the knowledge that "
     293             :            "the operand is zero. Don't return 'true' in 'data_is_zero' unless "
     294             :            "you also set 'Linearized' to 'true'.");
     295             :     const size_t num_points = mesh.number_of_grid_points();
     296             : 
     297             :     // This function and the one below allocate various Variables to compute
     298             :     // intermediate quantities. It could be a performance optimization to reduce
     299             :     // the number of these allocations and/or move some of the memory buffers
     300             :     // into the DataBox to keep them around permanently. The latter should be
     301             :     // informed by profiling.
     302             : 
     303             :     // Compute partial derivatives grad(u) of the system variables, and from
     304             :     // those the fluxes F^i(grad(u)) in the volume. We will take the divergence
     305             :     // of the fluxes in the `apply_operator` function below to compute the full
     306             :     // elliptic equation -div(F) + S = f(x).
     307             :     if (AllDataIsZero or local_data_is_zero) {
     308             :       primal_fluxes->initialize(num_points, 0.);
     309             :     } else {
     310             :       // Compute partial derivatives of the variables
     311             :       partial_derivatives(deriv_vars, primal_vars, mesh, inv_jacobian);
     312             :       // Compute the fluxes
     313             :       primal_fluxes->initialize(num_points);
     314             :       std::apply(
     315             :           [&primal_fluxes, &primal_vars, &deriv_vars,
     316             :            &element_id](const auto&... expanded_fluxes_args) {
     317             :             if constexpr (FluxesComputer::is_discontinuous) {
     318             :               FluxesComputer::apply(
     319             :                   make_not_null(&get<PrimalFluxesVars>(*primal_fluxes))...,
     320             :                   expanded_fluxes_args..., element_id,
     321             :                   get<PrimalVars>(primal_vars)...,
     322             :                   get<DerivTags>(*deriv_vars)...);
     323             :             } else {
     324             :               (void)element_id;
     325             :               FluxesComputer::apply(
     326             :                   make_not_null(&get<PrimalFluxesVars>(*primal_fluxes))...,
     327             :                   expanded_fluxes_args..., get<PrimalVars>(primal_vars)...,
     328             :                   get<DerivTags>(*deriv_vars)...);
     329             :             }
     330             :           },
     331             :           fluxes_args);
     332             :     }
     333             : 
     334             :     // Populate the mortar data on this element's side of the boundary so it's
     335             :     // ready to be sent to neighbors.
     336             :     for (const auto& direction : [&element]() -> const auto& {
     337             :            if constexpr (AllDataIsZero) {
     338             :              // Skipping internal boundaries for all-zero data because they
     339             :              // won't contribute boundary corrections anyway (data on both sides
     340             :              // of the boundary is the same). For all-zero data we are
     341             :              // interested in external boundaries, to extract inhomogeneous
     342             :              // boundary conditions from a non-linear operator.
     343             :              return element.external_boundaries();
     344             :            } else {
     345             :              (void)element;
     346             :              return Direction<Dim>::all_directions();
     347             :            };
     348             :          }()) {
     349             :       if (not directions_predicate(direction)) {
     350             :         continue;
     351             :       }
     352             :       const bool is_internal = element.neighbors().contains(direction);
     353             :       // Skip directions altogether when both this element and all neighbors in
     354             :       // the direction have zero data. These boundaries won't contribute
     355             :       // corrections, because the data is the same on both sides. External
     356             :       // boundaries also count as zero data here, because they are linearized
     357             :       // (see assert above).
     358             :       if (local_data_is_zero and
     359             :           (not is_internal or
     360             :            alg::all_of(element.neighbors().at(direction), data_is_zero))) {
     361             :         continue;
     362             :       }
     363             :       const auto face_mesh = mesh.slice_away(direction.dimension());
     364             :       const size_t face_num_points = face_mesh.number_of_grid_points();
     365             :       const auto& face_normal = face_normals.at(direction);
     366             :       Variables<tmpl::list<PrimalFluxesVars...>> primal_fluxes_on_face{};
     367             :       BoundaryData<tmpl::list<PrimalMortarVars...>,
     368             :                    tmpl::list<PrimalMortarFluxes...>>
     369             :           boundary_data{};
     370             :       if (AllDataIsZero or local_data_is_zero) {
     371             :         if (is_internal) {
     372             :           // We manufacture zero boundary data directly on the mortars below.
     373             :           // Nothing to do here.
     374             :         } else {
     375             :           boundary_data.field_data.initialize(face_num_points, 0.);
     376             :         }
     377             :       } else {
     378             :         boundary_data.field_data.initialize(face_num_points);
     379             :         primal_fluxes_on_face.initialize(face_num_points);
     380             :         // Project fields to faces
     381             :         // Note: need to convert tags of `Variables` because
     382             :         // `project_contiguous_data_to_boundary` requires that the face and
     383             :         // volume tags are subsets.
     384             :         Variables<
     385             :             tmpl::list<PrimalVars..., ::Tags::NormalDotFlux<PrimalVars>...>>
     386             :             boundary_data_ref{};
     387             :         boundary_data_ref.set_data_ref(boundary_data.field_data.data(),
     388             :                                        boundary_data.field_data.size());
     389             :         ::dg::project_contiguous_data_to_boundary(
     390             :             make_not_null(&boundary_data_ref), primal_vars, mesh, direction);
     391             :         // Compute n_i F^i on faces
     392             :         ::dg::project_contiguous_data_to_boundary(
     393             :             make_not_null(&primal_fluxes_on_face), *primal_fluxes, mesh,
     394             :             direction);
     395             :         EXPAND_PACK_LEFT_TO_RIGHT(normal_dot_flux(
     396             :             make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     397             :                 boundary_data.field_data)),
     398             :             face_normal, get<PrimalFluxesVars>(primal_fluxes_on_face)));
     399             :       }
     400             : 
     401             :       if (is_internal) {
     402             :         if constexpr (not AllDataIsZero) {
     403             :           // Project boundary data on internal faces to mortars
     404             :           for (const auto& neighbor_id : element.neighbors().at(direction)) {
     405             :             if (local_data_is_zero and data_is_zero(neighbor_id)) {
     406             :               continue;
     407             :             }
     408             :             const ::dg::MortarId<Dim> mortar_id{direction, neighbor_id};
     409             :             const auto& mortar_mesh = all_mortar_meshes.at(mortar_id);
     410             :             const auto& mortar_size = all_mortar_sizes.at(mortar_id);
     411             :             if (local_data_is_zero) {
     412             :               // No need to project anything. We just manufacture zero boundary
     413             :               // data on the mortar.
     414             :               BoundaryData<tmpl::list<PrimalMortarVars...>,
     415             :                            tmpl::list<PrimalMortarFluxes...>>
     416             :                   zero_boundary_data{};
     417             :               zero_boundary_data.field_data.initialize(
     418             :                   mortar_mesh.number_of_grid_points(), 0.);
     419             :               (*all_mortar_data)[mortar_id].local_insert(
     420             :                   temporal_id, std::move(zero_boundary_data));
     421             :               continue;
     422             :             }
     423             :             // When no projection is necessary we can safely move the boundary
     424             :             // data from the face as there is only a single neighbor in this
     425             :             // direction
     426             :             auto projected_boundary_data =
     427             :                 Spectral::needs_projection(face_mesh, mortar_mesh, mortar_size)
     428             :                     // NOLINTNEXTLINE
     429             :                     ? boundary_data.project_to_mortar(face_mesh, mortar_mesh,
     430             :                                                       mortar_size)
     431             :                     : std::move(boundary_data);  // NOLINT
     432             :             (*all_mortar_data)[mortar_id].local_insert(
     433             :                 temporal_id, std::move(projected_boundary_data));
     434             :           }
     435             :         }
     436             :       } else {
     437             :         // No need to do projections on external boundaries
     438             :         const ::dg::MortarId<Dim> mortar_id{
     439             :             direction, ElementId<Dim>::external_boundary_id()};
     440             :         (*all_mortar_data)[mortar_id].local_insert(temporal_id, boundary_data);
     441             : 
     442             :         // -------------------------
     443             :         // Apply boundary conditions
     444             :         // -------------------------
     445             :         //
     446             :         // To apply boundary conditions we fill the boundary data with
     447             :         // "exterior" or "ghost" data and set it as remote mortar data, so
     448             :         // external boundaries behave just like internal boundaries when
     449             :         // applying boundary corrections.
     450             :         //
     451             :         // The `apply_boundary_conditions` invocable is expected to impose
     452             :         // boundary conditions by modifying the fields and fluxes that are
     453             :         // passed by reference. Dirichlet-type boundary conditions are imposed
     454             :         // by modifying the fields, and Neumann-type boundary conditions are
     455             :         // imposed by modifying the interior n.F. Note that all data passed to
     456             :         // the boundary conditions is taken from the "interior" side of the
     457             :         // boundary, i.e. with a normal vector that points _out_ of the
     458             :         // computational domain.
     459             :         Variables<tmpl::list<DerivTags...>> deriv_vars_on_boundary{};
     460             :         if (AllDataIsZero or local_data_is_zero) {
     461             :           deriv_vars_on_boundary.initialize(face_num_points, 0.);
     462             :         } else {
     463             :           deriv_vars_on_boundary.initialize(face_num_points);
     464             :           ::dg::project_contiguous_data_to_boundary(
     465             :               make_not_null(&deriv_vars_on_boundary), *deriv_vars, mesh,
     466             :               direction);
     467             :         }
     468             :         apply_boundary_condition(
     469             :             direction,
     470             :             make_not_null(&get<PrimalMortarVars>(boundary_data.field_data))...,
     471             :             make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     472             :                 boundary_data.field_data))...,
     473             :             get<DerivTags>(deriv_vars_on_boundary)...);
     474             : 
     475             :         // Invert the sign of the fluxes to account for the inverted normal on
     476             :         // exterior faces. Also multiply by 2 and add the interior fluxes to
     477             :         // impose the boundary conditions on the _average_ instead of just
     478             :         // setting the fields on the exterior:
     479             :         //   (Dirichlet)  u_D = avg(u) = 1/2 (u_int + u_ext)
     480             :         //                => u_ext = 2 u_D - u_int
     481             :         //   (Neumann)    (n.F)_N = avg(n.F) = 1/2 [(n.F)_int - (n.F)_ext]
     482             :         //                => (n.F)_ext = -2 (n.F)_N + (n.F)_int]
     483             :         const auto impose_on_average = [](const auto exterior_field,
     484             :                                           const auto& interior_field) {
     485             :           for (size_t i = 0; i < interior_field.size(); ++i) {
     486             :             (*exterior_field)[i] *= 2.;
     487             :             (*exterior_field)[i] -= interior_field[i];
     488             :           }
     489             :         };
     490             :         EXPAND_PACK_LEFT_TO_RIGHT(impose_on_average(
     491             :             make_not_null(&get<PrimalMortarVars>(boundary_data.field_data)),
     492             :             get<PrimalMortarVars>(all_mortar_data->at(mortar_id)
     493             :                                       .local_data(temporal_id)
     494             :                                       .field_data)));
     495             :         const auto invert_sign_and_impose_on_average =
     496             :             [](const auto exterior_n_dot_flux,
     497             :                const auto& interior_n_dot_flux) {
     498             :               for (size_t i = 0; i < interior_n_dot_flux.size(); ++i) {
     499             :                 (*exterior_n_dot_flux)[i] *= -2.;
     500             :                 (*exterior_n_dot_flux)[i] += interior_n_dot_flux[i];
     501             :               }
     502             :             };
     503             :         EXPAND_PACK_LEFT_TO_RIGHT(invert_sign_and_impose_on_average(
     504             :             make_not_null(&get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     505             :                 boundary_data.field_data)),
     506             :             get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     507             :                 all_mortar_data->at(mortar_id)
     508             :                     .local_data(temporal_id)
     509             :                     .field_data)));
     510             : 
     511             :         // Store the exterior boundary data on the mortar
     512             :         all_mortar_data->at(mortar_id).remote_insert(temporal_id,
     513             :                                                      std::move(boundary_data));
     514             :       }  // if (is_internal)
     515             :     }  // loop directions
     516             :   }
     517             : 
     518             :   // --- This is essentially a break to communicate the mortar data ---
     519             : 
     520             :   template <typename... OperatorTags, typename... PrimalVars,
     521             :             typename... PrimalFluxesVars, typename... PrimalMortarVars,
     522             :             typename... PrimalMortarFluxes, typename TemporalId,
     523             :             typename... FluxesArgs, typename... SourcesArgs,
     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             :       // Taking the primal fluxes computed in the `prepare_mortar_data` function
     535             :       // by const-ref here because other code might use them and so we don't
     536             :       // want to modify them by adding boundary corrections. E.g. linearized
     537             :       // sources use the nonlinear fields and fluxes as background fields.
     538             :       const Variables<tmpl::list<PrimalFluxesVars...>>& primal_fluxes,
     539             :       const Element<Dim>& element, const Mesh<Dim>& mesh,
     540             :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     541             :                             Frame::Inertial>& inv_jacobian,
     542             :       const Scalar<DataVector>& det_inv_jacobian,
     543             :       const Scalar<DataVector>& det_jacobian,
     544             :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     545             :                             Frame::Inertial>& det_times_inv_jacobian,
     546             :       const DirectionMap<Dim, tnsr::i<DataVector, Dim>>& face_normals,
     547             :       const DirectionMap<Dim, tnsr::I<DataVector, Dim>>& face_normal_vectors,
     548             :       const DirectionMap<Dim, Scalar<DataVector>>& face_normal_magnitudes,
     549             :       const DirectionMap<Dim, Scalar<DataVector>>& face_jacobians,
     550             :       const DirectionMap<Dim,
     551             :                          InverseJacobian<DataVector, Dim, Frame::ElementLogical,
     552             :                                          Frame::Inertial>>&
     553             :           face_jacobian_times_inv_jacobians,
     554             :       const ::dg::MortarMap<Dim, Mesh<Dim - 1>>& all_mortar_meshes,
     555             :       const ::dg::MortarMap<Dim, ::dg::MortarSize<Dim - 1>>& all_mortar_sizes,
     556             :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& mortar_jacobians,
     557             :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& penalty_factors,
     558             :       const bool massive, const ::dg::Formulation formulation,
     559             :       const TemporalId& /*temporal_id*/,
     560             :       const DirectionMap<Dim, std::tuple<FluxesArgs...>>& fluxes_args_on_faces,
     561             :       const std::tuple<SourcesArgs...>& sources_args,
     562             :       const DataIsZero& data_is_zero = NoDataIsZero{},
     563             :       const DirectionsPredicate& directions_predicate = AllDirections{}) {
     564             :     static_assert(
     565             :         sizeof...(PrimalVars) == sizeof...(PrimalFields) and
     566             :             sizeof...(PrimalFluxesVars) == sizeof...(PrimalFluxes) and
     567             :             sizeof...(PrimalMortarVars) == sizeof...(PrimalFields) and
     568             :             sizeof...(PrimalMortarFluxes) == sizeof...(PrimalFluxes) and
     569             :             sizeof...(OperatorTags) == sizeof...(PrimalFields),
     570             :         "The number of variables must match the number of system fields.");
     571             :     static_assert(
     572             :         (std::is_same_v<typename PrimalVars::type,
     573             :                         typename PrimalFields::type> and
     574             :          ...) and
     575             :             (std::is_same_v<typename PrimalFluxesVars::type,
     576             :                             typename PrimalFluxes::type> and
     577             :              ...) and
     578             :             (std::is_same_v<typename PrimalMortarVars::type,
     579             :                             typename PrimalFields::type> and
     580             :              ...) and
     581             :             (std::is_same_v<typename PrimalMortarFluxes::type,
     582             :                             typename PrimalFluxes::type> and
     583             :              ...) and
     584             :             (std::is_same_v<typename OperatorTags::type,
     585             :                             typename PrimalFields::type> and
     586             :              ...),
     587             :         "The variables must have the same tensor types as the system fields.");
     588             : #ifdef SPECTRE_DEBUG
     589             :     for (size_t d = 0; d < Dim; ++d) {
     590             :       ASSERT(mesh.basis(d) == Spectral::Basis::Legendre and
     591             :                  (mesh.quadrature(d) == Spectral::Quadrature::GaussLobatto or
     592             :                   mesh.quadrature(d) == Spectral::Quadrature::Gauss),
     593             :              "The elliptic DG operator is currently only implemented for "
     594             :              "Legendre-Gauss(-Lobatto) grids. Found basis '"
     595             :                  << mesh.basis(d) << "' and quadrature '" << mesh.quadrature(d)
     596             :                  << "' in dimension " << d << ".");
     597             :     }
     598             : #endif  // SPECTRE_DEBUG
     599             :     const auto& element_id = element.id();
     600             :     const bool local_data_is_zero = data_is_zero(element_id);
     601             :     ASSERT(Linearized or not local_data_is_zero,
     602             :            "Only a linear operator can take advantage of the knowledge that "
     603             :            "the operand is zero. Don't return 'true' in 'data_is_zero' unless "
     604             :            "you also set 'Linearized' to 'true'.");
     605             :     const size_t num_points = mesh.number_of_grid_points();
     606             : 
     607             :     // This function and the one above allocate various Variables to compute
     608             :     // intermediate quantities. It could be a performance optimization to reduce
     609             :     // the number of these allocations and/or move some of the memory buffers
     610             :     // into the DataBox to keep them around permanently. The latter should be
     611             :     // informed by profiling.
     612             : 
     613             :     // Compute volume terms: -div(F) + S
     614             :     if (local_data_is_zero) {
     615             :       operator_applied_to_vars->initialize(num_points, 0.);
     616             :     } else {
     617             :       // "Massive" operators retain the factors from the volume integral:
     618             :       //   \int_volume div(F) \phi_p = w_p det(J)_p div(F)_p
     619             :       // Here, `w` are the quadrature weights (the diagonal logical mass matrix
     620             :       // with mass-lumping) and det(J) is the Jacobian determinant. The
     621             :       // quantities are evaluated at the grid point `p`.
     622             :       if (formulation == ::dg::Formulation::StrongInertial) {
     623             :         // Compute strong divergence:
     624             :         //   div(F) = (J^\hat{i}_i)_p \sum_q (D_\hat{i})_pq (F^i)_q.
     625             :         divergence(operator_applied_to_vars, primal_fluxes, mesh,
     626             :                    massive ? det_times_inv_jacobian : inv_jacobian);
     627             :         // This is the sign flip that makes the operator _minus_ the Laplacian
     628             :         // for a Poisson system
     629             :         *operator_applied_to_vars *= -1.;
     630             :       } else if (formulation == ::dg::Formulation::StrongLogical) {
     631             :         // Strong divergence but with the Jacobian moved into the divergence:
     632             :         //   div(F) = 1/J_p \sum_q (D_\hat{i})_pq J_q (J^\hat{i}_i)_q (F^i)_q.
     633             :         const auto logical_fluxes = transform::first_index_to_different_frame(
     634             :             primal_fluxes, det_times_inv_jacobian);
     635             :         logical_divergence(operator_applied_to_vars, logical_fluxes, mesh);
     636             :         if (massive) {
     637             :           *operator_applied_to_vars *= -1.;
     638             :         } else {
     639             :           *operator_applied_to_vars *= -1. * get(det_inv_jacobian);
     640             :         }
     641             :       } else if (formulation == ::dg::Formulation::WeakInertial) {
     642             :         // Compute weak divergence:
     643             :         //   F^i \partial_i \phi = 1/w_p \sum_q
     644             :         //     (D^T_\hat{i})_pq (w det(J) J^\hat{i}_i F^i)_q
     645             :         weak_divergence(operator_applied_to_vars, primal_fluxes, mesh,
     646             :                         det_times_inv_jacobian);
     647             :         if (not massive) {
     648             :           *operator_applied_to_vars *= get(det_inv_jacobian);
     649             :         }
     650             :       } else {
     651             :         ERROR("Unsupported DG formulation: "
     652             :               << formulation
     653             :               << "\nSupported formulations are: StrongInertial, WeakInertial, "
     654             :                  "StrongLogical.");
     655             :       }
     656             :       if constexpr (not std::is_same_v<SourcesComputer, void>) {
     657             :         Variables<tmpl::list<OperatorTags...>> sources{num_points, 0.};
     658             :         std::apply(
     659             :             [&sources, &primal_vars,
     660             :              &primal_fluxes](const auto&... expanded_sources_args) {
     661             :               SourcesComputer::apply(
     662             :                   make_not_null(&get<OperatorTags>(sources))...,
     663             :                   expanded_sources_args..., get<PrimalVars>(primal_vars)...,
     664             :                   get<PrimalFluxesVars>(primal_fluxes)...);
     665             :             },
     666             :             sources_args);
     667             :         if (massive) {
     668             :           sources *= get(det_jacobian);
     669             :         }
     670             :         *operator_applied_to_vars += sources;
     671             :       }
     672             :     }
     673             :     if (massive) {
     674             :       ::dg::apply_mass_matrix(operator_applied_to_vars, mesh);
     675             :     }
     676             : 
     677             :     // Add boundary corrections
     678             :     // Keeping track if any corrections were applied here, for an optimization
     679             :     // below
     680             :     bool has_any_boundary_corrections = false;
     681             :     Variables<tmpl::list<transform::Tags::TransformedFirstIndex<
     682             :         PrimalFluxesVars, Frame::ElementLogical>...>>
     683             :         lifted_logical_aux_boundary_corrections{num_points, 0.};
     684             :     for (auto& [mortar_id, mortar_data] : *all_mortar_data) {
     685             :       const auto& direction = mortar_id.direction();
     686             :       const auto& neighbor_id = mortar_id.id();
     687             :       const bool is_internal =
     688             :           (neighbor_id != ElementId<Dim>::external_boundary_id());
     689             :       if (not directions_predicate(direction)) {
     690             :         continue;
     691             :       }
     692             :       // When the data on both sides of the mortar is zero then we don't need to
     693             :       // handle this mortar at all.
     694             :       if (local_data_is_zero and
     695             :           (not is_internal or data_is_zero(neighbor_id))) {
     696             :         continue;
     697             :       }
     698             :       has_any_boundary_corrections = true;
     699             : 
     700             :       const auto face_mesh = mesh.slice_away(direction.dimension());
     701             :       auto [local_data, remote_data] = mortar_data.extract();
     702             :       const size_t face_num_points = face_mesh.number_of_grid_points();
     703             :       const auto& face_normal = face_normals.at(direction);
     704             :       const auto& face_normal_vector = face_normal_vectors.at(direction);
     705             :       const auto& fluxes_args_on_face = fluxes_args_on_faces.at(direction);
     706             :       const auto& face_normal_magnitude = face_normal_magnitudes.at(direction);
     707             :       const auto& face_jacobian = face_jacobians.at(direction);
     708             :       const auto& face_jacobian_times_inv_jacobian =
     709             :           face_jacobian_times_inv_jacobians.at(direction);
     710             :       const auto& mortar_mesh =
     711             :           is_internal ? all_mortar_meshes.at(mortar_id) : face_mesh;
     712             :       const auto& mortar_size =
     713             :           is_internal ? all_mortar_sizes.at(mortar_id) : full_mortar_size;
     714             : 
     715             :       // This is the strong auxiliary boundary correction:
     716             :       //   G^i = F^i(n_j (avg(u) - u))
     717             :       // where
     718             :       //   avg(u) - u = -0.5 * (u_int - u_ext)
     719             :       auto avg_vars_on_mortar = Variables<tmpl::list<PrimalMortarVars...>>(
     720             :           local_data.field_data
     721             :               .template extract_subset<tmpl::list<PrimalMortarVars...>>());
     722             :       const auto add_remote_contribution = [](auto& lhs, const auto& rhs) {
     723             :         for (size_t i = 0; i < lhs.size(); ++i) {
     724             :           lhs[i] -= rhs[i];
     725             :         }
     726             :       };
     727             :       EXPAND_PACK_LEFT_TO_RIGHT(add_remote_contribution(
     728             :           get<PrimalMortarVars>(avg_vars_on_mortar),
     729             :           get<PrimalMortarVars>(remote_data.field_data)));
     730             :       avg_vars_on_mortar *= -0.5;
     731             : 
     732             :       // Project from the mortar back down to the face if needed
     733             :       const auto avg_vars_on_face =
     734             :           Spectral::needs_projection(face_mesh, mortar_mesh, mortar_size)
     735             :               ? mass_conservative_restriction(
     736             :                     std::move(avg_vars_on_mortar), mortar_mesh, mortar_size,
     737             :                     mortar_jacobians.at(mortar_id), face_mesh, face_jacobian)
     738             :               : std::move(avg_vars_on_mortar);
     739             : 
     740             :       // Apply fluxes to get G^i
     741             :       Variables<tmpl::list<PrimalFluxesVars...>> auxiliary_boundary_corrections{
     742             :           face_num_points};
     743             :       std::apply(
     744             :           [&auxiliary_boundary_corrections, &face_normal, &face_normal_vector,
     745             :            &avg_vars_on_face,
     746             :            &element_id](const auto&... expanded_fluxes_args_on_face) {
     747             :             if constexpr (FluxesComputer::is_discontinuous) {
     748             :               FluxesComputer::apply(make_not_null(&get<PrimalFluxesVars>(
     749             :                                         auxiliary_boundary_corrections))...,
     750             :                                     expanded_fluxes_args_on_face..., element_id,
     751             :                                     face_normal, face_normal_vector,
     752             :                                     get<PrimalMortarVars>(avg_vars_on_face)...);
     753             :             } else {
     754             :               (void)element_id;
     755             :               FluxesComputer::apply(make_not_null(&get<PrimalFluxesVars>(
     756             :                                         auxiliary_boundary_corrections))...,
     757             :                                     expanded_fluxes_args_on_face...,
     758             :                                     face_normal, face_normal_vector,
     759             :                                     get<PrimalMortarVars>(avg_vars_on_face)...);
     760             :             }
     761             :           },
     762             :           fluxes_args_on_face);
     763             : 
     764             :       // Lifting for the auxiliary boundary correction:
     765             :       //   \int_face G^i \partial_i \phi
     766             :       // We first transform the flux index to the logical frame, apply the
     767             :       // quadrature weights and the Jacobian for the face integral, then take
     768             :       // the logical weak divergence in the volume after lifting (below the loop
     769             :       // over faces).
     770             :       auto logical_aux_boundary_corrections =
     771             :           transform::first_index_to_different_frame(
     772             :               auxiliary_boundary_corrections, face_jacobian_times_inv_jacobian);
     773             :       ::dg::apply_mass_matrix(make_not_null(&logical_aux_boundary_corrections),
     774             :                               face_mesh);
     775             :       if (mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto) {
     776             :         add_slice_to_data(
     777             :             make_not_null(&lifted_logical_aux_boundary_corrections),
     778             :             logical_aux_boundary_corrections, mesh.extents(),
     779             :             direction.dimension(),
     780             :             index_to_slice_at(mesh.extents(), direction));
     781             :       } else {
     782             :         ::dg::lift_boundary_terms_gauss_points(
     783             :             make_not_null(&lifted_logical_aux_boundary_corrections),
     784             :             logical_aux_boundary_corrections, mesh, direction);
     785             :       }
     786             : 
     787             :       // This is the strong primal boundary correction:
     788             :       //   -n.H = -avg(n.F) + n.F + penalty * n.F(n_j jump(u))
     789             :       // Note that the "internal penalty" numerical flux
     790             :       // (as opposed to the LLF flux) uses the raw field derivatives without
     791             :       // boundary corrections in the average, which is why we can communicate
     792             :       // the data so early together with the auxiliary boundary data. In this
     793             :       // case the penalty needs to include a factor N_points^2 / h (see the
     794             :       // `penalty` function).
     795             :       const auto& penalty_factor = penalty_factors.at(mortar_id);
     796             :       // Compute jump on mortar:
     797             :       //   penalty * jump(u) = penalty * (u_int - u_ext)
     798             :       const auto add_remote_jump_contribution =
     799             :           [&penalty_factor](auto& lhs, const auto& rhs) {
     800             :             for (size_t i = 0; i < lhs.size(); ++i) {
     801             :               lhs[i] -= rhs[i];
     802             :               lhs[i] *= get(penalty_factor);
     803             :             }
     804             :           };
     805             :       EXPAND_PACK_LEFT_TO_RIGHT(add_remote_jump_contribution(
     806             :           get<PrimalMortarVars>(local_data.field_data),
     807             :           get<PrimalMortarVars>(remote_data.field_data)));
     808             :       // Compute average on mortar:
     809             :       //   (strong)  -avg(n.F) + n.F = 0.5 * (n.F)_int + 0.5 * (n.F)_ext
     810             :       //   (weak)    -avg(n.F) = -0.5 * (n.F)_int + 0.5 * (n.F)_ext
     811             :       const auto add_avg_contribution = [](auto& lhs, const auto& rhs,
     812             :                                            const double factor) {
     813             :         for (size_t i = 0; i < lhs.size(); ++i) {
     814             :           lhs[i] *= factor;
     815             :           lhs[i] += 0.5 * rhs[i];
     816             :         }
     817             :       };
     818             :       const bool is_strong_formulation =
     819             :           formulation == ::dg::Formulation::StrongInertial or
     820             :           formulation == ::dg::Formulation::StrongLogical;
     821             :       EXPAND_PACK_LEFT_TO_RIGHT(add_avg_contribution(
     822             :           get<::Tags::NormalDotFlux<PrimalMortarVars>>(local_data.field_data),
     823             :           get<::Tags::NormalDotFlux<PrimalMortarVars>>(remote_data.field_data),
     824             :           is_strong_formulation ? 0.5 : -0.5));
     825             : 
     826             :       // Project from the mortar back down to the face if needed, lift and add
     827             :       // to operator. See auxiliary boundary corrections above for details.
     828             :       auto primal_boundary_corrections_on_face =
     829             :           Spectral::needs_projection(face_mesh, mortar_mesh, mortar_size)
     830             :               ? mass_conservative_restriction(
     831             :                     std::move(local_data.field_data), mortar_mesh, mortar_size,
     832             :                     mortar_jacobians.at(mortar_id), face_mesh, face_jacobian)
     833             :               : std::move(local_data.field_data);
     834             : 
     835             :       // Compute fluxes for jump term: n.F(n_j jump(u))
     836             :       // If the fluxes are trivial (just the spatial metric), we can skip this
     837             :       // step because the face normal is normalized.
     838             :       if constexpr (not FluxesComputer::is_trivial) {
     839             :         // We reuse the memory buffer from above for the result.
     840             :         std::apply(
     841             :             [&auxiliary_boundary_corrections, &face_normal, &face_normal_vector,
     842             :              &primal_boundary_corrections_on_face,
     843             :              &element_id](const auto&... expanded_fluxes_args_on_face) {
     844             :               if constexpr (FluxesComputer::is_discontinuous) {
     845             :                 FluxesComputer::apply(
     846             :                     make_not_null(&get<PrimalFluxesVars>(
     847             :                         auxiliary_boundary_corrections))...,
     848             :                     expanded_fluxes_args_on_face..., element_id, face_normal,
     849             :                     face_normal_vector,
     850             :                     get<PrimalMortarVars>(
     851             :                         primal_boundary_corrections_on_face)...);
     852             :               } else {
     853             :                 (void)element_id;
     854             :                 FluxesComputer::apply(
     855             :                     make_not_null(&get<PrimalFluxesVars>(
     856             :                         auxiliary_boundary_corrections))...,
     857             :                     expanded_fluxes_args_on_face..., face_normal,
     858             :                     face_normal_vector,
     859             :                     get<PrimalMortarVars>(
     860             :                         primal_boundary_corrections_on_face)...);
     861             :               }
     862             :             },
     863             :             fluxes_args_on_face);
     864             :         if constexpr (FluxesComputer::is_discontinuous) {
     865             :           if (is_internal) {
     866             :             // For penalty term with discontinuous fluxes: evaluate the fluxes
     867             :             // on the other side of the boundary as well and take average
     868             :             Variables<tmpl::list<PrimalFluxesVars...>> fluxes_other_side{
     869             :                 face_num_points};
     870             :             std::apply(
     871             :                 [&fluxes_other_side, &face_normal, &face_normal_vector,
     872             :                  &primal_boundary_corrections_on_face,
     873             :                  &local_neighbor_id =
     874             :                      neighbor_id](const auto&... expanded_fluxes_args_on_face) {
     875             :                   FluxesComputer::apply(
     876             :                       make_not_null(
     877             :                           &get<PrimalFluxesVars>(fluxes_other_side))...,
     878             :                       expanded_fluxes_args_on_face..., local_neighbor_id,
     879             :                       face_normal, face_normal_vector,
     880             :                       get<PrimalMortarVars>(
     881             :                           primal_boundary_corrections_on_face)...);
     882             :                 },
     883             :                 fluxes_args_on_face);
     884             :             auxiliary_boundary_corrections += fluxes_other_side;
     885             :             auxiliary_boundary_corrections *= 0.5;
     886             :           }
     887             :         }
     888             :         EXPAND_PACK_LEFT_TO_RIGHT(normal_dot_flux(
     889             :             make_not_null(
     890             :                 &get<PrimalMortarVars>(primal_boundary_corrections_on_face)),
     891             :             face_normal,
     892             :             get<PrimalFluxesVars>(auxiliary_boundary_corrections)));
     893             :       }
     894             : 
     895             :       // Add penalty term to average term
     896             :       Variables<tmpl::list<PrimalMortarVars...>> primal_boundary_corrections{};
     897             :       // First half of the memory allocated above is filled with the penalty
     898             :       // term, so just use that memory here.
     899             :       primal_boundary_corrections.set_data_ref(
     900             :           primal_boundary_corrections_on_face.data(), avg_vars_on_face.size());
     901             :       // Second half of the memory is filled with the average term. Add that to
     902             :       // the penalty term.
     903             :       const auto add_avg_term = [](auto& lhs, const auto& rhs) {
     904             :         for (size_t i = 0; i < lhs.size(); ++i) {
     905             :           lhs[i] += rhs[i];
     906             :         }
     907             :       };
     908             :       EXPAND_PACK_LEFT_TO_RIGHT(
     909             :           add_avg_term(get<PrimalMortarVars>(primal_boundary_corrections),
     910             :                        get<::Tags::NormalDotFlux<PrimalMortarVars>>(
     911             :                            primal_boundary_corrections_on_face)));
     912             : 
     913             :       // Lifting for the primal boundary correction:
     914             :       //   \int_face n.H \phi
     915             :       if (massive) {
     916             :         // We apply the quadrature weights and Jacobian for the face integral,
     917             :         // then lift to the volume
     918             :         primal_boundary_corrections *= get(face_jacobian);
     919             :         ::dg::apply_mass_matrix(make_not_null(&primal_boundary_corrections),
     920             :                                 face_mesh);
     921             :         if (mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto) {
     922             :           add_slice_to_data(operator_applied_to_vars,
     923             :                             primal_boundary_corrections, mesh.extents(),
     924             :                             direction.dimension(),
     925             :                             index_to_slice_at(mesh.extents(), direction));
     926             :         } else {
     927             :           ::dg::lift_boundary_terms_gauss_points(operator_applied_to_vars,
     928             :                                                  primal_boundary_corrections,
     929             :                                                  mesh, direction);
     930             :         }
     931             :       } else {
     932             :         // Apply an extra inverse mass matrix to the boundary corrections (with
     933             :         // mass lumping, so it's diagonal).
     934             :         // For Gauss-Lobatto grids this divides out the quadrature weights and
     935             :         // Jacobian on the face, leaving only factors perpendicular to the face.
     936             :         // Those are handled by `dg::lift_flux` (with an extra minus sign since
     937             :         // the function was written for evolution systems).
     938             :         // For Gauss grids the quadrature weights and Jacobians are handled
     939             :         // by `::dg::lift_boundary_terms_gauss_points` (which was also written
     940             :         // for evolution systems, hence the extra minus sign).
     941             :         primal_boundary_corrections *= -1.;
     942             :         if (mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto) {
     943             :           ::dg::lift_flux(make_not_null(&primal_boundary_corrections),
     944             :                           mesh.extents(direction.dimension()),
     945             :                           face_normal_magnitude);
     946             :           add_slice_to_data(operator_applied_to_vars,
     947             :                             primal_boundary_corrections, mesh.extents(),
     948             :                             direction.dimension(),
     949             :                             index_to_slice_at(mesh.extents(), direction));
     950             :         } else {
     951             :           // We already have the `face_jacobian = det(J) * magnitude(n)` here,
     952             :           // so just pass a constant 1 for `magnitude(n)`. This could be
     953             :           // optimized to avoid allocating the vector of ones.
     954             :           ::dg::lift_boundary_terms_gauss_points(
     955             :               operator_applied_to_vars, det_inv_jacobian, mesh, direction,
     956             :               primal_boundary_corrections,
     957             :               Scalar<DataVector>{face_mesh.number_of_grid_points(), 1.},
     958             :               face_jacobian);
     959             :         }
     960             :       }
     961             :     }  // loop over all mortars
     962             : 
     963             :     if (not has_any_boundary_corrections) {
     964             :       // No need to handle auxiliary boundary corrections; return early
     965             :       return;
     966             :     }
     967             : 
     968             :     // Apply weak divergence to lifted auxiliary boundary corrections and add to
     969             :     // operator
     970             :     if (massive) {
     971             :       logical_weak_divergence(operator_applied_to_vars,
     972             :                               lifted_logical_aux_boundary_corrections, mesh,
     973             :                               true);
     974             :     } else {
     975             :       // Possible optimization: eliminate this allocation by building the
     976             :       // inverse mass matrix into `logical_weak_divergence`
     977             :       Variables<tmpl::list<OperatorTags...>> massless_aux_boundary_corrections{
     978             :           num_points};
     979             :       logical_weak_divergence(make_not_null(&massless_aux_boundary_corrections),
     980             :                               lifted_logical_aux_boundary_corrections, mesh);
     981             :       massless_aux_boundary_corrections *= get(det_inv_jacobian);
     982             :       ::dg::apply_inverse_mass_matrix(
     983             :           make_not_null(&massless_aux_boundary_corrections), mesh);
     984             :       *operator_applied_to_vars += massless_aux_boundary_corrections;
     985             :     }
     986             :   }
     987             : 
     988             :   template <typename... FixedSourcesTags, typename ApplyBoundaryCondition,
     989             :             typename... FluxesArgs, typename... SourcesArgs,
     990             :             typename... ModifyBoundaryDataArgs,
     991             :             bool LocalLinearized = Linearized,
     992             :             // This function adds nothing to the fixed sources if the operator
     993             :             // is linearized, so it shouldn't be used in that case
     994             :             Requires<not LocalLinearized> = nullptr>
     995             :   static void impose_inhomogeneous_boundary_conditions_on_source(
     996             :       const gsl::not_null<Variables<tmpl::list<FixedSourcesTags...>>*>
     997             :           fixed_sources,
     998             :       const Element<Dim>& element, const Mesh<Dim>& mesh,
     999             :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
    1000             :                             Frame::Inertial>& inv_jacobian,
    1001             :       const Scalar<DataVector>& det_inv_jacobian,
    1002             :       const Scalar<DataVector>& det_jacobian,
    1003             :       const InverseJacobian<DataVector, Dim, Frame::ElementLogical,
    1004             :                             Frame::Inertial>& det_times_inv_jacobian,
    1005             :       const DirectionMap<Dim, tnsr::i<DataVector, Dim>>& face_normals,
    1006             :       const DirectionMap<Dim, tnsr::I<DataVector, Dim>>& face_normal_vectors,
    1007             :       const DirectionMap<Dim, Scalar<DataVector>>& face_normal_magnitudes,
    1008             :       const DirectionMap<Dim, Scalar<DataVector>>& face_jacobians,
    1009             :       const DirectionMap<Dim,
    1010             :                          InverseJacobian<DataVector, Dim, Frame::ElementLogical,
    1011             :                                          Frame::Inertial>>&
    1012             :           face_jacobian_times_inv_jacobians,
    1013             :       const ::dg::MortarMap<Dim, Mesh<Dim - 1>>& all_mortar_meshes,
    1014             :       const ::dg::MortarMap<Dim, ::dg::MortarSize<Dim - 1>>& all_mortar_sizes,
    1015             :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& mortar_jacobians,
    1016             :       const ::dg::MortarMap<Dim, Scalar<DataVector>>& penalty_factors,
    1017             :       const bool massive, const ::dg::Formulation formulation,
    1018             :       const ApplyBoundaryCondition& apply_boundary_condition,
    1019             :       const std::tuple<FluxesArgs...>& fluxes_args,
    1020             :       const std::tuple<SourcesArgs...>& sources_args,
    1021             :       const DirectionMap<Dim, std::tuple<FluxesArgs...>>& fluxes_args_on_faces,
    1022             :       const std::tuple<ModifyBoundaryDataArgs...>& modify_boundary_data_args) {
    1023             :     // We just feed zero variables through the nonlinear operator to extract the
    1024             :     // constant contribution at external boundaries. Since the variables are
    1025             :     // zero the operator simplifies quite a lot. The simplification is probably
    1026             :     // not very important for performance because this function will only be
    1027             :     // called when solving a linear elliptic system and only once during
    1028             :     // initialization, but we specialize the operator for zero data nonetheless
    1029             :     // just so we can ignore internal boundaries. Only when the system modifies
    1030             :     // boundary data do we need to handle internal boundaries (see below),
    1031             :     // otherwise we can skip them.
    1032             :     const size_t num_points = mesh.number_of_grid_points();
    1033             :     const Variables<tmpl::list<PrimalFields...>> zero_primal_vars{num_points,
    1034             :                                                                   0.};
    1035             :     Variables<tmpl::list<PrimalFluxes...>> primal_fluxes_buffer{num_points};
    1036             :     Variables<tmpl::list<
    1037             :         ::Tags::deriv<PrimalFields, tmpl::size_t<Dim>, Frame::Inertial>...>>
    1038             :         unused_deriv_vars_buffer{};
    1039             :     Variables<tmpl::list<FixedSourcesTags...>> operator_applied_to_zero_vars{
    1040             :         num_points};
    1041             :     // Set up data on mortars
    1042             :     ::dg::MortarMap<Dim, MortarData<size_t, tmpl::list<PrimalFields...>,
    1043             :                                     tmpl::list<PrimalFluxes...>>>
    1044             :         all_mortar_data{};
    1045             :     constexpr size_t temporal_id = std::numeric_limits<size_t>::max();
    1046             :     // Apply the operator to the zero variables, skipping internal boundaries
    1047             :     prepare_mortar_data<true>(make_not_null(&unused_deriv_vars_buffer),
    1048             :                               make_not_null(&primal_fluxes_buffer),
    1049             :                               make_not_null(&all_mortar_data), zero_primal_vars,
    1050             :                               element, mesh, inv_jacobian, face_normals,
    1051             :                               all_mortar_meshes, all_mortar_sizes, temporal_id,
    1052             :                               apply_boundary_condition, fluxes_args);
    1053             :     // Modify internal boundary data if needed, e.g. to transform from one
    1054             :     // variable to another when crossing element boundaries. This is a nonlinear
    1055             :     // operation in the sense that feeding zero through the operator is nonzero,
    1056             :     // so we evaluate it here and add the contribution to the fixed sources,
    1057             :     // just like inhomogeneous external boundary conditions.
    1058             :     if constexpr (not std::is_same_v<typename System::modify_boundary_data,
    1059             :                                      void>) {
    1060             :       for (const auto& [direction, neighbors] : element.neighbors()) {
    1061             :         for (const auto& neighbor_id : neighbors) {
    1062             :           const ::dg::MortarId<Dim> mortar_id{direction, neighbor_id};
    1063             :           ASSERT(not all_mortar_data.contains(mortar_id),
    1064             :                  "Mortar data with ID " << mortar_id << " already exists.");
    1065             :           auto& mortar_data = all_mortar_data[mortar_id];
    1066             :           // Manufacture zero mortar data, store as local data, then treat as
    1067             :           // received from neighbor and apply modifications
    1068             :           BoundaryData<tmpl::list<PrimalFields...>, tmpl::list<PrimalFluxes...>>
    1069             :               remote_boundary_data_on_mortar{};
    1070             :           remote_boundary_data_on_mortar.field_data.initialize(
    1071             :               all_mortar_meshes.at(mortar_id).number_of_grid_points(), 0.);
    1072             :           mortar_data.local_insert(temporal_id, remote_boundary_data_on_mortar);
    1073             :           // Modify "received" mortar data
    1074             :           std::apply(
    1075             :               [&remote_boundary_data_on_mortar,
    1076             :                &mortar_id](const auto&... args) {
    1077             :                 System::modify_boundary_data::apply(
    1078             :                     make_not_null(&get<PrimalFields>(
    1079             :                         remote_boundary_data_on_mortar.field_data))...,
    1080             :                     make_not_null(&get<::Tags::NormalDotFlux<PrimalFields>>(
    1081             :                         remote_boundary_data_on_mortar.field_data))...,
    1082             :                     mortar_id, args...);
    1083             :               },
    1084             :               modify_boundary_data_args);
    1085             :           // Insert as remote mortar data
    1086             :           mortar_data.remote_insert(temporal_id,
    1087             :                                     std::move(remote_boundary_data_on_mortar));
    1088             :         }
    1089             :       }
    1090             :     }
    1091             :     apply_operator(make_not_null(&operator_applied_to_zero_vars),
    1092             :                    make_not_null(&all_mortar_data), zero_primal_vars,
    1093             :                    primal_fluxes_buffer, element, mesh, inv_jacobian,
    1094             :                    det_inv_jacobian, det_jacobian, det_times_inv_jacobian,
    1095             :                    face_normals, face_normal_vectors, face_normal_magnitudes,
    1096             :                    face_jacobians, face_jacobian_times_inv_jacobians,
    1097             :                    all_mortar_meshes, all_mortar_sizes, mortar_jacobians,
    1098             :                    penalty_factors, massive, formulation, temporal_id,
    1099             :                    fluxes_args_on_faces, sources_args);
    1100             :     // Impose the nonlinear (constant) boundary contribution as fixed sources on
    1101             :     // the RHS of the equations
    1102             :     *fixed_sources -= operator_applied_to_zero_vars;
    1103             :   }
    1104             : };
    1105             : 
    1106             : }  // namespace detail
    1107             : 
    1108             : /*!
    1109             :  * \brief Prepare data on mortars so they can be communicated to neighbors
    1110             :  *
    1111             :  * Call this function on all elements and communicate the mortar data, then call
    1112             :  * `elliptic::dg::apply_operator`.
    1113             :  */
    1114             : template <typename System, bool Linearized, typename... Args>
    1115           1 : void prepare_mortar_data(Args&&... args) {
    1116             :   detail::DgOperatorImpl<System, Linearized>::template prepare_mortar_data<
    1117             :       false>(std::forward<Args>(args)...);
    1118             : }
    1119             : 
    1120             : /*!
    1121             :  * \brief Apply the elliptic DG operator
    1122             :  *
    1123             :  * This function applies the elliptic DG operator on an element, assuming all
    1124             :  * data on mortars is already available. Use the
    1125             :  * `elliptic::dg::prepare_mortar_data` function to prepare mortar data on
    1126             :  * neighboring elements, then communicate the data and insert them on the
    1127             :  * "remote" side of the mortars before calling this function.
    1128             :  */
    1129             : template <typename System, bool Linearized, typename... Args>
    1130           1 : void apply_operator(Args&&... args) {
    1131             :   detail::DgOperatorImpl<System, Linearized>::apply_operator(
    1132             :       std::forward<Args>(args)...);
    1133             : }
    1134             : 
    1135             : /*!
    1136             :  * \brief For linear systems, impose inhomogeneous boundary conditions as
    1137             :  * contributions to the fixed sources (i.e. the RHS of the equations).
    1138             :  *
    1139             :  * This function exists because the DG operator must typically be linear, but
    1140             :  * even for linear elliptic equations we typically apply boundary conditions
    1141             :  * with a constant, and therefore nonlinear, contribution. Standard examples are
    1142             :  * inhomogeneous (i.e. non-zero) Dirichlet or Neumann boundary conditions. This
    1143             :  * nonlinear contribution can be added to the fixed sources, leaving only the
    1144             :  * linearized boundary conditions in the DG operator. For standard constant
    1145             :  * Dirichlet or Neumann boundary conditions the linearization is of course just
    1146             :  * zero.
    1147             :  *
    1148             :  * This function essentially feeds zero variables through the nonlinear operator
    1149             :  * and subtracts the result from the fixed sources: `b -= A(x=0)`.
    1150             :  */
    1151             : template <typename System, typename... Args>
    1152           1 : void impose_inhomogeneous_boundary_conditions_on_source(Args&&... args) {
    1153             :   detail::DgOperatorImpl<System, false>::
    1154             :       impose_inhomogeneous_boundary_conditions_on_source(
    1155             :           std::forward<Args>(args)...);
    1156             : }
    1157             : 
    1158             : }  // namespace elliptic::dg

Generated by: LCOV version 1.14