diff --git a/CMakeLists.txt b/CMakeLists.txt
index d3306db95476b2e91773271f548b3937a417b421..bb8832a50565bde6f37ab589737a15e08f116030 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -737,6 +737,7 @@ target_link_libraries(
   PugsMesh
   PugsAlgebra
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsUtils
   PugsOutput
   PugsLanguageUtils
@@ -774,6 +775,7 @@ target_link_libraries(
   PugsLanguageModules
   PugsLanguageUtils
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsDev
   PugsAnalysis
   PugsAlgebra
@@ -816,6 +818,7 @@ install(TARGETS
   PugsMesh
   PugsOutput
   PugsScheme
+  PugsSchemeReconstructionUtils
   kokkos
   Catch2
 
diff --git a/src/geometry/LineTransformation.hpp b/src/geometry/LineTransformation.hpp
index b9e9ada96ca028f07497c910f06476f8b67eb61f..e3cb676e1aacae5965d649cf1e0ce0449e8f971c 100644
--- a/src/geometry/LineTransformation.hpp
+++ b/src/geometry/LineTransformation.hpp
@@ -24,12 +24,20 @@ class LineTransformation<1>
     return m_jacobian * x + m_shift;
   }
 
+  PUGS_INLINE
   double
   jacobianDeterminant() const
   {
     return m_jacobian;
   }
 
+  PUGS_INLINE
+  double
+  jacobianDeterminant(const TinyVector<1>&) const
+  {
+    return m_jacobian;
+  }
+
   PUGS_INLINE
   LineTransformation(const TinyVector<Dimension>& a, const TinyVector<Dimension>& b)
   {
diff --git a/src/geometry/SymmetryUtils.hpp b/src/geometry/SymmetryUtils.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6524a0791aef79b5440110d7180a472865c12d89
--- /dev/null
+++ b/src/geometry/SymmetryUtils.hpp
@@ -0,0 +1,32 @@
+#ifndef SYMMETRY_UTILS_HPP
+#define SYMMETRY_UTILS_HPP
+
+#include <algebra/TinyMatrix.hpp>
+#include <algebra/TinyVector.hpp>
+#include <utils/PugsMacros.hpp>
+
+template <size_t Dimension>
+PUGS_INLINE auto
+symmetrize_vector(const TinyVector<Dimension>& normal, const TinyVector<Dimension>& u)
+{
+  return u - 2 * dot(u, normal) * normal;
+}
+
+template <size_t Dimension>
+PUGS_INLINE auto
+symmetrize_matrix(const TinyVector<Dimension>& normal, const TinyMatrix<Dimension>& A)
+{
+  const TinyMatrix S = TinyMatrix<Dimension>{identity} - 2 * tensorProduct(normal, normal);
+  return S * A * S;
+}
+
+template <size_t Dimension>
+PUGS_INLINE auto
+symmetrize_coordinates(const TinyVector<Dimension>& origin,
+                       const TinyVector<Dimension>& normal,
+                       const TinyVector<Dimension>& u)
+{
+  return u - 2 * dot(u - origin, normal) * normal;
+}
+
+#endif   // SYMMETRY_UTILS_HPP
diff --git a/src/geometry/TetrahedronTransformation.hpp b/src/geometry/TetrahedronTransformation.hpp
index 64244266957a350e8be8f78c2eab3d5fead629e1..a81a344bfbabd0999e45b1bcab0b52519f9db4ed 100644
--- a/src/geometry/TetrahedronTransformation.hpp
+++ b/src/geometry/TetrahedronTransformation.hpp
@@ -24,12 +24,20 @@ class TetrahedronTransformation
     return m_jacobian * x + m_shift;
   }
 
+  PUGS_INLINE
   double
   jacobianDeterminant() const
   {
     return m_jacobian_determinant;
   }
 
+  PUGS_INLINE
+  double
+  jacobianDeterminant(const TinyVector<3>&) const
+  {
+    return m_jacobian_determinant;
+  }
+
   PUGS_INLINE
   TetrahedronTransformation(const TinyVector<Dimension>& a,
                             const TinyVector<Dimension>& b,
diff --git a/src/geometry/TriangleTransformation.hpp b/src/geometry/TriangleTransformation.hpp
index 9210e0bda55bb19dfdfd877e509801fa87f0a4f0..df8444d894a7ed56c0fe3825ffbf1cc14a742531 100644
--- a/src/geometry/TriangleTransformation.hpp
+++ b/src/geometry/TriangleTransformation.hpp
@@ -26,12 +26,20 @@ class TriangleTransformation<2>
     return m_jacobian * x + m_shift;
   }
 
+  PUGS_INLINE
   double
   jacobianDeterminant() const
   {
     return m_jacobian_determinant;
   }
 
+  PUGS_INLINE
+  double
+  jacobianDeterminant(const TinyVector<2>&) const
+  {
+    return m_jacobian_determinant;
+  }
+
   PUGS_INLINE
   TriangleTransformation(const TinyVector<Dimension>& a, const TinyVector<Dimension>& b, const TinyVector<Dimension>& c)
   {
diff --git a/src/mesh/StencilBuilder.cpp b/src/mesh/StencilBuilder.cpp
index 20fc3bf61dd6a59ef82ff7bdb25dc5e32c7eef24..de7501d44b5b0aef6e50f830460e9be110456331 100644
--- a/src/mesh/StencilBuilder.cpp
+++ b/src/mesh/StencilBuilder.cpp
@@ -447,8 +447,7 @@ StencilBuilder::_build_for_same_source_and_target(const ConnectivityType& connec
   }
 
   Array<uint32_t> column_indices{column_indices_vector.size()};
-  parallel_for(
-    column_indices_vector.size(), PUGS_LAMBDA(size_t i) { column_indices[i] = column_indices_vector[i]; });
+  parallel_for(column_indices_vector.size(), PUGS_LAMBDA(size_t i) { column_indices[i] = column_indices_vector[i]; });
 
   ConnectivityMatrix primal_stencil{row_map, column_indices};
 
@@ -743,8 +742,7 @@ StencilBuilder::_build_for_different_source_and_target(
   }
 
   Array<uint32_t> column_indices{column_indices_vector.size()};
-  parallel_for(
-    column_indices_vector.size(), PUGS_LAMBDA(size_t i) { column_indices[i] = column_indices_vector[i]; });
+  parallel_for(column_indices_vector.size(), PUGS_LAMBDA(size_t i) { column_indices[i] = column_indices_vector[i]; });
 
   ConnectivityMatrix primal_stencil{row_map, column_indices};
 
@@ -836,7 +834,7 @@ StencilBuilder::buildC2C(const IConnectivity& connectivity,
   if ((parallel::size() > 1) and
       (stencil_descriptor.numberOfLayers() > GlobalVariableManager::instance().getNumberOfGhostLayers())) {
     std::ostringstream error_msg;
-    error_msg << "Stencil builder requires" << rang::fgB::yellow << stencil_descriptor.numberOfLayers()
+    error_msg << "Stencil builder requires " << rang::fgB::yellow << stencil_descriptor.numberOfLayers()
               << rang::fg::reset << " layers while parallel number of ghost layer is "
               << GlobalVariableManager::instance().getNumberOfGhostLayers() << ".\n";
     error_msg << "Increase the number of ghost layers (using the '--number-of-ghost-layers' option).";
@@ -870,7 +868,7 @@ StencilBuilder::buildN2C(const IConnectivity& connectivity,
   if ((parallel::size() > 1) and
       (stencil_descriptor.numberOfLayers() > GlobalVariableManager::instance().getNumberOfGhostLayers())) {
     std::ostringstream error_msg;
-    error_msg << "Stencil builder requires" << rang::fgB::yellow << stencil_descriptor.numberOfLayers()
+    error_msg << "Stencil builder requires " << rang::fgB::yellow << stencil_descriptor.numberOfLayers()
               << rang::fg::reset << " layers while parallel number of ghost layer is "
               << GlobalVariableManager::instance().getNumberOfGhostLayers() << ".\n";
     error_msg << "Increase the number of ghost layers (using the '--number-of-ghost-layers' option).";
diff --git a/src/scheme/CMakeLists.txt b/src/scheme/CMakeLists.txt
index b533ab51516484131ea8909411c8b3c5b5f77098..8cf2d535895a555e172d33f6c11c20483995d9ce 100644
--- a/src/scheme/CMakeLists.txt
+++ b/src/scheme/CMakeLists.txt
@@ -1,5 +1,7 @@
 # ------------------- Source files --------------------
 
+add_subdirectory(reconstruction_utils)
+
 add_library(
   PugsScheme
   AcousticSolver.cpp
@@ -24,3 +26,9 @@ target_link_libraries(
   PugsScheme
   ${HIGHFIVE_TARGET}
 )
+
+# Additional dependencies
+add_dependencies(
+  PugsScheme
+  PugsSchemeReconstructionUtils
+)
diff --git a/src/scheme/PolynomialReconstruction.cpp b/src/scheme/PolynomialReconstruction.cpp
index eea42b65b93e20d03a4db2857ba1f2e32447d43f..c646dc4fbec060bf5983a24ec33ca8bdb2718725 100644
--- a/src/scheme/PolynomialReconstruction.cpp
+++ b/src/scheme/PolynomialReconstruction.cpp
@@ -2,21 +2,8 @@
 
 #include <algebra/Givens.hpp>
 #include <algebra/ShrinkMatrixView.hpp>
-#include <algebra/ShrinkVectorView.hpp>
 #include <algebra/SmallMatrix.hpp>
-#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
-#include <analysis/GaussQuadratureDescriptor.hpp>
-#include <analysis/QuadratureFormula.hpp>
-#include <analysis/QuadratureManager.hpp>
-#include <geometry/CubeTransformation.hpp>
-#include <geometry/LineCubicTransformation.hpp>
-#include <geometry/LineParabolicTransformation.hpp>
-#include <geometry/LineTransformation.hpp>
-#include <geometry/PrismTransformation.hpp>
-#include <geometry/PyramidTransformation.hpp>
-#include <geometry/SquareTransformation.hpp>
-#include <geometry/TetrahedronTransformation.hpp>
-#include <geometry/TriangleTransformation.hpp>
+#include <geometry/SymmetryUtils.hpp>
 #include <mesh/MeshData.hpp>
 #include <mesh/MeshDataManager.hpp>
 #include <mesh/MeshFlatFaceBoundary.hpp>
@@ -26,867 +13,532 @@
 #include <scheme/DiscreteFunctionDPkVariant.hpp>
 #include <scheme/DiscreteFunctionUtils.hpp>
 #include <scheme/DiscreteFunctionVariant.hpp>
+#include <scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp>
+#include <scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.hpp>
+#include <scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp>
+#include <scheme/reconstruction_utils/MutableDiscreteFunctionDPkVariant.hpp>
 
-template <size_t Dimension>
-PUGS_INLINE auto
-symmetrize_vector(const TinyVector<Dimension>& normal, const TinyVector<Dimension>& u)
-{
-  return u - 2 * dot(u, normal) * normal;
-}
-
-template <size_t Dimension>
-PUGS_INLINE auto
-symmetrize_matrix(const TinyVector<Dimension>& normal, const TinyMatrix<Dimension>& A)
-{
-  const TinyMatrix S = TinyMatrix<Dimension>{identity} - 2 * tensorProduct(normal, normal);
-  return S * A * S;
-}
-
-template <size_t Dimension>
-PUGS_INLINE auto
-symmetrize_coordinates(const TinyVector<Dimension>& origin,
-                       const TinyVector<Dimension>& normal,
-                       const TinyVector<Dimension>& u)
-{
-  return u - 2 * dot(u - origin, normal) * normal;
-}
-
-class PolynomialReconstruction::MutableDiscreteFunctionDPkVariant
+template <MeshConcept MeshType>
+class PolynomialReconstruction::Internal
 {
- public:
-  using Variant = std::variant<DiscreteFunctionDPk<1, double>,
-                               DiscreteFunctionDPk<1, TinyVector<1>>,
-                               DiscreteFunctionDPk<1, TinyVector<2>>,
-                               DiscreteFunctionDPk<1, TinyVector<3>>,
-                               DiscreteFunctionDPk<1, TinyMatrix<1>>,
-                               DiscreteFunctionDPk<1, TinyMatrix<2>>,
-                               DiscreteFunctionDPk<1, TinyMatrix<3>>,
-
-                               DiscreteFunctionDPk<2, double>,
-                               DiscreteFunctionDPk<2, TinyVector<1>>,
-                               DiscreteFunctionDPk<2, TinyVector<2>>,
-                               DiscreteFunctionDPk<2, TinyVector<3>>,
-                               DiscreteFunctionDPk<2, TinyMatrix<1>>,
-                               DiscreteFunctionDPk<2, TinyMatrix<2>>,
-                               DiscreteFunctionDPk<2, TinyMatrix<3>>,
-
-                               DiscreteFunctionDPk<3, double>,
-                               DiscreteFunctionDPk<3, TinyVector<1>>,
-                               DiscreteFunctionDPk<3, TinyVector<2>>,
-                               DiscreteFunctionDPk<3, TinyVector<3>>,
-                               DiscreteFunctionDPk<3, TinyMatrix<1>>,
-                               DiscreteFunctionDPk<3, TinyMatrix<2>>,
-                               DiscreteFunctionDPk<3, TinyMatrix<3>>,
-
-                               DiscreteFunctionDPkVector<1, double>,
-                               DiscreteFunctionDPkVector<1, TinyVector<1>>,
-                               DiscreteFunctionDPkVector<1, TinyVector<2>>,
-                               DiscreteFunctionDPkVector<1, TinyVector<3>>,
-                               DiscreteFunctionDPkVector<1, TinyMatrix<1>>,
-                               DiscreteFunctionDPkVector<1, TinyMatrix<2>>,
-                               DiscreteFunctionDPkVector<1, TinyMatrix<3>>,
-
-                               DiscreteFunctionDPkVector<2, double>,
-                               DiscreteFunctionDPkVector<2, TinyVector<1>>,
-                               DiscreteFunctionDPkVector<2, TinyVector<2>>,
-                               DiscreteFunctionDPkVector<2, TinyVector<3>>,
-                               DiscreteFunctionDPkVector<2, TinyMatrix<1>>,
-                               DiscreteFunctionDPkVector<2, TinyMatrix<2>>,
-                               DiscreteFunctionDPkVector<2, TinyMatrix<3>>,
-
-                               DiscreteFunctionDPkVector<3, double>,
-                               DiscreteFunctionDPkVector<3, TinyVector<1>>,
-                               DiscreteFunctionDPkVector<3, TinyVector<2>>,
-                               DiscreteFunctionDPkVector<3, TinyVector<3>>,
-                               DiscreteFunctionDPkVector<3, TinyMatrix<1>>,
-                               DiscreteFunctionDPkVector<3, TinyMatrix<2>>,
-                               DiscreteFunctionDPkVector<3, TinyMatrix<3>>>;
-
  private:
-  Variant m_mutable_discrete_function_dpk;
+  using Rd = TinyVector<MeshType::Dimension>;
 
- public:
-  PUGS_INLINE
-  const Variant&
-  mutableDiscreteFunctionDPk() const
-  {
-    return m_mutable_discrete_function_dpk;
-  }
+  friend PolynomialReconstruction;
 
-  template <typename DiscreteFunctionDPkT>
-  PUGS_INLINE auto&&
-  get() const
+  template <typename MatrixType>
+  static void
+  buildB(const CellId& cell_j_id,
+         const CellToCellStencilArray& stencil_array,
+         const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list,
+         SmallArray<const Rd> symmetry_normal_list,
+         ShrinkMatrixView<MatrixType>& B)
   {
-    static_assert(is_discrete_function_dpk_v<DiscreteFunctionDPkT>, "invalid template argument");
-
-#ifndef NDEBUG
-    if (not std::holds_alternative<DiscreteFunctionDPkT>(this->m_mutable_discrete_function_dpk)) {
-      std::ostringstream error_msg;
-      error_msg << "invalid discrete function type\n";
-      error_msg << "- required " << rang::fgB::red << demangle<DiscreteFunctionDPkT>() << rang::fg::reset << '\n';
-      error_msg << "- contains " << rang::fgB::yellow
-                << std::visit([](auto&& f) -> std::string { return demangle<decltype(f)>(); },
-                              this->m_mutable_discrete_function_dpk)
-                << rang::fg::reset;
-      throw NormalError(error_msg.str());
-    }
-#endif   // NDEBUG
-
-    return std::get<DiscreteFunctionDPkT>(this->mutableDiscreteFunctionDPk());
-  }
+    auto stencil_cell_list = stencil_array[cell_j_id];
+
+    size_t column_begin = 0;
+    for (size_t i_discrete_function_variant = 0; i_discrete_function_variant < discrete_function_variant_list.size();
+         ++i_discrete_function_variant) {
+      const auto& discrete_function_variant = discrete_function_variant_list[i_discrete_function_variant];
+
+      std::visit(
+        [&](auto&& discrete_function) {
+          using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
+          if constexpr (is_discrete_function_P0_v<DiscreteFunctionT>) {
+            using DataType     = std::decay_t<typename DiscreteFunctionT::data_type>;
+            const DataType& qj = discrete_function[cell_j_id];
+            size_t index       = 0;
+            for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+              const CellId cell_i_id = stencil_cell_list[i];
+              const DataType& qi_qj  = discrete_function[cell_i_id] - qj;
+              if constexpr (std::is_arithmetic_v<DataType>) {
+                B(index, column_begin) = qi_qj;
+              } else if constexpr (is_tiny_vector_v<DataType>) {
+                for (size_t kB = column_begin, k = 0; k < DataType::Dimension; ++k, ++kB) {
+                  B(index, kB) = qi_qj[k];
+                }
+              } else if constexpr (is_tiny_matrix_v<DataType>) {
+                for (size_t p = 0; p < DataType::NumberOfRows; ++p) {
+                  const size_t kB = column_begin + p * DataType::NumberOfColumns;
+                  for (size_t q = 0; q < DataType::NumberOfColumns; ++q) {
+                    B(index, kB + q) = qi_qj(p, q);
+                  }
+                }
+              }
+            }
 
-  template <size_t Dimension, typename DataType>
-  MutableDiscreteFunctionDPkVariant(const DiscreteFunctionDPk<Dimension, DataType>& discrete_function_dpk)
-    : m_mutable_discrete_function_dpk{discrete_function_dpk}
-  {
-    static_assert(std::is_same_v<DataType, double> or                       //
-                    std::is_same_v<DataType, TinyVector<1, double>> or      //
-                    std::is_same_v<DataType, TinyVector<2, double>> or      //
-                    std::is_same_v<DataType, TinyVector<3, double>> or      //
-                    std::is_same_v<DataType, TinyMatrix<1, 1, double>> or   //
-                    std::is_same_v<DataType, TinyMatrix<2, 2, double>> or   //
-                    std::is_same_v<DataType, TinyMatrix<3, 3, double>>,
-                  "DiscreteFunctionDPk with this DataType is not allowed in variant");
-  }
+            for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                 ++i_symmetry) {
+              auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+              auto ghost_cell_list = ghost_stencil[cell_j_id];
+              for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                const CellId cell_i_id = ghost_cell_list[i];
+                if constexpr (std::is_arithmetic_v<DataType>) {
+                  const DataType& qi_qj  = discrete_function[cell_i_id] - qj;
+                  B(index, column_begin) = qi_qj;
+                } else if constexpr (is_tiny_vector_v<DataType>) {
+                  if constexpr (DataType::Dimension == MeshType::Dimension) {
+                    const Rd& normal = symmetry_normal_list[i_symmetry];
 
-  template <size_t Dimension, typename DataType>
-  MutableDiscreteFunctionDPkVariant(const DiscreteFunctionDPkVector<Dimension, DataType>& discrete_function_dpk)
-    : m_mutable_discrete_function_dpk{discrete_function_dpk}
-  {
-    static_assert(std::is_same_v<DataType, double> or                       //
-                    std::is_same_v<DataType, TinyVector<1, double>> or      //
-                    std::is_same_v<DataType, TinyVector<2, double>> or      //
-                    std::is_same_v<DataType, TinyVector<3, double>> or      //
-                    std::is_same_v<DataType, TinyMatrix<1, 1, double>> or   //
-                    std::is_same_v<DataType, TinyMatrix<2, 2, double>> or   //
-                    std::is_same_v<DataType, TinyMatrix<3, 3, double>>,
-                  "DiscreteFunctionDPkVector with this DataType is not allowed in variant");
-  }
+                    const DataType& qi    = discrete_function[cell_i_id];
+                    const DataType& qi_qj = symmetrize_vector(normal, qi) - qj;
+                    for (size_t kB = column_begin, k = 0; k < DataType::Dimension; ++k, ++kB) {
+                      B(index, kB) = qi_qj[k];
+                    }
+                  } else {
+                    // LCOV_EXCL_START
+                    std::stringstream error_msg;
+                    error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                              << " using a mesh of dimension " << MeshType::Dimension;
+                    throw UnexpectedError(error_msg.str());
+                    // LCOV_EXCL_STOP
+                  }
+                } else if constexpr (is_tiny_matrix_v<DataType>) {
+                  if constexpr ((DataType::NumberOfColumns == DataType::NumberOfRows) and
+                                (DataType::NumberOfColumns == MeshType::Dimension)) {
+                    const Rd& normal = symmetry_normal_list[i_symmetry];
+
+                    const DataType& qi    = discrete_function[cell_i_id];
+                    const DataType& qi_qj = symmetrize_matrix(normal, qi) - qj;
+                    for (size_t p = 0; p < DataType::NumberOfRows; ++p) {
+                      for (size_t q = 0; q < DataType::NumberOfColumns; ++q) {
+                        B(index, column_begin + p * DataType::NumberOfColumns + q) = qi_qj(p, q);
+                      }
+                    }
+                  } else {
+                    // LCOV_EXCL_START
+                    std::stringstream error_msg;
+                    error_msg << "cannot symmetrize matrices of dimensions " << DataType::NumberOfRows << 'x'
+                              << DataType::NumberOfColumns << " using a mesh of dimension " << MeshType::Dimension;
+                    throw UnexpectedError(error_msg.str());
+                    // LCOV_EXCL_STOP
+                  }
+                }
+              }
+            }
 
-  MutableDiscreteFunctionDPkVariant& operator=(MutableDiscreteFunctionDPkVariant&&)      = default;
-  MutableDiscreteFunctionDPkVariant& operator=(const MutableDiscreteFunctionDPkVariant&) = default;
+            if constexpr (std::is_arithmetic_v<DataType>) {
+              ++column_begin;
+            } else if constexpr (is_tiny_vector_v<DataType> or is_tiny_matrix_v<DataType>) {
+              column_begin += DataType::Dimension;
+            }
+          } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
+            using DataType = std::decay_t<typename DiscreteFunctionT::data_type>;
 
-  MutableDiscreteFunctionDPkVariant(const MutableDiscreteFunctionDPkVariant&) = default;
-  MutableDiscreteFunctionDPkVariant(MutableDiscreteFunctionDPkVariant&&)      = default;
+            const auto qj_vector = discrete_function[cell_j_id];
 
-  MutableDiscreteFunctionDPkVariant()  = delete;
-  ~MutableDiscreteFunctionDPkVariant() = default;
-};
+            if constexpr (std::is_arithmetic_v<DataType>) {
+              size_t index = 0;
+              for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                const CellId cell_i_id = stencil_cell_list[i];
+                for (size_t l = 0; l < qj_vector.size(); ++l) {
+                  const DataType& qj         = qj_vector[l];
+                  const DataType& qi_qj      = discrete_function[cell_i_id][l] - qj;
+                  B(index, column_begin + l) = qi_qj;
+                }
+              }
 
-template <typename ConformTransformationT>
-void
-_computeEjkMean(const QuadratureFormula<1>& quadrature,
-                const ConformTransformationT& T,
-                const TinyVector<1>& Xj,
-                const double Vi,
-                const size_t degree,
-                const size_t dimension,
-                SmallArray<double>& inv_Vj_wq_detJ_ek,
-                SmallArray<double>& mean_of_ejk)
-{
-  using Rd = TinyVector<1>;
-  mean_of_ejk.fill(0);
+              for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                   ++i_symmetry) {
+                auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                auto ghost_cell_list = ghost_stencil[cell_j_id];
+                for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                  const CellId cell_i_id = ghost_cell_list[i];
+                  for (size_t l = 0; l < qj_vector.size(); ++l) {
+                    const DataType& qj         = qj_vector[l];
+                    const DataType& qi_qj      = discrete_function[cell_i_id][l] - qj;
+                    B(index, column_begin + l) = qi_qj;
+                  }
+                }
+              }
+            } else if constexpr (is_tiny_vector_v<DataType>) {
+              size_t index = 0;
+              for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                const CellId cell_i_id = stencil_cell_list[i];
+                for (size_t l = 0; l < qj_vector.size(); ++l) {
+                  const DataType& qj    = qj_vector[l];
+                  const DataType& qi_qj = discrete_function[cell_i_id][l] - qj;
+                  for (size_t kB = column_begin + l * DataType::Dimension, k = 0; k < DataType::Dimension; ++k, ++kB) {
+                    B(index, kB) = qi_qj[k];
+                  }
+                }
+              }
 
-  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-    const double wq   = quadrature.weight(i_q);
-    const Rd& xi_q    = quadrature.point(i_q);
-    const double detT = T.jacobianDeterminant();
+              for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                   ++i_symmetry) {
+                if constexpr (DataType::Dimension == MeshType::Dimension) {
+                  auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                  auto ghost_cell_list = ghost_stencil[cell_j_id];
 
-    const Rd X_Xj = T(xi_q) - Xj;
+                  const Rd& normal = symmetry_normal_list[i_symmetry];
 
-    const double x_xj = X_Xj[0];
+                  for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                    const CellId cell_i_id = ghost_cell_list[i];
 
-    {
-      size_t k               = 0;
-      inv_Vj_wq_detJ_ek[k++] = wq * detT / Vi;
-      for (; k <= degree; ++k) {
-        inv_Vj_wq_detJ_ek[k] = x_xj * inv_Vj_wq_detJ_ek[k - 1];
-      }
-    }
+                    for (size_t l = 0; l < qj_vector.size(); ++l) {
+                      const DataType& qj    = qj_vector[l];
+                      const DataType& qi    = discrete_function[cell_i_id][l];
+                      const DataType& qi_qj = symmetrize_vector(normal, qi) - qj;
+                      for (size_t kB = column_begin + l * DataType::Dimension, k = 0; k < DataType::Dimension;
+                           ++k, ++kB) {
+                        B(index, kB) = qi_qj[k];
+                      }
+                    }
+                  }
+                } else {
+                  // LCOV_EXCL_START
+                  std::stringstream error_msg;
+                  error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                            << " using a mesh of dimension " << MeshType::Dimension;
+                  throw UnexpectedError(error_msg.str());
+                  // LCOV_EXCL_STOP
+                }
+              }
+            } else if constexpr (is_tiny_matrix_v<DataType>) {
+              size_t index = 0;
+              for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                const CellId cell_i_id = stencil_cell_list[i];
+                for (size_t l = 0; l < qj_vector.size(); ++l) {
+                  const DataType& qj    = qj_vector[l];
+                  const DataType& qi    = discrete_function[cell_i_id][l];
+                  const DataType& qi_qj = qi - qj;
+
+                  for (size_t p = 0; p < DataType::NumberOfRows; ++p) {
+                    const size_t kB = column_begin + l * DataType::Dimension + p * DataType::NumberOfColumns;
+                    for (size_t q = 0; q < DataType::NumberOfColumns; ++q) {
+                      B(index, kB + q) = qi_qj(p, q);
+                    }
+                  }
+                }
+              }
 
-    for (size_t k = 1; k < dimension; ++k) {
-      mean_of_ejk[k - 1] += inv_Vj_wq_detJ_ek[k];
-    }
-  }
-}
+              for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                   ++i_symmetry) {
+                if constexpr ((DataType::NumberOfRows == MeshType::Dimension) and
+                              (DataType::NumberOfColumns == MeshType::Dimension)) {
+                  auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                  auto ghost_cell_list = ghost_stencil[cell_j_id];
 
-template <typename ConformTransformationT>
-void
-_computeEjkMean(const QuadratureFormula<2>& quadrature,
-                const ConformTransformationT& T,
-                const TinyVector<2>& Xj,
-                const double Vi,
-                const size_t degree,
-                const size_t dimension,
-                SmallArray<double>& inv_Vj_wq_detJ_ek,
-                SmallArray<double>& mean_of_ejk)
-{
-  using Rd = TinyVector<2>;
-
-  mean_of_ejk.fill(0);
-
-  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-    const double wq   = quadrature.weight(i_q);
-    const Rd& xi_q    = quadrature.point(i_q);
-    const double detT = [&] {
-      if constexpr (std::is_same_v<TriangleTransformation<2>, std::decay_t<decltype(T)>>) {
-        return T.jacobianDeterminant();
-      } else {
-        return T.jacobianDeterminant(xi_q);
-      }
-    }();
+                  const Rd& normal = symmetry_normal_list[i_symmetry];
 
-    const Rd X_Xj = T(xi_q) - Xj;
+                  for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                    const CellId cell_i_id = ghost_cell_list[i];
 
-    const double x_xj = X_Xj[0];
-    const double y_yj = X_Xj[1];
+                    for (size_t l = 0; l < qj_vector.size(); ++l) {
+                      const DataType& qj    = qj_vector[l];
+                      const DataType& qi    = discrete_function[cell_i_id][l];
+                      const DataType& qi_qj = symmetrize_matrix(normal, qi) - qj;
 
-    {
-      size_t k               = 0;
-      inv_Vj_wq_detJ_ek[k++] = wq * detT / Vi;
-      for (; k <= degree; ++k) {
-        inv_Vj_wq_detJ_ek[k] = x_xj * inv_Vj_wq_detJ_ek[k - 1];
-      }
+                      for (size_t p = 0; p < DataType::NumberOfRows; ++p) {
+                        const size_t kB = column_begin + l * DataType::Dimension + p * DataType::NumberOfColumns;
+                        for (size_t q = 0; q < DataType::NumberOfColumns; ++q) {
+                          B(index, kB + q) = qi_qj(p, q);
+                        }
+                      }
+                    }
+                  }
+                } else {
+                  // LCOV_EXCL_START
+                  std::stringstream error_msg;
+                  error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                            << " using a mesh of dimension " << MeshType::Dimension;
+                  throw UnexpectedError(error_msg.str());
+                  // LCOV_EXCL_STOP
+                }
+              }
+            }
 
-      for (size_t i_y = 1; i_y <= degree; ++i_y) {
-        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
-        for (size_t l = 0; l <= degree - i_y; ++l) {
-          inv_Vj_wq_detJ_ek[k++] = y_yj * inv_Vj_wq_detJ_ek[begin_i_y_1 + l];
-        }
-      }
-    }
+            if constexpr (std::is_arithmetic_v<DataType>) {
+              column_begin += qj_vector.size();
+            } else if constexpr (is_tiny_vector_v<DataType> or is_tiny_matrix_v<DataType>) {
+              column_begin += qj_vector.size() * DataType::Dimension;
+            }
 
-    for (size_t k = 1; k < dimension; ++k) {
-      mean_of_ejk[k - 1] += inv_Vj_wq_detJ_ek[k];
+          } else {
+            // LCOV_EXCL_START
+            throw UnexpectedError("invalid discrete function type");
+            // LCOV_EXCL_STOP
+          }
+        },
+        discrete_function_variant->discreteFunction());
     }
   }
-}
-
-template <typename ConformTransformationT>
-void
-_computeEjkMean(const QuadratureFormula<3>& quadrature,
-                const ConformTransformationT& T,
-                const TinyVector<3>& Xj,
-                const double Vi,
-                const size_t degree,
-                const size_t dimension,
-                SmallArray<double>& inv_Vj_wq_detJ_ek,
-                SmallArray<double>& mean_of_ejk)
-{
-  using Rd = TinyVector<3>;
-  mean_of_ejk.fill(0);
-
-  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-    const double wq   = quadrature.weight(i_q);
-    const Rd& xi_q    = quadrature.point(i_q);
-    const double detT = [&] {
-      if constexpr (std::is_same_v<TetrahedronTransformation, std::decay_t<decltype(T)>>) {
-        return T.jacobianDeterminant();
-      } else {
-        return T.jacobianDeterminant(xi_q);
-      }
-    }();
-
-    const Rd X_Xj = T(xi_q) - Xj;
-
-    const double x_xj = X_Xj[0];
-    const double y_yj = X_Xj[1];
-    const double z_zj = X_Xj[2];
-
-    {
-      size_t k               = 0;
-      inv_Vj_wq_detJ_ek[k++] = wq * detT / Vi;
-      for (; k <= degree; ++k) {
-        inv_Vj_wq_detJ_ek[k] = x_xj * inv_Vj_wq_detJ_ek[k - 1];
-      }
 
-      for (size_t i_y = 1; i_y <= degree; ++i_y) {
-        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
-        for (size_t l = 0; l <= degree - i_y; ++l) {
-          inv_Vj_wq_detJ_ek[k++] = y_yj * inv_Vj_wq_detJ_ek[begin_i_y_1 + l];
-        }
+  template <typename MatrixType>
+  static void
+  rowWeighting(const CellId& cell_j_id,
+               const CellToCellStencilArray& stencil_array,
+               const CellValue<const Rd>& xj,
+               const SmallArray<const Rd>& symmetry_origin_list,
+               const SmallArray<const Rd>& symmetry_normal_list,
+               ShrinkMatrixView<MatrixType>& A,
+               ShrinkMatrixView<MatrixType>& B)
+  {
+    // Add row weighting (give more importance to cells that are
+    // closer to j)
+    auto stencil_cell_list = stencil_array[cell_j_id];
+
+    const Rd& Xj = xj[cell_j_id];
+
+    size_t index = 0;
+    for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+      const CellId cell_i_id = stencil_cell_list[i];
+      const double weight    = 1. / l2Norm(xj[cell_i_id] - Xj);
+      for (size_t l = 0; l < A.numberOfColumns(); ++l) {
+        A(index, l) *= weight;
       }
-
-      for (size_t i_z = 1; i_z <= degree; ++i_z) {
-        const size_t begin_i_z_1 = ((i_z - 1) * (3 * (degree + 2) * (degree + 3) + (i_z - (3 * degree + 8)) * i_z)) / 6;
-        for (size_t i_y = 0; i_y <= degree - i_z; ++i_y) {
-          const size_t begin_i_y_1 = (i_y * (2 * degree - i_y + 3)) / 2 + begin_i_z_1;
-          for (size_t l = 0; l <= degree - i_y - i_z; ++l) {
-            inv_Vj_wq_detJ_ek[k++] = z_zj * inv_Vj_wq_detJ_ek[begin_i_y_1 + l];
-          }
-        }
+      for (size_t l = 0; l < B.numberOfColumns(); ++l) {
+        B(index, l) *= weight;
       }
     }
-
-    for (size_t k = 1; k < dimension; ++k) {
-      mean_of_ejk[k - 1] += inv_Vj_wq_detJ_ek[k];
-    }
-  }
-}
-
-template <typename NodeListT, size_t Dimension>
-void _computeEjkMean(const TinyVector<Dimension>& Xj,
-                     const NodeValue<const TinyVector<Dimension>>& xr,
-                     const NodeListT& node_list,
-                     const CellType& cell_type,
-                     const double Vi,
-                     const size_t degree,
-                     const size_t basis_dimension,
-                     SmallArray<double>& inv_Vj_wq_detJ_ek,
-                     SmallArray<double>& mean_of_ejk);
-
-template <typename NodeListT>
-void
-_computeEjkMean(const TinyVector<1>& Xj,
-                const NodeValue<const TinyVector<1>>& xr,
-                const NodeListT& node_list,
-                const CellType& cell_type,
-                const double Vi,
-                const size_t degree,
-                const size_t basis_dimension,
-                SmallArray<double>& inv_Vj_wq_detJ_ek,
-                SmallArray<double>& mean_of_ejk)
-{
-  if (cell_type == CellType::Line) {
-    const LineTransformation<1> T{xr[node_list[0]], xr[node_list[1]]};
-
-    const auto& quadrature = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{degree});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-
-  } else {
-    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-  }
-}
-
-template <typename NodeListT>
-void
-_computeEjkMeanInSymmetricCell(const TinyVector<1>& origin,
-                               const TinyVector<1>& normal,
-                               const TinyVector<1>& Xj,
-                               const NodeValue<const TinyVector<1>>& xr,
-                               const NodeListT& node_list,
-                               const CellType& cell_type,
-                               const double Vi,
-                               const size_t degree,
-                               const size_t basis_dimension,
-                               SmallArray<double>& inv_Vj_wq_detJ_ek,
-                               SmallArray<double>& mean_of_ejk)
-{
-  if (cell_type == CellType::Line) {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-
-    const LineTransformation<1> T{x0, x1};
-
-    const auto& quadrature = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{degree});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-
-  } else {
-    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-  }
-}
-
-template <typename ConformTransformationT>
-void _computeEjkBoundaryMean(const QuadratureFormula<1>& quadrature,
-                             const ConformTransformationT& T,
-                             const TinyVector<2>& Xj,
-                             const double Vi,
-                             const size_t degree,
-                             const size_t dimension,
-                             SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
-                             SmallArray<double>& mean_of_ejk);
-
-template <>
-void
-_computeEjkBoundaryMean(const QuadratureFormula<1>& quadrature,
-                        const LineTransformation<2>& T,
-                        const TinyVector<2>& Xj,
-                        const double Vi,
-                        const size_t degree,
-                        const size_t dimension,
-                        SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
-                        SmallArray<double>& mean_of_ejk)
-{
-  using Rd = TinyVector<2>;
-
-  const double velocity_perp_e1 = T.velocity()[1];
-
-  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-    const double wq          = quadrature.weight(i_q);
-    const TinyVector<1> xi_q = quadrature.point(i_q);
-
-    const Rd X_Xj = T(xi_q) - Xj;
-
-    const double x_xj = X_Xj[0];
-    const double y_yj = X_Xj[1];
-
-    {
-      size_t k                                 = 0;
-      inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = x_xj * wq * velocity_perp_e1 / Vi;
-      for (; k <= degree; ++k) {
-        inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k] =
-          x_xj * inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k - 1] * ((1. * k) / (k + 1));
-      }
-
-      for (size_t i_y = 1; i_y <= degree; ++i_y) {
-        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
-        for (size_t l = 0; l <= degree - i_y; ++l) {
-          inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = y_yj * inv_Vj_alpha_p_1_wq_X_prime_orth_ek[begin_i_y_1 + l];
+    for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry) {
+      auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+      auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+      const Rd& origin = symmetry_origin_list[i_symmetry];
+      const Rd& normal = symmetry_normal_list[i_symmetry];
+
+      for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+        const CellId cell_i_id = ghost_cell_list[i];
+        const double weight    = 1. / l2Norm(symmetrize_coordinates(origin, normal, xj[cell_i_id]) - Xj);
+        for (size_t l = 0; l < A.numberOfColumns(); ++l) {
+          A(index, l) *= weight;
+        }
+        for (size_t l = 0; l < B.numberOfColumns(); ++l) {
+          B(index, l) *= weight;
         }
       }
     }
-
-    for (size_t k = 1; k < dimension; ++k) {
-      mean_of_ejk[k - 1] += inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k];
-    }
   }
-}
-
-template <typename ConformTransformationT>
-void
-_computeEjkBoundaryMean(const QuadratureFormula<1>& quadrature,
-                        const ConformTransformationT& T,
-                        const TinyVector<2>& Xj,
-                        const double Vi,
-                        const size_t degree,
-                        const size_t dimension,
-                        SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
-                        SmallArray<double>& mean_of_ejk)
-{
-  using Rd = TinyVector<2>;
-
-  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-    const double wq          = quadrature.weight(i_q);
-    const TinyVector<1> xi_q = quadrature.point(i_q);
-
-    const double velocity_perp_e1 = T.velocity(xi_q)[1];
-    const Rd X_Xj                 = T(xi_q) - Xj;
-
-    const double x_xj = X_Xj[0];
-    const double y_yj = X_Xj[1];
 
-    {
-      size_t k                                 = 0;
-      inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = x_xj * wq * velocity_perp_e1 / Vi;
-      for (; k <= degree; ++k) {
-        inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k] =
-          x_xj * inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k - 1] * ((1. * k) / (k + 1));
-      }
+  template <typename MatrixType>
+  static void
+  solveCollectionInPlaceWithPreconditionner(const ShrinkMatrixView<MatrixType>& A,
+                                            const SmallMatrix<double>& X,
+                                            const ShrinkMatrixView<MatrixType>& B,
+                                            const SmallVector<double>& G)
+  {
+    for (size_t l = 0; l < A.numberOfColumns(); ++l) {
+      double g = 0;
+      for (size_t i = 0; i < A.numberOfRows(); ++i) {
+        const double Ail = A(i, l);
 
-      for (size_t i_y = 1; i_y <= degree; ++i_y) {
-        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
-        for (size_t l = 0; l <= degree - i_y; ++l) {
-          inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = y_yj * inv_Vj_alpha_p_1_wq_X_prime_orth_ek[begin_i_y_1 + l];
-        }
+        g += Ail * Ail;
       }
+      G[l] = std::sqrt(g);
     }
 
-    for (size_t k = 1; k < dimension; ++k) {
-      mean_of_ejk[k - 1] += inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k];
-    }
-  }
-}
-
-template <MeshConcept MeshType>
-void
-_computeEjkMeanByBoundary(const MeshType& mesh,
-                          const TinyVector<2>& Xj,
-                          const CellId& cell_id,
-                          const auto& cell_to_face_matrix,
-                          const auto& face_to_node_matrix,
-                          const auto& cell_face_is_reversed,
-                          const CellValue<const double>& Vi,
-                          const size_t degree,
-                          const size_t basis_dimension,
-                          SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
-                          SmallArray<double>& mean_of_ejk)
-{
-  if constexpr (is_polygonal_mesh_v<MeshType>) {
-    const auto& quadrature =
-      QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(degree + 1));
-
-    auto xr = mesh.xr();
-    mean_of_ejk.fill(0);
-    auto cell_face_list = cell_to_face_matrix[cell_id];
-    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
-      bool is_reversed = cell_face_is_reversed[cell_id][i_face];
-
-      const FaceId face_id = cell_face_list[i_face];
-      auto face_node_list  = face_to_node_matrix[face_id];
-      if (is_reversed) {
-        const LineTransformation<2> T{xr[face_node_list[1]], xr[face_node_list[0]]};
-        _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-      } else {
-        const LineTransformation<2> T{xr[face_node_list[0]], xr[face_node_list[1]]};
-        _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
+    for (size_t l = 0; l < A.numberOfColumns(); ++l) {
+      const double Gl = G[l];
+      for (size_t i = 0; i < A.numberOfRows(); ++i) {
+        A(i, l) *= Gl;
       }
     }
-  } else {
-    static_assert(is_polynomial_mesh_v<MeshType>);
-    // (degree + 1): divergence theorem
-    // mesh.degree(): transformation polynomial degree
-    // (mesh.degree() - 1): degree of the line abscissa velocity
-    const auto& quadrature = QuadratureManager::instance().getLineFormula(
-      GaussLegendreQuadratureDescriptor((degree + 1) * mesh.degree() + (mesh.degree() - 1)));
-
-    auto xr = mesh.xr();
-    auto xl = mesh.xl();
-    mean_of_ejk.fill(0);
-    auto cell_face_list = cell_to_face_matrix[cell_id];
-    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
-      bool is_reversed = cell_face_is_reversed[cell_id][i_face];
-
-      const FaceId face_id = cell_face_list[i_face];
-      auto face_node_list  = face_to_node_matrix[face_id];
-#warning rework
-      switch (mesh.degree()) {
-      case 2: {
-        if (is_reversed) {
-          const LineParabolicTransformation<2> T{xr[face_node_list[1]], xl[face_id][0], xr[face_node_list[0]]};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        } else {
-          const LineParabolicTransformation<2> T{xr[face_node_list[0]], xl[face_id][0], xr[face_node_list[1]]};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        }
-        break;
-      }
-      case 3: {
-        if (is_reversed) {
-          const LineCubicTransformation<2> T{xr[face_node_list[1]], xl[face_id][1], xl[face_id][0],
-                                             xr[face_node_list[0]]};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        } else {
-          const LineCubicTransformation<2> T{xr[face_node_list[0]], xl[face_id][0], xl[face_id][1],
-                                             xr[face_node_list[1]]};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        }
-        break;
-      }
-      default: {
-        std::ostringstream error_msg;
-        error_msg << "reconstruction on meshes of degree " << mesh.degree();
-        throw NotImplementedError(error_msg.str());
-      }
-      }
-    }
-  }
-}
 
-template <MeshConcept MeshType>
-void
-_computeEjkMeanByBoundaryInSymmetricCell(const MeshType& mesh,
-                                         const TinyVector<2>& origin,
-                                         const TinyVector<2>& normal,
-                                         const TinyVector<2>& Xj,
-                                         const CellId& cell_id,
-                                         const NodeValue<const TinyVector<2>>& xr,
-                                         const auto& cell_to_face_matrix,
-                                         const auto& face_to_node_matrix,
-                                         const auto& cell_face_is_reversed,
-                                         const CellValue<const double>& Vi,
-                                         const size_t degree,
-                                         const size_t basis_dimension,
-                                         SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
-                                         SmallArray<double>& mean_of_ejk)
-{
-  if constexpr (is_polygonal_mesh_v<MeshType>) {
-    const auto& quadrature =
-      QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(degree + 1));
-
-    mean_of_ejk.fill(0);
-    auto cell_face_list = cell_to_face_matrix[cell_id];
-    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
-      bool is_reversed = cell_face_is_reversed[cell_id][i_face];
-
-      const FaceId face_id = cell_face_list[i_face];
-      auto face_node_list  = face_to_node_matrix[face_id];
-
-      const auto x0 = symmetrize_coordinates(origin, normal, xr[face_node_list[1]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, xr[face_node_list[0]]);
-
-      if (is_reversed) {
-        const LineTransformation<2> T{x1, x0};
-        _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-      } else {
-        const LineTransformation<2> T{x0, x1};
-        _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-      }
-    }
+    Givens::solveCollectionInPlace(A, X, B);
 
-  } else {
-    static_assert(is_polynomial_mesh_v<MeshType>);
-    // (degree + 1): divergence theorem
-    // mesh.degree(): transformation polynomial degree
-    // (mesh.degree() - 1): degree of the line abscissa velocity
-    const auto& quadrature = QuadratureManager::instance().getLineFormula(
-      GaussLegendreQuadratureDescriptor((degree + 1) * mesh.degree() + (mesh.degree() - 1)));
-
-    auto xl = mesh.xl();
-
-    mean_of_ejk.fill(0);
-    auto cell_face_list = cell_to_face_matrix[cell_id];
-    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
-      bool is_reversed = cell_face_is_reversed[cell_id][i_face];
-
-      const FaceId face_id = cell_face_list[i_face];
-      auto face_node_list  = face_to_node_matrix[face_id];
-
-      switch (mesh.degree()) {
-      case 2: {
-        const auto x0 = symmetrize_coordinates(origin, normal, xr[face_node_list[1]]);
-        const auto x1 = symmetrize_coordinates(origin, normal, xl[face_id][0]);
-        const auto x2 = symmetrize_coordinates(origin, normal, xr[face_node_list[0]]);
-
-        if (is_reversed) {
-          const LineParabolicTransformation<2> T{x2, x1, x0};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        } else {
-          const LineParabolicTransformation<2> T{x0, x1, x2};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        }
-        break;
-      }
-      case 3: {
-        const auto x0 = symmetrize_coordinates(origin, normal, xr[face_node_list[1]]);
-        const auto x1 = symmetrize_coordinates(origin, normal, xl[face_id][1]);
-        const auto x2 = symmetrize_coordinates(origin, normal, xl[face_id][0]);
-        const auto x3 = symmetrize_coordinates(origin, normal, xr[face_node_list[0]]);
-
-        if (is_reversed) {
-          const LineCubicTransformation<2> T{x3, x2, x1, x0};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        } else {
-          const LineCubicTransformation<2> T{x0, x1, x2, x3};
-          _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
-                                  inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
-        }
-        break;
-      }
-      default: {
-        std::ostringstream error_msg;
-        error_msg << "reconstruction on meshes of degree " << mesh.degree();
-        throw NotImplementedError(error_msg.str());
-      }
+    for (size_t l = 0; l < X.numberOfRows(); ++l) {
+      const double Gl = G[l];
+      for (size_t i = 0; i < X.numberOfColumns(); ++i) {
+        X(l, i) *= Gl;
       }
     }
   }
-}
-
-template <typename NodeListT>
-void
-_computeEjkMean(const TinyVector<2>& Xj,
-                const NodeValue<const TinyVector<2>>& xr,
-                const NodeListT& node_list,
-                const CellType& cell_type,
-                const double Vi,
-                const size_t degree,
-                const size_t basis_dimension,
-                SmallArray<double>& inv_Vj_wq_detJ_ek,
-                SmallArray<double>& mean_of_ejk)
-{
-  switch (cell_type) {
-  case CellType::Triangle: {
-    const TriangleTransformation<2> T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]]};
-    const auto& quadrature = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{degree});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-    break;
-  }
-  case CellType::Quadrangle: {
-    const SquareTransformation<2> T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]]};
-    const auto& quadrature =
-      QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{degree + 1});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-    break;
-  }
-  default: {
-    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-  }
-  }
-}
-
-template <typename NodeListT>
-void
-_computeEjkMeanInSymmetricCell(const TinyVector<2>& origin,
-                               const TinyVector<2>& normal,
-                               const TinyVector<2>& Xj,
-                               const NodeValue<const TinyVector<2>>& xr,
-                               const NodeListT& node_list,
-                               const CellType& cell_type,
-                               const double Vi,
-                               const size_t degree,
-                               const size_t basis_dimension,
-                               SmallArray<double>& inv_Vj_wq_detJ_ek,
-                               SmallArray<double>& mean_of_ejk)
-{
-  switch (cell_type) {
-  case CellType::Triangle: {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-
-    const TriangleTransformation<2> T{x0, x1, x2};
-    const auto& quadrature = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{degree});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-    break;
-  }
-  case CellType::Quadrangle: {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
-    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-
-    const SquareTransformation<2> T{x0, x1, x2, x3};
-    const auto& quadrature =
-      QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{degree + 1});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-    break;
-  }
-  default: {
-    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-  }
-  }
-}
-
-template <typename NodeListT>
-void
-_computeEjkMean(const TinyVector<3>& Xj,
-                const NodeValue<const TinyVector<3>>& xr,
-                const NodeListT& node_list,
-                const CellType& cell_type,
-                const double Vi,
-                const size_t degree,
-                const size_t basis_dimension,
-                SmallArray<double>& inv_Vj_wq_detJ_ek,
-                SmallArray<double>& mean_of_ejk)
-{
-  if (cell_type == CellType::Tetrahedron) {
-    const TetrahedronTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]]};
-
-    const auto& quadrature = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{degree});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
 
-  } else if (cell_type == CellType::Prism) {
-    const PrismTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]],   //
-                                xr[node_list[3]], xr[node_list[4]], xr[node_list[5]]};
-
-    const auto& quadrature = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{degree + 1});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-
-  } else if (cell_type == CellType::Pyramid) {
-    const PyramidTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]],
-                                  xr[node_list[4]]};
-
-    const auto& quadrature = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{degree + 1});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-
-  } else if (cell_type == CellType::Hexahedron) {
-    const CubeTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]],
-                               xr[node_list[4]], xr[node_list[5]], xr[node_list[6]], xr[node_list[7]]};
-
-    const auto& quadrature =
-      QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{degree + 1});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-
-  } else {
-    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-  }
-}
-
-template <typename NodeListT>
-void
-_computeEjkMeanInSymmetricCell(const TinyVector<3>& origin,
-                               const TinyVector<3>& normal,
-                               const TinyVector<3>& Xj,
-                               const NodeValue<const TinyVector<3>>& xr,
-                               const NodeListT& node_list,
-                               const CellType& cell_type,
-                               const double Vi,
-                               const size_t degree,
-                               const size_t basis_dimension,
-                               SmallArray<double>& inv_Vj_wq_detJ_ek,
-                               SmallArray<double>& mean_of_ejk)
-{
-  if (cell_type == CellType::Tetrahedron) {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
-    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
-
-    const TetrahedronTransformation T{x0, x1, x2, x3};
-
-    const auto& quadrature = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{degree});
-
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
-
-  } else if (cell_type == CellType::Prism) {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
-    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[4]]);
-    const auto x4 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
-    const auto x5 = symmetrize_coordinates(origin, normal, xr[node_list[5]]);
-
-    const PrismTransformation T{x0, x1, x2,   //
-                                x3, x4, x5};
+  template <typename ReconstructionMatrixBuilderType>
+  static void
+  populateDiscreteFunctionDPkByCell(
+    const CellId& cell_j_id,
+    const size_t& degree,
+    const SmallMatrix<double>& X,
+    const ReconstructionMatrixBuilderType& reconstruction_matrix_builder,
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list,
+    const std::vector<PolynomialReconstruction::MutableDiscreteFunctionDPkVariant>&
+      mutable_discrete_function_dpk_variant_list)
+  {
+    size_t column_begin = 0;
+    for (size_t i_dpk_variant = 0; i_dpk_variant < mutable_discrete_function_dpk_variant_list.size(); ++i_dpk_variant) {
+      const auto& dpk_variant = mutable_discrete_function_dpk_variant_list[i_dpk_variant];
 
-    const auto& quadrature = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{degree + 1});
+      const auto& discrete_function_variant = discrete_function_variant_list[i_dpk_variant];
 
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+      std::visit(
+        [&](auto&& dpk_function, auto&& p0_function) {
+          using DPkFunctionT = std::decay_t<decltype(dpk_function)>;
+          using P0FunctionT  = std::decay_t<decltype(p0_function)>;
+          using DataType     = std::remove_const_t<std::decay_t<typename DPkFunctionT::data_type>>;
+          using P0DataType   = std::remove_const_t<std::decay_t<typename P0FunctionT::data_type>>;
 
-  } else if (cell_type == CellType::Pyramid) {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
-    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-    const auto x4 = symmetrize_coordinates(origin, normal, xr[node_list[4]]);
-    const PyramidTransformation T{x0, x1, x2, x3, x4};
+          if constexpr (std::is_same_v<DataType, P0DataType>) {
+            if constexpr (is_discrete_function_P0_v<P0FunctionT>) {
+              if constexpr (is_discrete_function_dpk_scalar_v<DPkFunctionT>) {
+                auto dpk_j = dpk_function.coefficients(cell_j_id);
+                dpk_j[0]   = p0_function[cell_j_id];
 
-    const auto& quadrature = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{degree + 1});
+                if constexpr (std::is_arithmetic_v<DataType>) {
+                  if constexpr (ReconstructionMatrixBuilderType::handles_high_degrees) {
+                    if (degree > 1) {
+                      const auto& mean_j_of_ejk = reconstruction_matrix_builder.meanjOfEjk();
+                      for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                        dpk_j[0] -= X(i, column_begin) * mean_j_of_ejk[i];
+                      }
+                    }
+                  }
 
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+                  for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                    auto& dpk_j_ip1 = dpk_j[i + 1];
+                    dpk_j_ip1       = X(i, column_begin);
+                  }
+                  ++column_begin;
+                } else if constexpr (is_tiny_vector_v<DataType>) {
+                  if constexpr (ReconstructionMatrixBuilderType::handles_high_degrees) {
+                    if (degree > 1) {
+                      const auto& mean_j_of_ejk = reconstruction_matrix_builder.meanjOfEjk();
+                      for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                        auto& dpk_j_0 = dpk_j[0];
+                        for (size_t k = 0; k < DataType::Dimension; ++k) {
+                          dpk_j_0[k] -= X(i, column_begin + k) * mean_j_of_ejk[i];
+                        }
+                      }
+                    }
+                  }
 
-  } else if (cell_type == CellType::Hexahedron) {
-    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
-    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
-    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
-    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
-    const auto x4 = symmetrize_coordinates(origin, normal, xr[node_list[7]]);
-    const auto x5 = symmetrize_coordinates(origin, normal, xr[node_list[6]]);
-    const auto x6 = symmetrize_coordinates(origin, normal, xr[node_list[5]]);
-    const auto x7 = symmetrize_coordinates(origin, normal, xr[node_list[4]]);
+                  for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                    auto& dpk_j_ip1 = dpk_j[i + 1];
+                    for (size_t k = 0; k < DataType::Dimension; ++k) {
+                      dpk_j_ip1[k] = X(i, column_begin + k);
+                    }
+                  }
+                  column_begin += DataType::Dimension;
+                } else if constexpr (is_tiny_matrix_v<DataType>) {
+                  if constexpr (ReconstructionMatrixBuilderType::handles_high_degrees) {
+                    if (degree > 1) {
+                      const auto& mean_j_of_ejk = reconstruction_matrix_builder.meanjOfEjk();
+                      for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                        auto& dpk_j_0 = dpk_j[0];
+                        for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                          for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                            dpk_j_0(k, l) -= X(i, column_begin + k * DataType::NumberOfColumns + l) * mean_j_of_ejk[i];
+                          }
+                        }
+                      }
+                    }
+                  }
 
-    const CubeTransformation T{x0, x1, x2, x3, x4, x5, x6, x7};
+                  for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                    auto& dpk_j_ip1 = dpk_j[i + 1];
+                    for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                      for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                        dpk_j_ip1(k, l) = X(i, column_begin + k * DataType::NumberOfColumns + l);
+                      }
+                    }
+                  }
+                  column_begin += DataType::Dimension;
+                } else {
+                  // LCOV_EXCL_START
+                  throw UnexpectedError("unexpected data type");
+                  // LCOV_EXCL_STOP
+                }
+              } else {
+                // LCOV_EXCL_START
+                throw UnexpectedError("unexpected discrete dpk function type");
+                // LCOV_EXCL_STOP
+              }
+            } else if constexpr (is_discrete_function_P0_vector_v<P0FunctionT>) {
+              if constexpr (is_discrete_function_dpk_vector_v<DPkFunctionT>) {
+                auto dpk_j        = dpk_function.coefficients(cell_j_id);
+                auto cell_vector  = p0_function[cell_j_id];
+                const size_t size = X.numberOfRows() + 1;
+
+                for (size_t l = 0; l < cell_vector.size(); ++l) {
+                  const size_t component_begin = l * size;
+                  dpk_j[component_begin]       = cell_vector[l];
+                  if constexpr (std::is_arithmetic_v<DataType>) {
+                    if constexpr (ReconstructionMatrixBuilderType::handles_high_degrees) {
+                      const auto& mean_j_of_ejk = reconstruction_matrix_builder.meanjOfEjk();
+                      if (degree > 1) {
+                        for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                          dpk_j[component_begin] -= X(i, column_begin) * mean_j_of_ejk[i];
+                        }
+                      }
+                    }
 
-    const auto& quadrature =
-      QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{degree + 1});
+                    for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                      auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
+                      dpk_j_ip1       = X(i, column_begin);
+                    }
+                    ++column_begin;
+                  } else if constexpr (is_tiny_vector_v<DataType>) {
+                    if constexpr (ReconstructionMatrixBuilderType::handles_high_degrees) {
+                      if (degree > 1) {
+                        const auto& mean_j_of_ejk = reconstruction_matrix_builder.meanjOfEjk();
+                        for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                          auto& dpk_j_0 = dpk_j[component_begin];
+                          for (size_t k = 0; k < DataType::Dimension; ++k) {
+                            dpk_j_0[k] -= X(i, column_begin + k) * mean_j_of_ejk[i];
+                          }
+                        }
+                      }
+                    }
 
-    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+                    for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                      auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
+                      for (size_t k = 0; k < DataType::Dimension; ++k) {
+                        dpk_j_ip1[k] = X(i, column_begin + k);
+                      }
+                    }
+                    column_begin += DataType::Dimension;
+                  } else if constexpr (is_tiny_matrix_v<DataType>) {
+                    if constexpr (ReconstructionMatrixBuilderType::handles_high_degrees) {
+                      if (degree > 1) {
+                        const auto& mean_j_of_ejk = reconstruction_matrix_builder.meanjOfEjk();
+                        for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                          auto& dpk_j_0 = dpk_j[component_begin];
+                          for (size_t p = 0; p < DataType::NumberOfRows; ++p) {
+                            for (size_t q = 0; q < DataType::NumberOfColumns; ++q) {
+                              dpk_j_0(p, q) -=
+                                X(i, column_begin + p * DataType::NumberOfColumns + q) * mean_j_of_ejk[i];
+                            }
+                          }
+                        }
+                      }
+                    }
 
-  } else {
-    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+                    for (size_t i = 0; i < X.numberOfRows(); ++i) {
+                      auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
+                      for (size_t p = 0; p < DataType::NumberOfRows; ++p) {
+                        for (size_t q = 0; q < DataType::NumberOfColumns; ++q) {
+                          dpk_j_ip1(p, q) = X(i, column_begin + p * DataType::NumberOfColumns + q);
+                        }
+                      }
+                    }
+                    column_begin += DataType::Dimension;
+                  } else {
+                    // LCOV_EXCL_START
+                    throw UnexpectedError("unexpected data type");
+                    // LCOV_EXCL_STOP
+                  }
+                }
+              } else {
+                // LCOV_EXCL_START
+                throw UnexpectedError("unexpected discrete dpk function type");
+                // LCOV_EXCL_STOP
+              }
+            } else {
+              // LCOV_EXCL_START
+              throw UnexpectedError("unexpected discrete function type");
+              // LCOV_EXCL_STOP
+            }
+          } else {
+            // LCOV_EXCL_START
+            throw UnexpectedError("incompatible data types");
+            // LCOV_EXCL_STOP
+          }
+        },
+        dpk_variant.mutableDiscreteFunctionDPk(), discrete_function_variant->discreteFunction());
+    }
   }
-}
+};
 
 size_t
 PolynomialReconstruction::_getNumberOfColumns(
@@ -1003,12 +655,14 @@ PolynomialReconstruction::_checkDataAndSymmetriesCompatibility(
   }
 }
 
-template <MeshConcept MeshType>
+template <typename ReconstructionMatrixBuilderType, MeshConcept MeshType>
 [[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>>
 PolynomialReconstruction::_build(
   const std::shared_ptr<const MeshType>& p_mesh,
   const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
 {
+  static_assert(std::is_same_v<MeshType, typename ReconstructionMatrixBuilderType::MeshType>);
+
   const MeshType& mesh = *p_mesh;
 
   using Rd = TinyVector<MeshType::Dimension>;
@@ -1030,10 +684,8 @@ PolynomialReconstruction::_build(
   auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
   auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
 
-  auto cell_is_owned       = mesh.connectivity().cellIsOwned();
-  auto cell_type           = mesh.connectivity().cellType();
-  auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
-  auto cell_to_face_matrix = mesh.connectivity().cellToFaceMatrix();
+  auto cell_is_owned = mesh.connectivity().cellIsOwned();
+  auto cell_type     = mesh.connectivity().cellType();
 
   auto full_stencil_size = [&](const CellId cell_id) {
     auto stencil_cell_list = stencil_array[cell_id];
@@ -1093,24 +745,19 @@ PolynomialReconstruction::_build(
   SmallArray<SmallVector<double>> G_pool(Kokkos::DefaultExecutionSpace::concurrency());
   SmallArray<SmallMatrix<double>> X_pool(Kokkos::DefaultExecutionSpace::concurrency());
 
-  SmallArray<SmallArray<double>> inv_Vj_wq_detJ_ek_pool(Kokkos::DefaultExecutionSpace::concurrency());
-  SmallArray<SmallArray<double>> mean_j_of_ejk_pool(Kokkos::DefaultExecutionSpace::concurrency());
-  SmallArray<SmallArray<double>> mean_i_of_ejk_pool(Kokkos::DefaultExecutionSpace::concurrency());
-
-  SmallArray<SmallArray<double>> inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool(Kokkos::DefaultExecutionSpace::concurrency());
-  SmallArray<SmallArray<double>> mean_l_of_ejk_pool(Kokkos::DefaultExecutionSpace::concurrency());
-
   for (size_t i = 0; i < A_pool.size(); ++i) {
     A_pool[i] = SmallMatrix<double>(max_stencil_size, basis_dimension - 1);
     B_pool[i] = SmallMatrix<double>(max_stencil_size, number_of_columns);
     G_pool[i] = SmallVector<double>(basis_dimension - 1);
     X_pool[i] = SmallMatrix<double>(basis_dimension - 1, number_of_columns);
+  }
 
-    inv_Vj_wq_detJ_ek_pool[i] = SmallArray<double>(basis_dimension);
-    mean_j_of_ejk_pool[i]     = SmallArray<double>(basis_dimension - 1);
-    mean_i_of_ejk_pool[i]     = SmallArray<double>(basis_dimension - 1);
+  SmallArray<std::shared_ptr<ReconstructionMatrixBuilderType>> reconstruction_matrix_builder_pool(A_pool.size());
 
-    inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool[i] = SmallArray<double>(basis_dimension);
+  for (size_t t = 0; t < reconstruction_matrix_builder_pool.size(); ++t) {
+    reconstruction_matrix_builder_pool[t] =
+      std::make_shared<ReconstructionMatrixBuilderType>(*p_mesh, m_descriptor.degree(), symmetry_origin_list,
+                                                        symmetry_normal_list, stencil_array);
   }
 
   parallel_for(
@@ -1118,545 +765,34 @@ PolynomialReconstruction::_build(
       if (cell_is_owned[cell_j_id]) {
         const int32_t t = tokens.acquire();
 
-        auto stencil_cell_list = stencil_array[cell_j_id];
-
-        ShrinkMatrixView B(B_pool[t], full_stencil_size(cell_j_id));
-
-        size_t column_begin = 0;
-        for (size_t i_discrete_function_variant = 0;
-             i_discrete_function_variant < discrete_function_variant_list.size(); ++i_discrete_function_variant) {
-          const auto& discrete_function_variant = discrete_function_variant_list[i_discrete_function_variant];
-
-          std::visit(
-            [&](auto&& discrete_function) {
-              using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
-              if constexpr (is_discrete_function_P0_v<DiscreteFunctionT>) {
-                using DataType     = std::decay_t<typename DiscreteFunctionT::data_type>;
-                const DataType& qj = discrete_function[cell_j_id];
-                size_t index       = 0;
-                for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-                  const CellId cell_i_id = stencil_cell_list[i];
-                  const DataType& qi_qj  = discrete_function[cell_i_id] - qj;
-                  if constexpr (std::is_arithmetic_v<DataType>) {
-                    B(index, column_begin) = qi_qj;
-                  } else if constexpr (is_tiny_vector_v<DataType>) {
-                    for (size_t kB = column_begin, k = 0; k < DataType::Dimension; ++k, ++kB) {
-                      B(index, kB) = qi_qj[k];
-                    }
-                  } else if constexpr (is_tiny_matrix_v<DataType>) {
-                    for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
-                      for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
-                        B(index, column_begin + k * DataType::NumberOfColumns + l) = qi_qj(k, l);
-                      }
-                    }
-                  }
-                }
-
-                for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-                     ++i_symmetry) {
-                  auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-                  auto ghost_cell_list = ghost_stencil[cell_j_id];
-                  for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-                    const CellId cell_i_id = ghost_cell_list[i];
-                    if constexpr (std::is_arithmetic_v<DataType>) {
-                      const DataType& qi_qj  = discrete_function[cell_i_id] - qj;
-                      B(index, column_begin) = qi_qj;
-                    } else if constexpr (is_tiny_vector_v<DataType>) {
-                      if constexpr (DataType::Dimension == MeshType::Dimension) {
-                        const Rd& normal = symmetry_normal_list[i_symmetry];
-
-                        const DataType& qi    = discrete_function[cell_i_id];
-                        const DataType& qi_qj = symmetrize_vector(normal, qi) - qj;
-                        for (size_t kB = column_begin, k = 0; k < DataType::Dimension; ++k, ++kB) {
-                          B(index, kB) = qi_qj[k];
-                        }
-                      } else {
-                        // LCOV_EXCL_START
-                        std::stringstream error_msg;
-                        error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
-                                  << " using a mesh of dimension " << MeshType::Dimension;
-                        throw UnexpectedError(error_msg.str());
-                        // LCOV_EXCL_STOP
-                      }
-                    } else if constexpr (is_tiny_matrix_v<DataType>) {
-                      if constexpr ((DataType::NumberOfColumns == DataType::NumberOfRows) and
-                                    (DataType::NumberOfColumns == MeshType::Dimension)) {
-                        const Rd& normal = symmetry_normal_list[i_symmetry];
-
-                        const DataType& qi    = discrete_function[cell_i_id];
-                        const DataType& qi_qj = symmetrize_matrix(normal, qi) - qj;
-                        for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
-                          for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
-                            B(index, column_begin + k * DataType::NumberOfColumns + l) = qi_qj(k, l);
-                          }
-                        }
-                      } else {
-                        // LCOV_EXCL_START
-                        std::stringstream error_msg;
-                        error_msg << "cannot symmetrize matrices of dimensions " << DataType::NumberOfRows << 'x'
-                                  << DataType::NumberOfColumns << " using a mesh of dimension " << MeshType::Dimension;
-                        throw UnexpectedError(error_msg.str());
-                        // LCOV_EXCL_STOP
-                      }
-                    }
-                  }
-                }
-
-                if constexpr (std::is_arithmetic_v<DataType>) {
-                  ++column_begin;
-                } else if constexpr (is_tiny_vector_v<DataType> or is_tiny_matrix_v<DataType>) {
-                  column_begin += DataType::Dimension;
-                }
-              } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-                using DataType = std::decay_t<typename DiscreteFunctionT::data_type>;
-
-                const auto qj_vector = discrete_function[cell_j_id];
-
-                if constexpr (std::is_arithmetic_v<DataType>) {
-                  size_t index = 0;
-                  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-                    const CellId cell_i_id = stencil_cell_list[i];
-                    for (size_t l = 0; l < qj_vector.size(); ++l) {
-                      const DataType& qj         = qj_vector[l];
-                      const DataType& qi_qj      = discrete_function[cell_i_id][l] - qj;
-                      B(index, column_begin + l) = qi_qj;
-                    }
-                  }
-
-                  for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-                       ++i_symmetry) {
-                    auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-                    auto ghost_cell_list = ghost_stencil[cell_j_id];
-                    for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-                      const CellId cell_i_id = ghost_cell_list[i];
-                      for (size_t l = 0; l < qj_vector.size(); ++l) {
-                        const DataType& qj         = qj_vector[l];
-                        const DataType& qi_qj      = discrete_function[cell_i_id][l] - qj;
-                        B(index, column_begin + l) = qi_qj;
-                      }
-                    }
-                  }
-                } else if constexpr (is_tiny_vector_v<DataType>) {
-                  size_t index = 0;
-                  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-                    const CellId cell_i_id = stencil_cell_list[i];
-                    for (size_t l = 0; l < qj_vector.size(); ++l) {
-                      const DataType& qj    = qj_vector[l];
-                      const DataType& qi_qj = discrete_function[cell_i_id][l] - qj;
-                      for (size_t kB = column_begin + l * DataType::Dimension, k = 0; k < DataType::Dimension;
-                           ++k, ++kB) {
-                        B(index, kB) = qi_qj[k];
-                      }
-                    }
-                  }
-
-                  for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-                       ++i_symmetry) {
-                    if constexpr (DataType::Dimension == MeshType::Dimension) {
-                      auto& ghost_stencil = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-                      auto ghost_cell_list = ghost_stencil[cell_j_id];
-
-                      const Rd& normal = symmetry_normal_list[i_symmetry];
-
-                      for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-                        const CellId cell_i_id = ghost_cell_list[i];
-
-                        for (size_t l = 0; l < qj_vector.size(); ++l) {
-                          const DataType& qj    = qj_vector[l];
-                          const DataType& qi    = discrete_function[cell_i_id][l];
-                          const DataType& qi_qj = symmetrize_vector(normal, qi) - qj;
-                          for (size_t kB = column_begin + l * DataType::Dimension, k = 0; k < DataType::Dimension;
-                               ++k, ++kB) {
-                            B(index, kB) = qi_qj[k];
-                          }
-                        }
-                      }
-                    } else {
-                      // LCOV_EXCL_START
-                      std::stringstream error_msg;
-                      error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
-                                << " using a mesh of dimension " << MeshType::Dimension;
-                      throw UnexpectedError(error_msg.str());
-                      // LCOV_EXCL_STOP
-                    }
-                  }
-                } else if constexpr (is_tiny_matrix_v<DataType>) {
-                  for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-                       ++i_symmetry) {
-                    auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-                    auto ghost_cell_list = ghost_stencil[cell_j_id];
-                    if (ghost_cell_list.size() > 0) {
-                      throw NotImplementedError("TinyMatrix symmetries for reconstruction of DiscreteFunctionP0Vector");
-                    }
-                  }
-                }
-
-                if constexpr (std::is_arithmetic_v<DataType>) {
-                  column_begin += qj_vector.size();
-                } else if constexpr (is_tiny_vector_v<DataType> or is_tiny_matrix_v<DataType>) {
-                  column_begin += qj_vector.size() * DataType::Dimension;
-                }
-
-              } else {
-                // LCOV_EXCL_START
-                throw UnexpectedError("invalid discrete function type");
-                // LCOV_EXCL_STOP
-              }
-            },
-            discrete_function_variant->discreteFunction());
-        }
-
         ShrinkMatrixView A(A_pool[t], full_stencil_size(cell_j_id));
+        ShrinkMatrixView B(B_pool[t], full_stencil_size(cell_j_id));
 
-        if ((m_descriptor.degree() == 1) and
-            (m_descriptor.integrationMethodType() == IntegrationMethodType::cell_center)) {
-          const Rd& Xj = xj[cell_j_id];
-          size_t index = 0;
-          for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-            const CellId cell_i_id = stencil_cell_list[i];
-            const Rd Xi_Xj         = xj[cell_i_id] - Xj;
-            for (size_t l = 0; l < basis_dimension - 1; ++l) {
-              A(index, l) = Xi_Xj[l];
-            }
-          }
-          for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-               ++i_symmetry) {
-            auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-            auto ghost_cell_list = ghost_stencil[cell_j_id];
-
-            const Rd& origin = symmetry_origin_list[i_symmetry];
-            const Rd& normal = symmetry_normal_list[i_symmetry];
-
-            for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-              const CellId cell_i_id = ghost_cell_list[i];
-              const Rd Xi_Xj         = symmetrize_coordinates(origin, normal, xj[cell_i_id]) - Xj;
-              for (size_t l = 0; l < basis_dimension - 1; ++l) {
-                A(index, l) = Xi_Xj[l];
-              }
-            }
-          }
-
-        } else if ((m_descriptor.integrationMethodType() == IntegrationMethodType::element) or
-                   (m_descriptor.integrationMethodType() == IntegrationMethodType::boundary)) {
-          if ((m_descriptor.integrationMethodType() == IntegrationMethodType::boundary) and
-              (MeshType::Dimension == 2)) {
-            if constexpr (MeshType::Dimension == 2) {
-              SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek = inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool[t];
-              SmallArray<double>& mean_j_of_ejk                       = mean_j_of_ejk_pool[t];
-              SmallArray<double>& mean_i_of_ejk                       = mean_i_of_ejk_pool[t];
-
-              auto face_to_node_matrix   = p_mesh->connectivity().faceToNodeMatrix();
-              auto cell_face_is_reversed = p_mesh->connectivity().cellFaceIsReversed();
-              const Rd& Xj               = xj[cell_j_id];
-
-              _computeEjkMeanByBoundary(*p_mesh, Xj, cell_j_id, cell_to_face_matrix, face_to_node_matrix,
-                                        cell_face_is_reversed, Vj, m_descriptor.degree(), basis_dimension,
-                                        inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_j_of_ejk);
-
-              size_t index = 0;
-              for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-                const CellId cell_i_id = stencil_cell_list[i];
-
-                _computeEjkMeanByBoundary(*p_mesh, Xj, cell_i_id, cell_to_face_matrix, face_to_node_matrix,
-                                          cell_face_is_reversed, Vj, m_descriptor.degree(), basis_dimension,
-                                          inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_i_of_ejk);
-
-                for (size_t l = 0; l < basis_dimension - 1; ++l) {
-                  A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
-                }
-              }
-              for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-                   ++i_symmetry) {
-                auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-                auto ghost_cell_list = ghost_stencil[cell_j_id];
-
-                const Rd& origin = symmetry_origin_list[i_symmetry];
-                const Rd& normal = symmetry_normal_list[i_symmetry];
-
-                for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-                  const CellId cell_i_id = ghost_cell_list[i];
-
-                  _computeEjkMeanByBoundaryInSymmetricCell(*p_mesh, origin, normal,   //
-                                                           Xj, cell_i_id, xr, cell_to_face_matrix, face_to_node_matrix,
-                                                           cell_face_is_reversed, Vj, m_descriptor.degree(),
-                                                           basis_dimension, inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
-                                                           mean_i_of_ejk);
-
-                  for (size_t l = 0; l < basis_dimension - 1; ++l) {
-                    A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
-                  }
-                }
-              }
-            } else {
-              throw UnexpectedError("invalid mesh dimension");
-            }
-          } else {
-            SmallArray<double>& inv_Vj_wq_detJ_ek = inv_Vj_wq_detJ_ek_pool[t];
-            SmallArray<double>& mean_j_of_ejk     = mean_j_of_ejk_pool[t];
-            SmallArray<double>& mean_i_of_ejk     = mean_i_of_ejk_pool[t];
-
-            const Rd& Xj = xj[cell_j_id];
-
-            _computeEjkMean(Xj, xr, cell_to_node_matrix[cell_j_id], cell_type[cell_j_id], Vj[cell_j_id],
-                            m_descriptor.degree(), basis_dimension, inv_Vj_wq_detJ_ek, mean_j_of_ejk);
-
-            size_t index = 0;
-            for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-              const CellId cell_i_id = stencil_cell_list[i];
-
-              _computeEjkMean(Xj, xr, cell_to_node_matrix[cell_i_id], cell_type[cell_i_id], Vj[cell_i_id],
-                              m_descriptor.degree(), basis_dimension, inv_Vj_wq_detJ_ek, mean_i_of_ejk);
-
-              for (size_t l = 0; l < basis_dimension - 1; ++l) {
-                A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
-              }
-            }
-            for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-                 ++i_symmetry) {
-              auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-              auto ghost_cell_list = ghost_stencil[cell_j_id];
-
-              const Rd& origin = symmetry_origin_list[i_symmetry];
-              const Rd& normal = symmetry_normal_list[i_symmetry];
-
-              for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-                const CellId cell_i_id = ghost_cell_list[i];
-
-                _computeEjkMeanInSymmetricCell(origin, normal,   //
-                                               Xj, xr, cell_to_node_matrix[cell_i_id], cell_type[cell_i_id],
-                                               Vj[cell_i_id], m_descriptor.degree(), basis_dimension, inv_Vj_wq_detJ_ek,
-                                               mean_i_of_ejk);
+        Internal<MeshType>::buildB(cell_j_id, stencil_array, discrete_function_variant_list, symmetry_normal_list, B);
 
-                for (size_t l = 0; l < basis_dimension - 1; ++l) {
-                  A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
-                }
-              }
-            }
-          }
-        } else {
-          throw UnexpectedError("invalid integration strategy");
-        }
+        ReconstructionMatrixBuilderType& reconstruction_matrix_builder = *reconstruction_matrix_builder_pool[t];
+        reconstruction_matrix_builder.build(cell_j_id, A);
 
         if (m_descriptor.rowWeighting()) {
-          // Add row weighting (give more importance to cells that are
-          // closer to j)
-          const Rd& Xj = xj[cell_j_id];
-
-          size_t index = 0;
-          for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-            const CellId cell_i_id = stencil_cell_list[i];
-            const double weight    = 1. / l2Norm(xj[cell_i_id] - Xj);
-            for (size_t l = 0; l < basis_dimension - 1; ++l) {
-              A(index, l) *= weight;
-            }
-            for (size_t l = 0; l < number_of_columns; ++l) {
-              B(index, l) *= weight;
-            }
-          }
-          for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
-               ++i_symmetry) {
-            auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-            auto ghost_cell_list = ghost_stencil[cell_j_id];
-
-            const Rd& origin = symmetry_origin_list[i_symmetry];
-            const Rd& normal = symmetry_normal_list[i_symmetry];
-
-            for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-              const CellId cell_i_id = ghost_cell_list[i];
-              const double weight    = 1. / l2Norm(symmetrize_coordinates(origin, normal, xj[cell_i_id]) - Xj);
-              for (size_t l = 0; l < basis_dimension - 1; ++l) {
-                A(index, l) *= weight;
-              }
-              for (size_t l = 0; l < number_of_columns; ++l) {
-                B(index, l) *= weight;
-              }
-            }
-          }
+          Internal<MeshType>::rowWeighting(cell_j_id, stencil_array, xj, symmetry_origin_list, symmetry_normal_list, A,
+                                           B);
         }
 
         const SmallMatrix<double>& X = X_pool[t];
 
         if (m_descriptor.preconditioning()) {
-          // Add column  weighting preconditioning (increase the presition)
+          // Add column  weighting preconditioning (increase the precision)
           SmallVector<double>& G = G_pool[t];
 
-          for (size_t l = 0; l < A.numberOfColumns(); ++l) {
-            double g = 0;
-            for (size_t i = 0; i < A.numberOfRows(); ++i) {
-              const double Ail = A(i, l);
-
-              g += Ail * Ail;
-            }
-            G[l] = std::sqrt(g);
-          }
-
-          for (size_t l = 0; l < A.numberOfColumns(); ++l) {
-            const double Gl = G[l];
-            for (size_t i = 0; i < A.numberOfRows(); ++i) {
-              A(i, l) *= Gl;
-            }
-          }
-
-          Givens::solveCollectionInPlace(A, X, B);
-
-          for (size_t l = 0; l < X.numberOfRows(); ++l) {
-            const double Gl = G[l];
-            for (size_t i = 0; i < X.numberOfColumns(); ++i) {
-              X(l, i) *= Gl;
-            }
-          }
+          Internal<MeshType>::solveCollectionInPlaceWithPreconditionner(A, X, B, G);
         } else {
           Givens::solveCollectionInPlace(A, X, B);
         }
 
-        column_begin = 0;
-        for (size_t i_dpk_variant = 0; i_dpk_variant < mutable_discrete_function_dpk_variant_list.size();
-             ++i_dpk_variant) {
-          const auto& dpk_variant = mutable_discrete_function_dpk_variant_list[i_dpk_variant];
-
-          const auto& discrete_function_variant = discrete_function_variant_list[i_dpk_variant];
-
-          std::visit(
-            [&](auto&& dpk_function, auto&& p0_function) {
-              using DPkFunctionT = std::decay_t<decltype(dpk_function)>;
-              using P0FunctionT  = std::decay_t<decltype(p0_function)>;
-              using DataType     = std::remove_const_t<std::decay_t<typename DPkFunctionT::data_type>>;
-              using P0DataType   = std::remove_const_t<std::decay_t<typename P0FunctionT::data_type>>;
-
-              if constexpr (std::is_same_v<DataType, P0DataType>) {
-                if constexpr (is_discrete_function_P0_v<P0FunctionT>) {
-                  if constexpr (is_discrete_function_dpk_scalar_v<DPkFunctionT>) {
-                    auto dpk_j = dpk_function.coefficients(cell_j_id);
-                    dpk_j[0]   = p0_function[cell_j_id];
-
-                    if constexpr (std::is_arithmetic_v<DataType>) {
-                      if (m_descriptor.degree() > 1) {
-                        auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
-                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                          dpk_j[0] -= X(i, column_begin) * mean_j_of_ejk[i];
-                        }
-                      }
-
-                      for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                        auto& dpk_j_ip1 = dpk_j[i + 1];
-                        dpk_j_ip1       = X(i, column_begin);
-                      }
-                      ++column_begin;
-                    } else if constexpr (is_tiny_vector_v<DataType>) {
-                      if (m_descriptor.degree() > 1) {
-                        auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
-                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                          auto& dpk_j_0 = dpk_j[0];
-                          for (size_t k = 0; k < DataType::Dimension; ++k) {
-                            dpk_j_0[k] -= X(i, column_begin + k) * mean_j_of_ejk[i];
-                          }
-                        }
-                      }
-
-                      for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                        auto& dpk_j_ip1 = dpk_j[i + 1];
-                        for (size_t k = 0; k < DataType::Dimension; ++k) {
-                          dpk_j_ip1[k] = X(i, column_begin + k);
-                        }
-                      }
-                      column_begin += DataType::Dimension;
-                    } else if constexpr (is_tiny_matrix_v<DataType>) {
-                      if (m_descriptor.degree() > 1) {
-                        auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
-                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                          auto& dpk_j_0 = dpk_j[0];
-                          for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
-                            for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
-                              dpk_j_0(k, l) -=
-                                X(i, column_begin + k * DataType::NumberOfColumns + l) * mean_j_of_ejk[i];
-                            }
-                          }
-                        }
-                      }
-
-                      for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                        auto& dpk_j_ip1 = dpk_j[i + 1];
-                        for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
-                          for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
-                            dpk_j_ip1(k, l) = X(i, column_begin + k * DataType::NumberOfColumns + l);
-                          }
-                        }
-                      }
-                      column_begin += DataType::Dimension;
-                    } else {
-                      // LCOV_EXCL_START
-                      throw UnexpectedError("unexpected data type");
-                      // LCOV_EXCL_STOP
-                    }
-                  } else {
-                    // LCOV_EXCL_START
-                    throw UnexpectedError("unexpected discrete dpk function type");
-                    // LCOV_EXCL_STOP
-                  }
-                } else if constexpr (is_discrete_function_P0_vector_v<P0FunctionT>) {
-                  if constexpr (is_discrete_function_dpk_vector_v<DPkFunctionT>) {
-                    auto dpk_j        = dpk_function.coefficients(cell_j_id);
-                    auto cell_vector  = p0_function[cell_j_id];
-                    const size_t size = basis_dimension;
-
-                    for (size_t l = 0; l < cell_vector.size(); ++l) {
-                      const size_t component_begin = l * size;
-                      dpk_j[component_begin]       = cell_vector[l];
-                      if constexpr (std::is_arithmetic_v<DataType>) {
-                        if (m_descriptor.degree() > 1) {
-                          auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
-                          for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                            dpk_j[component_begin] -= X(i, column_begin) * mean_j_of_ejk[i];
-                          }
-                        }
-
-                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                          auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
-                          dpk_j_ip1       = X(i, column_begin);
-                        }
-                        ++column_begin;
-                      } else if constexpr (is_tiny_vector_v<DataType>) {
-                        if (m_descriptor.degree() > 1) {
-                          auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
-                          for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                            auto& dpk_j_0 = dpk_j[0];
-                            for (size_t k = 0; k < DataType::Dimension; ++k) {
-                              dpk_j_0[k] -= X(i, column_begin + k) * mean_j_of_ejk[i];
-                            }
-                          }
-                        }
-
-                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
-                          auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
-                          for (size_t k = 0; k < DataType::Dimension; ++k) {
-                            dpk_j_ip1[k] = X(i, column_begin + k);
-                          }
-                        }
-                        column_begin += DataType::Dimension;
-                      } else {
-                        // LCOV_EXCL_START
-                        throw UnexpectedError("unexpected data type");
-                        // LCOV_EXCL_STOP
-                      }
-                    }
-                  } else {
-                    // LCOV_EXCL_START
-                    throw UnexpectedError("unexpected discrete dpk function type");
-                    // LCOV_EXCL_STOP
-                  }
-                } else {
-                  // LCOV_EXCL_START
-                  throw UnexpectedError("unexpected discrete function type");
-                  // LCOV_EXCL_STOP
-                }
-              } else {
-                // LCOV_EXCL_START
-                throw UnexpectedError("incompatible data types");
-                // LCOV_EXCL_STOP
-              }
-            },
-            dpk_variant.mutableDiscreteFunctionDPk(), discrete_function_variant->discreteFunction());
-        }
+        Internal<MeshType>::template populateDiscreteFunctionDPkByCell(cell_j_id, m_descriptor.degree(), X,
+                                                                       reconstruction_matrix_builder,
+                                                                       discrete_function_variant_list,
+                                                                       mutable_discrete_function_dpk_variant_list);
 
         tokens.release(t);
       }
@@ -1677,6 +813,34 @@ PolynomialReconstruction::_build(
   return discrete_function_dpk_variant_list;
 }
 
+template <MeshConcept MeshType>
+[[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>>
+PolynomialReconstruction::_build(
+  const std::shared_ptr<const MeshType>& p_mesh,
+  const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
+{
+  switch (m_descriptor.integrationMethodType()) {
+  case IntegrationMethodType::cell_center: {
+    return this->_build<CellCenterReconstructionMatrixBuilder<MeshType>>(p_mesh, discrete_function_variant_list);
+  }
+  case IntegrationMethodType::boundary: {
+    if constexpr (MeshType::Dimension == 2) {
+      return this->_build<BoundaryIntegralReconstructionMatrixBuilder<MeshType>>(p_mesh,
+                                                                                 discrete_function_variant_list);
+    }
+    [[fallthrough]];
+  }
+  case IntegrationMethodType::element: {
+    return this->_build<ElementIntegralReconstructionMatrixBuilder<MeshType>>(p_mesh, discrete_function_variant_list);
+  }
+    // LCOV_EXCL_START
+  default: {
+    throw UnexpectedError("invalid reconstruction matrix builder type");
+  }
+    // LCOV_EXCL_STOP
+  }
+}
+
 std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>>
 PolynomialReconstruction::build(
   const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
diff --git a/src/scheme/PolynomialReconstruction.hpp b/src/scheme/PolynomialReconstruction.hpp
index bf6013639f732fd52f9c145ba4982701c0e4bcfc..4f76844711ada4641b69234c5ac787c472247a37 100644
--- a/src/scheme/PolynomialReconstruction.hpp
+++ b/src/scheme/PolynomialReconstruction.hpp
@@ -10,8 +10,20 @@ class DiscreteFunctionVariant;
 class PolynomialReconstruction
 {
  private:
+  template <MeshConcept MeshType>
+  class Internal;
+
   class MutableDiscreteFunctionDPkVariant;
 
+  template <MeshConcept MeshType>
+  class CellCenterReconstructionMatrixBuilder;
+
+  template <MeshConcept MeshType>
+  class ElementIntegralReconstructionMatrixBuilder;
+
+  template <MeshConcept MeshType>
+  class BoundaryIntegralReconstructionMatrixBuilder;
+
   const PolynomialReconstructionDescriptor m_descriptor;
 
   size_t _getNumberOfColumns(
@@ -26,6 +38,11 @@ class PolynomialReconstruction
     const std::shared_ptr<const MeshType>& p_mesh,
     const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
 
+  template <typename ReconstructionMatrixBuilderType, MeshConcept MeshType>
+  [[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>> _build(
+    const std::shared_ptr<const MeshType>& p_mesh,
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
+
   template <MeshConcept MeshType>
   [[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>> _build(
     const std::shared_ptr<const MeshType>& p_mesh,
diff --git a/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.cpp b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d030ac6d51d22d70eb209fa437a8684064d57529
--- /dev/null
+++ b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.cpp
@@ -0,0 +1,365 @@
+#include <scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp>
+
+#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <geometry/LineCubicTransformation.hpp>
+#include <geometry/LineParabolicTransformation.hpp>
+#include <geometry/SymmetryUtils.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <mesh/PolynomialMesh.hpp>
+#include <scheme/DiscreteFunctionDPk.hpp>
+
+template <MeshConcept MeshTypeT>
+template <typename ConformTransformationT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkBoundaryMean(
+  const QuadratureFormula<MeshType::Dimension - 1>& quadrature,
+  const ConformTransformationT& T,
+  const Rd& Xj,
+  const double inv_Vi,
+  SmallArray<double>& mean_of_ejk)
+{
+  if constexpr (std::is_same_v<LineTransformation<MeshType::Dimension>, ConformTransformationT>) {
+    const double velocity_perp_e1 = T.velocity()[1] * inv_Vi;
+
+    for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+      const double wq          = quadrature.weight(i_q);
+      const TinyVector<1> xi_q = quadrature.point(i_q);
+
+      const Rd X_Xj = T(xi_q) - Xj;
+
+      const double x_xj = X_Xj[0];
+      const double y_yj = X_Xj[1];
+
+      {
+        size_t k                                   = 0;
+        m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = x_xj * wq * velocity_perp_e1;
+        for (; k <= m_polynomial_degree; ++k) {
+          m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k] =
+            x_xj * m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k - 1] * m_antiderivative_coef[k];
+        }
+
+        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
+          const size_t begin_i_y_1 = ((i_y - 1) * (2 * m_polynomial_degree - i_y + 4)) / 2;
+          for (size_t l = 0; l <= m_polynomial_degree - i_y; ++l) {
+            m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = y_yj * m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[begin_i_y_1 + l];
+          }
+        }
+      }
+
+      for (size_t k = 1; k < m_basis_dimension; ++k) {
+        mean_of_ejk[k - 1] += m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k];
+      }
+    }
+  } else {
+    for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+      const double wq          = quadrature.weight(i_q);
+      const TinyVector<1> xi_q = quadrature.point(i_q);
+
+      const double velocity_perp_e1 = T.velocity(xi_q)[1] * inv_Vi;
+
+      const Rd X_Xj = T(xi_q) - Xj;
+
+      const double x_xj = X_Xj[0];
+      const double y_yj = X_Xj[1];
+
+      {
+        size_t k                                   = 0;
+        m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = x_xj * wq * velocity_perp_e1;
+        for (; k <= m_polynomial_degree; ++k) {
+          m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k] =
+            x_xj * m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k - 1] * m_antiderivative_coef[k];
+        }
+
+        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
+          const size_t begin_i_y_1 = ((i_y - 1) * (2 * m_polynomial_degree - i_y + 4)) / 2;
+          for (size_t l = 0; l <= m_polynomial_degree - i_y; ++l) {
+            m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = y_yj * m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[begin_i_y_1 + l];
+          }
+        }
+      }
+
+      for (size_t k = 1; k < m_basis_dimension; ++k) {
+        mean_of_ejk[k - 1] += m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k];
+      }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMeanByBoundary(
+  const Rd& Xj,
+  const CellId& cell_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  if constexpr (is_polygonal_mesh_v<MeshType>) {
+    const auto& quadrature =
+      QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(m_polynomial_degree + 1));
+
+    const double inv_Vi = 1. / m_Vj[cell_id];
+
+    mean_of_ejk.fill(0);
+    auto cell_face_list = m_cell_to_face_matrix[cell_id];
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      bool is_reversed = m_cell_face_is_reversed[cell_id][i_face];
+
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = m_face_to_node_matrix[face_id];
+      if (is_reversed) {
+        const LineTransformation<2> T{m_xr[face_node_list[1]], m_xr[face_node_list[0]]};
+        _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+      } else {
+        const LineTransformation<2> T{m_xr[face_node_list[0]], m_xr[face_node_list[1]]};
+        _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+      }
+    }
+  } else {
+    static_assert(is_polynomial_mesh_v<MeshType>);
+    // (degree + 1): divergence theorem
+    // mesh.degree(): transformation polynomial degree
+    // (mesh.degree() - 1): degree of the line abscissa velocity
+    const auto& quadrature = QuadratureManager::instance().getLineFormula(
+      GaussLegendreQuadratureDescriptor((m_polynomial_degree + 1) * m_mesh.degree() + (m_mesh.degree() - 1)));
+
+    const double inv_Vi = 1. / m_Vj[cell_id];
+
+    auto xr = m_mesh.xr();
+    auto xl = m_mesh.xl();
+    mean_of_ejk.fill(0);
+    auto cell_face_list = m_cell_to_face_matrix[cell_id];
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      bool is_reversed = m_cell_face_is_reversed[cell_id][i_face];
+
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = m_face_to_node_matrix[face_id];
+#warning rework
+      switch (m_mesh.degree()) {
+      case 2: {
+        if (is_reversed) {
+          const LineParabolicTransformation<2> T{xr[face_node_list[1]], xl[face_id][0], xr[face_node_list[0]]};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const LineParabolicTransformation<2> T{xr[face_node_list[0]], xl[face_id][0], xr[face_node_list[1]]};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      case 3: {
+        if (is_reversed) {
+          const LineCubicTransformation<2> T{xr[face_node_list[1]], xl[face_id][1], xl[face_id][0],
+                                             xr[face_node_list[0]]};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const LineCubicTransformation<2> T{xr[face_node_list[0]], xl[face_id][0], xl[face_id][1],
+                                             xr[face_node_list[1]]};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      default: {
+        std::ostringstream error_msg;
+        error_msg << "reconstruction on meshes of degree " << m_mesh.degree();
+        throw NotImplementedError(error_msg.str());
+      }
+      }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  MeshTypeT>::_computeEjkMeanByBoundaryInSymmetricCell(const Rd& origin,
+                                                       const Rd& normal,
+                                                       const Rd& Xj,
+                                                       const CellId& cell_id,
+                                                       SmallArray<double>& mean_of_ejk)
+{
+  if constexpr (is_polygonal_mesh_v<MeshType>) {
+    const auto& quadrature =
+      QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(m_polynomial_degree + 1));
+
+    const double inv_Vi = 1. / m_Vj[cell_id];
+
+    mean_of_ejk.fill(0);
+    auto cell_face_list = m_cell_to_face_matrix[cell_id];
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      bool is_reversed = m_cell_face_is_reversed[cell_id][i_face];
+
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = m_face_to_node_matrix[face_id];
+
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[0]]);
+
+      if (is_reversed) {
+        const LineTransformation<2> T{x1, x0};
+        _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+      } else {
+        const LineTransformation<2> T{x0, x1};
+        _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+      }
+    }
+  } else {
+    static_assert(is_polynomial_mesh_v<MeshType>);
+    // (degree + 1): divergence theorem
+    // mesh.degree(): transformation polynomial degree
+    // (mesh.degree() - 1): degree of the line abscissa velocity
+    const auto& quadrature = QuadratureManager::instance().getLineFormula(
+      GaussLegendreQuadratureDescriptor((m_polynomial_degree + 1) * m_mesh.degree() + (m_mesh.degree() - 1)));
+
+    const double inv_Vi = 1. / m_Vj[cell_id];
+
+    auto xl = m_mesh.xl();
+
+    mean_of_ejk.fill(0);
+    auto cell_face_list = m_cell_to_face_matrix[cell_id];
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      bool is_reversed = m_cell_face_is_reversed[cell_id][i_face];
+
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = m_face_to_node_matrix[face_id];
+
+      switch (m_mesh.degree()) {
+      case 2: {
+        const auto x0 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[1]]);
+        const auto x1 = symmetrize_coordinates(origin, normal, xl[face_id][0]);
+        const auto x2 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[0]]);
+
+        if (is_reversed) {
+          const LineParabolicTransformation<2> T{x2, x1, x0};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const LineParabolicTransformation<2> T{x0, x1, x2};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      case 3: {
+        const auto x0 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[1]]);
+        const auto x1 = symmetrize_coordinates(origin, normal, xl[face_id][1]);
+        const auto x2 = symmetrize_coordinates(origin, normal, xl[face_id][0]);
+        const auto x3 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[0]]);
+
+        if (is_reversed) {
+          const LineCubicTransformation<2> T{x3, x2, x1, x0};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const LineCubicTransformation<2> T{x0, x1, x2, x3};
+          _computeEjkBoundaryMean(quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      default: {
+        std::ostringstream error_msg;
+        error_msg << "reconstruction on meshes of degree " << m_mesh.degree();
+        throw NotImplementedError(error_msg.str());
+      }
+      }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<MeshTypeT>::build(
+  const CellId cell_j_id,
+  ShrinkMatrixView<SmallMatrix<double>>& A)
+{
+  if constexpr (MeshType::Dimension == 2) {
+    const auto& stencil_cell_list = m_stencil_array[cell_j_id];
+
+    const Rd& Xj = m_xj[cell_j_id];
+
+    _computeEjkMeanByBoundary(Xj, cell_j_id, m_mean_j_of_ejk);
+
+    size_t index = 0;
+    for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+      const CellId cell_i_id = stencil_cell_list[i];
+
+      _computeEjkMeanByBoundary(Xj, cell_i_id, m_mean_i_of_ejk);
+
+      for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+        A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
+      }
+    }
+    for (size_t i_symmetry = 0; i_symmetry < m_stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry) {
+      auto& ghost_stencil  = m_stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+      auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+      const Rd& origin = m_symmetry_origin_list[i_symmetry];
+      const Rd& normal = m_symmetry_normal_list[i_symmetry];
+
+      for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+        const CellId cell_i_id = ghost_cell_list[i];
+
+        _computeEjkMeanByBoundaryInSymmetricCell(origin, normal, Xj, cell_i_id, m_mean_i_of_ejk);
+
+        for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+          A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
+        }
+      }
+    }
+  } else {
+    throw NotImplementedError("invalid mesh dimension");
+  }
+}
+
+template <MeshConcept MeshTypeT>
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  MeshTypeT>::BoundaryIntegralReconstructionMatrixBuilder(const MeshType& mesh,
+                                                          const size_t polynomial_degree,
+                                                          const SmallArray<const Rd>& symmetry_origin_list,
+                                                          const SmallArray<const Rd>& symmetry_normal_list,
+                                                          const CellToCellStencilArray& stencil_array)
+  : m_mesh{mesh},
+    m_basis_dimension{
+      DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(polynomial_degree)},
+    m_polynomial_degree{polynomial_degree},
+    m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek{m_basis_dimension},
+    m_mean_j_of_ejk{m_basis_dimension - 1},
+    m_mean_i_of_ejk{m_basis_dimension - 1},
+    m_cell_to_face_matrix{mesh.connectivity().cellToFaceMatrix()},
+    m_face_to_node_matrix{mesh.connectivity().faceToNodeMatrix()},
+    m_cell_face_is_reversed{mesh.connectivity().cellFaceIsReversed()},
+    m_stencil_array{stencil_array},
+    m_symmetry_origin_list{symmetry_origin_list},
+    m_symmetry_normal_list{symmetry_normal_list},
+    m_Vj{MeshDataManager::instance().getMeshData(mesh).Vj()},
+    m_xj{MeshDataManager::instance().getMeshData(mesh).xj()},
+    m_xr{mesh.xr()}
+{
+  if constexpr (MeshType::Dimension == 2) {
+    SmallArray<double> antiderivative_coef(m_polynomial_degree + 1);
+    for (size_t k = 0; k < antiderivative_coef.size(); ++k) {
+      antiderivative_coef[k] = ((1. * k) / (k + 1));
+    }
+
+    m_antiderivative_coef = antiderivative_coef;
+  }
+}
+
+template void PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<PolynomialMesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  Mesh<2>>::BoundaryIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                        const size_t,
+                                                        const SmallArray<const Rd>&,
+                                                        const SmallArray<const Rd>&,
+                                                        const CellToCellStencilArray&);
+
+template PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  PolynomialMesh<2>>::BoundaryIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                                  const size_t,
+                                                                  const SmallArray<const Rd>&,
+                                                                  const SmallArray<const Rd>&,
+                                                                  const CellToCellStencilArray&);
diff --git a/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..df24d76a73097d13f6379681780d90aed6f49e1d
--- /dev/null
+++ b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp
@@ -0,0 +1,83 @@
+#ifndef BOUNDARY_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+#define BOUNDARY_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+
+#include <algebra/ShrinkMatrixView.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <analysis/QuadratureFormula.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/StencilArray.hpp>
+#include <mesh/SubItemValuePerItem.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+#include <utils/SmallArray.hpp>
+
+template <MeshConcept MeshTypeT>
+class PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder
+{
+ public:
+  using MeshType = MeshTypeT;
+
+  constexpr static bool handles_high_degrees = true;
+
+ private:
+  using Rd = TinyVector<MeshType::Dimension>;
+
+  const MeshType& m_mesh;
+  const size_t m_basis_dimension;
+  const size_t m_polynomial_degree;
+
+  const SmallArray<double> m_inv_Vj_alpha_p_1_wq_X_prime_orth_ek;
+  SmallArray<double> m_mean_j_of_ejk;
+  SmallArray<double> m_mean_i_of_ejk;
+
+  const ItemToItemMatrix<ItemType::cell, ItemType::face> m_cell_to_face_matrix;
+  const ItemToItemMatrix<ItemType::face, ItemType::node> m_face_to_node_matrix;
+  const FaceValuePerCell<const bool> m_cell_face_is_reversed;
+
+  const CellToCellStencilArray& m_stencil_array;
+
+  const SmallArray<const Rd> m_symmetry_origin_list;
+  const SmallArray<const Rd> m_symmetry_normal_list;
+
+  const CellValue<const double> m_Vj;
+  const CellValue<const Rd> m_xj;
+  const NodeValue<const Rd> m_xr;
+
+  SmallArray<const double> m_antiderivative_coef;
+
+  template <typename ConformTransformationT>
+  void _computeEjkBoundaryMean(const QuadratureFormula<MeshType::Dimension - 1>& quadrature,
+                               const ConformTransformationT& T,
+                               const Rd& Xj,
+                               const double inv_Vi,
+                               SmallArray<double>& mean_of_ejk);
+
+  void _computeEjkMeanByBoundary(const Rd& Xj, const CellId& cell_id, SmallArray<double>& mean_of_ejk);
+
+  void _computeEjkMeanByBoundaryInSymmetricCell(const Rd& origin,
+                                                const Rd& normal,
+                                                const Rd& Xj,
+                                                const CellId& cell_id,
+                                                SmallArray<double>& mean_of_ejk);
+
+ public:
+  PUGS_INLINE
+  SmallArray<const double>
+  meanjOfEjk() const
+  {
+    return m_mean_j_of_ejk;
+  }
+
+  void build(const CellId cell_j_id, ShrinkMatrixView<SmallMatrix<double>>& A);
+
+  BoundaryIntegralReconstructionMatrixBuilder(const MeshType& mesh,
+                                              const size_t polynomial_degree,
+                                              const SmallArray<const Rd>& symmetry_origin_list,
+                                              const SmallArray<const Rd>& symmetry_normal_list,
+                                              const CellToCellStencilArray& stencil_array);
+
+  BoundaryIntegralReconstructionMatrixBuilder()  = default;
+  ~BoundaryIntegralReconstructionMatrixBuilder() = default;
+};
+
+#endif   // BOUNDARY_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
diff --git a/src/scheme/reconstruction_utils/CMakeLists.txt b/src/scheme/reconstruction_utils/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1d4ebcb48c3d47c9c19421151257deb8a31bf9a9
--- /dev/null
+++ b/src/scheme/reconstruction_utils/CMakeLists.txt
@@ -0,0 +1,17 @@
+# ------------------- Source files --------------------
+
+add_library(PugsSchemeReconstructionUtils
+  BoundaryIntegralReconstructionMatrixBuilder.cpp
+  CellCenterReconstructionMatrixBuilder.cpp
+  ElementIntegralReconstructionMatrixBuilder.cpp
+)
+
+add_dependencies(
+  PugsUtils
+  PugsMesh
+)
+
+target_link_libraries(
+  PugsSchemeReconstructionUtils
+  ${HIGHFIVE_TARGET}
+)
diff --git a/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.cpp b/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ca06fdca5e5f5366879776c8630a74f17fe994cf
--- /dev/null
+++ b/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.cpp
@@ -0,0 +1,104 @@
+#include <scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.hpp>
+
+#include <geometry/SymmetryUtils.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/PolynomialMesh.hpp>
+#include <scheme/DiscreteFunctionDPk.hpp>
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<MeshTypeT>::build(
+  const CellId cell_j_id,
+  ShrinkMatrixView<SmallMatrix<double>>& A)
+{
+  const auto& stencil_cell_list = m_stencil_array[cell_j_id];
+
+  const Rd& Xj = m_xj[cell_j_id];
+  size_t index = 0;
+  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+    const CellId cell_i_id = stencil_cell_list[i];
+    const Rd Xi_Xj         = m_xj[cell_i_id] - Xj;
+    for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+      A(index, l) = Xi_Xj[l];
+    }
+  }
+  for (size_t i_symmetry = 0; i_symmetry < m_stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry) {
+    auto& ghost_stencil  = m_stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+    auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+    const Rd& origin = m_symmetry_origin_list[i_symmetry];
+    const Rd& normal = m_symmetry_normal_list[i_symmetry];
+
+    for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+      const CellId cell_i_id = ghost_cell_list[i];
+      const Rd Xi_Xj         = symmetrize_coordinates(origin, normal, m_xj[cell_i_id]) - Xj;
+      for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+        A(index, l) = Xi_Xj[l];
+      }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<MeshTypeT>::CellCenterReconstructionMatrixBuilder(
+  const MeshType& mesh,
+  const size_t polynomial_degree,
+  const SmallArray<const Rd>& symmetry_origin_list,
+  const SmallArray<const Rd>& symmetry_normal_list,
+  const CellToCellStencilArray& stencil_array)
+  : m_basis_dimension{DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(
+      polynomial_degree)},
+    m_symmetry_origin_list{symmetry_origin_list},
+    m_symmetry_normal_list{symmetry_normal_list},
+    m_stencil_array{stencil_array},
+    m_xj{MeshDataManager::instance().getMeshData(mesh).xj()}
+{
+  if (polynomial_degree != 1) {
+    throw NormalError("cell center based reconstruction is only valid for first order");
+  }
+}
+
+template void PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<Mesh<1>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<Mesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<Mesh<3>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<PolynomialMesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<
+  Mesh<1>>::CellCenterReconstructionMatrixBuilder(const MeshType&,
+                                                  const size_t,
+                                                  const SmallArray<const Rd>&,
+                                                  const SmallArray<const Rd>&,
+                                                  const CellToCellStencilArray&);
+
+template PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<
+  Mesh<2>>::CellCenterReconstructionMatrixBuilder(const MeshType&,
+                                                  const size_t,
+                                                  const SmallArray<const Rd>&,
+                                                  const SmallArray<const Rd>&,
+                                                  const CellToCellStencilArray&);
+
+template PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<
+  Mesh<3>>::CellCenterReconstructionMatrixBuilder(const MeshType&,
+                                                  const size_t,
+                                                  const SmallArray<const Rd>&,
+                                                  const SmallArray<const Rd>&,
+                                                  const CellToCellStencilArray&);
+
+template PolynomialReconstruction::CellCenterReconstructionMatrixBuilder<
+  PolynomialMesh<2>>::CellCenterReconstructionMatrixBuilder(const MeshType&,
+                                                            const size_t,
+                                                            const SmallArray<const Rd>&,
+                                                            const SmallArray<const Rd>&,
+                                                            const CellToCellStencilArray&);
diff --git a/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.hpp b/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a8205a625dc0d62e35a33469fe2864719f8147a6
--- /dev/null
+++ b/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.hpp
@@ -0,0 +1,41 @@
+#ifndef CELL_CENTER_RECONSTRUCTION_MATRIX_BUILDER_HPP
+#define CELL_CENTER_RECONSTRUCTION_MATRIX_BUILDER_HPP
+
+#include <algebra/ShrinkMatrixView.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/StencilArray.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+#include <utils/SmallArray.hpp>
+
+template <MeshConcept MeshTypeT>
+class PolynomialReconstruction::CellCenterReconstructionMatrixBuilder
+{
+ public:
+  using MeshType = MeshTypeT;
+
+  constexpr static bool handles_high_degrees = false;
+
+ private:
+  using Rd = TinyVector<MeshType::Dimension>;
+
+  const size_t m_basis_dimension;
+  const SmallArray<const Rd> m_symmetry_origin_list;
+  const SmallArray<const Rd> m_symmetry_normal_list;
+
+  const CellToCellStencilArray& m_stencil_array;
+  const CellValue<const Rd> m_xj;
+
+ public:
+  void build(const CellId cell_j_id, ShrinkMatrixView<SmallMatrix<double>>& A);
+
+  CellCenterReconstructionMatrixBuilder(const MeshType& mesh,
+                                        const size_t polynomial_degree,
+                                        const SmallArray<const Rd>& symmetry_origin_list,
+                                        const SmallArray<const Rd>& symmetry_normal_list,
+                                        const CellToCellStencilArray& stencil_array);
+
+  ~CellCenterReconstructionMatrixBuilder() = default;
+};
+
+#endif   // CELL_CENTER_RECONSTRUCTION_MATRIX_BUILDER_HPP
diff --git a/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..198dba9afe1faf1c324a8c03ae68376dc27ac799
--- /dev/null
+++ b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp
@@ -0,0 +1,523 @@
+#include <scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp>
+
+#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
+#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <geometry/CubeTransformation.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <geometry/PrismTransformation.hpp>
+#include <geometry/PyramidTransformation.hpp>
+#include <geometry/SquareTransformation.hpp>
+#include <geometry/SymmetryUtils.hpp>
+#include <geometry/TetrahedronTransformation.hpp>
+#include <geometry/TriangleTransformation.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <mesh/PolynomialMesh.hpp>
+#include <scheme/DiscreteFunctionDPk.hpp>
+
+template <MeshConcept MeshTypeT>
+template <typename ConformTransformationT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMean(
+  const QuadratureFormula<MeshType::Dimension>& quadrature,
+  const ConformTransformationT& T,
+  const Rd& Xj,
+  const double Vi,
+  SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT)
+{
+  mean_of_ejk.fill(0);
+
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq = quadrature.weight(i_q);
+    const Rd& xi_q  = quadrature.point(i_q);
+
+    const Rd X_Xj = T(xi_q) - Xj;
+
+    if constexpr (MeshType::Dimension == 1) {
+      const double detT = T.jacobianDeterminant();
+
+      const double x_xj = X_Xj[0];
+
+      {
+        m_wq_detJ_ek[0] = wq * detT;
+        for (size_t k = 1; k <= m_polynomial_degree; ++k) {
+          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
+        }
+      }
+
+    } else if constexpr (MeshType::Dimension == 2) {
+      const double detT = [&] {
+        if constexpr (std::is_same_v<TriangleTransformation<2>, std::decay_t<decltype(T)>>) {
+          return T.jacobianDeterminant();
+        } else {
+          return T.jacobianDeterminant(xi_q);
+        }
+      }();
+
+      const double x_xj = X_Xj[0];
+      const double y_yj = X_Xj[1];
+
+      {
+        size_t k          = 0;
+        m_wq_detJ_ek[k++] = wq * detT;
+        for (; k <= m_polynomial_degree; ++k) {
+          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
+        }
+
+        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
+          const size_t begin_i_y_1 = m_y_row_index[i_y - 1];
+          for (size_t l = 0; l <= m_polynomial_degree - i_y; ++l, ++k) {
+            m_wq_detJ_ek[k] = y_yj * m_wq_detJ_ek[begin_i_y_1 + l];
+          }
+        }
+      }
+
+    } else if constexpr (MeshType::Dimension == 3) {
+      static_assert(MeshType::Dimension == 3);
+
+      const double detT = [&] {
+        if constexpr (std::is_same_v<TetrahedronTransformation, std::decay_t<decltype(T)>>) {
+          return T.jacobianDeterminant();
+        } else {
+          return T.jacobianDeterminant(xi_q);
+        }
+      }();
+
+      const double x_xj = X_Xj[0];
+      const double y_yj = X_Xj[1];
+      const double z_zj = X_Xj[2];
+
+      {
+        size_t k          = 0;
+        m_wq_detJ_ek[k++] = wq * detT;
+        for (; k <= m_polynomial_degree; ++k) {
+          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
+        }
+
+        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
+          const size_t begin_i_y_1 = m_yz_row_index[i_y - 1];
+          const size_t nb_monoms   = m_yz_row_size[i_y];
+          for (size_t l = 0; l < nb_monoms; ++l, ++k) {
+            m_wq_detJ_ek[k] = y_yj * m_wq_detJ_ek[begin_i_y_1 + l];
+          }
+        }
+
+        for (size_t i_z = 1; i_z <= m_polynomial_degree; ++i_z) {
+          const size_t nb_y      = m_yz_row_size[m_z_triangle_index[i_z]];
+          const size_t index_z   = m_z_triangle_index[i_z];
+          const size_t index_z_1 = m_z_triangle_index[i_z - 1];
+          for (size_t i_y = 0; i_y < nb_y; ++i_y) {
+            const size_t begin_i_yz_1 = m_yz_row_index[index_z_1 + i_y];
+            const size_t nb_monoms    = m_yz_row_size[index_z + i_y];
+            for (size_t l = 0; l < nb_monoms; ++l, ++k) {
+              m_wq_detJ_ek[k] = z_zj * m_wq_detJ_ek[begin_i_yz_1 + l];
+            }
+          }
+        }
+      }
+    }
+
+    for (size_t k = 1; k < m_basis_dimension; ++k) {
+      mean_of_ejk[k - 1] += m_wq_detJ_ek[k];
+    }
+  }
+
+  const double inv_Vi = 1. / Vi;
+  for (size_t k = 0; k < mean_of_ejk.size(); ++k) {
+    mean_of_ejk[k] *= inv_Vi;
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMean(
+  const Rd& Xj,
+  const CellId& cell_i_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  const CellType cell_type = m_cell_type[cell_i_id];
+  const auto node_list     = m_cell_to_node_matrix[cell_i_id];
+  const double Vi          = m_Vj[cell_i_id];
+
+  if constexpr (MeshType::Dimension == 1) {
+    if (m_cell_type[cell_i_id] == CellType::Line) {
+      const LineTransformation<1> T{m_xr[node_list[0]], m_xr[node_list[1]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+    } else {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+  } else if constexpr (MeshType::Dimension == 2) {
+    switch (cell_type) {
+    case CellType::Triangle: {
+      const TriangleTransformation<2> T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]]};
+      const auto& quadrature =
+        QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Quadrangle: {
+      const SquareTransformation<2> T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]]};
+      const auto& quadrature =
+        QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  } else {
+    static_assert(MeshType::Dimension == 3);
+
+    switch (cell_type) {
+    case CellType::Tetrahedron: {
+      const TetrahedronTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+      break;
+    }
+    case CellType::Prism: {
+      const PrismTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]],   //
+                                  m_xr[node_list[3]], m_xr[node_list[4]], m_xr[node_list[5]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+      break;
+    }
+    case CellType::Pyramid: {
+      const PyramidTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]],
+                                    m_xr[node_list[4]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Hexahedron: {
+      const CubeTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]],
+                                 m_xr[node_list[4]], m_xr[node_list[5]], m_xr[node_list[6]], m_xr[node_list[7]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMeanInSymmetricCell(
+  const Rd& origin,
+  const Rd& normal,
+  const Rd& Xj,
+  const CellId& cell_i_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  if constexpr (MeshType::Dimension == 1) {
+    auto node_list           = m_cell_to_node_matrix[cell_i_id];
+    const CellType cell_type = m_cell_type[cell_i_id];
+    const double Vi          = m_Vj[cell_i_id];
+
+    if (cell_type == CellType::Line) {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+
+      const LineTransformation<1> T{x0, x1};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+    } else {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+  } else if constexpr (MeshType::Dimension == 2) {
+    auto node_list           = m_cell_to_node_matrix[cell_i_id];
+    const CellType cell_type = m_cell_type[cell_i_id];
+    const double Vi          = m_Vj[cell_i_id];
+
+    switch (cell_type) {
+    case CellType::Triangle: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+
+      const TriangleTransformation<2> T{x0, x1, x2};
+      const auto& quadrature =
+        QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Quadrangle: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+
+      const SquareTransformation<2> T{x0, x1, x2, x3};
+      const auto& quadrature =
+        QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  } else {
+    static_assert(MeshType::Dimension == 3);
+    auto node_list           = m_cell_to_node_matrix[cell_i_id];
+    const CellType cell_type = m_cell_type[cell_i_id];
+    const double Vi          = m_Vj[cell_i_id];
+    switch (cell_type) {
+    case CellType::Tetrahedron: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+
+      const TetrahedronTransformation T{x0, x1, x2, x3};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Prism: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
+      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x5 = symmetrize_coordinates(origin, normal, m_xr[node_list[5]]);
+
+      const PrismTransformation T{x0, x1, x2,   //
+                                  x3, x4, x5};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Pyramid: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
+      const PyramidTransformation T{x0, x1, x2, x3, x4};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Hexahedron: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[7]]);
+      const auto x5 = symmetrize_coordinates(origin, normal, m_xr[node_list[6]]);
+      const auto x6 = symmetrize_coordinates(origin, normal, m_xr[node_list[5]]);
+      const auto x7 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
+
+      const CubeTransformation T{x0, x1, x2, x3, x4, x5, x6, x7};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::build(
+  const CellId cell_j_id,
+  ShrinkMatrixView<SmallMatrix<double>>& A)
+{
+  const auto& stencil_cell_list = m_stencil_array[cell_j_id];
+
+  const Rd& Xj = m_xj[cell_j_id];
+
+  this->_computeEjkMean(Xj, cell_j_id, m_mean_j_of_ejk);
+
+  size_t index = 0;
+  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+    const CellId cell_i_id = stencil_cell_list[i];
+
+    this->_computeEjkMean(Xj, cell_i_id, m_mean_i_of_ejk);
+
+    for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+      A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
+    }
+  }
+
+  for (size_t i_symmetry = 0; i_symmetry < m_stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry) {
+    auto& ghost_stencil  = m_stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+    auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+    const Rd& origin = m_symmetry_origin_list[i_symmetry];
+    const Rd& normal = m_symmetry_normal_list[i_symmetry];
+
+    for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+      const CellId cell_i_id = ghost_cell_list[i];
+
+      this->_computeEjkMeanInSymmetricCell(origin, normal, Xj, cell_i_id, m_mean_i_of_ejk);
+
+      for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+        A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
+      }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  MeshTypeT>::ElementIntegralReconstructionMatrixBuilder(const MeshType& mesh,
+                                                         const size_t polynomial_degree,
+                                                         const SmallArray<const Rd>& symmetry_origin_list,
+                                                         const SmallArray<const Rd>& symmetry_normal_list,
+                                                         const CellToCellStencilArray& stencil_array)
+  : m_basis_dimension{DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(
+      polynomial_degree)},
+    m_polynomial_degree{polynomial_degree},
+
+    m_wq_detJ_ek{m_basis_dimension},
+    m_mean_j_of_ejk{m_basis_dimension - 1},
+    m_mean_i_of_ejk{m_basis_dimension - 1},
+
+    m_cell_to_node_matrix{mesh.connectivity().cellToNodeMatrix()},
+    m_stencil_array{stencil_array},
+    m_symmetry_origin_list{symmetry_origin_list},
+    m_symmetry_normal_list{symmetry_normal_list},
+    m_cell_type{mesh.connectivity().cellType()},
+    m_Vj{MeshDataManager::instance().getMeshData(mesh).Vj()},
+    m_xj{MeshDataManager::instance().getMeshData(mesh).xj()},
+    m_xr{mesh.xr()}
+{
+  if constexpr (is_polynomial_mesh_v<MeshType>) {
+    if (polynomial_degree != 1) {
+      throw NotImplementedError("cannot reconstruct polynomials of degree != 1 on polynomial meshes");
+    }
+  }
+
+  if constexpr (MeshType::Dimension == 2) {
+    SmallArray<size_t> y_row_index(m_polynomial_degree + 1);
+
+    size_t i_y = 0;
+
+    y_row_index[i_y++] = 0;
+    for (ssize_t n = m_polynomial_degree + 1; n > 1; --n, ++i_y) {
+      y_row_index[i_y] = y_row_index[i_y - 1] + n;
+    }
+
+    m_y_row_index = y_row_index;
+
+  } else if constexpr (MeshType::Dimension == 3) {
+    SmallArray<size_t> yz_row_index((m_polynomial_degree + 2) * (m_polynomial_degree + 1) / 2 + 1);
+    SmallArray<size_t> z_triangle_index(m_polynomial_degree + 1);
+
+    {
+      size_t i_z  = 0;
+      size_t i_yz = 0;
+
+      yz_row_index[i_yz++] = 0;
+      for (ssize_t n = m_polynomial_degree + 1; n >= 1; --n) {
+        z_triangle_index[i_z++] = i_yz - 1;
+        for (ssize_t i = n; i >= 1; --i) {
+          yz_row_index[i_yz] = yz_row_index[i_yz - 1] + i;
+          ++i_yz;
+        }
+      }
+    }
+
+    SmallArray<size_t> yz_row_size{yz_row_index.size() - 1};
+    for (size_t i = 0; i < yz_row_size.size(); ++i) {
+      yz_row_size[i] = yz_row_index[i + 1] - yz_row_index[i];
+    }
+
+    m_yz_row_index     = yz_row_index;
+    m_z_triangle_index = z_triangle_index;
+    m_yz_row_size      = yz_row_size;
+  }
+}
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<Mesh<1>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<Mesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<Mesh<3>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<PolynomialMesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  Mesh<1>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                       const size_t,
+                                                       const SmallArray<const Rd>&,
+                                                       const SmallArray<const Rd>&,
+                                                       const CellToCellStencilArray&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  Mesh<2>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                       const size_t,
+                                                       const SmallArray<const Rd>&,
+                                                       const SmallArray<const Rd>&,
+                                                       const CellToCellStencilArray&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  Mesh<3>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                       const size_t,
+                                                       const SmallArray<const Rd>&,
+                                                       const SmallArray<const Rd>&,
+                                                       const CellToCellStencilArray&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  PolynomialMesh<2>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                                 const size_t,
+                                                                 const SmallArray<const Rd>&,
+                                                                 const SmallArray<const Rd>&,
+                                                                 const CellToCellStencilArray&);
diff --git a/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1f4573438260e321dbd1461d2566f854236b25f4
--- /dev/null
+++ b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
@@ -0,0 +1,87 @@
+#ifndef ELEMENT_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+#define ELEMENT_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+
+#include <algebra/ShrinkMatrixView.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <analysis/QuadratureFormula.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <mesh/CellType.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/StencilArray.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+#include <utils/SmallArray.hpp>
+
+template <MeshConcept MeshTypeT>
+class PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder
+{
+ public:
+  using MeshType = MeshTypeT;
+
+  constexpr static bool handles_high_degrees = true;
+
+ private:
+  using Rd = TinyVector<MeshType::Dimension>;
+
+  const size_t m_basis_dimension;
+  const size_t m_polynomial_degree;
+
+  const SmallArray<double> m_wq_detJ_ek;
+  SmallArray<double> m_mean_j_of_ejk;
+  SmallArray<double> m_mean_i_of_ejk;
+
+  const ItemToItemMatrix<ItemType::cell, ItemType::node> m_cell_to_node_matrix;
+  const CellToCellStencilArray& m_stencil_array;
+
+  const SmallArray<const Rd> m_symmetry_origin_list;
+  const SmallArray<const Rd> m_symmetry_normal_list;
+
+  const CellValue<const CellType> m_cell_type;
+  const CellValue<const double> m_Vj;
+  const CellValue<const Rd> m_xj;
+  const NodeValue<const Rd> m_xr;
+
+  // 2D
+  SmallArray<const size_t> m_y_row_index;
+
+  // 3D
+  SmallArray<const size_t> m_yz_row_index;
+  SmallArray<const size_t> m_z_triangle_index;
+  SmallArray<const size_t> m_yz_row_size;
+
+  template <typename ConformTransformationT>
+  void _computeEjkMean(const QuadratureFormula<MeshType::Dimension>& quadrature,
+                       const ConformTransformationT& T,
+                       const Rd& Xj,
+                       const double Vi,
+                       SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT);
+
+  void _computeEjkMean(const TinyVector<MeshType::Dimension>& Xj,
+                       const CellId& cell_i_id,
+                       SmallArray<double>& mean_of_ejk);
+
+  void _computeEjkMeanInSymmetricCell(const Rd& origin,
+                                      const Rd& normal,
+                                      const Rd& Xj,
+                                      const CellId& cell_i_id,
+                                      SmallArray<double>& mean_of_ejk);
+
+ public:
+  PUGS_INLINE
+  SmallArray<const double>
+  meanjOfEjk() const
+  {
+    return m_mean_j_of_ejk;
+  }
+
+  void build(const CellId cell_j_id, ShrinkMatrixView<SmallMatrix<double>>& A);
+
+  ElementIntegralReconstructionMatrixBuilder(const MeshType& mesh,
+                                             const size_t polynomial_degree,
+                                             const SmallArray<const Rd>& symmetry_origin_list,
+                                             const SmallArray<const Rd>& symmetry_normal_list,
+                                             const CellToCellStencilArray& stencil_array);
+
+  ~ElementIntegralReconstructionMatrixBuilder() = default;
+};
+
+#endif   // ELEMENT_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
diff --git a/src/scheme/reconstruction_utils/MutableDiscreteFunctionDPkVariant.hpp b/src/scheme/reconstruction_utils/MutableDiscreteFunctionDPkVariant.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..761f3c1129b00609b1be9a23b5d256db4b48c084
--- /dev/null
+++ b/src/scheme/reconstruction_utils/MutableDiscreteFunctionDPkVariant.hpp
@@ -0,0 +1,130 @@
+#ifndef MUTABLE_DISCRETE_FUNCTION_D_PK_VARIANT_HPP
+#define MUTABLE_DISCRETE_FUNCTION_D_PK_VARIANT_HPP
+
+#include <scheme/DiscreteFunctionDPk.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <variant>
+
+class PolynomialReconstruction::MutableDiscreteFunctionDPkVariant
+{
+ public:
+  using Variant = std::variant<DiscreteFunctionDPk<1, double>,
+                               DiscreteFunctionDPk<1, TinyVector<1>>,
+                               DiscreteFunctionDPk<1, TinyVector<2>>,
+                               DiscreteFunctionDPk<1, TinyVector<3>>,
+                               DiscreteFunctionDPk<1, TinyMatrix<1>>,
+                               DiscreteFunctionDPk<1, TinyMatrix<2>>,
+                               DiscreteFunctionDPk<1, TinyMatrix<3>>,
+
+                               DiscreteFunctionDPk<2, double>,
+                               DiscreteFunctionDPk<2, TinyVector<1>>,
+                               DiscreteFunctionDPk<2, TinyVector<2>>,
+                               DiscreteFunctionDPk<2, TinyVector<3>>,
+                               DiscreteFunctionDPk<2, TinyMatrix<1>>,
+                               DiscreteFunctionDPk<2, TinyMatrix<2>>,
+                               DiscreteFunctionDPk<2, TinyMatrix<3>>,
+
+                               DiscreteFunctionDPk<3, double>,
+                               DiscreteFunctionDPk<3, TinyVector<1>>,
+                               DiscreteFunctionDPk<3, TinyVector<2>>,
+                               DiscreteFunctionDPk<3, TinyVector<3>>,
+                               DiscreteFunctionDPk<3, TinyMatrix<1>>,
+                               DiscreteFunctionDPk<3, TinyMatrix<2>>,
+                               DiscreteFunctionDPk<3, TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<1, double>,
+                               DiscreteFunctionDPkVector<1, TinyVector<1>>,
+                               DiscreteFunctionDPkVector<1, TinyVector<2>>,
+                               DiscreteFunctionDPkVector<1, TinyVector<3>>,
+                               DiscreteFunctionDPkVector<1, TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<1, TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<1, TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<2, double>,
+                               DiscreteFunctionDPkVector<2, TinyVector<1>>,
+                               DiscreteFunctionDPkVector<2, TinyVector<2>>,
+                               DiscreteFunctionDPkVector<2, TinyVector<3>>,
+                               DiscreteFunctionDPkVector<2, TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<2, TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<2, TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<3, double>,
+                               DiscreteFunctionDPkVector<3, TinyVector<1>>,
+                               DiscreteFunctionDPkVector<3, TinyVector<2>>,
+                               DiscreteFunctionDPkVector<3, TinyVector<3>>,
+                               DiscreteFunctionDPkVector<3, TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<3, TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<3, TinyMatrix<3>>>;
+
+ private:
+  Variant m_mutable_discrete_function_dpk;
+
+ public:
+  PUGS_INLINE
+  const Variant&
+  mutableDiscreteFunctionDPk() const
+  {
+    return m_mutable_discrete_function_dpk;
+  }
+
+  template <typename DiscreteFunctionDPkT>
+  PUGS_INLINE auto&&
+  get() const
+  {
+    static_assert(is_discrete_function_dpk_v<DiscreteFunctionDPkT>, "invalid template argument");
+#ifndef NDEBUG
+    if (not std::holds_alternative<DiscreteFunctionDPkT>(this->m_mutable_discrete_function_dpk)) {
+      std::ostringstream error_msg;
+      error_msg << "invalid discrete function type\n";
+      error_msg << "- required " << rang::fgB::red << demangle<DiscreteFunctionDPkT>() << rang::fg::reset << '\n';
+      error_msg << "- contains " << rang::fgB::yellow
+                << std::visit([](auto&& f) -> std::string { return demangle<decltype(f)>(); },
+                              this->m_mutable_discrete_function_dpk)
+                << rang::fg::reset;
+      throw NormalError(error_msg.str());
+    }
+#endif   // NDEBUG
+
+    return std::get<DiscreteFunctionDPkT>(this->mutableDiscreteFunctionDPk());
+  }
+
+  template <size_t Dimension, typename DataType>
+  MutableDiscreteFunctionDPkVariant(const DiscreteFunctionDPk<Dimension, DataType>& discrete_function_dpk)
+    : m_mutable_discrete_function_dpk{discrete_function_dpk}
+  {
+    static_assert(std::is_same_v<DataType, double> or                       //
+                    std::is_same_v<DataType, TinyVector<1, double>> or      //
+                    std::is_same_v<DataType, TinyVector<2, double>> or      //
+                    std::is_same_v<DataType, TinyVector<3, double>> or      //
+                    std::is_same_v<DataType, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<DataType, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<DataType, TinyMatrix<3, 3, double>>,
+                  "DiscreteFunctionDPk with this DataType is not allowed in variant");
+  }
+
+  template <size_t Dimension, typename DataType>
+  MutableDiscreteFunctionDPkVariant(const DiscreteFunctionDPkVector<Dimension, DataType>& discrete_function_dpk)
+    : m_mutable_discrete_function_dpk{discrete_function_dpk}
+  {
+    static_assert(std::is_same_v<DataType, double> or                       //
+                    std::is_same_v<DataType, TinyVector<1, double>> or      //
+                    std::is_same_v<DataType, TinyVector<2, double>> or      //
+                    std::is_same_v<DataType, TinyVector<3, double>> or      //
+                    std::is_same_v<DataType, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<DataType, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<DataType, TinyMatrix<3, 3, double>>,
+                  "DiscreteFunctionDPkVector with this DataType is not allowed in variant");
+  }
+
+  MutableDiscreteFunctionDPkVariant& operator=(MutableDiscreteFunctionDPkVariant&&)      = default;
+  MutableDiscreteFunctionDPkVariant& operator=(const MutableDiscreteFunctionDPkVariant&) = default;
+
+  MutableDiscreteFunctionDPkVariant(const MutableDiscreteFunctionDPkVariant&) = default;
+  MutableDiscreteFunctionDPkVariant(MutableDiscreteFunctionDPkVariant&&)      = default;
+
+  MutableDiscreteFunctionDPkVariant()  = delete;
+  ~MutableDiscreteFunctionDPkVariant() = default;
+};
+
+#endif   // MUTABLE_DISCRETE_FUNCTION_D_PK_VARIANT_HPP
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 31e9149852a79c0cd4c2c5fc6e181f3a295231dd..a000a642063b8e68dcec6758b7b5950a4ca69595 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -289,6 +289,8 @@ add_executable (mpi_unit_tests
   test_ParallelChecker_read.cpp
   test_Partitioner.cpp
   test_PolynomialReconstruction_degree_1.cpp
+  test_PolynomialReconstruction_degree_2.cpp
+  test_PolynomialReconstruction_degree_3.cpp
   test_PolynomialReconstructionDescriptor.cpp
   test_RandomEngine.cpp
   test_StencilBuilder_cell2cell.cpp
@@ -324,6 +326,7 @@ target_link_libraries (unit_tests
   PugsAlgebra
   PugsAnalysis
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsOutput
   PugsUtils
   PugsCheckpointing
@@ -354,6 +357,7 @@ target_link_libraries (mpi_unit_tests
   PugsUtils
   PugsLanguageUtils
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsOutput
   PugsUtils
   PugsCheckpointing
diff --git a/tests/DiscreteFunctionDPkForTests.hpp b/tests/DiscreteFunctionDPkForTests.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5b51861d811b2d8a35c91375bc9e8dd368e2b281
--- /dev/null
+++ b/tests/DiscreteFunctionDPkForTests.hpp
@@ -0,0 +1,365 @@
+#ifndef DISCRETE_FUNCTION_DPK_FOR_TESTS_HPP
+#define DISCRETE_FUNCTION_DPK_FOR_TESTS_HPP
+
+#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <analysis/QuadratureFormula.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <geometry/CubeTransformation.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <geometry/PrismTransformation.hpp>
+#include <geometry/PyramidTransformation.hpp>
+#include <geometry/SquareTransformation.hpp>
+#include <geometry/TetrahedronTransformation.hpp>
+#include <geometry/TriangleTransformation.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <type_traits>
+
+namespace test_only
+{
+
+template <MeshConcept MeshType, typename DataType>
+DiscreteFunctionP0<std::remove_const_t<DataType>>
+exact_projection(const MeshType& mesh,
+                 size_t degree,
+                 std::function<DataType(const TinyVector<MeshType::Dimension>&)> exact_function)
+{
+  DiscreteFunctionP0<std::remove_const_t<DataType>> P0_function{mesh.meshVariant()};
+
+  auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+  auto xr                  = mesh.xr();
+  auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+  auto cell_type           = mesh.connectivity().cellType();
+
+  auto sum = [&exact_function, &Vj](const CellId cell_id, const auto& T,
+                                    const auto& qf) -> std::remove_const_t<DataType> {
+    std::remove_const_t<DataType> integral =
+      (qf.weight(0) * T.jacobianDeterminant(qf.point(0))) * exact_function(T(qf.point(0)));
+    for (size_t i_quadrarture = 1; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+      integral += (qf.weight(i_quadrarture) * T.jacobianDeterminant(qf.point(i_quadrarture))) *
+                  exact_function(T(qf.point(i_quadrarture)));
+    }
+    return 1. / Vj[cell_id] * integral;
+  };
+
+  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+    auto cell_nodes = cell_to_node_matrix[cell_id];
+    if constexpr (MeshType::Dimension == 1) {
+      LineTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]]};
+      auto qf              = QuadratureManager::instance().getLineFormula(GaussQuadratureDescriptor{degree + 1});
+      P0_function[cell_id] = sum(cell_id, T, qf);
+    } else if constexpr (MeshType::Dimension == 2) {
+      switch (cell_type[cell_id]) {
+      case CellType::Triangle: {
+        TriangleTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]]};
+        auto qf              = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{degree + 2});
+        P0_function[cell_id] = sum(cell_id, T, qf);
+        break;
+      }
+      case CellType::Quadrangle: {
+        SquareTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]],
+                                                    xr[cell_nodes[3]]};
+        auto qf              = QuadratureManager::instance().getSquareFormula(GaussQuadratureDescriptor{degree + 2});
+        P0_function[cell_id] = sum(cell_id, T, qf);
+        break;
+      }
+      default: {
+        throw UnexpectedError("unexpected cell type");
+      }
+      }
+    } else if constexpr (MeshType::Dimension == 3) {
+      switch (cell_type[cell_id]) {
+      case CellType::Tetrahedron: {
+        TetrahedronTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]]};
+        auto qf = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{degree + 3});
+        P0_function[cell_id] = sum(cell_id, T, qf);
+        break;
+      }
+      case CellType::Pyramid: {
+        PyramidTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]],
+                                xr[cell_nodes[4]]};
+        auto qf              = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{degree + 3});
+        P0_function[cell_id] = sum(cell_id, T, qf);
+        break;
+      }
+      case CellType::Prism: {
+        PrismTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]],
+                              xr[cell_nodes[3]], xr[cell_nodes[4]], xr[cell_nodes[5]]};
+        auto qf              = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{degree + 3});
+        P0_function[cell_id] = sum(cell_id, T, qf);
+        break;
+      }
+      case CellType::Hexahedron: {
+        CubeTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]],
+                             xr[cell_nodes[4]], xr[cell_nodes[5]], xr[cell_nodes[6]], xr[cell_nodes[7]]};
+        auto qf              = QuadratureManager::instance().getCubeFormula(GaussQuadratureDescriptor{degree + 3});
+        P0_function[cell_id] = sum(cell_id, T, qf);
+        break;
+      }
+      default: {
+        throw UnexpectedError("unexpected cell type");
+      }
+      }
+    } else {
+      throw UnexpectedError("invalid mesh dimension");
+    }
+  }
+
+  return P0_function;
+}
+
+template <MeshConcept MeshType, typename DataType, size_t NbComponents>
+DiscreteFunctionP0Vector<std::remove_const_t<DataType>>
+exact_projection(
+  const MeshType& mesh,
+  size_t degree,
+  const std::array<std::function<DataType(const TinyVector<MeshType::Dimension>&)>, NbComponents>& vector_exact)
+{
+  DiscreteFunctionP0Vector<std::remove_const_t<DataType>> P0_function_vector{mesh.meshVariant(), vector_exact.size()};
+
+  for (size_t i_component = 0; i_component < vector_exact.size(); ++i_component) {
+    auto exact_function = vector_exact[i_component];
+
+    DiscreteFunctionP0 P0_function = exact_projection(mesh, degree, vector_exact[i_component]);
+
+    parallel_for(
+      mesh.numberOfCells(),
+      PUGS_LAMBDA(const CellId cell_id) { P0_function_vector[cell_id][i_component] = P0_function[cell_id]; });
+  }
+
+  return P0_function_vector;
+}
+
+template <typename DataType>
+PUGS_INLINE double
+get_max_error(const DataType& x, const DataType& y)
+{
+  if constexpr (is_tiny_matrix_v<DataType>) {
+    return frobeniusNorm(x - y);
+  } else if constexpr (is_tiny_vector_v<DataType>) {
+    return l2Norm(x - y);
+  } else {
+    static_assert(std::is_arithmetic_v<DataType>, "expecting arithmetic type");
+    return std::abs(x - y);
+  }
+}
+
+template <MeshConcept MeshType, typename DataType>
+double
+max_reconstruction_error(const MeshType& mesh,
+                         DiscreteFunctionDPk<MeshType::Dimension, const DataType> dpk_f,
+                         std::function<DataType(const TinyVector<MeshType::Dimension>&)> exact)
+{
+  auto xr                  = mesh.xr();
+  auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+  auto cell_type           = mesh.connectivity().cellType();
+
+  double max_error = 0;
+  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+    auto cell_nodes = cell_to_node_matrix[cell_id];
+    if constexpr (MeshType::Dimension == 1) {
+      Assert(cell_type[cell_id] == CellType::Line);
+      LineTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]]};
+      auto qf = QuadratureManager::instance().getLineFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+      for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+        auto x    = T(qf.point(i_quadrarture));
+        max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+      }
+    } else if constexpr (MeshType::Dimension == 2) {
+      switch (cell_type[cell_id]) {
+      case CellType::Triangle: {
+        TriangleTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]]};
+        auto qf = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x    = T(qf.point(i_quadrarture));
+          max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+        }
+        break;
+      }
+      case CellType::Quadrangle: {
+        SquareTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]],
+                                                    xr[cell_nodes[3]]};
+        auto qf = QuadratureManager::instance().getSquareFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x    = T(qf.point(i_quadrarture));
+          max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+        }
+        break;
+      }
+      default: {
+        throw UnexpectedError("unexpected cell type");
+      }
+      }
+    } else if constexpr (MeshType::Dimension == 3) {
+      switch (cell_type[cell_id]) {
+      case CellType::Tetrahedron: {
+        TetrahedronTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]]};
+        auto qf = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x    = T(qf.point(i_quadrarture));
+          max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+        }
+        break;
+      }
+      case CellType::Pyramid: {
+        PyramidTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]],
+                                xr[cell_nodes[4]]};
+        auto qf = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x    = T(qf.point(i_quadrarture));
+          max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+        }
+        break;
+      }
+      case CellType::Prism: {
+        PrismTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]],
+                              xr[cell_nodes[3]], xr[cell_nodes[4]], xr[cell_nodes[5]]};
+        auto qf = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x    = T(qf.point(i_quadrarture));
+          max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+        }
+        break;
+      }
+      case CellType::Hexahedron: {
+        CubeTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]],
+                             xr[cell_nodes[4]], xr[cell_nodes[5]], xr[cell_nodes[6]], xr[cell_nodes[7]]};
+        auto qf = QuadratureManager::instance().getCubeFormula(GaussQuadratureDescriptor{dpk_f.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x    = T(qf.point(i_quadrarture));
+          max_error = std::max(max_error, get_max_error(dpk_f[cell_id](x), exact(x)));
+        }
+        break;
+      }
+      default: {
+        throw UnexpectedError("unexpected cell type");
+      }
+      }
+    }
+  }
+  return max_error;
+}
+
+template <MeshConcept MeshType, typename DataType, size_t NbComponents>
+double
+max_reconstruction_error(
+  const MeshType& mesh,
+  DiscreteFunctionDPkVector<MeshType::Dimension, const DataType> dpk_v,
+  const std::array<std::function<DataType(const TinyVector<MeshType::Dimension>&)>, NbComponents>& vector_exact)
+{
+  auto xr                  = mesh.xr();
+  auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+  double max_error         = 0;
+  auto cell_type           = mesh.connectivity().cellType();
+
+  REQUIRE(NbComponents == dpk_v.numberOfComponents());
+
+  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+    auto cell_nodes = cell_to_node_matrix[cell_id];
+    if constexpr (MeshType::Dimension == 1) {
+      Assert(cell_type[cell_id] == CellType::Line);
+      LineTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]]};
+      auto qf = QuadratureManager::instance().getLineFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+      for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+        auto x = T(qf.point(i_quadrarture));
+        for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+          max_error = std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+        }
+      }
+    } else if constexpr (MeshType::Dimension == 2) {
+      switch (cell_type[cell_id]) {
+      case CellType::Triangle: {
+        TriangleTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]]};
+        auto qf = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x = T(qf.point(i_quadrarture));
+          for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+            max_error =
+              std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+          }
+        }
+        break;
+      }
+      case CellType::Quadrangle: {
+        SquareTransformation<MeshType::Dimension> T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]],
+                                                    xr[cell_nodes[3]]};
+        auto qf = QuadratureManager::instance().getSquareFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x = T(qf.point(i_quadrarture));
+          for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+            max_error =
+              std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+          }
+        }
+        break;
+      }
+      default: {
+        throw UnexpectedError("unexpected cell type");
+      }
+      }
+    } else if constexpr (MeshType::Dimension == 3) {
+      switch (cell_type[cell_id]) {
+      case CellType::Tetrahedron: {
+        TetrahedronTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]]};
+        auto qf = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x = T(qf.point(i_quadrarture));
+          for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+            max_error =
+              std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+          }
+        }
+        break;
+      }
+      case CellType::Pyramid: {
+        PyramidTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]],
+                                xr[cell_nodes[4]]};
+        auto qf = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x = T(qf.point(i_quadrarture));
+          for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+            max_error =
+              std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+          }
+        }
+        break;
+      }
+      case CellType::Prism: {
+        PrismTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]],
+                              xr[cell_nodes[3]], xr[cell_nodes[4]], xr[cell_nodes[5]]};
+        auto qf = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x = T(qf.point(i_quadrarture));
+          for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+            max_error =
+              std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+          }
+        }
+        break;
+      }
+      case CellType::Hexahedron: {
+        CubeTransformation T{xr[cell_nodes[0]], xr[cell_nodes[1]], xr[cell_nodes[2]], xr[cell_nodes[3]],
+                             xr[cell_nodes[4]], xr[cell_nodes[5]], xr[cell_nodes[6]], xr[cell_nodes[7]]};
+        auto qf = QuadratureManager::instance().getCubeFormula(GaussQuadratureDescriptor{dpk_v.degree() + 1});
+        for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+          auto x = T(qf.point(i_quadrarture));
+          for (size_t i_component = 0; i_component < NbComponents; ++i_component) {
+            max_error =
+              std::max(max_error, get_max_error(dpk_v(cell_id, i_component)(x), vector_exact[i_component](x)));
+          }
+        }
+        break;
+      }
+      default: {
+        throw UnexpectedError("unexpected cell type");
+      }
+      }
+    }
+  }
+  return max_error;
+}
+
+}   // namespace test_only
+
+#endif   // DISCRETE_FUNCTION_DPK_FOR_TESTS_HPP
diff --git a/tests/MeshDataBaseForTests.cpp b/tests/MeshDataBaseForTests.cpp
index 1f2b485edcb7f7b3fadf35d845b59f23c1a03274..1615673d0ba28d7328ab5faef7830a774d5c75f4 100644
--- a/tests/MeshDataBaseForTests.cpp
+++ b/tests/MeshDataBaseForTests.cpp
@@ -1,11 +1,15 @@
 #include <MeshDataBaseForTests.hpp>
+
 #include <mesh/CartesianMeshBuilder.hpp>
 #include <mesh/Connectivity.hpp>
 #include <mesh/GmshReader.hpp>
 #include <mesh/MeshVariant.hpp>
+#include <utils/GlobalVariableManager.hpp>
 #include <utils/Messenger.hpp>
 #include <utils/PugsAssert.hpp>
 
+#include <NbGhostLayersTester.hpp>
+
 #include <filesystem>
 #include <fstream>
 
@@ -13,17 +17,22 @@ const MeshDataBaseForTests* MeshDataBaseForTests::m_instance = nullptr;
 
 MeshDataBaseForTests::MeshDataBaseForTests()
 {
-  m_cartesian_1d_mesh = CartesianMeshBuilder{TinyVector<1>{-1}, TinyVector<1>{3}, TinyVector<1, size_t>{23}}.mesh();
+  for (size_t nb_ghost_layers = 1; nb_ghost_layers <= m_max_nb_ghost_layers; ++nb_ghost_layers) {
+    NbGhostLayersTester t{nb_ghost_layers};
 
-  m_cartesian_2d_mesh =
-    CartesianMeshBuilder{TinyVector<2>{0, -1}, TinyVector<2>{3, 2}, TinyVector<2, size_t>{6, 7}}.mesh();
+    m_cartesian_1d_mesh.push_back(
+      CartesianMeshBuilder{TinyVector<1>{-1}, TinyVector<1>{3}, TinyVector<1, size_t>{23}}.mesh());
 
-  m_cartesian_3d_mesh =
-    CartesianMeshBuilder{TinyVector<3>{0, 1, 0}, TinyVector<3>{2, -1, 3}, TinyVector<3, size_t>{6, 7, 4}}.mesh();
+    m_cartesian_2d_mesh.push_back(
+      CartesianMeshBuilder{TinyVector<2>{0, -1}, TinyVector<2>{3, 2}, TinyVector<2, size_t>{6, 7}}.mesh());
 
-  m_unordered_1d_mesh = _buildUnordered1dMesh();
-  m_hybrid_2d_mesh    = _buildHybrid2dMesh();
-  m_hybrid_3d_mesh    = _buildHybrid3dMesh();
+    m_cartesian_3d_mesh.push_back(
+      CartesianMeshBuilder{TinyVector<3>{0, 1, 0}, TinyVector<3>{2, -1, 3}, TinyVector<3, size_t>{6, 7, 4}}.mesh());
+
+    m_unordered_1d_mesh.push_back(_buildUnordered1dMesh());
+    m_hybrid_2d_mesh.push_back(_buildHybrid2dMesh());
+    m_hybrid_3d_mesh.push_back(_buildHybrid3dMesh());
+  }
 }
 
 const MeshDataBaseForTests&
@@ -50,37 +59,49 @@ MeshDataBaseForTests::destroy()
 std::shared_ptr<const MeshVariant>
 MeshDataBaseForTests::cartesian1DMesh() const
 {
-  return m_cartesian_1d_mesh;
+  const size_t nb_ghost_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+  Assert((nb_ghost_layers >= 1) and (nb_ghost_layers <= m_max_nb_ghost_layers));
+  return m_cartesian_1d_mesh[nb_ghost_layers - 1];
 }
 
 std::shared_ptr<const MeshVariant>
 MeshDataBaseForTests::cartesian2DMesh() const
 {
-  return m_cartesian_2d_mesh;
+  const size_t nb_ghost_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+  Assert((nb_ghost_layers >= 1) and (nb_ghost_layers <= m_max_nb_ghost_layers));
+  return m_cartesian_2d_mesh[nb_ghost_layers - 1];
 }
 
 std::shared_ptr<const MeshVariant>
 MeshDataBaseForTests::cartesian3DMesh() const
 {
-  return m_cartesian_3d_mesh;
+  const size_t nb_ghost_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+  Assert((nb_ghost_layers >= 1) and (nb_ghost_layers <= m_max_nb_ghost_layers));
+  return m_cartesian_3d_mesh[nb_ghost_layers - 1];
 }
 
 std::shared_ptr<const MeshVariant>
 MeshDataBaseForTests::unordered1DMesh() const
 {
-  return m_unordered_1d_mesh;
+  const size_t nb_ghost_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+  Assert((nb_ghost_layers >= 1) and (nb_ghost_layers <= m_max_nb_ghost_layers));
+  return m_unordered_1d_mesh[nb_ghost_layers - 1];
 }
 
 std::shared_ptr<const MeshVariant>
 MeshDataBaseForTests::hybrid2DMesh() const
 {
-  return m_hybrid_2d_mesh;
+  const size_t nb_ghost_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+  Assert((nb_ghost_layers >= 1) and (nb_ghost_layers <= m_max_nb_ghost_layers));
+  return m_hybrid_2d_mesh[nb_ghost_layers - 1];
 }
 
 std::shared_ptr<const MeshVariant>
 MeshDataBaseForTests::hybrid3DMesh() const
 {
-  return m_hybrid_3d_mesh;
+  const size_t nb_ghost_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+  Assert((nb_ghost_layers >= 1) and (nb_ghost_layers <= m_max_nb_ghost_layers));
+  return m_hybrid_3d_mesh[nb_ghost_layers - 1];
 }
 
 std::shared_ptr<const MeshVariant>
diff --git a/tests/MeshDataBaseForTests.hpp b/tests/MeshDataBaseForTests.hpp
index 2b092d544271fbd1b1d0488ed73c9c5a401d548b..f0ab29fa9a927c01f16f94925fba7eb750ffd591 100644
--- a/tests/MeshDataBaseForTests.hpp
+++ b/tests/MeshDataBaseForTests.hpp
@@ -4,6 +4,7 @@
 #include <array>
 #include <memory>
 #include <string>
+#include <vector>
 
 class MeshVariant;
 
@@ -35,15 +36,17 @@ class MeshDataBaseForTests
  private:
   explicit MeshDataBaseForTests();
 
+  constexpr static size_t m_max_nb_ghost_layers = 3;
+
   static const MeshDataBaseForTests* m_instance;
 
-  std::shared_ptr<const MeshVariant> m_cartesian_1d_mesh;
-  std::shared_ptr<const MeshVariant> m_cartesian_2d_mesh;
-  std::shared_ptr<const MeshVariant> m_cartesian_3d_mesh;
+  std::vector<std::shared_ptr<const MeshVariant>> m_cartesian_1d_mesh;
+  std::vector<std::shared_ptr<const MeshVariant>> m_cartesian_2d_mesh;
+  std::vector<std::shared_ptr<const MeshVariant>> m_cartesian_3d_mesh;
 
-  std::shared_ptr<const MeshVariant> m_unordered_1d_mesh;
-  std::shared_ptr<const MeshVariant> m_hybrid_2d_mesh;
-  std::shared_ptr<const MeshVariant> m_hybrid_3d_mesh;
+  std::vector<std::shared_ptr<const MeshVariant>> m_unordered_1d_mesh;
+  std::vector<std::shared_ptr<const MeshVariant>> m_hybrid_2d_mesh;
+  std::vector<std::shared_ptr<const MeshVariant>> m_hybrid_3d_mesh;
 
   std::shared_ptr<const MeshVariant> _buildUnordered1dMesh();
   std::shared_ptr<const MeshVariant> _buildHybrid2dMesh();
diff --git a/tests/test_PolynomialReconstruction_degree_1.cpp b/tests/test_PolynomialReconstruction_degree_1.cpp
index 7c4f07266514eaf8b277af5f26751c2ec17bc8ae..fb2c7b2ec4a56239e37314704b2cc30122182e8c 100644
--- a/tests/test_PolynomialReconstruction_degree_1.cpp
+++ b/tests/test_PolynomialReconstruction_degree_1.cpp
@@ -2,37 +2,30 @@
 #include <catch2/catch_test_macros.hpp>
 #include <catch2/matchers/catch_matchers_all.hpp>
 
-#include <Kokkos_Core.hpp>
-
 #include <utils/PugsAssert.hpp>
-#include <utils/Types.hpp>
-
-#include <algebra/SmallMatrix.hpp>
-#include <algebra/SmallVector.hpp>
-#include <analysis/GaussQuadratureDescriptor.hpp>
-#include <analysis/QuadratureFormula.hpp>
-#include <analysis/QuadratureManager.hpp>
-#include <geometry/LineTransformation.hpp>
+
 #include <mesh/Mesh.hpp>
-#include <mesh/MeshDataManager.hpp>
 #include <mesh/NamedBoundaryDescriptor.hpp>
 #include <scheme/DiscreteFunctionDPkVariant.hpp>
 #include <scheme/DiscreteFunctionP0.hpp>
 #include <scheme/DiscreteFunctionVariant.hpp>
 #include <scheme/PolynomialReconstruction.hpp>
 
+#include <DiscreteFunctionDPkForTests.hpp>
 #include <MeshDataBaseForTests.hpp>
 
 // clazy:excludeall=non-pod-global-static
 
 TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
 {
+  constexpr size_t degree = 1;
+
   SECTION("without symmetries")
   {
     std::vector<PolynomialReconstructionDescriptor> descriptor_list =
-      {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, 1},
-       PolynomialReconstructionDescriptor{IntegrationMethodType::element, 1},
-       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, 1}};
+      {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, degree},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree}};
 
     for (auto descriptor : descriptor_list) {
       SECTION(name(descriptor.integrationMethodType()))
@@ -49,55 +42,16 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
 
-                auto R_affine = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
-                auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
-                auto xr       = mesh.xr();
-
-                auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+                auto R_exact = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
 
-                DiscreteFunctionP0<double> fh{p_mesh};
-
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
 
                 auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
 
-                auto qf = QuadratureManager::instance().getLineFormula(GaussQuadratureDescriptor{2});
-                {
-                  double max_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    auto cell_nodes = cell_to_node_matrix[cell_id];
-                    LineTransformation<1> T{xr[cell_nodes[0]], xr[cell_nodes[1]]};
-                    for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
-                      max_error = std::max(max_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-                    }
-                  }
-
-                  REQUIRE(parallel::allReduceMax(max_error) == 0   // Catch::Approx(0).margin(0 * 1E-14)
-                  );
-                }
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1})) / 0.2;
-
-                    max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -112,41 +66,20 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
 
-                auto R3_affine = [](const R1& x) -> R3 {
+                auto R3_exact = [](const R1& x) -> R3 {
                   return R3{+2.3 + 1.7 * x[0],   //
                             +1.4 - 0.6 * x[0],   //
                             -0.2 + 3.1 * x[0]};
                 };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<R3> uh{p_mesh};
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
 
                 auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope =
-                      (1 / 0.2) * (dpk_uh[cell_id](R1{0.1} + xj[cell_id]) - dpk_uh[cell_id](xj[cell_id] - R1{0.1}));
-
-                    max_slope_error = std::max(max_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -161,106 +94,45 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
 
-                auto R3x3_affine = [](const R1& x) -> R3x3 {
+                auto R3x3_exact = [](const R1& x) -> R3x3 {
                   return R3x3{
                     +2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0],   //
                     +2.4 - 2.3 * x[0], -0.2 + 3.1 * x[0], -3.2 - 3.6 * x[0],
                     -4.1 + 3.1 * x[0], +0.8 + 2.9 * x[0], -1.6 + 2.3 * x[0],
                   };
                 };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<R3x3> Ah{p_mesh};
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R3x3_affine(xj[cell_id]); });
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
 
                 auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3x3>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R3x3_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3x3 reconstructed_slope =
-                      (1 / 0.2) * (dpk_Ah[cell_id](R1{0.1} + xj[cell_id]) - dpk_Ah[cell_id](xj[cell_id] - R1{0.1}));
-
-                    R3x3 slops = R3x3{+1.7, +2.1, -0.6,   //
-                                      -2.3, +3.1, -3.6,   //
-                                      +3.1, +2.9, +2.3};
-
-                    max_slope_error = std::max(max_slope_error,   //
-                                               frobeniusNorm(reconstructed_slope - slops));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
 
           SECTION("R vector data")
           {
-            using R3 = TinyVector<3>;
-
             for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
               SECTION(named_mesh.name())
               {
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
+                std::array<std::function<double(const R1&)>, 3> vector_exact =
+                  {[](const R1& x) -> double { return +2.3 + 1.7 * x[0]; },
+                   [](const R1& x) -> double { return -1.7 + 2.1 * x[0]; },
+                   [](const R1& x) -> double { return +1.4 - 0.6 * x[0]; }};
 
-                auto vector_affine = [](const R1& x) -> R3 {
-                  return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
-                };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0Vector<double> Vh{p_mesh, 3};
-
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      Vh[cell_id][i] = vector[i];
-                    }
-                  });
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+                auto dpk_Vh          = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
 
-                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  const TinyVector<3> slope{+1.7, +2.1, -0.6};
-
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R1{0.1} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R1{0.1}));
-
-                      max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -275,68 +147,22 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
 
-                auto vector_affine0 = [](const R1& x) -> R3 {
-                  return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
-                };
-
-                auto vector_affine1 = [](const R1& x) -> R3 {
-                  return R3{+1.6 + 0.7 * x[0], -2.1 + 1.2 * x[0], +1.1 - 0.3 * x[0]};
-                };
+                std::array<std::function<R3(const R1&)>, 2> vector_exact =
+                  {[](const R1& x) -> R3 {
+                     return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
+                   },
+                   [](const R1& x) -> R3 {
+                     return R3{+1.6 + 0.7 * x[0], -2.1 + 1.2 * x[0], +1.1 - 0.3 * x[0]};
+                   }};
 
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0Vector<R3> Vh{p_mesh, 2};
-
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-                    Vh[cell_id][0] = vector_affine0(xj[cell_id]);
-                    Vh[cell_id][1] = vector_affine1(xj[cell_id]);
-                  });
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
 
                 auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R3>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 0)(xj[cell_id]) - vector_affine0(xj[cell_id])));
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 1)(xj[cell_id]) - vector_affine1(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  {
-                    const TinyVector<3> slope0{+1.7, +2.1, -0.6};
-
-                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                      for (size_t i = 0; i < R3::Dimension; ++i) {
-                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 0)(R1{0.1} + xj[cell_id])[i] -
-                                                                        dpk_Vh(cell_id, 0)(xj[cell_id] - R1{0.1})[i]);
-
-                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope0[i]));
-                      }
-                    }
-                  }
-
-                  {
-                    const TinyVector<3> slope1{+0.7, +1.2, -0.3};
-
-                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                      for (size_t i = 0; i < R3::Dimension; ++i) {
-                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 1)(R1{0.1} + xj[cell_id])[i] -
-                                                                        dpk_Vh(cell_id, 1)(xj[cell_id] - R1{0.1})[i]);
-
-                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope1[i]));
-                      }
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -352,15 +178,15 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
 
-                auto R_affine = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
+                auto R_exact = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
 
-                auto R3_affine = [](const R1& x) -> R3 {
+                auto R3_exact = [](const R1& x) -> R3 {
                   return R3{+2.3 + 1.7 * x[0],   //
                             +1.4 - 0.6 * x[0],   //
                             -0.2 + 3.1 * x[0]};
                 };
 
-                auto R3x3_affine = [](const R1& x) -> R3x3 {
+                auto R3x3_exact = [](const R1& x) -> R3x3 {
                   return R3x3{
                     +2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0],   //
                     +2.4 - 2.3 * x[0], -0.2 + 3.1 * x[0], -3.2 - 3.6 * x[0],
@@ -368,135 +194,43 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                   };
                 };
 
-                auto vector_affine = [](const R1& x) -> R3 {
-                  return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
-                };
-
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<double> fh{p_mesh};
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+                std::array<std::function<double(const R1&)>, 3> vector_exact =
+                  {[](const R1& x) -> double { return +2.3 + 1.7 * x[0]; },
+                   [](const R1& x) -> double { return -1.7 + 2.1 * x[0]; },
+                   [](const R1& x) -> double { return +1.4 - 0.6 * x[0]; }};
 
-                DiscreteFunctionP0<R3> uh{p_mesh};
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
-
-                DiscreteFunctionP0<R3x3> Ah{p_mesh};
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R3x3_affine(xj[cell_id]); });
-
-                DiscreteFunctionP0Vector<double> Vh{p_mesh, 3};
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      Vh[cell_id][i] = vector[i];
-                    }
-                  });
+                DiscreteFunctionP0 fh       = test_only::exact_projection(mesh, degree, std::function(R_exact));
+                DiscreteFunctionP0 uh       = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+                DiscreteFunctionP0 Ah       = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
 
                 auto reconstructions =
                   PolynomialReconstruction{descriptor}.build(std::make_shared<DiscreteFunctionVariant>(fh), uh,
                                                              std::make_shared<DiscreteFunctionP0<R3x3>>(Ah),
                                                              DiscreteFunctionVariant(Vh));
 
-                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
                 {
-                  double max_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1})) / 0.2;
-
-                    max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                  auto dpk_fh      = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
                 }
 
-                auto dpk_uh = reconstructions[1]->get<DiscreteFunctionDPk<1, const R3>>();
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope =
-                      (1 / 0.2) * (dpk_uh[cell_id](R1{0.1} + xj[cell_id]) - dpk_uh[cell_id](xj[cell_id] - R1{0.1}));
-
-                    max_slope_error = std::max(max_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                auto dpk_Ah = reconstructions[2]->get<DiscreteFunctionDPk<1, const R3x3>>();
-
                 {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R3x3_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                  auto dpk_uh      = reconstructions[1]->get<DiscreteFunctionDPk<1, const R3>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
                 }
 
                 {
-                  double max_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3x3 reconstructed_slope =
-                      (1 / 0.2) * (dpk_Ah[cell_id](R1{0.1} + xj[cell_id]) - dpk_Ah[cell_id](xj[cell_id] - R1{0.1}));
-
-                    R3x3 slops = R3x3{+1.7, +2.1, -0.6,   //
-                                      -2.3, +3.1, -3.6,   //
-                                      +3.1, +2.9, +2.3};
-
-                    max_slope_error = std::max(max_slope_error,   //
-                                               frobeniusNorm(reconstructed_slope - slops));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-13));
+                  auto dpk_Ah      = reconstructions[2]->get<DiscreteFunctionDPk<1, const R3x3>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
                 }
 
-                auto dpk_Vh = reconstructions[3]->get<DiscreteFunctionDPkVector<1, const double>>();
-
                 {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  const TinyVector<3> slope{+1.7, +2.1, -0.6};
-
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R1{0.1} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R1{0.1}));
-
-                      max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                  auto dpk_Vh      = reconstructions[3]->get<DiscreteFunctionDPkVector<1, const double>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
                 }
               }
             }
@@ -515,48 +249,16 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
                 auto& mesh  = *p_mesh;
 
-                auto R_affine = [](const R2& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1]; };
-                auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<double> fh{p_mesh};
+                auto R_exact = [](const R2& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1]; };
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
 
                 auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R2{0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R2{0.1, 0})) / 0.2;
-
-                    max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R2{0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R2{0, 0.1})) / 0.2;
-
-                    max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -571,52 +273,20 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
                 auto& mesh  = *p_mesh;
 
-                auto R3_affine = [](const R2& x) -> R3 {
+                auto R3_exact = [](const R2& x) -> R3 {
                   return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1],   //
                             +1.4 - 0.6 * x[0] + 1.3 * x[1],   //
                             -0.2 + 3.1 * x[0] - 1.1 * x[1]};
                 };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
 
-                DiscreteFunctionP0<R3> uh{p_mesh};
-
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
 
                 auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R3>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R2{0.1, 0} + xj[cell_id]) -
-                                                                dpk_uh[cell_id](xj[cell_id] - R2{0.1, 0}));
-
-                    max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R2{0, 0.1} + xj[cell_id]) -
-                                                                dpk_uh[cell_id](xj[cell_id] - R2{0, 0.1}));
-
-                    max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3, -1.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -631,127 +301,43 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
                 auto& mesh  = *p_mesh;
 
-                auto R2x2_affine = [](const R2& x) -> R2x2 {
+                auto R2x2_exact = [](const R2& x) -> R2x2 {
                   return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1], -1.7 + 2.1 * x[0] - 2.2 * x[1],   //
                               +1.4 - 0.6 * x[0] - 2.1 * x[1], +2.4 - 2.3 * x[0] + 1.3 * x[1]};
                 };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
 
-                DiscreteFunctionP0<R2x2> Ah{p_mesh};
-
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]); });
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
 
-                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R2x2_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R2{0.1, 0} + xj[cell_id]) -
-                                                                  dpk_Ah[cell_id](xj[cell_id] - R2{0.1, 0}));
-
-                    max_x_slope_error =
-                      std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, +2.1,   //
-                                                                                           -0.6, -2.3}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R2{0, 0.1} + xj[cell_id]) -
-                                                                  dpk_Ah[cell_id](xj[cell_id] - R2{0, 0.1}));
-
-                    max_y_slope_error =
-                      std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
-                                                                                           -2.1, +1.3}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
+                auto dpk_Ah      = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
 
           SECTION("vector data")
           {
-            using R4 = TinyVector<4>;
-
             for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
               SECTION(named_mesh.name())
               {
                 auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
                 auto& mesh  = *p_mesh;
 
-                auto vector_affine = [](const R2& x) -> R4 {
-                  return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1], -1.7 + 2.1 * x[0] - 2.2 * x[1],   //
-                            +1.4 - 0.6 * x[0] - 2.1 * x[1], +2.4 - 2.3 * x[0] + 1.3 * x[1]};
-                };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
+                std::array<std::function<double(const R2&)>, 4> vector_exact =
+                  {[](const R2& x) -> double { return +2.3 + 1.7 * x[0] + 1.2 * x[1]; },
+                   [](const R2& x) -> double { return -1.7 + 2.1 * x[0] - 2.2 * x[1]; },
+                   [](const R2& x) -> double { return +1.4 - 0.6 * x[0] - 2.1 * x[1]; },
+                   [](const R2& x) -> double { return +2.4 - 2.3 * x[0] + 1.3 * x[1]; }};
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      Vh[cell_id][i] = vector[i];
-                    }
-                  });
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
 
-                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  const R4 slope{+1.7, +2.1, -0.6, -2.3};
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R2{0.1, 0} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R2{0.1, 0}));
-
-                      max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  const R4 slope{+1.2, -2.2, -2.1, +1.3};
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R2{0, 0.1} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R2{0, 0.1}));
-
-                      max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
+                auto dpk_Vh      = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
               }
             }
           }
@@ -769,62 +355,16 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
                 auto& mesh  = *p_mesh;
 
-                auto R_affine = [](const R3& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]; };
-                auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<double> fh{p_mesh};
+                auto R_exact = [](const R3& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]; };
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
 
                 auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0.1, 0, 0})) /
-                      0.2;
-
-                    max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0.1, 0})) /
-                      0.2;
-
-                    max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_z_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const double reconstructed_slope =
-                      (dpk_fh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0, 0.1})) /
-                      0.2;
-
-                    max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - 2.1));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
               }
             }
           }
@@ -837,63 +377,20 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
                 auto& mesh  = *p_mesh;
 
-                auto R3_affine = [](const R3& x) -> R3 {
+                auto R3_exact = [](const R3& x) -> R3 {
                   return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1] + 1.8 * x[2],   //
                             +1.4 - 0.6 * x[0] + 1.3 * x[1] - 3.7 * x[2],   //
                             -0.2 + 3.1 * x[0] - 1.1 * x[1] + 1.9 * x[2]};
                 };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<R3> uh{p_mesh};
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
 
                 auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
-                                                                dpk_uh[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
-
-                    max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
-                                                                dpk_uh[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
-
-                    max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3, -1.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
-
-                {
-                  double max_z_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
-                                                                dpk_uh[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
-
-                    max_z_slope_error = std::max(max_z_slope_error, l2Norm(reconstructed_slope - R3{1.8, -3.7, 1.9}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
               }
             }
           }
@@ -908,158 +405,47 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
                 auto& mesh  = *p_mesh;
 
-                auto R2x2_affine = [](const R3& x) -> R2x2 {
+                auto R2x2_exact = [](const R3& x) -> R2x2 {
                   return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2],
                               //
                               +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 * x[2]};
                 };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0<R2x2> Ah{p_mesh};
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]); });
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
 
                 descriptor.setRowWeighting(false);
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
 
-                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
-
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R2x2_affine(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
-                                                                  dpk_Ah[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
-
-                    max_x_slope_error =
-                      std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, 2.1,   //
-                                                                                           -2.3, +3.1}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
-                                                                  dpk_Ah[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
-
-                    max_y_slope_error =
-                      std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
-                                                                                           1.3, +0.8}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
-
-                {
-                  double max_z_slope_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
-                                                                  dpk_Ah[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
-
-                    max_z_slope_error =
-                      std::max(max_z_slope_error, frobeniusNorm(reconstructed_slope - R2x2{-1.3, -2.4,   //
-                                                                                           +1.4, -1.8}));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
+                auto dpk_Ah      = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
               }
             }
           }
 
           SECTION("vector data")
           {
-            using R4 = TinyVector<4>;
-
             for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
               SECTION(named_mesh.name())
               {
                 auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
                 auto& mesh  = *p_mesh;
 
-                auto vector_affine = [](const R3& x) -> R4 {
-                  return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2],
-                            //
-                            +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 * x[2]};
-                };
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
+                std::array<std::function<double(const R3&)>, 4> vector_exact =
+                  {[](const R3& x) -> double { return +2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2]; },
+                   [](const R3& x) -> double { return -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2]; },
+                   [](const R3& x) -> double { return +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2]; },
+                   [](const R3& x) -> double { return -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 * x[2]; }};
 
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      Vh[cell_id][i] = vector[i];
-                    }
-                  });
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
 
                 descriptor.setPreconditioning(false);
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
 
                 auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    auto vector = vector_affine(xj[cell_id]);
-                    for (size_t i = 0; i < vector.dimension(); ++i) {
-                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_x_slope_error = 0;
-                  const R4 slope{+1.7, 2.1, -2.3, +3.1};
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0.1, 0, 0} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R3{0.1, 0, 0}));
-
-                      max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
-
-                {
-                  double max_y_slope_error = 0;
-                  const R4 slope{+1.2, -2.2, 1.3, +0.8};
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0.1, 0} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0.1, 0}));
-
-                      max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
-                }
-
-                {
-                  double max_z_slope_error = 0;
-                  const R4 slope{-1.3, -2.4, +1.4, -1.8};
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    for (size_t i = 0; i < slope.dimension(); ++i) {
-                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0, 0.1} + xj[cell_id]) -
-                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0, 0.1}));
-
-                      max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - slope[i]));
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-13));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
               }
             }
           }
@@ -1088,7 +474,7 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
       {
         auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
 
-        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 1,
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, degree,
                                                       std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
                                                         std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
 
@@ -1125,7 +511,7 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
       {
         auto p_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
 
-        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 1,
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, degree,
                                                       std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
                                                         std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
 
@@ -1162,7 +548,7 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
       {
         auto p_mesh = MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
 
-        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 1,
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, degree,
                                                       std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
                                                         std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
 
@@ -1196,61 +582,40 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
       }
     }
 
-    std::vector<PolynomialReconstructionDescriptor> descriptor_list =
-      {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, 1,
-                                          std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
-                                            std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
-       PolynomialReconstructionDescriptor{IntegrationMethodType::element, 1,
-                                          std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
-                                            std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
-       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, 1,
-                                          std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
-                                            std::make_shared<NamedBoundaryDescriptor>("XMIN")}}};
-
-    for (auto descriptor : descriptor_list) {
-      SECTION(name(descriptor.integrationMethodType()))
-      {
-        SECTION("1D")
+    SECTION("1D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}}};
+
+      using R1 = TinyVector<1>;
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
         {
-          using R1 = TinyVector<1>;
-
           SECTION("R^1 data")
           {
             auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
 
             auto& mesh = *p_mesh;
 
-            auto R1_affine = [](const R1& x) { return R1{1.7 * (x[0] + 1)}; };
-            auto xj        = MeshDataManager::instance().getMeshData(mesh).xj();
+            auto R1_exact = [](const R1& x) { return R1{1.7 * (x[0] + 1)}; };
 
-            DiscreteFunctionP0<R1> fh{p_mesh};
-
-            parallel_for(
-              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R1_affine(xj[cell_id]); });
+            DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R1_exact));
 
             auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
 
             auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R1>>();
 
-            {
-              double max_mean_error = 0;
-              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                max_mean_error =
-                  std::max(max_mean_error, std::abs((dpk_fh[cell_id](xj[cell_id]) - R1_affine(xj[cell_id]))[0]));
-              }
-              REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-            }
-
-            {
-              double max_slope_error = 0;
-              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                const double reconstructed_slope =
-                  (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1}))[0] / 0.2;
-
-                max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
-              }
-              REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-            }
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R1_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
           }
 
           SECTION("R1 vector data")
@@ -1261,650 +626,173 @@ TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
                 auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
                 auto& mesh  = *p_mesh;
 
-                auto vector_affine0 = [](const R1& x) -> R1 { return R1{+1.7 * (x[0] + 1)}; };
+                std::array<std::function<R1(const R1&)>, 2> vector_exact   //
+                  = {[](const R1& x) -> R1 { return R1{+1.7 * (x[0] + 1)}; },
+                     [](const R1& x) -> R1 { return R1{-0.3 * (x[0] + 1)}; }};
 
-                auto vector_affine1 = [](const R1& x) -> R1 { return R1{-0.3 * (x[0] + 1)}; };
-
-                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-                DiscreteFunctionP0Vector<R1> Vh{p_mesh, 2};
-
-                parallel_for(
-                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-                    Vh[cell_id][0] = vector_affine0(xj[cell_id]);
-                    Vh[cell_id][1] = vector_affine1(xj[cell_id]);
-                  });
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
 
                 auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
 
                 auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R1>>();
 
-                {
-                  double max_mean_error = 0;
-                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 0)(xj[cell_id]) - vector_affine0(xj[cell_id])));
-                    max_mean_error =
-                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 1)(xj[cell_id]) - vector_affine1(xj[cell_id])));
-                  }
-                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-                }
-
-                {
-                  double max_slope_error = 0;
-                  {
-                    const TinyVector<1> slope0{+1.7};
-
-                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                      for (size_t i = 0; i < R1::Dimension; ++i) {
-                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 0)(R1{0.1} + xj[cell_id])[i] -
-                                                                        dpk_Vh(cell_id, 0)(xj[cell_id] - R1{0.1})[i]);
-
-                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope0[i]));
-                      }
-                    }
-                  }
-
-                  {
-                    const TinyVector<1> slope1{-0.3};
-
-                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-                      for (size_t i = 0; i < R1::Dimension; ++i) {
-                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 1)(R1{0.1} + xj[cell_id])[i] -
-                                                                        dpk_Vh(cell_id, 1)(xj[cell_id] - R1{0.1})[i]);
-
-                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope1[i]));
-                      }
-                    }
-                  }
-                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
-                }
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
               }
             }
           }
         }
+      }
+    }
 
-        SECTION("2D")
+    SECTION("2D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}}};
+
+      using R2 = TinyVector<2>;
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
         {
-#warning not done
-          using R2 = TinyVector<2>;
+          SECTION("R^2 data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+            auto& mesh  = *p_mesh;
 
-          // SECTION("R data")
-          // {
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto R_affine = [](const R2& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1]; };
-          //       auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0<double> fh{p_mesh};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
-
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
-
-          //       auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           max_mean_error =
-          //             std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const double reconstructed_slope =
-          //             (dpk_fh[cell_id](R2{0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R2{0.1, 0})) / 0.2;
-
-          //           max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const double reconstructed_slope =
-          //             (dpk_fh[cell_id](R2{0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R2{0, 0.1})) / 0.2;
-
-          //           max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-          //     }
-          //   }
-          // }
-
-          // SECTION("R^3 data")
-          // {
-          //   using R3 = TinyVector<3>;
-
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto R3_affine = [](const R2& x) -> R3 {
-          //         return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1],   //
-          //                   +1.4 - 0.6 * x[0] + 1.3 * x[1],   //
-          //                   -0.2 + 3.1 * x[0] - 1.1 * x[1]};
-          //       };
-          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0<R3> uh{p_mesh};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
-
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
-
-          //       auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R3>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           max_mean_error =
-          //             std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R2{0.1, 0} + xj[cell_id]) -
-          //                                                       dpk_uh[cell_id](xj[cell_id] - R2{0.1, 0}));
-
-          //           max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7,
-          //           -0.6, 3.1}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R2{0, 0.1} + xj[cell_id]) -
-          //                                                       dpk_uh[cell_id](xj[cell_id] - R2{0, 0.1}));
-
-          //           max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3,
-          //           -1.1}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-          //     }
-          //   }
-          // }
-
-          // SECTION("R^2x2 data")
-          // {
-          //   using R2x2 = TinyMatrix<2, 2>;
-
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto R2x2_affine = [](const R2& x) -> R2x2 {
-          //         return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1], -1.7 + 2.1 * x[0] - 2.2 * x[1],   //
-          //                     +1.4 - 0.6 * x[0] - 2.1 * x[1], +2.4 - 2.3 * x[0] + 1.3 * x[1]};
-          //       };
-          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0<R2x2> Ah{p_mesh};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]);
-          //         });
-
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
-
-          //       auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           max_mean_error =
-          //             std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) -
-          //             R2x2_affine(xj[cell_id])));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R2{0.1, 0} + xj[cell_id]) -
-          //                                                         dpk_Ah[cell_id](xj[cell_id] - R2{0.1, 0}));
-
-          //           max_x_slope_error =
-          //             std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, +2.1,   //
-          //                                                                                  -0.6, -2.3}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R2{0, 0.1} + xj[cell_id]) -
-          //                                                         dpk_Ah[cell_id](xj[cell_id] - R2{0, 0.1}));
-
-          //           max_y_slope_error =
-          //             std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
-          //                                                                                  -2.1, +1.3}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-          //     }
-          //   }
-          // }
-
-          // SECTION("vector data")
-          // {
-          //   using R4 = TinyVector<4>;
-
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto vector_affine = [](const R2& x) -> R4 {
-          //         return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1], -1.7 + 2.1 * x[0] - 2.2 * x[1],   //
-          //                   +1.4 - 0.6 * x[0] - 2.1 * x[1], +2.4 - 2.3 * x[0] + 1.3 * x[1]};
-          //       };
-          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-          //           auto vector = vector_affine(xj[cell_id]);
-          //           for (size_t i = 0; i < vector.dimension(); ++i) {
-          //             Vh[cell_id][i] = vector[i];
-          //           }
-          //         });
-
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
-
-          //       auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           auto vector = vector_affine(xj[cell_id]);
-          //           for (size_t i = 0; i < vector.dimension(); ++i) {
-          //             max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) -
-          //             vector[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         const R4 slope{+1.7, +2.1, -0.6, -2.3};
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           for (size_t i = 0; i < slope.dimension(); ++i) {
-          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R2{0.1, 0} + xj[cell_id]) -
-          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R2{0.1, 0}));
-
-          //             max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         const R4 slope{+1.2, -2.2, -2.1, +1.3};
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           for (size_t i = 0; i < slope.dimension(); ++i) {
-          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R2{0, 0.1} + xj[cell_id]) -
-          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R2{0, 0.1}));
-
-          //             max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-          //     }
-          //   }
-          // }
+            auto R2_exact = [](const R2& x) -> R2 { return R2{2.3 * (x[0] - 2), -1.3 * (x[1] - 1)}; };
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R2_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R2_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R2")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+            auto& mesh  = *p_mesh;
+
+            std::array<std::function<R2(const R2&)>, 2> vector_exact   //
+              = {[](const R2& x) -> R2 {
+                   return R2{+1.7 * (x[0] - 2), -0.6 * (x[1] - 1)};
+                 },
+                 [](const R2& x) -> R2 {
+                   return R2{-2.3 * (x[0] - 2), +1.1 * (x[1] - 1)};
+                 }};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const R2>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
         }
+      }
+    }
 
-        SECTION("3D")
+    SECTION("3D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "ZMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "ZMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "ZMAX")}}};
+
+      using R3 = TinyVector<3>;
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
         {
-          using R3 = TinyVector<3>;
-#warning not done
-
-          // SECTION("R data")
-          // {
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto R_affine = [](const R3& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]; };
-          //       auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0<double> fh{p_mesh};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
-
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
-
-          //       auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           max_mean_error =
-          //             std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const double reconstructed_slope =
-          //             (dpk_fh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0.1, 0, 0})) /
-          //             0.2;
-
-          //           max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const double reconstructed_slope =
-          //             (dpk_fh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0.1, 0})) /
-          //             0.2;
-
-          //           max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_z_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const double reconstructed_slope =
-          //             (dpk_fh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0, 0.1})) /
-          //             0.2;
-
-          //           max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - 2.1));
-          //         }//         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-          //     }
-          //   }
-          // }
-
-          // SECTION("R^3 data")
-          // {
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto R3_affine = [](const R3& x) -> R3 {
-          //         return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1] + 1.8 * x[2],   //
-          //                   +1.4 - 0.6 * x[0] + 1.3 * x[1] - 3.7 * x[2],   //
-          //                   -0.2 + 3.1 * x[0] - 1.1 * x[1] + 1.9 * x[2]};
-          //       };
-          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0<R3> uh{p_mesh};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
-
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
-
-          //       auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           max_mean_error =
-          //             std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
-          //                                                       dpk_uh[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
-
-          //           max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7,
-          //           -0.6, 3.1}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
-          //                                                       dpk_uh[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
-
-          //           max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3,
-          //           -1.1}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-
-          //       {
-          //         double max_z_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
-          //                                                       dpk_uh[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
-
-          //           max_z_slope_error = std::max(max_z_slope_error, l2Norm(reconstructed_slope - R3{1.8,
-          //           -3.7, 1.9}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-          //     }
-          //   }
-          // }
-
-          // SECTION("R^2x2 data")
-          // {
-          //   using R2x2 = TinyMatrix<2, 2>;
-
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto R2x2_affine = [](const R3& x) -> R2x2 {
-          //         return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 *
-          //         x[2],
-          //                     //
-          //                     +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 *
-          //                     x[2]};
-          //       };
-          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0<R2x2> Ah{p_mesh};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]);
-          //         });
-
-          //       descriptor.setRowWeighting(false);
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
-
-          //       auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           max_mean_error =
-          //             std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) -
-          //             R2x2_affine(xj[cell_id])));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
-          //                                                         dpk_Ah[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
-
-          //           max_x_slope_error =
-          //             std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, 2.1,   //
-          //                                                                                  -2.3, +3.1}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
-          //                                                         dpk_Ah[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
-
-          //           max_y_slope_error =
-          //             std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
-          //                                                                                  1.3, +0.8}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-
-          //       {
-          //         double max_z_slope_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
-          //                                                         dpk_Ah[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
-
-          //           max_z_slope_error =
-          //             std::max(max_z_slope_error, frobeniusNorm(reconstructed_slope - R2x2{-1.3, -2.4,   //
-          //                                                                                  +1.4, -1.8}));
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-          //     }
-          //   }
-          // }
-
-          // SECTION("vector data")
-          // {
-          //   using R4 = TinyVector<4>;
-
-          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
-          //     SECTION(named_mesh.name())
-          //     {
-          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
-          //       auto& mesh  = *p_mesh;
-
-          //       auto vector_affine = [](const R3& x) -> R4 {
-          //         return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2],
-          //                   //
-          //                   +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 *
-          //                   x[2]};
-          //       };
-          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
-
-          //       DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
-
-          //       parallel_for(
-          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
-          //           auto vector = vector_affine(xj[cell_id]);
-          //           for (size_t i = 0; i < vector.dimension(); ++i) {
-          //             Vh[cell_id][i] = vector[i];
-          //           }
-          //         });
-
-          //       descriptor.setPreconditioning(false);
-          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
-
-          //       auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
-
-          //       {
-          //         double max_mean_error = 0;
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           auto vector = vector_affine(xj[cell_id]);
-          //           for (size_t i = 0; i < vector.dimension(); ++i) {
-          //             max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) -
-          //             vector[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
-          //       }
-
-          //       {
-          //         double max_x_slope_error = 0;
-          //         const R4 slope{+1.7, 2.1, -2.3, +3.1};
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           for (size_t i = 0; i < slope.dimension(); ++i) {
-          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0.1, 0, 0} + xj[cell_id])
-          //             -
-          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R3{0.1, 0,
-          //                                                             0}));
-
-          //             max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-
-          //       {
-          //         double max_y_slope_error = 0;
-          //         const R4 slope{+1.2, -2.2, 1.3, +0.8};
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           for (size_t i = 0; i < slope.dimension(); ++i) {
-          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0.1, 0} + xj[cell_id])
-          //             -
-          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0.1,
-          //                                                             0}));
-
-          //             max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
-          //       }
-
-          //       {
-          //         double max_z_slope_error = 0;
-          //         const R4 slope{-1.3, -2.4, +1.4, -1.8};
-          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
-          //           for (size_t i = 0; i < slope.dimension(); ++i) {
-          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0, 0.1} + xj[cell_id])
-          //             -
-          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0,
-          //                                                             0.1}));
-
-          //             max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - slope[i]));
-          //           }
-          //         }
-          //         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-13));
-          //       }
-          //     }
-          //   }
-          // }
+          SECTION("R^3 data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+            auto& mesh  = *p_mesh;
+
+            auto R3_exact = [](const R3& x) -> R3 { return R3{2.3 * (x[0] - 2), -1.3 * (x[1] - 1), 1.4 * (x[2] - 1)}; };
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R3")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+            auto& mesh  = *p_mesh;
+
+            std::array<std::function<R3(const R3&)>, 2> vector_exact   //
+              = {[](const R3& x) -> R3 {
+                   return R3{+1.7 * (x[0] - 2), -0.6 * (x[1] - 1), +1.2 * (x[2] - 1)};
+                 },
+                 [](const R3& x) -> R3 {
+                   return R3{-2.3 * (x[0] - 2), +1.1 * (x[1] - 1), -0.3 * (x[2] - 1)};
+                 }};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const R3>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
         }
       }
     }
diff --git a/tests/test_PolynomialReconstruction_degree_2.cpp b/tests/test_PolynomialReconstruction_degree_2.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a1c5c058975c17f9a5ec7e40c41f28b9abb18a66
--- /dev/null
+++ b/tests/test_PolynomialReconstruction_degree_2.cpp
@@ -0,0 +1,1043 @@
+#include <catch2/catch_approx.hpp>
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <utils/PugsAssert.hpp>
+
+#include <mesh/Mesh.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <scheme/DiscreteFunctionDPkVariant.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionVariant.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <DiscreteFunctionDPkForTests.hpp>
+#include <MeshDataBaseForTests.hpp>
+
+#include <NbGhostLayersTester.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("PolynomialReconstruction_degree_2", "[scheme]")
+{
+  constexpr size_t degree = 2;
+
+  constexpr size_t nb_ghost_layers = 2;
+  NbGhostLayersTester t{nb_ghost_layers};
+
+  SECTION("without symmetries")
+  {
+    std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+      {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree}};
+
+    for (auto descriptor : descriptor_list) {
+      SECTION(name(descriptor.integrationMethodType()))
+      {
+        SECTION("1D")
+        {
+          using R1 = TinyVector<1>;
+
+          auto p0 = [](const R1& x) { return +2.3 + 1.7 * x[0] - 2.3 * x[0] * x[0]; };
+          auto p1 = [](const R1& x) { return -1.7 + 2.1 * x[0] + 1.2 * x[0] * x[0]; };
+          auto p2 = [](const R1& x) { return +1.4 - 0.6 * x[0] - 2.0 * x[0] * x[0]; };
+          auto p3 = [](const R1& x) { return +2.4 - 2.3 * x[0] + 1.1 * x[0] * x[0]; };
+          auto p4 = [](const R1& x) { return -0.2 + 3.1 * x[0] - 0.7 * x[0] * x[0]; };
+          auto p5 = [](const R1& x) { return -3.2 - 3.6 * x[0] + 0.1 * x[0] * x[0]; };
+          auto p6 = [](const R1& x) { return -4.1 + 3.1 * x[0] - 0.2 * x[0] * x[0]; };
+          auto p7 = [](const R1& x) { return +0.8 + 2.9 * x[0] + 4.1 * x[0] * x[0]; };
+          auto p8 = [](const R1& x) { return -1.6 + 2.3 * x[0] - 1.7 * x[0] * x[0]; };
+          auto p9 = [](const R1& x) { return +2.3 + 1.7 * x[0] - 1.4 * x[0] * x[0]; };
+
+          SECTION("R data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_exact = [&](const R1& x) -> R3 { return R3{p2(x), p4(x), p1(x)}; };
+
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+              }
+            }
+          }
+
+          SECTION("R^3x3 data")
+          {
+            using R3x3 = TinyMatrix<3, 3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3x3_exact = [&](const R1& x) -> R3x3 {
+                  return R3x3{p1(x), p2(x), p3(x),   //
+                              p4(x), p5(x), p6(x),   //
+                              p7(x), p8(x), p9(x)};
+                };
+
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3x3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+              }
+            }
+          }
+
+          SECTION("R vector data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<double(const R1&)>, 3> vector_exact = {p1, p7, p9};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+                auto dpk_Vh          = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+              }
+            }
+          }
+
+          SECTION("R3 vector data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<R3(const R1&)>, 3> vector_exact   //
+                  = {[&](const R1& x) -> R3 {
+                       return R3{p1(x), p2(x), p3(x)};
+                     },
+                     [&](const R1& x) -> R3 {
+                       return R3{p5(x), p7(x), p0(x)};
+                     },
+                     [&](const R1& x) -> R3 {
+                       return R3{p9(x), p8(x), p4(x)};
+                     }};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+              }
+            }
+          }
+
+          SECTION("list of various types")
+          {
+            using R3x3 = TinyMatrix<3>;
+            using R3   = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                auto R3_exact = [&](const R1& x) -> R3 { return R3{p9(x), p4(x), p7(x)}; };
+
+                auto R3x3_exact = [&](const R1& x) -> R3x3 {
+                  return R3x3{p2(x), p1(x), p0(x),   //
+                              p3(x), p2(x), p4(x),   //
+                              p6(x), p5(x), p9(x)};
+                };
+
+                std::array<std::function<double(const R1&)>, 3> vector_exact = {p1, p8, p7};
+
+                DiscreteFunctionP0 fh       = test_only::exact_projection(mesh, degree, std::function(R_exact));
+                DiscreteFunctionP0 uh       = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+                DiscreteFunctionP0 Ah       = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions =
+                  PolynomialReconstruction{descriptor}.build(std::make_shared<DiscreteFunctionVariant>(fh), uh,
+                                                             std::make_shared<DiscreteFunctionP0<R3x3>>(Ah),
+                                                             DiscreteFunctionVariant(Vh));
+
+                {
+                  auto dpk_fh      = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  auto dpk_uh      = reconstructions[1]->get<DiscreteFunctionDPk<1, const R3>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  auto dpk_Ah      = reconstructions[2]->get<DiscreteFunctionDPk<1, const R3x3>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  auto dpk_Vh      = reconstructions[3]->get<DiscreteFunctionDPkVector<1, const double>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+        }
+
+        SECTION("2D")
+        {
+          using R2 = TinyVector<2>;
+          auto p0  = [](const R2& x) {
+            return +2.3 + 1.7 * x[0] - 1.3 * x[1] + 1.2 * x[0] * x[0] + 1.3 * x[0] * x[1] - 3.2 * x[1] * x[1];
+          };
+
+          auto p1 = [](const R2& x) {
+            return +2.3 + 1.7 * x[0] - 2.2 * x[1] - 2.1 * x[0] * x[0] - 2.3 * x[0] * x[1] - 3.2 * x[1] * x[1];
+          };
+          auto p2 = [](const R2& x) {
+            return +1.4 - 0.6 * x[0] + 1.3 * x[1] + 2.3 * x[0] * x[0] - 1.3 * x[0] * x[1] + 1.2 * x[1] * x[1];
+          };
+          auto p3 = [](const R2& x) {
+            return -0.2 + 3.1 * x[0] - 1.1 * x[1] - 2.1 * x[0] * x[0] + 1.3 * x[0] * x[1] - 1.1 * x[1] * x[1];
+          };
+
+          SECTION("R data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_exact = [&](const R2& x) -> R3 { return R3{p1(x), p2(x), p3(x)}; };
+
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^2x2 data")
+          {
+            using R2x2 = TinyMatrix<2, 2>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R2x2_exact = [&](const R2& x) -> R2x2 {
+                  return R2x2{p0(x), p1(x),   //
+                              p2(x), p3(x)};
+                };
+
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah      = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("vector data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<double(const R2&)>, 4> vector_exact = {p0, p1, p2, p3};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh      = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+        }
+
+        SECTION("3D")
+        {
+          using R3 = TinyVector<3>;
+
+          auto p0 = [](const R3& x) {
+            return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]                    //
+                   + 1.7 * x[0] * x[0] + 1.4 * x[1] * x[1] + 1.7 * x[2] * x[2]   //
+                   - 2.3 * x[0] * x[1] + 1.6 * x[0] * x[2] - 1.9 * x[1] * x[2];
+          };
+
+          auto p1 = [](const R3& x) {
+            return +2.3 + 1.7 * x[0] - 2.2 * x[1] + 1.8 * x[2]                   //
+                   + 1.7 * x[0] * x[0] - 2.4 * x[1] * x[1] - 2.3 * x[2] * x[2]   //
+                   - 2.1 * x[0] * x[1] + 2.6 * x[0] * x[2] + 1.6 * x[1] * x[2];
+          };
+
+          auto p2 = [](const R3& x) {
+            return +1.4 - 0.6 * x[0] + 1.3 * x[1] - 3.7 * x[2]                   //
+                   + 3.1 * x[0] * x[0] - 1.1 * x[1] * x[1] + 1.7 * x[2] * x[2]   //
+                   - 2.3 * x[0] * x[1] - 2.6 * x[0] * x[2] - 1.9 * x[1] * x[2];
+          };
+
+          auto p3 = [](const R3& x) {
+            return -0.2 + 3.1 * x[0] - 1.1 * x[1] + 1.9 * x[2]                   //
+                   - 1.5 * x[0] * x[0] + 1.4 * x[1] * x[1] - 1.2 * x[2] * x[2]   //
+                   - 1.7 * x[0] * x[1] - 1.3 * x[0] * x[2] + 2.1 * x[1] * x[2];
+          };
+
+          SECTION("R data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_exact = [&](const R3& x) -> R3 { return R3{p1(x), p2(x), p3(x)}; };
+
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^2x2 data")
+          {
+            using R2x2 = TinyMatrix<2, 2>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                auto R2x2_exact = [&](const R3& x) -> R2x2 {
+                  return R2x2{p0(x), p1(x),   //
+                              p2(x), p3(x)};
+                };
+
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
+
+                descriptor.setRowWeighting(false);
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah      = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("vector data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<double(const R3&)>, 4> vector_exact = {p0, p1, p2, p3};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                descriptor.setPreconditioning(false);
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("with symmetries")
+  {
+    SECTION("1D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}}};
+      using R1 = TinyVector<1>;
+
+      auto p0 = [](const R1& x) { return +1.7 * (x[0] + 1) * (x[0] + 1) - 1.1; };
+      auto p1 = [](const R1& x) { return -1.2 * (x[0] + 1) * (x[0] + 1) + 1.3; };
+      auto p2 = [](const R1& x) { return +1.4 * (x[0] + 1) * (x[0] + 1) - 0.6; };
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
+        {
+          SECTION("R data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+            auto& mesh = *p_mesh;
+
+            auto R_exact = p0;
+
+            DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R1x1 data")
+          {
+            using R1x1 = TinyMatrix<1>;
+
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+            auto& mesh = *p_mesh;
+
+            auto R1x1_exact = [&](const R1& x) { return R1x1{p0(x)}; };
+
+            DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R1x1_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R1x1>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R1x1_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R vector data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+            auto& mesh  = *p_mesh;
+
+            std::array<std::function<double(const R1&)>, 3> vector_exact   //
+              = {p0, p1, p2};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R1x1 vector data")
+          {
+            using R1x1 = TinyMatrix<1>;
+
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+            auto& mesh  = *p_mesh;
+
+            std::array<std::function<R1x1(const R1&)>, 3> vector_exact   //
+              = {[&](const R1& x) { return R1x1{p2(x)}; },               //
+                 [&](const R1& x) { return R1x1{p0(x)}; },               //
+                 [&](const R1& x) { return R1x1{p1(x)}; }};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R1x1>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}}};
+
+      using R2 = TinyVector<2>;
+
+      auto p_initial_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+      auto& initial_mesh  = *p_initial_mesh;
+
+      constexpr double theta = 1;
+      TinyMatrix<2> T{std::cos(theta), -std::sin(theta),   //
+                      std::sin(theta), std::cos(theta)};
+
+      auto xr = initial_mesh.xr();
+
+      NodeValue<R2> new_xr{initial_mesh.connectivity()};
+      parallel_for(
+        initial_mesh.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { new_xr[node_id] = T * xr[node_id]; });
+
+      std::shared_ptr p_mesh = std::make_shared<const Mesh<2>>(initial_mesh.shared_connectivity(), new_xr);
+      const Mesh<2>& mesh    = *p_mesh;
+      // inverse rotation
+      TinyMatrix<2> inv_T{std::cos(theta), std::sin(theta),   //
+                          -std::sin(theta), std::cos(theta)};
+
+      auto p0 = [&inv_T](const R2& X) {
+        const R2 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        return +1.7 * x * x + 2 * y * y - 1.1;
+      };
+
+      auto p1 = [&inv_T](const R2& X) {
+        const R2 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        return -1.3 * x * x - 0.2 * y * y + 0.7;
+      };
+
+      auto p2 = [&inv_T](const R2& X) {
+        const R2 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        return +2.6 * x * x - 1.4 * y * y - 1.9;
+      };
+
+      auto p3 = [&inv_T](const R2& X) {
+        const R2 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        return -0.6 * x * x + 1.4 * y * y + 2.3;
+      };
+
+      auto q0 = [&inv_T](const R2& X) {
+        const R2 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        return 3 * x * y;
+      };
+
+      auto q1 = [&inv_T](const R2& X) {
+        const R2 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        return -1.3 * x * y;
+      };
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
+        {
+          SECTION("R data")
+          {
+            auto R_exact = p0;
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R2x2 data")
+          {
+            using R2x2      = TinyMatrix<2>;
+            auto R2x2_exact = [&](const R2& X) -> R2x2 {
+              return T * TinyMatrix<2>{p0(X), q0(X), q1(X), p1(X)} * inv_T;
+            };
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R2x2_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R")
+          {
+            std::array<std::function<double(const R2&)>, 4> vector_exact   //
+              = {p0, p1, p2, p3};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R2x2")
+          {
+            using R2x2 = TinyMatrix<2>;
+
+            std::array<std::function<R2x2(const R2&)>, 2> vector_R2x2_exact   //
+              = {[&](const R2& X) {
+                   return T * R2x2{p0(X), q0(X), q1(X), p1(X)} * inv_T;
+                 },
+                 [&](const R2& X) {
+                   return T * R2x2{p0(X), q1(X), 0, p1(X)} * inv_T;
+                 }};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_R2x2_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const R2x2>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_R2x2_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("list")
+          {
+            using R2x2 = TinyMatrix<2>;
+
+            auto R_exact = p0;
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+            std::array<std::function<double(const R2&)>, 4> vector_exact   //
+              = {p0, p1, p2, p3};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            std::array<std::function<R2x2(const R2&)>, 2> vector_R2x2_exact   //
+              = {[&](const R2& X) {
+                   return T * R2x2{p0(X), q0(X), q1(X), p1(X)} * inv_T;
+                 },
+                 [&](const R2& X) {
+                   return T * R2x2{p2(X), q1(X), 0, p3(X)} * inv_T;
+                 }};
+
+            DiscreteFunctionP0Vector Wh = test_only::exact_projection(mesh, degree, vector_R2x2_exact);
+
+            auto R2x2_exact = [&](const R2& X) -> R2x2 {
+              return T * TinyMatrix<2>{p0(X), q1(X), q0(X), p3(X)} * inv_T;
+            };
+
+            DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh, Vh, Wh, Ah);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
+            auto dpk_Vh = reconstructions[1]->get<DiscreteFunctionDPkVector<2, const double>>();
+            auto dpk_Wh = reconstructions[2]->get<DiscreteFunctionDPkVector<2, const R2x2>>();
+            auto dpk_Ah = reconstructions[3]->get<DiscreteFunctionDPk<2, const R2x2>>();
+
+            {
+              double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R_exact));
+              REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+            }
+            {
+              double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+              REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+            }
+            {
+              double max_error = test_only::max_reconstruction_error(mesh, dpk_Wh, vector_R2x2_exact);
+              REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+            }
+            {
+              double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+              REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+            }
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      using R3 = TinyVector<3>;
+
+      auto p_initial_mesh = MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+      auto& initial_mesh  = *p_initial_mesh;
+
+      constexpr double theta = 1;
+      TinyMatrix<3> T{std::cos(theta), -std::sin(theta), 0,
+                      //
+                      std::sin(theta), std::cos(theta), 0,
+                      //
+                      0, 0, 1};
+
+      auto xr = initial_mesh.xr();
+
+      NodeValue<R3> new_xr{initial_mesh.connectivity()};
+      parallel_for(
+        initial_mesh.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { new_xr[node_id] = T * xr[node_id]; });
+
+      std::shared_ptr p_mesh = std::make_shared<const Mesh<3>>(initial_mesh.shared_connectivity(), new_xr);
+      const Mesh<3>& mesh    = *p_mesh;
+      // inverse rotation
+      TinyMatrix<3> inv_T{std::cos(theta), std::sin(theta), 0,
+                          //
+                          -std::sin(theta), std::cos(theta), 0,
+                          //
+                          0, 0, 1};
+
+      auto p0 = [&inv_T](const R3& X) {
+        const R3 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        const double z = Y[2] - 1;
+        return +1.7 * x * x + 2 * y * y + 1.3 * z * z - 1.1;
+      };
+
+      auto p1 = [&inv_T](const R3& X) {
+        const R3 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        const double z = Y[2] - 1;
+        return +2.1 * x * x - 1.4 * y * y - 3.1 * z * z + 2.2;
+      };
+
+      auto p2 = [&inv_T](const R3& X) {
+        const R3 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        const double z = Y[2] - 1;
+        return -1.1 * x * x - 1.2 * y * y + 1.3 * z * z - 1.7;
+      };
+
+      auto p3 = [&inv_T](const R3& X) {
+        const R3 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        const double z = Y[2] - 1;
+        return 1.9 * x * x + 2.1 * y * y - 3.1 * z * z + 1.6;
+      };
+
+      auto p4 = [&inv_T](const R3& X) {
+        const R3 Y     = inv_T * X;
+        const double x = Y[0] - 2;
+        const double y = Y[1] - 1;
+        const double z = Y[2] - 1;
+        return -2.4 * x * x + 3.3 * y * y - 1.7 * z * z + 2.1;
+      };
+
+      SECTION("3 symmetries")
+      {
+        std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+          {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                              std::vector<std::shared_ptr<
+                                                const IBoundaryDescriptor>>{std::
+                                                                              make_shared<NamedBoundaryDescriptor>(
+                                                                                "XMAX"),
+                                                                            std::make_shared<NamedBoundaryDescriptor>(
+                                                                              "YMAX"),
+                                                                            std::make_shared<NamedBoundaryDescriptor>(
+                                                                              "ZMAX")}},
+           PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                              std::vector<std::shared_ptr<
+                                                const IBoundaryDescriptor>>{std::
+                                                                              make_shared<NamedBoundaryDescriptor>(
+                                                                                "XMAX"),
+                                                                            std::make_shared<NamedBoundaryDescriptor>(
+                                                                              "YMAX"),
+                                                                            std::make_shared<NamedBoundaryDescriptor>(
+                                                                              "ZMAX")}}};
+
+        for (auto descriptor : descriptor_list) {
+          SECTION("R data")
+          {
+            auto R_exact = p0;
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R")
+          {
+            std::array<std::function<double(const R3&)>, 4> vector_exact   //
+              = {p0, p1, p2, p3};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+        }
+      }
+
+      SECTION("1 symmetry")
+      {
+        // Matrix and their transformations are kept simple to
+        // derive exact solutions
+        std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+          {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                              std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                std::make_shared<NamedBoundaryDescriptor>("XMAX")}},
+           PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                              std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                std::make_shared<NamedBoundaryDescriptor>("XMAX")}}};
+        for (auto descriptor : descriptor_list) {
+          SECTION(name(descriptor.integrationMethodType()))
+          {
+            SECTION("R3x3 data")
+            {
+              // Matrix and their transformations are kept simple to
+              // derive exact solutions
+
+              using R3x3      = TinyMatrix<3>;
+              auto R2x2_exact = [&](const R3& X) -> R3x3 {
+                return T * TinyMatrix<3>{p0(X), 0,     0,       //
+                                         0,     p1(X), p2(X),   //
+                                         0,     p3(X), p4(X)} *
+                       inv_T;
+              };
+
+              DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
+
+              auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+              auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3x3>>();
+
+              double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+              REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+            }
+
+            SECTION("vector of R3x3")
+            {
+              using R3x3 = TinyMatrix<3>;
+
+              std::array<std::function<R3x3(const R3&)>, 2> vector_R3x3_exact   //
+                = {[&](const R3& X) {
+                     return T * R3x3{p0(X), 0,     0,       //
+                                     0,     p1(X), p2(X),   //
+                                     0,     p3(X), p4(X)} *
+                            inv_T;
+                   },
+                   [&](const R3& X) {
+                     return T * R3x3{p0(X), 0,     0,       //
+                                     0,     p2(X), p4(X),   //
+                                     0,     p3(X), p1(X)} *
+                            inv_T;
+                   }};
+
+              DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_R3x3_exact);
+
+              auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+              auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const R3x3>>();
+
+              double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_R3x3_exact);
+              REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+            }
+
+            SECTION("list")
+            {
+              using R3x3 = TinyMatrix<3>;
+
+              auto R_exact = p0;
+
+              DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+              std::array<std::function<double(const R3&)>, 4> vector_exact   //
+                = {p0, p1, p2, p3};
+
+              DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+              std::array<std::function<R3x3(const R3&)>, 2> vector_R3x3_exact   //
+                = {[&](const R3& X) {
+                     return T * R3x3{p1(X), 0,     0,       //
+                                     0,     p3(X), p0(X),   //
+                                     0,     p2(X), p4(X)} *
+                            inv_T;
+                   },
+                   [&](const R3& X) {
+                     return T * R3x3{p2(X), 0,     0,       //
+                                     0,     p0(X), p1(X),   //
+                                     0,     p3(X), p4(X)} *
+                            inv_T;
+                   }};
+
+              DiscreteFunctionP0Vector Wh = test_only::exact_projection(mesh, degree, vector_R3x3_exact);
+
+              auto R3x3_exact = [&](const R3& X) -> R3x3 {
+                return T * R3x3{p0(X), 0,     0,       //
+                                0,     p1(X), p2(X),   //
+                                0,     p3(X), p4(X)} *
+                       inv_T;
+              };
+
+              DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
+
+              auto reconstructions = PolynomialReconstruction{descriptor}.build(uh, Vh, Wh, Ah);
+
+              auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+              auto dpk_Vh = reconstructions[1]->get<DiscreteFunctionDPkVector<3, const double>>();
+              auto dpk_Wh = reconstructions[2]->get<DiscreteFunctionDPkVector<3, const R3x3>>();
+              auto dpk_Ah = reconstructions[3]->get<DiscreteFunctionDPk<3, const R3x3>>();
+
+              {
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+              {
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+              {
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Wh, vector_R3x3_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+              {
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_PolynomialReconstruction_degree_3.cpp b/tests/test_PolynomialReconstruction_degree_3.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e20fa89a3bdcba29f61a771deacba273c536f28b
--- /dev/null
+++ b/tests/test_PolynomialReconstruction_degree_3.cpp
@@ -0,0 +1,766 @@
+#include <catch2/catch_approx.hpp>
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <utils/PugsAssert.hpp>
+
+#include <mesh/Mesh.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <scheme/DiscreteFunctionDPkVariant.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionVariant.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <mesh/CartesianMeshBuilder.hpp>
+
+#include <DiscreteFunctionDPkForTests.hpp>
+#include <MeshDataBaseForTests.hpp>
+
+#include <NbGhostLayersTester.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("PolynomialReconstruction_degree_3", "[scheme]")
+{
+  constexpr size_t degree = 3;
+
+  constexpr size_t nb_ghost_layers = 3;
+  NbGhostLayersTester t{nb_ghost_layers};
+
+  SECTION("without symmetries")
+  {
+    std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+      {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree}};
+
+    for (auto descriptor : descriptor_list) {
+      SECTION(name(descriptor.integrationMethodType()))
+      {
+        SECTION("1D")
+        {
+          using R1 = TinyVector<1>;
+
+          auto p0 = [](const R1& X) {
+            const double x = X[0];
+            return +2.3 + (1.4 + (1.7 - 2.3 * x) * x) * x;
+          };
+          auto p1 = [](const R1& X) {
+            const double x = X[0];
+            return -1.2 - (2.3 - (1.1 + 2.1 * x) * x) * x;
+          };
+          auto p2 = [](const R1& X) {
+            const double x = X[0];
+            return +2.1 + (2.1 + (3.1 - 1.7 * x) * x) * x;
+          };
+          auto p3 = [](const R1& X) {
+            const double x = X[0];
+            return -1.7 + (1.4 + (1.6 - 3.1 * x) * x) * x;
+          };
+          auto p4 = [](const R1& X) {
+            const double x = X[0];
+            return +1.1 - (1.3 - (2.1 + 1.5 * x) * x) * x;
+          };
+          auto p5 = [](const R1& X) {
+            const double x = X[0];
+            return +1.9 - (2.1 + (1.6 - 2.7 * x) * x) * x;
+          };
+          auto p6 = [](const R1& X) {
+            const double x = X[0];
+            return -0.7 + (1.4 + (2.1 + 1.1 * x) * x) * x;
+          };
+          auto p7 = [](const R1& X) {
+            const double x = X[0];
+            return -1.4 - (1.2 + (1.5 - 2.1 * x) * x) * x;
+          };
+          auto p8 = [](const R1& X) {
+            const double x = X[0];
+            return -2.1 + (1.1 - (1.7 + 1.2 * x) * x) * x;
+          };
+          auto p9 = [](const R1& X) {
+            const double x = X[0];
+            return +1.8 - (3.1 + (2.1 - 2.4 * x) * x) * x;
+          };
+
+          SECTION("R data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_exact = [&](const R1& x) -> R3 { return R3{p2(x), p4(x), p1(x)}; };
+
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^3x3 data")
+          {
+            using R3x3 = TinyMatrix<3, 3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3x3_exact = [&](const R1& x) -> R3x3 {
+                  return R3x3{p1(x), p2(x), p3(x),   //
+                              p4(x), p5(x), p6(x),   //
+                              p7(x), p8(x), p9(x)};
+                };
+
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3x3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R vector data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<double(const R1&)>, 3> vector_exact = {p1, p7, p9};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+                auto dpk_Vh          = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R3 vector data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<R3(const R1&)>, 3> vector_exact   //
+                  = {[&](const R1& x) -> R3 { return R3{p1(x), p2(x), p3(x)}; },
+                     [&](const R1& x) -> R3 { return R3{p5(x), p7(x), p0(x)}; },
+                     [&](const R1& x) -> R3 { return R3{p9(x), p8(x), p4(x)}; }};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("list of various types")
+          {
+            using R3x3 = TinyMatrix<3>;
+            using R3   = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                auto R3_exact = [&](const R1& x) -> R3 { return R3{p9(x), p4(x), p7(x)}; };
+
+                auto R3x3_exact = [&](const R1& x) -> R3x3 {
+                  return R3x3{p2(x), p1(x), p0(x),   //
+                              p3(x), p2(x), p4(x),   //
+                              p6(x), p5(x), p9(x)};
+                };
+
+                std::array<std::function<double(const R1&)>, 3> vector_exact = {p1, p8, p7};
+
+                DiscreteFunctionP0 fh       = test_only::exact_projection(mesh, degree, std::function(R_exact));
+                DiscreteFunctionP0 uh       = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+                DiscreteFunctionP0 Ah       = test_only::exact_projection(mesh, degree, std::function(R3x3_exact));
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+                auto reconstructions =
+                  PolynomialReconstruction{descriptor}.build(std::make_shared<DiscreteFunctionVariant>(fh), uh,
+                                                             std::make_shared<DiscreteFunctionP0<R3x3>>(Ah),
+                                                             DiscreteFunctionVariant(Vh));
+
+                {
+                  auto dpk_fh      = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  auto dpk_uh      = reconstructions[1]->get<DiscreteFunctionDPk<1, const R3>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  auto dpk_Ah      = reconstructions[2]->get<DiscreteFunctionDPk<1, const R3x3>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R3x3_exact));
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  auto dpk_Vh      = reconstructions[3]->get<DiscreteFunctionDPkVector<1, const double>>();
+                  double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                  REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+                }
+              }
+            }
+          }
+        }
+
+        SECTION("2D")
+        {
+          using R2 = TinyVector<2>;
+          auto p0  = [](const R2& X) {
+            const double x   = X[0];
+            const double y   = X[1];
+            const double x2  = x * x;
+            const double xy  = x * y;
+            const double y2  = y * y;
+            const double x3  = x2 * x;
+            const double x2y = x2 * y;
+            const double xy2 = x * y2;
+            const double y3  = y * y2;
+
+            return +2.3                               //
+                   + 1.7 * x - 1.3 * y                //
+                   + 1.2 * x2 + 1.3 * xy - 3.2 * y2   //
+                   - 1.3 * x3 + 2.1 * x2y - 1.6 * xy2 + 2.1 * y3;
+          };
+
+          auto p1 = [](const R2& X) {
+            const double x   = X[0];
+            const double y   = X[1];
+            const double x2  = x * x;
+            const double xy  = x * y;
+            const double y2  = y * y;
+            const double x3  = x2 * x;
+            const double x2y = x2 * y;
+            const double xy2 = x * y2;
+            const double y3  = y * y2;
+
+            return +1.4                               //
+                   + 1.6 * x - 2.1 * y                //
+                   + 1.3 * x2 + 2.6 * xy - 1.4 * y2   //
+                   - 1.2 * x3 - 1.7 * x2y + 2.1 * xy2 - 2.2 * y3;
+          };
+
+          auto p2 = [](const R2& X) {
+            const double x   = X[0];
+            const double y   = X[1];
+            const double x2  = x * x;
+            const double xy  = x * y;
+            const double y2  = y * y;
+            const double x3  = x2 * x;
+            const double x2y = x2 * y;
+            const double xy2 = x * y2;
+            const double y3  = y * y2;
+
+            return -1.2                               //
+                   + 2.3 * x - 1.6 * y                //
+                   - 1.2 * x2 + 2.4 * xy - 1.9 * y2   //
+                   + 0.9 * x3 + 1.5 * x2y - 2.3 * xy2 - 1.6 * y3;
+          };
+
+          auto p3 = [](const R2& X) {
+            const double x   = X[0];
+            const double y   = X[1];
+            const double x2  = x * x;
+            const double xy  = x * y;
+            const double y2  = y * y;
+            const double x3  = x2 * x;
+            const double x2y = x2 * y;
+            const double xy2 = x * y2;
+            const double y3  = y * y2;
+
+            return +2.4                               //
+                   + 2.5 * x + 1.4 * y                //
+                   - 2.7 * x2 + 1.9 * xy - 2.2 * y2   //
+                   - 1.3 * x3 + 2.3 * x2y - 1.4 * xy2 + 2.2 * y3;
+          };
+
+          SECTION("R data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R_exact = p0;
+
+                DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree + 1, std::function(R_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_exact = [&](const R2& x) -> R3 { return R3{p1(x), p2(x), p3(x)}; };
+
+                DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree + 1, std::function(R3_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R3>>();
+
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("R^2x2 data")
+          {
+            using R2x2 = TinyMatrix<2, 2>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R2x2_exact = [&](const R2& x) -> R2x2 {
+                  return R2x2{p0(x), p1(x),   //
+                              p2(x), p3(x)};
+                };
+
+                DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree + 1, std::function(R2x2_exact));
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah      = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+
+          SECTION("vector data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                std::array<std::function<double(const R2&)>, 4> vector_exact = {p0, p1, p2, p3};
+
+                DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree + 1, vector_exact);
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh      = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
+                double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+                REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+              }
+            }
+          }
+        }
+
+        SECTION("3D")
+        {
+          using R3 = TinyVector<3>;
+
+          auto p = [](const R3& X, const std::array<double, 20>& a) -> double {
+            const double x   = X[0];
+            const double y   = X[1];
+            const double z   = X[2];
+            const double xy  = x * y;
+            const double xz  = x * z;
+            const double yz  = y * z;
+            const double x2  = x * x;
+            const double y2  = y * y;
+            const double z2  = z * z;
+            const double xyz = x * y * z;
+            const double x2y = x2 * y;
+            const double x2z = x2 * z;
+            const double xy2 = x * y2;
+            const double y2z = y2 * z;
+            const double xz2 = x * z2;
+            const double yz2 = y * z2;
+            const double x3  = x2 * x;
+            const double y3  = y2 * y;
+            const double z3  = z2 * z;
+
+            return a[0] + a[1] * x + a[2] * y + a[3] * z       //
+                   + a[4] * x2 + a[5] * y2 + a[6] * z2         //
+                   + a[7] * xy + a[8] * xz + a[9] * yz         //
+                   + a[10] * x3 + a[11] * y3 + a[12] * z3      //
+                   + a[13] * x2y + a[14] * x2z + a[15] * xy2   //
+                   + a[16] * y2z + a[17] * xz2 + a[18] * yz2   //
+                   + a[19] * xyz                               //
+              ;
+          };
+
+          constexpr std::array<double, 20> a0 = {+2.3, +1.7, -1.3, +2.1, +1.7, +1.4, +1.7, -2.3, +1.6, -1.9,
+                                                 +1.2, -2.1, -1.1, -1.7, -1.3, +0.9, -0.7, +1.5, -0.7, +2.8};
+
+          auto p0 = [&p, &a0](const R3& X) -> double { return p(X, a0); };
+
+          constexpr std::array<double, 20> a1 = {-1.3, +2.2, +0.1, -2.5, +0.2, -2.3, -1.4, +0.9, +0.2, -0.3,
+                                                 +2.4, -1.2, +1.7, -2.2, +0.6, +1.9, +1.0, -0.8, +2.4, +2.4};
+
+          auto p1 = [&p, &a1](const R3& X) -> double { return p(X, a1); };
+
+          constexpr std::array<double, 20> a2 = {+1.9, -1.2, -0.4, -1.2, -0.8, +1.4, +0.5, -1.6, +1.1, -0.7,
+                                                 +0.6, +2.3, -1.8, -1.9, -0.3, -2.4, -1.7, +0.2, -2.4, +1.9};
+
+          auto p2 = [&p, &a2](const R3& X) -> double { return p(X, a2); };
+
+          constexpr std::array<double, 20> a3 = {+0.8, +0.5, +1.3, -2.3, +0.9, -0.4, -2.0, +1.8, +0.5, +0.7,
+                                                 +1.0, -0.4, +1.1, +1.8, -0.4, +1.1, -0.0, +1.4, +1.9, -2.2};
+
+          auto p3 = [&p, &a3](const R3& X) -> double { return p(X, a3); };
+
+          auto p_mesh = CartesianMeshBuilder{TinyVector<3>{-0.5, -0.5, -0.5}, TinyVector<3>{3.5, 3.5, 3.5},
+                                             TinyVector<3, size_t>{4, 4, 4}}
+                          .mesh()
+                          ->get<Mesh<3>>();
+          const auto& mesh = *p_mesh;
+
+          SECTION("R data")
+          {
+            auto R_exact = p0;
+
+            DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree + 3, std::function(R_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+            auto dpk_fh          = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R^3 data")
+          {
+            auto R3_exact = [&](const R3& x) -> R3 { return R3{p1(x), p2(x), p3(x)}; };
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R^2x2 data")
+          {
+            using R2x2 = TinyMatrix<2, 2>;
+
+            auto R2x2_exact = [&](const R3& x) -> R2x2 {
+              return R2x2{p0(x), p1(x),   //
+                          p2(x), p3(x)};
+            };
+
+            DiscreteFunctionP0 Ah = test_only::exact_projection(mesh, degree, std::function(R2x2_exact));
+
+            descriptor.setRowWeighting(false);
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+            auto dpk_Ah      = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Ah, std::function(R2x2_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector data")
+          {
+            std::array<std::function<double(const R3&)>, 4> vector_exact = {p0, p1, p2, p3};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            descriptor.setPreconditioning(false);
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("with symmetries")
+  {
+    SECTION("1D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                              std::make_shared<NamedBoundaryDescriptor>("XMIN")}}};
+      using R1 = TinyVector<1>;
+
+      auto p0 = [](const R1& x) -> R1 { return R1{+1.7 * (x[0] + 1) * (x[0] + 1) * (x[0] + 1)}; };
+      auto p1 = [](const R1& x) -> R1 { return R1{-1.2 * (x[0] + 1) * (x[0] + 1) * (x[0] + 1)}; };
+      auto p2 = [](const R1& x) -> R1 { return R1{+1.4 * (x[0] + 1) * (x[0] + 1) * (x[0] + 1)}; };
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
+        {
+          SECTION("R1 data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+            auto& mesh = *p_mesh;
+
+            auto R1_exact = p0;
+
+            DiscreteFunctionP0 fh = test_only::exact_projection(mesh, degree, std::function(R1_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R1>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_fh, std::function(R1_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("R1 vector data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+            auto& mesh  = *p_mesh;
+
+            std::array<std::function<R1(const R1&)>, 3> vector_exact   //
+              = {p0, p1, p2};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R1>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX")}}};
+
+      using R2 = TinyVector<2>;
+
+      auto p_mesh = CartesianMeshBuilder{TinyVector<2>{0, 0}, TinyVector<2>{2, 1}, TinyVector<2, size_t>{3, 3}}
+                      .mesh()
+                      ->get<Mesh<2>>();
+
+      auto& mesh = *p_mesh;
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
+        {
+          SECTION("R^2 data")
+          {
+            auto R2_exact = [](const R2& x) -> R2 {
+              return R2{+2.3 * (x[0] - 2) * (x[0] - 2) * (x[0] - 2),   //
+                        -1.3 * (x[1] - 1) * (x[1] - 1) * (x[1] - 1)};
+            };
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R2_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R2_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R2")
+          {
+            std::array<std::function<R2(const R2&)>, 2> vector_exact   //
+              = {[](const R2& x) -> R2 {
+                   return R2{+1.7 * (x[0] - 2) * (x[0] - 2) * (x[0] - 2),   //
+                             -0.6 * (x[1] - 1) * (x[1] - 1) * (x[1] - 1)};
+                 },
+                 [](const R2& x) -> R2 {
+                   return R2{-2.3 * (x[0] - 2) * (x[0] - 2) * (x[0] - 2),   //
+                             +1.1 * (x[1] - 1) * (x[1] - 1) * (x[1] - 1)};
+                 }};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const R2>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "ZMAX")}},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree,
+                                            std::vector<std::shared_ptr<
+                                              const IBoundaryDescriptor>>{std::
+                                                                            make_shared<NamedBoundaryDescriptor>(
+                                                                              "XMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "YMAX"),
+                                                                          std::make_shared<NamedBoundaryDescriptor>(
+                                                                            "ZMAX")}}};
+
+      using R3 = TinyVector<3>;
+
+      auto p_mesh = CartesianMeshBuilder{TinyVector<3>{0, 0, 0}, TinyVector<3>{2, 1, 1}, TinyVector<3, size_t>{3, 3, 3}}
+                      .mesh()
+                      ->get<Mesh<3>>();
+
+      auto& mesh = *p_mesh;
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
+        {
+          SECTION("R^3 data")
+          {
+            auto R3_exact = [](const R3& x) -> R3 {
+              return R3{+2.3 * (x[0] - 2) * (x[0] - 2) * (x[0] - 2),   //
+                        -1.3 * (x[1] - 1) * (x[1] - 1) * (x[1] - 1),   //
+                        +1.4 * (x[2] - 1) * (x[2] - 1) * (x[2] - 1)};
+            };
+
+            DiscreteFunctionP0 uh = test_only::exact_projection(mesh, degree, std::function(R3_exact));
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_uh, std::function(R3_exact));
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+
+          SECTION("vector of R3")
+          {
+            std::array<std::function<R3(const R3&)>, 2> vector_exact   //
+              = {[](const R3& x) -> R3 {
+                   return R3{+1.7 * (x[0] - 2) * (x[0] - 2) * (x[0] - 2),   //
+                             -0.6 * (x[1] - 1) * (x[1] - 1) * (x[1] - 1),   //
+                             +1.2 * (x[2] - 1) * (x[2] - 1) * (x[2] - 1)};
+                 },
+                 [](const R3& x) -> R3 {
+                   return R3{-2.3 * (x[0] - 2) * (x[0] - 2) * (x[0] - 2),   //
+                             +1.1 * (x[1] - 1) * (x[1] - 1) * (x[1] - 1),   //
+                             -0.3 * (x[2] - 1) * (x[2] - 1) * (x[2] - 1)};
+                 }};
+
+            DiscreteFunctionP0Vector Vh = test_only::exact_projection(mesh, degree, vector_exact);
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+            auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const R3>>();
+
+            double max_error = test_only::max_reconstruction_error(mesh, dpk_Vh, vector_exact);
+            REQUIRE(parallel::allReduceMax(max_error) == Catch::Approx(0).margin(1E-12));
+          }
+        }
+      }
+    }
+  }
+}