ryujin 2.1.1 revision 0348cbb53a3e4b1da2a4c037e81f88f2d21ce219
solution_transfer.template.h
Go to the documentation of this file.
1//
2// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception or LGPL-2.1-or-later
3// Copyright (C) 2024 - 2024 by the ryujin authors
4//
5
6#pragma once
7
8#include <compile_time_options.h>
9
10#include "discretization.h"
11#include "solution_transfer.h"
12#if DEAL_II_VERSION_GTE(9, 6, 0)
14#endif
15
16#include <deal.II/base/config.h>
17#include <deal.II/distributed/tria.h>
18#include <deal.II/dofs/dof_accessor.h>
19#include <deal.II/dofs/dof_tools.h>
20#if DEAL_II_VERSION_GTE(9, 6, 0)
21#include <deal.II/grid/cell_status.h>
22#endif
23#include <deal.II/grid/tria_accessor.h>
24#include <deal.II/grid/tria_iterator.h>
25#include <deal.II/lac/block_vector.h>
26#include <deal.II/lac/la_parallel_block_vector.h>
27#include <deal.II/lac/la_parallel_vector.h>
28#include <deal.II/lac/petsc_block_vector.h>
29#include <deal.II/lac/petsc_vector.h>
30#include <deal.II/lac/trilinos_parallel_block_vector.h>
31#include <deal.II/lac/trilinos_vector.h>
32#include <deal.II/lac/vector.h>
33#include <deal.II/matrix_free/fe_point_evaluation.h>
34
35
36namespace ryujin
37{
38 template <typename Description, int dim, typename Number>
40 const MPIEnsemble &mpi_ensemble,
41 typename Discretization<dim>::Triangulation &triangulation,
42 const OfflineData<dim, Number> &offline_data,
43 const HyperbolicSystem &hyperbolic_system,
44 const ParabolicSystem &parabolic_system)
45 : mpi_ensemble_(mpi_ensemble)
46 , triangulation_(&triangulation)
47 , offline_data_(&offline_data)
48 , hyperbolic_system_(&hyperbolic_system)
49 , parabolic_system_(&parabolic_system)
50 , handle_(dealii::numbers::invalid_unsigned_int)
51
52 {
53 AssertThrow(have_distributed_triangulation<dim>,
54 dealii::ExcMessage(
55 "The SolutionTransfer class is not implemented for a "
56 "distributed::shared::Triangulation which we use in 1D"));
57 }
58
59
60 namespace
61 {
65 template <typename state_type>
66 std::vector<char>
67 pack_state_values(const std::vector<state_type> &state_values)
68 {
69 std::vector<char> buffer(sizeof(state_type) * state_values.size());
70 std::memcpy(buffer.data(), state_values.data(), buffer.size());
71 return buffer;
72 }
73
74
78 template <typename state_type>
79 std::vector<state_type> unpack_state_values(
80 const boost::iterator_range<std::vector<char>::const_iterator>
81 &data_range)
82 {
83 const std::size_t n_bytes = data_range.size();
84 Assert(n_bytes % sizeof(state_type) == 0, dealii::ExcInternalError());
85 std::vector<state_type> state_values(n_bytes / sizeof(state_type));
86 std::memcpy(state_values.data(),
87 &data_range[0],
88 state_values.size() * sizeof(state_type));
89 return state_values;
90 }
91 } // namespace
92
93
94 template <typename Description, int dim, typename Number>
95 inline DEAL_II_ALWAYS_INLINE auto
96 SolutionTransfer<Description, dim, Number>::get_tensor(
97 const HyperbolicVector &U, const dealii::types::global_dof_index global_i)
98 -> state_type
99 {
100 const auto &scalar_partitioner = offline_data_->scalar_partitioner();
101 const auto &affine_constraints = offline_data_->affine_constraints();
102 const auto local_i = scalar_partitioner->global_to_local(global_i);
103 if (affine_constraints.is_constrained(global_i)) {
104 state_type result;
105 const auto &line = *affine_constraints.get_constraint_entries(global_i);
106 for (const auto &[global_k, c_k] : line) {
107 const auto local_k = scalar_partitioner->global_to_local(global_k);
108 result += c_k * U.get_tensor(local_k);
109 }
110 return result;
111 } else {
112 return U.get_tensor(local_i);
113 }
114 }
115
116
117 template <typename Description, int dim, typename Number>
118 inline DEAL_II_ALWAYS_INLINE void
119 SolutionTransfer<Description, dim, Number>::add_tensor(
120 HyperbolicVector &U,
121 const state_type &new_U_i,
122 const dealii::types::global_dof_index global_i)
123 {
124 const auto &scalar_partitioner = offline_data_->scalar_partitioner();
125 const auto local_i = scalar_partitioner->global_to_local(global_i);
126 U.add_tensor(new_U_i, local_i);
127 }
128
129
130 template <typename Description, int dim, typename Number>
132 const StateVector &old_state_vector [[maybe_unused]])
133 {
134#ifdef DEBUG_OUTPUT
135 std::cout
136 << "SolutionTransfer<Description, dim, Number>::prepare_projection()"
137 << std::endl;
138#endif
139
140#if !DEAL_II_VERSION_GTE(9, 6, 0)
141 AssertThrow(
142 false,
143 dealii::ExcMessage(
144 "The SolutionTransfer class needs deal.II version 9.6.0 or newer"));
145
146#else
147 AssertThrow(have_distributed_triangulation<dim>,
148 dealii::ExcMessage(
149 "The SolutionTransfer class is not implemented for a "
150 "distributed::shared::Triangulation which we use in 1D"));
151
152 const auto &discretization = offline_data_->discretization();
153 const auto &triangulation [[maybe_unused]] = discretization.triangulation();
154 Assert(triangulation_ == &triangulation,
155 dealii::ExcMessage(
156 "The attached triangulation object must be the same object that "
157 "is stored in Discretization/OfflineData"));
158
159 Assert(handle_ == dealii::numbers::invalid_unsigned_int,
160 dealii::ExcMessage(
161 "You can only add one solution per SolutionTransfer object."));
162
163 /*
164 * Add a register_data_attach callback that
165 */
166
167 handle_ = triangulation_->register_data_attach(
168 [this, &old_state_vector](const auto cell,
169 const dealii::CellStatus status) {
170 const auto &dof_handler = offline_data_->dof_handler();
171 const auto dof_cell = typename dealii::DoFHandler<dim>::cell_iterator(
172 &cell->get_triangulation(),
173 cell->level(),
174 cell->index(),
175 &dof_handler);
176
177 const auto &U = std::get<0>(old_state_vector);
178
179 /*
180 * Collect values for packing:
181 */
182
183 const auto n_dofs_per_cell = dof_handler.get_fe().n_dofs_per_cell();
184 std::vector<state_type> state_values(n_dofs_per_cell);
185
186 switch (status) {
187 case dealii::CellStatus::cell_will_persist:
188 [[fallthrough]];
189 case dealii::CellStatus::cell_will_be_refined: {
190 /*
191 * For both cases we need state values from the currently
192 * active cell:
193 */
194
195 Assert(dof_cell->is_active(), dealii::ExcInternalError());
196 std::vector<dealii::types::global_dof_index> dof_indices(
197 n_dofs_per_cell);
198 dof_cell->get_dof_indices(dof_indices);
199
200 std::transform(std::begin(dof_indices),
201 std::end(dof_indices),
202 std::begin(state_values),
203 [&](const auto i) {
204 const auto U_i = get_tensor(U, i);
205 return U_i;
206 });
207 } break;
208
209 case dealii::CellStatus::children_will_be_coarsened: {
210 /*
211 * We need to project values from the active child cells up to
212 * the present parent cell that will become active after
213 * coarsening.
214 */
215
216 Assert(dof_cell->has_children(), dealii::ExcInternalError());
217
218 const auto &discretization = offline_data_->discretization();
219 const auto &finite_element = discretization.finite_element();
220 const auto &mapping = discretization.mapping();
221 const auto &quadrature = discretization.quadrature();
222
223 dealii::FEValues<dim> fe_values(
224 mapping,
225 finite_element,
226 quadrature,
227 dealii::update_values | dealii::update_JxW_values |
228 dealii::update_quadrature_points);
229
230 const auto polynomial_space =
231 dealii::internal::FEPointEvaluation::get_polynomial_space(
232 finite_element);
233
234 std::vector<dealii::Point<dim, Number>> unit_points(
235 quadrature.size());
236 /*
237 * for Number == float we need a temporary vector for the
238 * transform_points_real_to_unit_cell() function:
239 */
240 std::vector<dealii::Point<dim>> unit_points_temp(
241 std::is_same_v<Number, float> ? quadrature.size() : 0);
242
243 /* Step 1: build up right hand side by iterating over children: */
244
245 std::vector<state_type> state_values_quad(quadrature.size());
246 std::vector<state_type> local_rhs(n_dofs_per_cell);
247
248 std::vector<dealii::types::global_dof_index> dof_indices(
249 n_dofs_per_cell);
250
251 for (unsigned int child = 0; child < dof_cell->n_children();
252 ++child) {
253 const auto child_cell = dof_cell->child(child);
254 Assert(child_cell->is_active(), dealii::ExcInternalError());
255
256 fe_values.reinit(child_cell);
257
258 if constexpr (std::is_same_v<Number, float>) {
259 mapping.transform_points_real_to_unit_cell(
260 dof_cell,
261 fe_values.get_quadrature_points(),
262 unit_points_temp);
263 std::transform(std::begin(unit_points_temp),
264 std::end(unit_points_temp),
265 std::begin(unit_points),
266 [](const auto &x) { return x; });
267 } else {
268 mapping.transform_points_real_to_unit_cell(
269 dof_cell, fe_values.get_quadrature_points(), unit_points);
270 }
271
272 child_cell->get_dof_indices(dof_indices);
273
274 for (auto &it : state_values_quad)
275 it = state_type{};
276
277 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
278 const auto U_i = get_tensor(U, dof_indices[i]);
279 for (unsigned int q = 0; q < quadrature.size(); ++q) {
280 state_values_quad[q] += U_i * fe_values.shape_value(i, q);
281 }
282 }
283
284 for (unsigned int q = 0; q < quadrature.size(); ++q)
285 state_values_quad[q] *= fe_values.JxW(q);
286
287 for (unsigned int q = 0; q < quadrature.size(); ++q) {
288 const unsigned int n_shapes = polynomial_space.size();
289 AssertIndexRange(n_shapes, 10);
290 dealii::ndarray<Number, 10, 2, dim> shapes;
291 // Evaluate 1d polynomials and their derivatives
292 std::array<Number, dim> point;
293 for (unsigned int d = 0; d < dim; ++d)
294 point[d] = unit_points[q][d];
295 for (unsigned int i = 0; i < n_shapes; ++i)
296 polynomial_space[i].values_of_array(point, 1, &shapes[i][0]);
297
298 Assert(finite_element.degree == 1, dealii::ExcNotImplemented());
299
301 /*is linear*/ true,
302 dim,
303 Number,
304 state_type>(shapes.data(),
305 n_shapes,
306 state_values_quad[q],
307 local_rhs.data(),
308 unit_points[q],
309 true);
310 }
311 }
312
313 /* Step 2: solve with inverse mass matrix on coarse cell: */
314
315 fe_values.reinit(dof_cell);
316
317 dealii::FullMatrix<double> mij(n_dofs_per_cell, n_dofs_per_cell);
318 dealii::Vector<double> mi(n_dofs_per_cell);
319 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
320 for (unsigned int j = 0; j < n_dofs_per_cell; ++j) {
321 double sum = 0;
322 for (unsigned int q = 0; q < quadrature.size(); ++q)
323 sum += fe_values.shape_value(i, q) *
324 fe_values.shape_value(j, q) * fe_values.JxW(q);
325 mij(i, j) = sum;
326 mi(i) += sum;
327 }
328 }
329
330 mij.gauss_jordan();
331
332 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
333 for (unsigned int j = 0; j < n_dofs_per_cell; ++j) {
334 state_values[i] += mij(i, j) * local_rhs[j];
335 }
336 }
337 } break;
338
339 case dealii::CellStatus::cell_invalid:
340 Assert(false, dealii::ExcInternalError());
341 __builtin_trap();
342 break;
343 }
344
345 return pack_state_values(state_values);
346 },
347 /* returns_variable_size_data =*/false);
348#endif
349 }
350
351
352 template <typename Description, int dim, typename Number>
354 StateVector &new_state_vector [[maybe_unused]])
355 {
356#ifdef DEBUG_OUTPUT
357 std::cout << "SolutionTransfer<Description, dim, Number>::project()"
358 << std::endl;
359#endif
360
361#if !DEAL_II_VERSION_GTE(9, 6, 0)
362 AssertThrow(
363 false,
364 dealii::ExcMessage(
365 "The SolutionTransfer class needs deal.II version 9.6.0 or newer"));
366
367#else
368
369 AssertThrow(have_distributed_triangulation<dim>,
370 dealii::ExcMessage(
371 "The SolutionTransfer class is not implemented for a "
372 "distributed::shared::Triangulation which we use in 1D"));
373
374 const auto &scalar_partitioner = offline_data_->scalar_partitioner();
375 const auto &affine_constraints = offline_data_->affine_constraints();
376 const auto &discretization = offline_data_->discretization();
377 const auto &triangulation [[maybe_unused]] = discretization.triangulation();
378 Assert(triangulation_ == &triangulation,
379 dealii::ExcMessage(
380 "The attached triangulation object must be the same object that "
381 "is stored in Discretization/OfflineData"));
382
383 Assert(
384 handle_ != dealii::numbers::invalid_unsigned_int,
385 dealii::ExcMessage(
386 "Cannot project() a state vector without valid handle. "
387 "prepare_projection() or set_handle() have to be called first."));
388
389 /*
390 * Reconstruct and project state vector:
391 */
392
393 ScalarVector projected_mass;
394 projected_mass.reinit(offline_data_->scalar_partitioner());
395 HyperbolicVector projected_state;
396 projected_state.reinit(offline_data_->hyperbolic_vector_partitioner());
397
398 triangulation_->notify_ready_to_unpack( //
399 handle_,
400 [this, &projected_mass, &projected_state](
401 const auto &cell,
402 const dealii::CellStatus status,
403 const auto &data_range) {
404 const auto &dof_handler = offline_data_->dof_handler();
405 const auto dof_cell = typename dealii::DoFHandler<dim>::cell_iterator(
406 &cell->get_triangulation(),
407 cell->level(),
408 cell->index(),
409 &dof_handler);
410
411 /*
412 * Retrieve packed values and project onto cell:
413 */
414
415 const auto n_dofs_per_cell = dof_handler.get_fe().n_dofs_per_cell();
416 std::vector<dealii::types::global_dof_index> dof_indices(
417 n_dofs_per_cell);
418
419 const auto state_values = unpack_state_values<state_type>(data_range);
420
421 switch (status) {
422 case dealii::CellStatus::cell_will_persist:
423 [[fallthrough]];
424 case dealii::CellStatus::children_will_be_coarsened: {
425 /*
426 * For both cases we distribute stored state_values to the
427 * projected_state and projected_mass vectors.
428 */
429
430 Assert(dof_cell->is_active(), dealii::ExcInternalError());
431 dof_cell->get_dof_indices(dof_indices);
432
433 const auto &discretization = offline_data_->discretization();
434 const auto &finite_element = discretization.finite_element();
435 const auto &mapping = discretization.mapping();
436 const auto &quadrature = discretization.quadrature();
437
438 dealii::FEValues<dim> fe_values(mapping,
439 finite_element,
440 quadrature,
441 dealii::update_values |
442 dealii::update_JxW_values);
443
444 fe_values.reinit(dof_cell);
445
446 dealii::Vector<double> mi(n_dofs_per_cell);
447 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
448 double sum = 0;
449 for (unsigned int q = 0; q < quadrature.size(); ++q)
450 sum += fe_values.shape_value(i, q) * fe_values.JxW(q);
451 mi(i) += sum;
452 }
453
454 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
455 const auto global_i = dof_indices[i];
456 add_tensor(projected_state, mi(i) * state_values[i], global_i);
457 projected_mass(global_i) += mi(i);
458 }
459
460 } break;
461
462 case dealii::CellStatus::cell_will_be_refined: {
463 /*
464 * We are on a (non active) cell that has been refined. Project
465 * onto the children and do a local mass projection there:
466 */
467
468 Assert(dof_cell->has_children(), dealii::ExcInternalError());
469
470 const auto &discretization = offline_data_->discretization();
471 const auto &finite_element = discretization.finite_element();
472 const auto &mapping = discretization.mapping();
473 const auto &quadrature = discretization.quadrature();
474
475 dealii::FEValues<dim> fe_values(
476 mapping,
477 finite_element,
478 quadrature,
479 dealii::update_values | dealii::update_JxW_values |
480 dealii::update_quadrature_points);
481
482 const auto polynomial_space =
483 dealii::internal::FEPointEvaluation::get_polynomial_space(
484 finite_element);
485 std::vector<dealii::Point<dim, Number>> unit_points(
486 quadrature.size());
487 /*
488 * for Number == float we need a temporary vector for the
489 * transform_points_real_to_unit_cell() function:
490 */
491 std::vector<dealii::Point<dim>> unit_points_temp(
492 std::is_same_v<Number, float> ? quadrature.size() : 0);
493
494 dealii::FullMatrix<double> mij(n_dofs_per_cell, n_dofs_per_cell);
495 dealii::Vector<double> mi(n_dofs_per_cell);
496 std::vector<state_type> local_rhs(n_dofs_per_cell);
497
498 for (unsigned int child = 0; child < dof_cell->n_children();
499 ++child) {
500 const auto child_cell = dof_cell->child(child);
501
502 Assert(child_cell->is_active(), dealii::ExcInternalError());
503 child_cell->get_dof_indices(dof_indices);
504
505 /* Step 1: build up right hand side on child cell: */
506
507 fe_values.reinit(child_cell);
508
509 if constexpr (std::is_same_v<Number, float>) {
510 mapping.transform_points_real_to_unit_cell(
511 dof_cell,
512 fe_values.get_quadrature_points(),
513 unit_points_temp);
514 std::transform(std::begin(unit_points_temp),
515 std::end(unit_points_temp),
516 std::begin(unit_points),
517 [](const auto &x) { return x; });
518 } else {
519 mapping.transform_points_real_to_unit_cell(
520 dof_cell, fe_values.get_quadrature_points(), unit_points);
521 }
522
523 for (auto &it : local_rhs)
524 it = state_type{};
525
526 for (unsigned int q = 0; q < quadrature.size(); ++q) {
527 Assert(finite_element.degree == 1, dealii::ExcNotImplemented());
528 auto coefficient =
529 dealii::internal::evaluate_tensor_product_value(
530 polynomial_space,
531 make_const_array_view(state_values),
532 unit_points[q],
533 /*is linear*/ true);
534 coefficient *= fe_values.JxW(q);
535
536 for (unsigned int i = 0; i < n_dofs_per_cell; ++i)
537 local_rhs[i] += coefficient * fe_values.shape_value(i, q);
538 }
539
540 /* Step 2: solve with inverse mass matrix on child cell: */
541
542 mi = 0.;
543 mij = 0.;
544 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
545 for (unsigned int j = 0; j < n_dofs_per_cell; ++j) {
546 double sum = 0;
547 for (unsigned int q = 0; q < quadrature.size(); ++q)
548 sum += fe_values.shape_value(i, q) *
549 fe_values.shape_value(j, q) * fe_values.JxW(q);
550 mij(i, j) = sum;
551 mi(i) += sum;
552 }
553 }
554
555 mij.gauss_jordan();
556
557 for (unsigned int i = 0; i < n_dofs_per_cell; ++i) {
558 state_type U_i;
559 for (unsigned int j = 0; j < n_dofs_per_cell; ++j) {
560 U_i += mij(i, j) * local_rhs[j];
561 }
562
563 const auto global_i = dof_indices[i];
564 add_tensor(projected_state, mi(i) * U_i, global_i);
565 projected_mass(global_i) += mi(i);
566 }
567 } /*child*/
568 } break;
569
570 case dealii::CellStatus::cell_invalid:
571 Assert(false, dealii::ExcInternalError());
572 __builtin_trap();
573 break;
574 }
575 });
576
577 /*
578 * Distribute values, take the weighted average of unconstrained
579 * degrees of freedom, and store the result in new_U:
580 */
581
582 auto &new_U = std::get<0>(new_state_vector);
583 const auto n_locally_owned = offline_data_->n_locally_owned();
584
585 // We have to perform the following operation twice, so let's create a
586 // small lambda for it.
587 const auto update_new_state_vector = [&]() {
588 projected_mass.compress(dealii::VectorOperation::add);
589 projected_state.compress(dealii::VectorOperation::add);
590
591 for (unsigned int local_i = 0; local_i < n_locally_owned; ++local_i) {
592 const auto global_i = scalar_partitioner->local_to_global(local_i);
593 if (affine_constraints.is_constrained(global_i))
594 continue;
595
596 const auto U_i = projected_state.get_tensor(local_i);
597 const auto m_i = projected_mass.local_element(local_i);
598 new_U.write_tensor(U_i / m_i, local_i);
599 }
600 new_U.update_ghost_values();
601 };
602
603 update_new_state_vector();
604
605 /*
606 * Now redistribute the mass defect introduced by constrained degrees
607 * of freedom. This mostly affects hanging nodes neighboring a
608 * coarsened cell. Here, cell-wise mass projection might lead to a
609 * value that differs from the algebraic relationship expressed by our
610 * affine constraints. Thus, we first compute the defect and then we
611 * redistribute it to all degrees of freedom on the constraint line.
612 */
613
614 for (const auto &line : affine_constraints.get_lines()) {
615 const auto global_i = line.index;
616 const auto local_i = scalar_partitioner->global_to_local(global_i);
617
618 /* Only operate on a locally owned, constrained degree of freedom: */
619 if (local_i >= n_locally_owned)
620 continue;
621
622 /* The result of the mass projection: */
623 const auto m_i_star = projected_mass.local_element(local_i);
624 const auto U_i_star = projected_state.get_tensor(local_i) / m_i_star;
625
626 /* The value obtained from the affine constraints object: */
627 state_type U_i_interp;
628 for (const auto &[global_k, c_k] : line.entries) {
629 const auto local_k = scalar_partitioner->global_to_local(global_k);
630 U_i_interp += c_k * new_U.get_tensor(local_k);
631 }
632
633 /* And distribute the defect: */
634 const auto defect = U_i_star - U_i_interp;
635 for (const auto &[global_k, c_k] : line.entries) {
636 const auto local_k = scalar_partitioner->global_to_local(global_k);
637 const auto U_j = new_U.get_tensor(local_k);
638
639 projected_state.add_tensor(c_k * m_i_star * (U_j + defect), local_k);
640 projected_mass.local_element(local_k) += c_k * m_i_star;
641 }
642 }
643
644 update_new_state_vector();
645
646#ifdef DEBUG
647 /*
648 * Sanity check: Final masses must agree:
649 */
650 const auto &lumped_mass_matrix = offline_data_->lumped_mass_matrix();
651 for (unsigned int local_i = 0; local_i < n_locally_owned; ++local_i) {
652 const auto global_i = scalar_partitioner->local_to_global(local_i);
653 if (affine_constraints.is_constrained(global_i))
654 continue;
655
656 const auto m_i = projected_mass.local_element(local_i);
657 const auto m_i_reference = lumped_mass_matrix.local_element(local_i);
658 Assert(std::abs(m_i - m_i_reference) < 1.e-10,
659 dealii::ExcMessage(
660 "SolutionTransfer::projection(): something went wrong. Final "
661 "masses do not agree with those computed in OfflineData."));
662 }
663#endif
664#endif
665 }
666} // namespace ryujin
typename Proxy< dim >::Triangulation Triangulation
typename View::HyperbolicVector HyperbolicVector
Vectors::ScalarVector< Number > ScalarVector
typename View::state_type state_type
typename Description::ParabolicSystem ParabolicSystem
void prepare_projection(const StateVector &old_state_vector)
SolutionTransfer(const MPIEnsemble &mpi_ensemble, typename Discretization< dim >::Triangulation &triangulation, const OfflineData< dim, Number > &offline_data, const HyperbolicSystem &hyperbolic_system, const ParabolicSystem &parabolic_system)
typename Description::HyperbolicSystem HyperbolicSystem
void project(StateVector &new_state_vector)
typename View::StateVector StateVector
void integrate_tensor_product_value(const dealii::ndarray< Number, 2, dim > *shapes, const unsigned int n_shapes, const Number2 &value, Number2 *values, const dealii::Point< dim, Number > &p, const bool do_add)
DEAL_II_ALWAYS_INLINE FT add(const FT &flux_left_ij, const FT &flux_right_ij)