Line data Source code
1 0 : // Distributed under the MIT License.
2 : // See LICENSE.txt for details.
3 :
4 : #pragma once
5 :
6 : #include <atomic>
7 : #include <cstddef>
8 : #include <limits>
9 : #include <map>
10 : #include <mutex>
11 : #include <optional>
12 : #include <tuple>
13 : #include <type_traits>
14 : #include <utility>
15 : #include <vector>
16 :
17 : #include "DataStructures/DataBox/AsAccess.hpp"
18 : #include "DataStructures/DataBox/DataBox.hpp"
19 : #include "DataStructures/DataBox/PrefixHelpers.hpp"
20 : #include "DataStructures/DataBox/Prefixes.hpp"
21 : #include "DataStructures/Tensor/EagerMath/Magnitude.hpp"
22 : #include "Domain/FaceNormal.hpp"
23 : #include "Domain/Structure/DirectionalIdMap.hpp"
24 : #include "Domain/Structure/Element.hpp"
25 : #include "Domain/Structure/ElementId.hpp"
26 : #include "Domain/Structure/Topology.hpp"
27 : #include "Domain/Structure/TrimMap.hpp"
28 : #include "Domain/Tags.hpp"
29 : #include "Domain/Tags/NeighborMesh.hpp"
30 : #include "Evolution/BoundaryCorrection.hpp"
31 : #include "Evolution/BoundaryCorrectionTags.hpp"
32 : #include "Evolution/DiscontinuousGalerkin/BoundaryData.hpp"
33 : #include "Evolution/DiscontinuousGalerkin/InboxTags.hpp"
34 : #include "Evolution/DiscontinuousGalerkin/MortarData.hpp"
35 : #include "Evolution/DiscontinuousGalerkin/MortarDataHolder.hpp"
36 : #include "Evolution/DiscontinuousGalerkin/MortarTags.hpp"
37 : #include "Evolution/DiscontinuousGalerkin/NormalVectorTags.hpp"
38 : #include "Evolution/DiscontinuousGalerkin/TimeSteppingPolicy.hpp"
39 : #include "Evolution/DiscontinuousGalerkin/UsingSubcell.hpp"
40 : #include "NumericalAlgorithms/DiscontinuousGalerkin/Formulation.hpp"
41 : #include "NumericalAlgorithms/DiscontinuousGalerkin/LiftFlux.hpp"
42 : #include "NumericalAlgorithms/DiscontinuousGalerkin/LiftFromBoundary.hpp"
43 : #include "NumericalAlgorithms/DiscontinuousGalerkin/MortarHelpers.hpp"
44 : #include "NumericalAlgorithms/DiscontinuousGalerkin/Tags/Formulation.hpp"
45 : #include "NumericalAlgorithms/Spectral/BoundaryInterpolationMatrices.hpp"
46 : #include "NumericalAlgorithms/Spectral/Mesh.hpp"
47 : #include "NumericalAlgorithms/Spectral/Quadrature.hpp"
48 : #include "NumericalAlgorithms/Spectral/SegmentSize.hpp"
49 : #include "Parallel/AlgorithmExecution.hpp"
50 : #include "Parallel/ArrayCollection/IsDgElementCollection.hpp"
51 : #include "Parallel/GlobalCache.hpp"
52 : #include "Time/BoundaryHistory.hpp"
53 : #include "Time/EvolutionOrdering.hpp"
54 : #include "Time/SelfStart.hpp"
55 : #include "Time/Time.hpp"
56 : #include "Time/TimeStepId.hpp"
57 : #include "Time/TimeSteppers/LtsTimeStepper.hpp"
58 : #include "Time/TimeSteppers/TimeStepper.hpp"
59 : #include "Utilities/Algorithm.hpp"
60 : #include "Utilities/CallWithDynamicType.hpp"
61 : #include "Utilities/ErrorHandling/Error.hpp"
62 : #include "Utilities/Gsl.hpp"
63 : #include "Utilities/MakeArray.hpp"
64 : #include "Utilities/TMPL.hpp"
65 : #include "Utilities/TaggedTuple.hpp"
66 :
67 : /// \cond
68 : namespace Tags {
69 : struct Time;
70 : struct TimeStep;
71 : struct TimeStepId;
72 : template <typename StepperInterface>
73 : struct TimeStepper;
74 : } // namespace Tags
75 :
76 : namespace evolution::dg::subcell {
77 : // We use a forward declaration instead of including a header file to avoid
78 : // coupling to the DG-subcell libraries for executables that don't use subcell.
79 : template <size_t VolumeDim, typename DgComputeSubcellNeighborPackagedData>
80 : void neighbor_reconstructed_face_solution(
81 : gsl::not_null<db::Access*> box,
82 : gsl::not_null<std::pair<
83 : TimeStepId,
84 : DirectionalIdMap<VolumeDim, evolution::dg::BoundaryData<VolumeDim>>>*>
85 : received_temporal_id_and_data);
86 : template <size_t Dim>
87 : void neighbor_tci_decision(
88 : gsl::not_null<db::Access*> box,
89 : const std::pair<TimeStepId,
90 : DirectionalIdMap<Dim, evolution::dg::BoundaryData<Dim>>>&
91 : received_temporal_id_and_data);
92 : } // namespace evolution::dg::subcell
93 : /// \endcond
94 :
95 : namespace evolution::dg {
96 : namespace detail {
97 : template <typename BoundaryCorrectionClass>
98 : struct get_dg_boundary_terms {
99 : using type = typename BoundaryCorrectionClass::dg_boundary_terms_volume_tags;
100 : };
101 :
102 : template <typename Tag, typename Type = db::const_item_type<Tag, tmpl::list<>>>
103 : struct TemporaryReference {
104 : using tag = Tag;
105 : using type = const Type&;
106 : };
107 : } // namespace detail
108 :
109 : /// Receive boundary data for global time-stepping. Returns true if
110 : /// all necessary data has been received.
111 : template <bool UseNodegroupDgElements, typename Metavariables,
112 : typename DbTagsList, typename... InboxTags>
113 1 : bool receive_boundary_data_global_time_stepping(
114 : const gsl::not_null<db::DataBox<DbTagsList>*> box,
115 : const gsl::not_null<tuples::TaggedTuple<InboxTags...>*> inboxes) {
116 : constexpr size_t volume_dim = Metavariables::system::volume_dim;
117 :
118 : const TimeStepId& temporal_id = get<::Tags::TimeStepId>(*box);
119 :
120 : const auto number_of_neighbors =
121 : db::get<domain::Tags::Element<volume_dim>>(*box).number_of_neighbors();
122 :
123 : auto& inbox =
124 : tuples::get<evolution::dg::Tags::BoundaryCorrectionAndGhostCellsInbox<
125 : volume_dim, UseNodegroupDgElements>>(*inboxes);
126 : collect_messages:
127 : inbox.collect_messages();
128 : const auto received_record = inbox.messages.find(temporal_id);
129 : if (received_record == inbox.messages.end()) {
130 : if (inbox.set_missing_messages(number_of_neighbors)) {
131 : // We've received new messages while this function was running,
132 : // so try again.
133 : goto collect_messages; // NOLINT(cppcoreguidelines-avoid-goto)
134 : }
135 : return false;
136 : }
137 : auto& received_neighbor_data = received_record->second;
138 : if (received_neighbor_data.size() != number_of_neighbors) {
139 : ASSERT(received_neighbor_data.size() < number_of_neighbors,
140 : "Received too many messages: " << received_neighbor_data);
141 : if (inbox.set_missing_messages(number_of_neighbors -
142 : received_neighbor_data.size())) {
143 : // We've received new messages while this function was running,
144 : // so try again.
145 : goto collect_messages; // NOLINT(cppcoreguidelines-avoid-goto)
146 : }
147 : return false;
148 : }
149 :
150 : std::pair received_temporal_id_and_data{temporal_id,
151 : std::move(received_neighbor_data)};
152 : inbox.messages.erase(received_record);
153 :
154 : // Move inbox contents into the DataBox
155 : if constexpr (using_subcell_v<Metavariables>) {
156 : evolution::dg::subcell::neighbor_reconstructed_face_solution<
157 : volume_dim, typename Metavariables::SubcellOptions::
158 : DgComputeSubcellNeighborPackagedData>(
159 : &db::as_access(*box), make_not_null(&received_temporal_id_and_data));
160 : evolution::dg::subcell::neighbor_tci_decision<volume_dim>(
161 : make_not_null(&db::as_access(*box)), received_temporal_id_and_data);
162 : }
163 :
164 : db::mutate<evolution::dg::Tags::MortarMesh<volume_dim>,
165 : evolution::dg::Tags::MortarData<volume_dim>,
166 : evolution::dg::Tags::MortarNextTemporalId<volume_dim>,
167 : domain::Tags::NeighborMesh<volume_dim>>(
168 : [&received_temporal_id_and_data](
169 : const gsl::not_null<
170 : DirectionalIdMap<volume_dim, Mesh<volume_dim - 1>>*>
171 : mortar_meshes,
172 : const gsl::not_null<DirectionalIdMap<
173 : volume_dim, evolution::dg::MortarDataHolder<volume_dim>>*>
174 : mortar_data,
175 : const gsl::not_null<DirectionalIdMap<volume_dim, TimeStepId>*>
176 : mortar_next_time_step_id,
177 : const gsl::not_null<DirectionalIdMap<volume_dim, Mesh<volume_dim>>*>
178 : neighbor_mesh,
179 : const Mesh<volume_dim>& volume_mesh) {
180 : neighbor_mesh->clear();
181 : for (auto& received_mortar_data :
182 : received_temporal_id_and_data.second) {
183 : const auto& mortar_id = received_mortar_data.first;
184 : const size_t sliced_away_dim = mortar_id.direction().dimension();
185 : const Mesh<volume_dim - 1> face_mesh =
186 : volume_mesh.slice_away(sliced_away_dim);
187 : const Mesh<volume_dim - 1> neighbor_face_mesh =
188 : received_mortar_data.second.volume_mesh.slice_away(
189 : sliced_away_dim);
190 : const Mesh<volume_dim - 1> mortar_mesh =
191 : ::dg::mortar_mesh(face_mesh, neighbor_face_mesh);
192 : mortar_meshes->at(mortar_id) = mortar_mesh;
193 : p_project_mortar_data(
194 : make_not_null(&mortar_data->at(mortar_id).local()), mortar_mesh);
195 : neighbor_mesh->insert_or_assign(
196 : mortar_id, received_mortar_data.second.volume_mesh);
197 : mortar_next_time_step_id->at(mortar_id) =
198 : received_mortar_data.second.validity_range;
199 : ASSERT(using_subcell_v<Metavariables> or
200 : received_mortar_data.second.boundary_correction_data
201 : .has_value(),
202 : "Must receive number boundary correction data when not using "
203 : "DG-subcell. Mortar ID is: ("
204 : << mortar_id.direction() << "," << mortar_id.id()
205 : << ") and TimeStepId is "
206 : << received_temporal_id_and_data.first);
207 : if (received_mortar_data.second.boundary_correction_data
208 : .has_value()) {
209 : mortar_data->at(mortar_id).neighbor().face_mesh =
210 : neighbor_face_mesh;
211 : mortar_data->at(mortar_id).neighbor().mortar_mesh =
212 : received_mortar_data.second.boundary_correction_mesh.value();
213 : mortar_data->at(mortar_id).neighbor().mortar_data = std::move(
214 : received_mortar_data.second.boundary_correction_data.value());
215 : p_project_mortar_data(
216 : make_not_null(&mortar_data->at(mortar_id).neighbor()),
217 : mortar_mesh);
218 : }
219 : }
220 : },
221 : box, db::get<domain::Tags::Mesh<volume_dim>>(*box));
222 : return true;
223 : }
224 :
225 : /// Receive boundary data for local time-stepping. Returns true if
226 : /// all necessary data has been received.
227 : ///
228 : /// Setting \p DenseOutput to true receives data required for output
229 : /// at `::Tags::Time` instead of `::Tags::Next<::Tags::TimeStepId>`.
230 : template <bool UseNodegroupDgElements, typename System, size_t Dim,
231 : bool DenseOutput, typename DbTagsList, typename... InboxTags>
232 1 : bool receive_boundary_data_local_time_stepping(
233 : const gsl::not_null<db::DataBox<DbTagsList>*> box,
234 : const gsl::not_null<tuples::TaggedTuple<InboxTags...>*> inboxes) {
235 : using variables_tag = typename System::variables_tag;
236 : using dt_variables_tag = db::add_tag_prefix<::Tags::dt, variables_tag>;
237 :
238 : const auto needed_time = [&box]() {
239 : const LtsTimeStepper& time_stepper =
240 : db::get<::Tags::TimeStepper<LtsTimeStepper>>(*box);
241 : if constexpr (DenseOutput) {
242 : const auto& dense_output_time = db::get<::Tags::Time>(*box);
243 : return [&dense_output_time, &time_stepper](const TimeStepId& id) {
244 : return time_stepper.neighbor_data_required(dense_output_time, id);
245 : };
246 : } else {
247 : const auto& next_temporal_id =
248 : db::get<::Tags::Next<::Tags::TimeStepId>>(*box);
249 : return [&next_temporal_id, &time_stepper](const TimeStepId& id) {
250 : return time_stepper.neighbor_data_required(next_temporal_id, id);
251 : };
252 : }
253 : }();
254 :
255 : auto& inbox =
256 : tuples::get<evolution::dg::Tags::BoundaryCorrectionAndGhostCellsInbox<
257 : Dim, UseNodegroupDgElements>>(*inboxes);
258 :
259 : size_t missing_messages{};
260 : do {
261 : // The boundary history coupling computation (which computes the _lifted_
262 : // boundary correction) returns a Variables<dt<EvolvedVars>> instead of
263 : // using the `NormalDotNumericalFlux` prefix tag. This is because the
264 : // returned quantity is more a `dt` quantity than a
265 : // `NormalDotNormalDotFlux` since it's been lifted to the volume.
266 : using InboxMap =
267 : std::map<TimeStepId,
268 : DirectionalIdMap<Dim, evolution::dg::BoundaryData<Dim>>>;
269 : inbox.collect_messages();
270 : InboxMap& inbox_data = inbox.messages;
271 :
272 : missing_messages = 0;
273 :
274 : db::mutate<evolution::dg::Tags::MortarMesh<Dim>,
275 : evolution::dg::Tags::MortarDataHistory<
276 : Dim, typename dt_variables_tag::type>,
277 : evolution::dg::Tags::MortarNextTemporalId<Dim>,
278 : domain::Tags::NeighborMesh<Dim>>(
279 : [&inbox_data, &missing_messages, &needed_time](
280 : const gsl::not_null<DirectionalIdMap<Dim, Mesh<Dim - 1>>*>
281 : mortar_meshes,
282 : const gsl::not_null<
283 : DirectionalIdMap<Dim, TimeSteppers::BoundaryHistory<
284 : evolution::dg::MortarData<Dim>,
285 : evolution::dg::MortarData<Dim>,
286 : typename dt_variables_tag::type>>*>
287 : boundary_data_history,
288 : const gsl::not_null<DirectionalIdMap<Dim, TimeStepId>*>
289 : mortar_next_time_step_ids,
290 : const gsl::not_null<DirectionalIdMap<Dim, Mesh<Dim>>*>
291 : neighbor_mesh,
292 : const Element<Dim>& element, const Mesh<Dim>& volume_mesh) {
293 : // Remove neighbor meshes for neighbors that don't exist anymore
294 : domain::remove_nonexistent_neighbors(neighbor_mesh, element);
295 :
296 : // Move received boundary data into boundary history.
297 : for (auto& [mortar_id, mortar_next_time_step_id] :
298 : *mortar_next_time_step_ids) {
299 : if (mortar_id.id() == ElementId<Dim>::external_boundary_id()) {
300 : continue;
301 : }
302 : const size_t sliced_away_dim = mortar_id.direction().dimension();
303 : const Mesh<Dim - 1> face_mesh =
304 : volume_mesh.slice_away(sliced_away_dim);
305 : while (needed_time(mortar_next_time_step_id)) {
306 : const auto time_entry = inbox_data.find(mortar_next_time_step_id);
307 : if (time_entry == inbox_data.end()) {
308 : ++missing_messages;
309 : break;
310 : }
311 : const auto received_mortar_data =
312 : time_entry->second.find(mortar_id);
313 : if (received_mortar_data == time_entry->second.end()) {
314 : ++missing_messages;
315 : break;
316 : }
317 :
318 : const Mesh<Dim - 1> neighbor_face_mesh =
319 : received_mortar_data->second.volume_mesh.slice_away(
320 : sliced_away_dim);
321 : const Mesh<Dim - 1> mortar_mesh =
322 : ::dg::mortar_mesh(face_mesh, neighbor_face_mesh);
323 :
324 : const auto project_boundary_mortar_data =
325 : [&mortar_mesh](
326 : const TimeStepId& /*id*/,
327 : const gsl::not_null<::evolution::dg::MortarData<Dim>*>
328 : mortar_data) {
329 : return p_project_mortar_data(mortar_data, mortar_mesh);
330 : };
331 :
332 : mortar_meshes->at(mortar_id) = mortar_mesh;
333 : boundary_data_history->at(mortar_id).local().for_each(
334 : project_boundary_mortar_data);
335 :
336 : MortarData<Dim> neighbor_mortar_data{};
337 : // Insert:
338 : // - the current TimeStepId of the neighbor
339 : // - the current face mesh of the neighbor
340 : // - the current boundary correction data of the neighbor
341 : ASSERT(received_mortar_data->second.boundary_correction_data
342 : .has_value(),
343 : "Did not receive boundary correction data from the "
344 : "neighbor\nMortarId: "
345 : << mortar_id
346 : << "\nTimeStepId: " << mortar_next_time_step_id);
347 : neighbor_mesh->insert_or_assign(
348 : mortar_id, received_mortar_data->second.volume_mesh);
349 : neighbor_mortar_data.mortar_mesh =
350 : received_mortar_data->second.boundary_correction_mesh.value();
351 : neighbor_mortar_data.mortar_data =
352 : std::move(received_mortar_data->second
353 : .boundary_correction_data.value());
354 : boundary_data_history->at(mortar_id).remote().insert(
355 : time_entry->first,
356 : received_mortar_data->second.integration_order,
357 : std::move(neighbor_mortar_data));
358 : boundary_data_history->at(mortar_id).remote().for_each(
359 : project_boundary_mortar_data);
360 : mortar_next_time_step_id =
361 : received_mortar_data->second.validity_range;
362 : time_entry->second.erase(received_mortar_data);
363 : if (time_entry->second.empty()) {
364 : inbox_data.erase(time_entry);
365 : }
366 : }
367 : }
368 : },
369 : box, db::get<::domain::Tags::Element<Dim>>(*box),
370 : db::get<domain::Tags::Mesh<Dim>>(*box));
371 :
372 : if (missing_messages == 0) {
373 : return true;
374 : }
375 : } while (inbox.set_missing_messages(missing_messages));
376 : return false;
377 : }
378 :
379 : /// Apply corrections from boundary communication.
380 : ///
381 : /// If `LocalTimeStepping` is false, updates the derivative of the variables,
382 : /// which should be done before taking a time step. If
383 : /// `LocalTimeStepping` is true, updates the variables themselves, which should
384 : /// be done after the volume update.
385 : ///
386 : /// Setting \p DenseOutput to true receives data required for output
387 : /// at ::Tags::Time instead of performing a full step. This is only
388 : /// used for local time-stepping.
389 : template <bool LocalTimeStepping, typename Metavariables, size_t VolumeDim,
390 : bool DenseOutput>
391 1 : struct ApplyBoundaryCorrections {
392 0 : static constexpr bool local_time_stepping = LocalTimeStepping;
393 : static_assert(local_time_stepping or not DenseOutput,
394 : "GTS does not use ApplyBoundaryCorrections for dense output.");
395 :
396 0 : using system = typename Metavariables::system;
397 0 : static constexpr size_t volume_dim = VolumeDim;
398 0 : using variables_tag = typename system::variables_tag;
399 0 : using dt_variables_tag = db::add_tag_prefix<::Tags::dt, variables_tag>;
400 0 : using DtVariables = typename dt_variables_tag::type;
401 0 : using derived_boundary_corrections =
402 : tmpl::at<typename Metavariables::factory_creation::factory_classes,
403 : evolution::BoundaryCorrection>;
404 0 : using volume_tags_for_dg_boundary_terms = tmpl::remove_duplicates<
405 : tmpl::flatten<tmpl::transform<derived_boundary_corrections,
406 : detail::get_dg_boundary_terms<tmpl::_1>>>>;
407 :
408 0 : using TimeStepperType =
409 : tmpl::conditional_t<local_time_stepping, LtsTimeStepper, TimeStepper>;
410 :
411 0 : using tag_to_update =
412 : tmpl::conditional_t<local_time_stepping, variables_tag, dt_variables_tag>;
413 0 : using mortar_data_tag = tmpl::conditional_t<
414 : local_time_stepping,
415 : evolution::dg::Tags::MortarDataHistory<volume_dim, DtVariables>,
416 : evolution::dg::Tags::MortarData<volume_dim>>;
417 :
418 0 : using return_tags = tmpl::list<tag_to_update>;
419 0 : using argument_tags = tmpl::append<
420 : tmpl::flatten<tmpl::list<
421 : mortar_data_tag, domain::Tags::Mesh<volume_dim>,
422 : domain::Tags::Element<volume_dim>, Tags::MortarMesh<volume_dim>,
423 : Tags::MortarInfo<volume_dim>, ::dg::Tags::Formulation,
424 : evolution::dg::Tags::NormalCovectorAndMagnitude<volume_dim>,
425 : ::Tags::TimeStepper<TimeStepperType>,
426 : evolution::Tags::BoundaryCorrection,
427 : tmpl::conditional_t<DenseOutput, ::Tags::Time, ::Tags::TimeStep>,
428 : tmpl::conditional_t<local_time_stepping, tmpl::list<>,
429 : domain::Tags::DetInvJacobian<
430 : Frame::ElementLogical, Frame::Inertial>>>>,
431 : volume_tags_for_dg_boundary_terms>;
432 :
433 : // full step
434 : template <typename... VolumeArgs>
435 0 : static void apply(
436 : const gsl::not_null<typename tag_to_update::type*> vars_to_update,
437 : const typename mortar_data_tag::type& mortar_data,
438 : const Mesh<volume_dim>& volume_mesh, const Element<volume_dim>& element,
439 : const typename Tags::MortarMesh<volume_dim>::type& mortar_meshes,
440 : const typename Tags::MortarInfo<volume_dim>::type& mortar_infos,
441 : const ::dg::Formulation dg_formulation,
442 : const DirectionMap<
443 : volume_dim, std::optional<Variables<tmpl::list<
444 : evolution::dg::Tags::MagnitudeOfNormal,
445 : evolution::dg::Tags::NormalCovector<volume_dim>>>>>&
446 : face_normal_covector_and_magnitude,
447 : const TimeStepperType& time_stepper,
448 : const evolution::BoundaryCorrection& boundary_correction,
449 : const TimeDelta& time_step,
450 : const Scalar<DataVector>& gts_det_inv_jacobian,
451 : const VolumeArgs&... volume_args) {
452 : apply_impl(vars_to_update, mortar_data, volume_mesh, element, mortar_meshes,
453 : mortar_infos, dg_formulation, face_normal_covector_and_magnitude,
454 : time_stepper, boundary_correction, time_step,
455 : std::numeric_limits<double>::signaling_NaN(),
456 : gts_det_inv_jacobian, volume_args...);
457 : }
458 :
459 : template <typename... VolumeArgs>
460 0 : static void apply(
461 : const gsl::not_null<typename tag_to_update::type*> vars_to_update,
462 : const typename mortar_data_tag::type& mortar_data,
463 : const Mesh<volume_dim>& volume_mesh, const Element<volume_dim>& element,
464 : const typename Tags::MortarMesh<volume_dim>::type& mortar_meshes,
465 : const typename Tags::MortarInfo<volume_dim>::type& mortar_infos,
466 : const ::dg::Formulation dg_formulation,
467 : const DirectionMap<
468 : volume_dim, std::optional<Variables<tmpl::list<
469 : evolution::dg::Tags::MagnitudeOfNormal,
470 : evolution::dg::Tags::NormalCovector<volume_dim>>>>>&
471 : face_normal_covector_and_magnitude,
472 : const TimeStepperType& time_stepper,
473 : const evolution::BoundaryCorrection& boundary_correction,
474 : const TimeDelta& time_step, const VolumeArgs&... volume_args) {
475 : apply_impl(vars_to_update, mortar_data, volume_mesh, element, mortar_meshes,
476 : mortar_infos, dg_formulation, face_normal_covector_and_magnitude,
477 : time_stepper, boundary_correction, time_step,
478 : std::numeric_limits<double>::signaling_NaN(), {},
479 : volume_args...);
480 : }
481 :
482 : // dense output (LTS only)
483 : template <typename... VolumeArgs>
484 0 : static void apply(
485 : const gsl::not_null<typename variables_tag::type*> vars_to_update,
486 : const typename mortar_data_tag::type& mortar_data,
487 : const Mesh<volume_dim>& volume_mesh, const Element<volume_dim>& element,
488 : const typename Tags::MortarMesh<volume_dim>::type& mortar_meshes,
489 : const typename Tags::MortarInfo<volume_dim>::type& mortar_infos,
490 : const ::dg::Formulation dg_formulation,
491 : const DirectionMap<
492 : volume_dim, std::optional<Variables<tmpl::list<
493 : evolution::dg::Tags::MagnitudeOfNormal,
494 : evolution::dg::Tags::NormalCovector<volume_dim>>>>>&
495 : face_normal_covector_and_magnitude,
496 : const LtsTimeStepper& time_stepper,
497 : const evolution::BoundaryCorrection& boundary_correction,
498 : const double dense_output_time, const VolumeArgs&... volume_args) {
499 : apply_impl(vars_to_update, mortar_data, volume_mesh, element, mortar_meshes,
500 : mortar_infos, dg_formulation, face_normal_covector_and_magnitude,
501 : time_stepper, boundary_correction, TimeDelta{},
502 : dense_output_time, {}, volume_args...);
503 : }
504 :
505 : template <typename DbTagsList, typename... InboxTags, typename ArrayIndex,
506 : typename ParallelComponent>
507 0 : static bool is_ready(
508 : const gsl::not_null<db::DataBox<DbTagsList>*> box,
509 : const gsl::not_null<tuples::TaggedTuple<InboxTags...>*> inboxes,
510 : Parallel::GlobalCache<Metavariables>& /*cache*/,
511 : const ArrayIndex& /*array_index*/,
512 : const ParallelComponent* const /*component*/) {
513 : if constexpr (local_time_stepping) {
514 : return receive_boundary_data_local_time_stepping<
515 : Parallel::is_dg_element_collection_v<ParallelComponent>, system,
516 : VolumeDim, DenseOutput>(box, inboxes);
517 : } else {
518 : return receive_boundary_data_global_time_stepping<
519 : Parallel::is_dg_element_collection_v<ParallelComponent>,
520 : Metavariables>(box, inboxes);
521 : }
522 : }
523 :
524 : private:
525 : template <typename... VolumeArgs>
526 0 : static void apply_impl(
527 : const gsl::not_null<typename tag_to_update::type*> vars_to_update,
528 : const typename mortar_data_tag::type& mortar_data,
529 : const Mesh<volume_dim>& volume_mesh, const Element<volume_dim>& element,
530 : const typename Tags::MortarMesh<volume_dim>::type& mortar_meshes,
531 : const typename Tags::MortarInfo<volume_dim>::type& mortar_infos,
532 : const ::dg::Formulation dg_formulation,
533 : const DirectionMap<
534 : volume_dim, std::optional<Variables<tmpl::list<
535 : evolution::dg::Tags::MagnitudeOfNormal,
536 : evolution::dg::Tags::NormalCovector<volume_dim>>>>>&
537 : face_normal_covector_and_magnitude,
538 : const TimeStepperType& time_stepper,
539 : const evolution::BoundaryCorrection& boundary_correction,
540 : const TimeDelta& time_step, const double dense_output_time,
541 : const Scalar<DataVector>& gts_det_inv_jacobian,
542 : const VolumeArgs&... volume_args) {
543 : // We treat this as a set, but use a map because we don't have a
544 : // non-allocating set type.
545 : DirectionalIdMap<volume_dim, bool> mortars_to_act_on{};
546 : for (const auto& [mortar, info] : mortar_infos) {
547 : const auto& time_stepping_policy = info.time_stepping_policy();
548 : switch (time_stepping_policy) {
549 : case TimeSteppingPolicy::EqualRate:
550 : if (not local_time_stepping) {
551 : mortars_to_act_on.emplace(mortar, true);
552 : }
553 : break;
554 : case TimeSteppingPolicy::Conservative:
555 : if (local_time_stepping) {
556 : mortars_to_act_on.emplace(mortar, true);
557 : }
558 : break;
559 : default:
560 : ERROR("Unhandled TimeSteppingPolicy: " << time_stepping_policy);
561 : }
562 : }
563 : if (mortars_to_act_on.empty()) {
564 : return;
565 : }
566 :
567 : tuples::tagged_tuple_from_typelist<db::wrap_tags_in<
568 : detail::TemporaryReference, volume_tags_for_dg_boundary_terms>>
569 : volume_args_tuple{volume_args...};
570 :
571 : // Set up helper lambda that will compute and lift the boundary corrections
572 : ASSERT(
573 : volume_mesh.quadrature() ==
574 : make_array<volume_dim>(volume_mesh.quadrature(0)) or
575 : element.topologies() != domain::topologies::hypercube<volume_dim>,
576 : "Must have isotropic quadrature, but got volume mesh: " << volume_mesh);
577 : const bool using_gauss_lobatto_points =
578 : volume_mesh.quadrature(0) == Spectral::Quadrature::GaussLobatto;
579 :
580 : Scalar<DataVector> volume_det_inv_jacobian{};
581 : Scalar<DataVector> volume_det_jacobian{};
582 : if constexpr (not local_time_stepping) {
583 : if (not using_gauss_lobatto_points) {
584 : get(volume_det_inv_jacobian)
585 : .set_data_ref(make_not_null(
586 : // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
587 : &const_cast<DataVector&>(get(gts_det_inv_jacobian))));
588 : get(volume_det_jacobian) = 1.0 / get(volume_det_inv_jacobian);
589 : }
590 : }
591 :
592 : static_assert(
593 : tmpl::all<derived_boundary_corrections, std::is_final<tmpl::_1>>::value,
594 : "All createable classes for boundary corrections must be marked "
595 : "final.");
596 : call_with_dynamic_type<void, derived_boundary_corrections>(
597 : &boundary_correction,
598 : [&dense_output_time, &dg_formulation,
599 : &face_normal_covector_and_magnitude, &mortar_data, &mortar_meshes,
600 : &mortar_infos, &mortars_to_act_on, &time_step, &time_stepper,
601 : using_gauss_lobatto_points, &vars_to_update, &volume_args_tuple,
602 : &volume_det_jacobian, &volume_det_inv_jacobian,
603 : &volume_mesh](auto* typed_boundary_correction) {
604 : using BcType = std::decay_t<decltype(*typed_boundary_correction)>;
605 : // Compute internal boundary quantities on the mortar for sides of
606 : // the element that have neighbors, i.e. they are not an external
607 : // side.
608 : using mortar_tags_list = typename BcType::dg_package_field_tags;
609 :
610 : // Variables for reusing allocations. The actual values are
611 : // not reused.
612 : DtVariables dt_boundary_correction_on_mortar{};
613 : DtVariables volume_dt_correction{};
614 : // These variables may change size for each mortar and require
615 : // a new memory allocation, but they may also happen to need
616 : // to be the same size twice in a row, in which case holding
617 : // on to the allocation is a win.
618 : Scalar<DataVector> face_det_jacobian{};
619 : Variables<mortar_tags_list> local_data_on_mortar{};
620 : Variables<mortar_tags_list> neighbor_data_on_mortar{};
621 :
622 : for (const auto& mortar_id_and_data : mortar_data) {
623 : const auto& mortar_id = mortar_id_and_data.first;
624 : if (not mortars_to_act_on.contains(mortar_id)) {
625 : continue;
626 : }
627 : const auto& direction = mortar_id.direction();
628 : if (UNLIKELY(mortar_id.id() ==
629 : ElementId<volume_dim>::external_boundary_id())) {
630 : ERROR(
631 : "Cannot impose boundary conditions on external boundary in "
632 : "direction "
633 : << direction
634 : << " in the ApplyBoundaryCorrections action. Boundary "
635 : "conditions are applied in the ComputeTimeDerivative "
636 : "action "
637 : "instead. You may have unintentionally added external "
638 : "mortars in one of the initialization actions.");
639 : }
640 :
641 : const Mesh<volume_dim - 1> face_mesh =
642 : volume_mesh.slice_away(direction.dimension());
643 :
644 : const auto compute_correction_coupling =
645 : [&typed_boundary_correction, &direction, dg_formulation,
646 : &dt_boundary_correction_on_mortar, &face_det_jacobian,
647 : &face_mesh, &face_normal_covector_and_magnitude,
648 : &local_data_on_mortar, &mortar_id, &mortar_meshes,
649 : &mortar_infos, &neighbor_data_on_mortar,
650 : using_gauss_lobatto_points, &volume_args_tuple,
651 : &volume_det_jacobian, &volume_det_inv_jacobian,
652 : &volume_dt_correction, &volume_mesh](
653 : const MortarData<volume_dim>& local_mortar_data,
654 : const MortarData<volume_dim>& neighbor_mortar_data)
655 : -> DtVariables {
656 : if (local_time_stepping and not using_gauss_lobatto_points) {
657 : // This needs to be updated every call because the Jacobian
658 : // may be time-dependent. In the case of time-independent maps
659 : // and local time stepping we could first perform the integral
660 : // on the boundaries, and then lift to the volume. This is
661 : // left as a future optimization.
662 : volume_det_inv_jacobian =
663 : local_mortar_data.volume_det_inv_jacobian.value();
664 : get(volume_det_jacobian) = 1.0 / get(volume_det_inv_jacobian);
665 : }
666 : const auto& mortar_mesh = mortar_meshes.at(mortar_id);
667 :
668 : // Extract local and neighbor data, copy into Variables because
669 : // we store them in a std::vector for type erasure.
670 : ASSERT(*local_mortar_data.mortar_mesh ==
671 : *neighbor_mortar_data.mortar_mesh and
672 : *local_mortar_data.mortar_mesh == mortar_mesh,
673 : "local mortar mesh: " << *local_mortar_data.mortar_mesh
674 : << "\nneighbor mortar mesh: "
675 : << *neighbor_mortar_data.mortar_mesh
676 : << "\nmortar mesh: " << mortar_mesh
677 : << "\n");
678 : const DataVector& local_data = *local_mortar_data.mortar_data;
679 : const DataVector& neighbor_data =
680 : *neighbor_mortar_data.mortar_data;
681 : ASSERT(local_data.size() == neighbor_data.size(),
682 : "local data size: "
683 : << local_data.size()
684 : << "\nneighbor_data: " << neighbor_data.size()
685 : << "\n mortar_mesh: " << mortar_mesh << "\n");
686 : ASSERT(local_data_on_mortar.number_of_grid_points() ==
687 : neighbor_data_on_mortar.number_of_grid_points(),
688 : "Local data size = "
689 : << local_data_on_mortar.number_of_grid_points()
690 : << ", but neighbor size = "
691 : << neighbor_data_on_mortar.number_of_grid_points());
692 : local_data_on_mortar.set_data_ref(
693 : // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
694 : const_cast<double*>(local_data.data()), local_data.size());
695 : neighbor_data_on_mortar.set_data_ref(
696 : // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
697 : const_cast<double*>(neighbor_data.data()),
698 : neighbor_data.size());
699 :
700 : // The boundary computations and lifting can be further
701 : // optimized by in the h-refinement case having only one
702 : // allocation for the face and having the projection from the
703 : // mortar to the face be done in place. E.g.
704 : // local_data_on_mortar and neighbor_data_on_mortar could be
705 : // allocated fewer times, as well as `needs_projection` section
706 : // below could do an in-place projection.
707 : dt_boundary_correction_on_mortar.initialize(
708 : mortar_mesh.number_of_grid_points());
709 :
710 : call_boundary_correction(
711 : make_not_null(&dt_boundary_correction_on_mortar),
712 : local_data_on_mortar, neighbor_data_on_mortar,
713 : *typed_boundary_correction, dg_formulation, volume_args_tuple,
714 : typename BcType::dg_boundary_terms_volume_tags{});
715 :
716 : const std::array<Spectral::SegmentSize, volume_dim - 1>&
717 : mortar_size = mortar_infos.at(mortar_id).mortar_size();
718 :
719 : // This cannot reuse an allocation because it is initialized
720 : // via move-assignment. (If it is used at all.)
721 : DtVariables dt_boundary_correction_projected_onto_face{};
722 : auto& dt_boundary_correction =
723 : [&dt_boundary_correction_on_mortar,
724 : &dt_boundary_correction_projected_onto_face, &face_mesh,
725 : &mortar_mesh, &mortar_size]() -> DtVariables& {
726 : if (Spectral::needs_projection(face_mesh, mortar_mesh,
727 : mortar_size)) {
728 : dt_boundary_correction_projected_onto_face =
729 : ::dg::project_from_mortar(
730 : dt_boundary_correction_on_mortar, face_mesh,
731 : mortar_mesh, mortar_size);
732 : return dt_boundary_correction_projected_onto_face;
733 : }
734 : return dt_boundary_correction_on_mortar;
735 : }();
736 :
737 : // Both paths initialize this to be non-owning.
738 : Scalar<DataVector> magnitude_of_face_normal{};
739 : if constexpr (local_time_stepping) {
740 : (void)face_normal_covector_and_magnitude;
741 : get(magnitude_of_face_normal)
742 : .set_data_ref(make_not_null(&const_cast<DataVector&>(
743 : get(local_mortar_data.face_normal_magnitude.value()))));
744 : } else {
745 : ASSERT(
746 : face_normal_covector_and_magnitude.count(direction) == 1 and
747 : face_normal_covector_and_magnitude.at(direction)
748 : .has_value(),
749 : "Face normal covector and magnitude not set in "
750 : "direction: "
751 : << direction);
752 : get(magnitude_of_face_normal)
753 : .set_data_ref(make_not_null(&const_cast<DataVector&>(
754 : get(get<evolution::dg::Tags::MagnitudeOfNormal>(
755 : *face_normal_covector_and_magnitude.at(
756 : direction))))));
757 : }
758 :
759 : if (using_gauss_lobatto_points) {
760 : // The lift_flux function lifts only on the slice, it does not
761 : // add the contribution to the volume.
762 : ::dg::lift_flux(make_not_null(&dt_boundary_correction),
763 : volume_mesh.extents(direction.dimension()),
764 : magnitude_of_face_normal);
765 : return std::move(dt_boundary_correction);
766 : } else {
767 : // We are using Gauss points.
768 : //
769 : // Notes:
770 : // - We should really lift both sides simultaneously since this
771 : // reduces memory accesses. Lifting all sides at the same
772 : // time is unlikely to improve performance since we lift by
773 : // jumping through slices. There may also be compatibility
774 : // issues with local time stepping.
775 : // - If we lift both sides at the same time we first need to
776 : // deal with projecting from mortars to the face, then lift
777 : // off the faces. With non-owning Variables memory
778 : // allocations could be significantly reduced in this code.
779 : if constexpr (local_time_stepping) {
780 : ASSERT(get(volume_det_inv_jacobian).size() > 0,
781 : "For local time stepping the volume determinant of "
782 : "the inverse Jacobian has not been set.");
783 :
784 : get(face_det_jacobian)
785 : .set_data_ref(make_not_null(&const_cast<DataVector&>(
786 : get(local_mortar_data.face_det_jacobian.value()))));
787 : } else {
788 : // Project the determinant of the Jacobian to the face. This
789 : // could be optimized by caching in the time-independent case.
790 : get(face_det_jacobian)
791 : .destructive_resize(face_mesh.number_of_grid_points());
792 : const Matrix identity{};
793 : auto interpolation_matrices =
794 : make_array<volume_dim>(std::cref(identity));
795 : const std::pair<Matrix, Matrix>& matrices =
796 : Spectral::boundary_interpolation_matrices(
797 : volume_mesh.slice_through(direction.dimension()));
798 : gsl::at(interpolation_matrices, direction.dimension()) =
799 : direction.side() == Side::Upper ? matrices.second
800 : : matrices.first;
801 : apply_matrices(make_not_null(&get(face_det_jacobian)),
802 : interpolation_matrices,
803 : get(volume_det_jacobian),
804 : volume_mesh.extents());
805 : }
806 :
807 : volume_dt_correction.initialize(
808 : volume_mesh.number_of_grid_points(), 0.0);
809 : ::dg::lift_boundary_terms_gauss_points(
810 : make_not_null(&volume_dt_correction),
811 : volume_det_inv_jacobian, volume_mesh, direction,
812 : dt_boundary_correction, magnitude_of_face_normal,
813 : face_det_jacobian);
814 : return std::move(volume_dt_correction);
815 : }
816 : };
817 :
818 : if constexpr (local_time_stepping) {
819 : typename variables_tag::type lgl_lifted_data{};
820 : auto& lifted_data = using_gauss_lobatto_points ? lgl_lifted_data
821 : : *vars_to_update;
822 : if (using_gauss_lobatto_points) {
823 : lifted_data.initialize(face_mesh.number_of_grid_points(), 0.0);
824 : }
825 :
826 : const auto& mortar_data_history = mortar_id_and_data.second;
827 : if constexpr (DenseOutput) {
828 : (void)time_step;
829 : time_stepper.boundary_dense_output(
830 : &lifted_data, mortar_data_history, dense_output_time,
831 : compute_correction_coupling);
832 : } else {
833 : (void)dense_output_time;
834 : time_stepper.add_boundary_delta(&lifted_data,
835 : mortar_data_history, time_step,
836 : compute_correction_coupling);
837 : }
838 :
839 : if (using_gauss_lobatto_points) {
840 : // Add the flux contribution to the volume data
841 : add_slice_to_data(
842 : vars_to_update, lifted_data, volume_mesh.extents(),
843 : direction.dimension(),
844 : index_to_slice_at(volume_mesh.extents(), direction));
845 : }
846 : } else {
847 : (void)time_step;
848 : (void)time_stepper;
849 : (void)dense_output_time;
850 :
851 : // Choose an allocation cache that may be empty, so we
852 : // might be able to reuse the allocation obtained for the
853 : // lifted data. This may result in a self assignment,
854 : // depending on the code paths taken, but handling the
855 : // results this way makes the GTS and LTS paths more
856 : // similar because the LTS code always stores the result
857 : // in the history and so sometimes benefits from moving
858 : // into the return value of compute_correction_coupling.
859 : auto& lifted_data = using_gauss_lobatto_points
860 : ? dt_boundary_correction_on_mortar
861 : : volume_dt_correction;
862 : lifted_data = compute_correction_coupling(
863 : mortar_id_and_data.second.local(),
864 : mortar_id_and_data.second.neighbor());
865 :
866 : if (using_gauss_lobatto_points) {
867 : // Add the flux contribution to the volume data
868 : add_slice_to_data(
869 : vars_to_update, lifted_data, volume_mesh.extents(),
870 : direction.dimension(),
871 : index_to_slice_at(volume_mesh.extents(), direction));
872 : } else {
873 : *vars_to_update += lifted_data;
874 : }
875 : }
876 : }
877 : });
878 : }
879 :
880 : template <typename... BoundaryCorrectionTags, typename... Tags,
881 : typename BoundaryCorrection, typename... AllVolumeArgs,
882 : typename... VolumeTagsForCorrection>
883 0 : static void call_boundary_correction(
884 : const gsl::not_null<Variables<tmpl::list<BoundaryCorrectionTags...>>*>
885 : boundary_corrections_on_mortar,
886 : const Variables<tmpl::list<Tags...>>& local_boundary_data,
887 : const Variables<tmpl::list<Tags...>>& neighbor_boundary_data,
888 : const BoundaryCorrection& boundary_correction,
889 : const ::dg::Formulation dg_formulation,
890 : const tuples::TaggedTuple<detail::TemporaryReference<AllVolumeArgs>...>&
891 : volume_args_tuple,
892 : tmpl::list<VolumeTagsForCorrection...> /*meta*/) {
893 : boundary_correction.dg_boundary_terms(
894 : make_not_null(
895 : &get<BoundaryCorrectionTags>(*boundary_corrections_on_mortar))...,
896 : get<Tags>(local_boundary_data)..., get<Tags>(neighbor_boundary_data)...,
897 : dg_formulation,
898 : tuples::get<detail::TemporaryReference<VolumeTagsForCorrection>>(
899 : volume_args_tuple)...);
900 : }
901 : };
902 :
903 1 : namespace Actions {
904 : namespace ApplyBoundaryCorrections_detail {
905 : template <bool LocalTimeStepping, size_t VolumeDim, bool DenseOutput,
906 : bool UseNodegroupDgElements>
907 : struct ActionImpl {
908 : using inbox_tags =
909 : tmpl::list<evolution::dg::Tags::BoundaryCorrectionAndGhostCellsInbox<
910 : VolumeDim, UseNodegroupDgElements>>;
911 : using const_global_cache_tags =
912 : tmpl::list<evolution::Tags::BoundaryCorrection, ::dg::Tags::Formulation>;
913 :
914 : template <typename DbTagsList, typename... InboxTags, typename Metavariables,
915 : typename ArrayIndex, typename ActionList,
916 : typename ParallelComponent>
917 : static Parallel::iterable_action_return_t apply(
918 : db::DataBox<DbTagsList>& box, tuples::TaggedTuple<InboxTags...>& inboxes,
919 : const Parallel::GlobalCache<Metavariables>& /*cache*/,
920 : const ArrayIndex& /*array_index*/, ActionList /*meta*/,
921 : const ParallelComponent* const /*meta*/) {
922 : static_assert(
923 : UseNodegroupDgElements ==
924 : Parallel::is_dg_element_collection_v<ParallelComponent>,
925 : "The action is told by the template parameter UseNodegroupDgElements "
926 : "that it is being used with a DgElementCollection, but the "
927 : "ParallelComponent is not a DgElementCollection. You need to change "
928 : "the template parameter on the action in your action list.");
929 : constexpr size_t volume_dim = Metavariables::system::volume_dim;
930 : const Element<volume_dim>& element =
931 : db::get<domain::Tags::Element<volume_dim>>(box);
932 :
933 : if (UNLIKELY(element.number_of_neighbors() == 0)) {
934 : // We have no neighbors, yay!
935 : return {Parallel::AlgorithmExecution::Continue, std::nullopt};
936 : }
937 :
938 : if constexpr (LocalTimeStepping) {
939 : if (not receive_boundary_data_local_time_stepping<
940 : Parallel::is_dg_element_collection_v<ParallelComponent>,
941 : typename Metavariables::system, VolumeDim, false>(
942 : make_not_null(&box), make_not_null(&inboxes))) {
943 : return {Parallel::AlgorithmExecution::Retry, std::nullopt};
944 : }
945 : } else {
946 : if (not receive_boundary_data_global_time_stepping<
947 : Parallel::is_dg_element_collection_v<ParallelComponent>,
948 : Metavariables>(make_not_null(&box), make_not_null(&inboxes))) {
949 : return {Parallel::AlgorithmExecution::Retry, std::nullopt};
950 : }
951 : }
952 :
953 : // LTS updates the evolved variables, so we can skip that if they
954 : // are unused. GTS updates the derivatives, which are always
955 : // needed to update the history.
956 : if (LocalTimeStepping and
957 : ::SelfStart::step_unused(
958 : db::get<::Tags::TimeStepId>(box),
959 : db::get<::Tags::Next<::Tags::TimeStepId>>(box))) {
960 : return {Parallel::AlgorithmExecution::Continue, std::nullopt};
961 : }
962 :
963 : db::mutate_apply<ApplyBoundaryCorrections<LocalTimeStepping, Metavariables,
964 : VolumeDim, DenseOutput>>(
965 : make_not_null(&box));
966 : return {Parallel::AlgorithmExecution::Continue, std::nullopt};
967 : }
968 : };
969 : } // namespace ApplyBoundaryCorrections_detail
970 :
971 : /*!
972 : * \brief Computes the boundary corrections for global time-stepping
973 : * and adds them to the time derivative.
974 : */
975 : template <size_t VolumeDim, bool UseNodegroupDgElements>
976 1 : struct ApplyBoundaryCorrectionsToTimeDerivative
977 : : ApplyBoundaryCorrections_detail::ActionImpl<false, VolumeDim, false,
978 : UseNodegroupDgElements> {};
979 :
980 : /*!
981 : * \brief Computes the boundary corrections for local time-stepping
982 : * and adds them to the variables.
983 : *
984 : * When using local time stepping the neighbor sends data at the neighbor's
985 : * current temporal id. Along with the boundary data, the next temporal id at
986 : * which the neighbor will send data is also sent. This is equal to the
987 : * neighbor's `::Tags::Next<::Tags::TimeStepId>`. When inserting into the mortar
988 : * data history, we insert the received temporal id, that is, the current time
989 : * of the neighbor, along with the boundary correction data.
990 : */
991 : template <size_t VolumeDim, bool DenseOutput, bool UseNodegroupDgElements>
992 1 : struct ApplyLtsBoundaryCorrections
993 : : ApplyBoundaryCorrections_detail::ActionImpl<true, VolumeDim, DenseOutput,
994 : UseNodegroupDgElements> {};
995 : } // namespace Actions
996 : } // namespace evolution::dg
|