diff --git a/src/mesh/StencilBuilder.cpp b/src/mesh/StencilBuilder.cpp
index 44313040017f24ee7d79a8dec35697b3ebae1bc7..34015395ae6d57c5e0c2850333ad90ef65211ef1 100644
--- a/src/mesh/StencilBuilder.cpp
+++ b/src/mesh/StencilBuilder.cpp
@@ -1,6 +1,11 @@
 #include <mesh/Connectivity.hpp>
 #include <mesh/StencilBuilder.hpp>
 
+#include <utils/Messenger.hpp>
+
+#warning remove after rework
+#include <set>
+
 template <typename ConnectivityType>
 Array<const uint32_t>
 StencilBuilder::_getRowMap(const ConnectivityType& connectivity) const
@@ -104,26 +109,91 @@ StencilBuilder::_getColumnIndices(const ConnectivityType& connectivity, const Ar
 
 template <typename ConnectivityType>
 Stencil
-StencilBuilder::_build(const ConnectivityType& connectivity) const
+StencilBuilder::_build(const ConnectivityType& connectivity, size_t number_of_layers) const
 {
-  Array<const uint32_t> row_map        = this->_getRowMap(connectivity);
-  Array<const uint32_t> column_indices = this->_getColumnIndices(connectivity, row_map);
+  if (number_of_layers == 1) {
+    Array<const uint32_t> row_map        = this->_getRowMap(connectivity);
+    Array<const uint32_t> column_indices = this->_getColumnIndices(connectivity, row_map);
+
+    return ConnectivityMatrix{row_map, column_indices};
+  } else {
+    if (number_of_layers > 2) {
+      throw NotImplementedError("number of layers too large");
+    }
+    if (parallel::size() > 1) {
+      throw NotImplementedError("stencils with more than 1 layers are not defined in parallel");
+    }
+
+    auto cell_to_node_matrix = connectivity.cellToNodeMatrix();
+    auto node_to_cell_matrix = connectivity.nodeToCellMatrix();
+
+    auto cell_is_owned = connectivity.cellIsOwned();
+    auto cell_number   = connectivity.cellNumber();
+
+    Array<uint32_t> row_map{connectivity.numberOfCells() + 1};
+    row_map[0] = 0;
+
+    std::vector<CellId> column_indices_vector;
 
-  return ConnectivityMatrix{row_map, column_indices};
+    for (CellId cell_id = 0; cell_id < connectivity.numberOfCells(); ++cell_id) {
+      if (cell_is_owned[cell_id]) {
+        std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+          [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+        for (size_t i_node_1 = 0; i_node_1 < cell_to_node_matrix[cell_id].size(); ++i_node_1) {
+          const NodeId layer_1_node_id = cell_to_node_matrix[cell_id][i_node_1];
+
+          for (size_t i_cell_1 = 0; i_cell_1 < node_to_cell_matrix[layer_1_node_id].size(); ++i_cell_1) {
+            CellId cell_1_id = node_to_cell_matrix[layer_1_node_id][i_cell_1];
+
+            for (size_t i_node_2 = 0; i_node_2 < cell_to_node_matrix[cell_1_id].size(); ++i_node_2) {
+              const NodeId layer_2_node_id = cell_to_node_matrix[cell_1_id][i_node_2];
+
+              for (size_t i_cell_2 = 0; i_cell_2 < node_to_cell_matrix[layer_2_node_id].size(); ++i_cell_2) {
+                CellId cell_2_id = node_to_cell_matrix[layer_2_node_id][i_cell_2];
+
+                if (cell_2_id != cell_id) {
+                  cell_set.insert(cell_2_id);
+                }
+              }
+            }
+          }
+        }
+
+        for (auto stencil_cell_id : cell_set) {
+          column_indices_vector.push_back(stencil_cell_id);
+        }
+        row_map[cell_id + 1] = row_map[cell_id] + cell_set.size();
+      }
+    }
+
+    if (row_map[row_map.size() - 1] != column_indices_vector.size()) {
+      throw UnexpectedError("incorrect stencil size");
+    }
+
+    Array<uint32_t> column_indices(row_map[row_map.size() - 1]);
+    column_indices.fill(std::numeric_limits<uint32_t>::max());
+
+    for (size_t i = 0; i < column_indices.size(); ++i) {
+      column_indices[i] = column_indices_vector[i];
+    }
+
+    return ConnectivityMatrix{row_map, column_indices};
+  }
 }
 
 Stencil
-StencilBuilder::build(const IConnectivity& connectivity) const
+StencilBuilder::build(const IConnectivity& connectivity, size_t number_of_layers) const
 {
   switch (connectivity.dimension()) {
   case 1: {
-    return StencilBuilder::_build(dynamic_cast<const Connectivity<1>&>(connectivity));
+    return StencilBuilder::_build(dynamic_cast<const Connectivity<1>&>(connectivity), number_of_layers);
   }
   case 2: {
-    return StencilBuilder::_build(dynamic_cast<const Connectivity<2>&>(connectivity));
+    return StencilBuilder::_build(dynamic_cast<const Connectivity<2>&>(connectivity), number_of_layers);
   }
   case 3: {
-    return StencilBuilder::_build(dynamic_cast<const Connectivity<3>&>(connectivity));
+    return StencilBuilder::_build(dynamic_cast<const Connectivity<3>&>(connectivity), number_of_layers);
   }
   default: {
     throw UnexpectedError("invalid connectivity dimension");
diff --git a/src/mesh/StencilBuilder.hpp b/src/mesh/StencilBuilder.hpp
index 1743a2d719fd920d121b604e61732d9f6450197d..53918e64ac9234c396986f2bfef3f90510f287e0 100644
--- a/src/mesh/StencilBuilder.hpp
+++ b/src/mesh/StencilBuilder.hpp
@@ -15,10 +15,10 @@ class StencilBuilder
                                           const Array<const uint32_t>& row_map) const;
 
   template <typename ConnectivityType>
-  Stencil _build(const ConnectivityType& connectivity) const;
+  Stencil _build(const ConnectivityType& connectivity, size_t number_of_layers) const;
 
   friend class StencilManager;
-  Stencil build(const IConnectivity& connectivity) const;
+  Stencil build(const IConnectivity& connectivity, size_t number_of_layers) const;
 
  public:
   StencilBuilder()                      = default;
diff --git a/src/mesh/StencilManager.cpp b/src/mesh/StencilManager.cpp
index 58190f002739ca0070ac14c35214598c4476d8d9..120a95e0dfc54a38f9ea7a4ab94bbe4607014981 100644
--- a/src/mesh/StencilManager.cpp
+++ b/src/mesh/StencilManager.cpp
@@ -18,11 +18,11 @@ StencilManager::destroy()
   Assert(m_instance != nullptr, "StencilManager was not created");
 
   // LCOV_EXCL_START
-  if (m_instance->m_connectivity_id_to_stencil_map.size() > 0) {
+  if (m_instance->m_stored_stencil_map.size() > 0) {
     std::stringstream error;
     error << ": some connectivities are still registered\n";
-    for (const auto& [id, stencil_p] : m_instance->m_connectivity_id_to_stencil_map) {
-      error << " - connectivity of id " << rang::fgB::magenta << id << rang::style::reset << '\n';
+    for (const auto& [key, stencil_p] : m_instance->m_stored_stencil_map) {
+      error << " - connectivity of id " << rang::fgB::magenta << key.connectivity_id << rang::style::reset << '\n';
     }
     throw UnexpectedError(error.str());
     // LCOV_EXCL_STOP
@@ -32,14 +32,14 @@ StencilManager::destroy()
 }
 
 const Stencil&
-StencilManager::getStencil(const IConnectivity& connectivity)
+StencilManager::getStencil(const IConnectivity& connectivity, size_t degree)
 {
-  if (not m_connectivity_id_to_stencil_map.contains(connectivity.id())) {
-    m_connectivity_id_to_stencil_map[connectivity.id()] =
-      std::make_shared<Stencil>(StencilBuilder{}.build(connectivity));
+  if (not m_stored_stencil_map.contains(Key{connectivity.id(), degree})) {
+    m_stored_stencil_map[Key{connectivity.id(), degree}] =
+      std::make_shared<Stencil>(StencilBuilder{}.build(connectivity, degree));
   }
 
-  return *m_connectivity_id_to_stencil_map.at(connectivity.id());
+  return *m_stored_stencil_map.at(Key{connectivity.id(), degree});
 }
 
 void
@@ -48,9 +48,9 @@ StencilManager::deleteConnectivity(const size_t connectivity_id)
   bool has_removed = false;
   do {
     has_removed = false;
-    for (const auto& [id, p_stencil] : m_connectivity_id_to_stencil_map) {
-      if (connectivity_id == id) {
-        m_connectivity_id_to_stencil_map.erase(connectivity_id);
+    for (const auto& [key, p_stencil] : m_stored_stencil_map) {
+      if (connectivity_id == key.connectivity_id) {
+        m_stored_stencil_map.erase(key);
         has_removed = true;
         break;
       }
diff --git a/src/mesh/StencilManager.hpp b/src/mesh/StencilManager.hpp
index d1503e60f64eb44e18ed03c228d5c754afcbe248..f2912be1fca197aab6b9e010c600de3d8e507827 100644
--- a/src/mesh/StencilManager.hpp
+++ b/src/mesh/StencilManager.hpp
@@ -15,7 +15,28 @@ class StencilManager
 
   static StencilManager* m_instance;
 
-  std::unordered_map<size_t, std::shared_ptr<const Stencil>> m_connectivity_id_to_stencil_map;
+  struct Key
+  {
+    size_t connectivity_id;
+    size_t degree;
+
+    PUGS_INLINE bool
+    operator==(const Key& k) const
+    {
+      return (connectivity_id == k.connectivity_id) and (degree == k.degree);
+    }
+  };
+  struct HashKey
+  {
+    size_t
+    operator()(const Key& key) const
+    {
+      return (std::hash<decltype(Key::connectivity_id)>()(key.connectivity_id)) ^
+             (std::hash<decltype(Key::degree)>()(key.degree) << 1);
+    }
+  };
+
+  std::unordered_map<Key, std::shared_ptr<const Stencil>, HashKey> m_stored_stencil_map;
 
  public:
   static void create();
@@ -31,7 +52,7 @@ class StencilManager
 
   void deleteConnectivity(const size_t connectivity_id);
 
-  const Stencil& getStencil(const IConnectivity& i_connectivity);
+  const Stencil& getStencil(const IConnectivity& i_connectivity, size_t degree = 1);
 
   StencilManager(const StencilManager&) = delete;
   StencilManager(StencilManager&&)      = delete;
diff --git a/src/scheme/PolynomialReconstruction.cpp b/src/scheme/PolynomialReconstruction.cpp
index 0d0c829c7ce0d7f91c635dd59ae7636717ab7e2b..270d663c3363471ecc05de12a5d1bc8661e2a19f 100644
--- a/src/scheme/PolynomialReconstruction.cpp
+++ b/src/scheme/PolynomialReconstruction.cpp
@@ -253,7 +253,7 @@ _computeEjkMean(const QuadratureFormula<3>& quadrature,
       }
 
       for (size_t i_z = 1; i_z <= degree; ++i_z) {
-        const size_t begin_i_z_1 = i_z * (i_z + 1) * (i_z + 2) / 6 - 1;
+        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) {
@@ -550,7 +550,7 @@ PolynomialReconstruction::_build(
   const size_t basis_dimension =
     DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(m_descriptor.degree());
 
-  const Stencil& stencil = StencilManager::instance().getStencil(mesh.connectivity());
+  const Stencil& stencil = StencilManager::instance().getStencil(mesh.connectivity(), m_descriptor.degree());
 
   auto xr = mesh.xr();
   auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
@@ -604,16 +604,6 @@ PolynomialReconstruction::_build(
     inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool[i] = SmallArray<double>(basis_dimension);
   }
 
-#warning remove after rework
-  [[maybe_unused]] const int INTEGRATE = []() {
-    auto value = std::getenv("INTEGRATE");
-    if (value == nullptr) {
-      return 0;
-    } else {
-      return std::atoi(value);
-    }
-  }();
-
   parallel_for(
     mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_j_id) {
       if (cell_is_owned[cell_j_id]) {
@@ -680,7 +670,8 @@ PolynomialReconstruction::_build(
 
         ShrinkMatrixView A(A_pool[t], stencil_cell_list.size());
 
-        if ((m_descriptor.degree() == 1) and (INTEGRATE == 0)) {
+        if ((m_descriptor.degree() == 1) and
+            (m_descriptor.integrationMethod() == PolynomialReconstructionDescriptor::IntegrationMethod::cell_center)) {
           const Rd& Xj = xj[cell_j_id];
           for (size_t i = 0; i < stencil_cell_list.size(); ++i) {
             const CellId cell_i_id = stencil_cell_list[i];
@@ -690,8 +681,12 @@ PolynomialReconstruction::_build(
             }
           }
 
-        } else if ((INTEGRATE == 2) or (INTEGRATE == 1)) {
-          if ((INTEGRATE == 2) and (MeshType::Dimension == 2)) {
+        } else if ((m_descriptor.integrationMethod() ==
+                    PolynomialReconstructionDescriptor::IntegrationMethod::element) or
+                   (m_descriptor.integrationMethod() ==
+                    PolynomialReconstructionDescriptor::IntegrationMethod::boundary)) {
+          if ((m_descriptor.integrationMethod() == PolynomialReconstructionDescriptor::IntegrationMethod::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];
@@ -817,12 +812,29 @@ PolynomialReconstruction::_build(
                     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) {
@@ -831,6 +843,19 @@ PolynomialReconstruction::_build(
                       }
                       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) {
@@ -860,6 +885,13 @@ PolynomialReconstruction::_build(
                       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);
diff --git a/src/scheme/PolynomialReconstructionDescriptor.hpp b/src/scheme/PolynomialReconstructionDescriptor.hpp
index a7394500106b2f8a43c5e62f9ee74912b4d8fee5..b561f7080b3c47d4bd2029a0d0a03791e02c4411 100644
--- a/src/scheme/PolynomialReconstructionDescriptor.hpp
+++ b/src/scheme/PolynomialReconstructionDescriptor.hpp
@@ -7,12 +7,31 @@
 
 class PolynomialReconstructionDescriptor
 {
+ public:
+  enum class IntegrationMethod
+  {
+    cell_center,   // use exact integrals for degree 1 polynomials
+                   // using evaluation at mass center
+    element,       // use element based quadrature to compute
+                   // polynomial integrals
+    boundary       // use divergence theorem to compute polynomial
+                   // integrals
+  };
+
  private:
+  IntegrationMethod m_integration_method;
   size_t m_degree;
   bool m_preconditioning = true;
   bool m_row_weighting   = true;
 
  public:
+  PUGS_INLINE
+  IntegrationMethod
+  integrationMethod() const
+  {
+    return m_integration_method;
+  }
+
   PUGS_INLINE
   size_t
   degree() const
@@ -55,7 +74,9 @@ class PolynomialReconstructionDescriptor
     m_row_weighting = row_weighting;
   }
 
-  PolynomialReconstructionDescriptor(const size_t degree) : m_degree{degree} {}
+  PolynomialReconstructionDescriptor(const IntegrationMethod integration_method, const size_t degree)
+    : m_integration_method{integration_method}, m_degree{degree}
+  {}
 
   PolynomialReconstructionDescriptor() = delete;
 
diff --git a/src/scheme/test_reconstruction.cpp b/src/scheme/test_reconstruction.cpp
index 4ba54bda2dc6a586e14e16fb01afee0417bb2476..bf1c51121d943b37e854e295b5c472140239fc78 100644
--- a/src/scheme/test_reconstruction.cpp
+++ b/src/scheme/test_reconstruction.cpp
@@ -7,7 +7,7 @@ void
 test_reconstruction(const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list,
                     uint64_t degree)
 {
-  PolynomialReconstructionDescriptor descriptor{degree};
+  PolynomialReconstructionDescriptor descriptor{PolynomialReconstructionDescriptor::IntegrationMethod::element, degree};
   {
     const size_t nb_loops = 1;
     std::cout << "** variable list at once (" << nb_loops << " times)\n";
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index b242c06dc5ff8c5d0882b46f962926890b51e433..e2dff14cced997684da1d214bfd20f9df19df664 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -128,6 +128,7 @@ add_executable (unit_tests
   test_PugsUtils.cpp
   test_PyramidGaussQuadrature.cpp
   test_PyramidTransformation.cpp
+  test_QuadraticPolynomialReconstruction.cpp
   test_QuadratureType.cpp
   test_RefId.cpp
   test_RefItemList.cpp
diff --git a/tests/test_PolynomialReconstruction.cpp b/tests/test_PolynomialReconstruction.cpp
index b7c7019f57e6227de20f746900d87416e0a400ff..dfa092bb109c2540892e0d921e11683a4be3dfd4 100644
--- a/tests/test_PolynomialReconstruction.cpp
+++ b/tests/test_PolynomialReconstruction.cpp
@@ -24,7 +24,9 @@ TEST_CASE("PolynomialReconstruction", "[scheme]")
 {
   SECTION("degree 1")
   {
-    PolynomialReconstructionDescriptor descriptor{1};
+    PolynomialReconstructionDescriptor::IntegrationMethod method =
+      PolynomialReconstructionDescriptor::IntegrationMethod::cell_center;
+    PolynomialReconstructionDescriptor descriptor{method, 1};
 
     SECTION("1D")
     {
diff --git a/tests/test_PolynomialReconstructionDescriptor.cpp b/tests/test_PolynomialReconstructionDescriptor.cpp
index 6b7cd2853c783449f8b8f9167c06241a47b00b38..eda18e3d6447461347cf4018f2165700bcb9fdb4 100644
--- a/tests/test_PolynomialReconstructionDescriptor.cpp
+++ b/tests/test_PolynomialReconstructionDescriptor.cpp
@@ -8,9 +8,12 @@
 
 TEST_CASE("PolynomialReconstructionDescriptor", "[scheme]")
 {
+#warning tests are not completed
   SECTION("degree 1")
   {
-    PolynomialReconstructionDescriptor descriptor{1};
+    PolynomialReconstructionDescriptor::IntegrationMethod method =
+      PolynomialReconstructionDescriptor::IntegrationMethod::cell_center;
+    PolynomialReconstructionDescriptor descriptor{method, 1};
 
     REQUIRE(descriptor.degree() == 1);
     REQUIRE(descriptor.preconditioning() == true);
@@ -19,7 +22,10 @@ TEST_CASE("PolynomialReconstructionDescriptor", "[scheme]")
 
   SECTION("degree 4")
   {
-    PolynomialReconstructionDescriptor descriptor{4};
+    PolynomialReconstructionDescriptor::IntegrationMethod method =
+      PolynomialReconstructionDescriptor::IntegrationMethod::cell_center;
+
+    PolynomialReconstructionDescriptor descriptor{method, 4};
 
     REQUIRE(descriptor.degree() == 4);
     REQUIRE(descriptor.preconditioning() == true);
@@ -28,7 +34,9 @@ TEST_CASE("PolynomialReconstructionDescriptor", "[scheme]")
 
   SECTION("utlities")
   {
-    PolynomialReconstructionDescriptor descriptor{3};
+    PolynomialReconstructionDescriptor::IntegrationMethod method =
+      PolynomialReconstructionDescriptor::IntegrationMethod::cell_center;
+    PolynomialReconstructionDescriptor descriptor{method, 3};
 
     REQUIRE(descriptor.degree() == 3);
     REQUIRE(descriptor.preconditioning() == true);
diff --git a/tests/test_QuadraticPolynomialReconstruction.cpp b/tests/test_QuadraticPolynomialReconstruction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..69c685f544f774205effbcf304dca43ff7d84658
--- /dev/null
+++ b/tests/test_QuadraticPolynomialReconstruction.cpp
@@ -0,0 +1,1198 @@
+#include <catch2/catch_approx.hpp>
+#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/GaussLegendreQuadratureDescriptor.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 <scheme/DiscreteFunctionDPkVariant.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionVariant.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("QuadraticPolynomialReconstruction", "[scheme]")
+{
+  SECTION("degree 2")
+  {
+    PolynomialReconstructionDescriptor::IntegrationMethod method =
+      PolynomialReconstructionDescriptor::IntegrationMethod::element;
+    PolynomialReconstructionDescriptor descriptor{method, 2};
+
+    SECTION("1D")
+    {
+      using R1 = TinyVector<1>;
+
+      QuadratureFormula<1> qf = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{2});
+
+      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 f_exact = [](const R1& x) { return 2.3 + 1.7 * x[0] - 3.2 * x[0] * x[0]; };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0<double> fh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                double value        = 0;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * f_exact(T(qf.point(i_q))) * T.jacobianDeterminant();
+                }
+
+                fh[cell_id] = value / Vj[cell_id];
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+                const auto dpkj           = dpk_fh[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  error += std::abs(qf.weight(i_q) * (f_exact(T(qf.point(i_q))) - dpkj(T(qf.point(i_q)))) *
+                                    T.jacobianDeterminant());
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+        }
+      }
+
+      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 u_exact = [](const R1& X) -> R3 {
+              const double x = X[0];
+
+              return R3{2.3 + 1.7 * x - 3.2 * x * x,   //
+                        7 + 5 * x + 3 * x * x,         //
+                        -4 + 3.5 * x + 2.7 * x * x};
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0<R3> uh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R3 value            = zero;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * T.jacobianDeterminant() * u_exact(T(qf.point(i_q)));
+                }
+
+                uh[cell_id] = 1. / Vj[cell_id] * value;
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3>>();
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+                const auto dpkj           = dpk_fh[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  R3 diff =
+                    qf.weight(i_q) * T.jacobianDeterminant() * (u_exact(T(qf.point(i_q))) - dpkj(T(qf.point(i_q))));
+                  error += std::abs(diff[0]) + std::abs(diff[1]) + std::abs(diff[2]);
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+        }
+      }
+
+      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 A_exact = [](const R1& X) -> R3x3 {
+              const double x  = X[0];
+              const double x2 = x * x;
+              return R3x3{+2.3 + 1.7 * x + 2.9 * x2,   //
+                          -1.7 + 2.1 * x + 1.7 * x2,   //
+                          +1.4 - 0.6 * x - 0.5 * x2,   //
+                                                       //
+                          +2.4 - 2.3 * x + 2.3 * x2,   //
+                          -0.2 + 3.1 * x - 1.9 * x2,   //
+                          -3.2 - 3.6 * x - 0.3 * x2,   //
+                                                       //
+                          -4.1 + 3.1 * x - 1.3 * x2,   //
+                          +0.8 + 2.9 * x + 2.1 * x2,   //
+                          -1.6 + 2.3 * x + 0.8 * x2};
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+            DiscreteFunctionP0<R3x3> Ah{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R3x3 value          = zero;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * T.jacobianDeterminant() * A_exact(T(qf.point(i_q)));
+                }
+
+                Ah[cell_id] = 1. / Vj[cell_id] * value;
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+            auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3x3>>();
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+                const auto dpkj           = dpk_Ah[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  R3x3 diff =
+                    qf.weight(i_q) * T.jacobianDeterminant() * (A_exact(T(qf.point(i_q))) - dpkj(T(qf.point(i_q))));
+                  for (size_t i = 0; i < diff.numberOfRows(); ++i) {
+                    for (size_t j = 0; j < diff.numberOfColumns(); ++j) {
+                      error += std::abs(diff(i, j));
+                    }
+                  }
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+        }
+      }
+
+      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;
+
+            auto u_exact = [](const R1& X) -> R3 {
+              const double x = X[0];
+
+              return R3{2.3 + 1.7 * x - 3.2 * x * x,   //
+                        7 + 5 * x + 3 * x * x,         //
+                        -4 + 3.5 * x + 2.7 * x * x};
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0Vector<double> uh{p_mesh, 3};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R3 value            = zero;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * T.jacobianDeterminant() * u_exact(T(qf.point(i_q)));
+                }
+
+                value *= 1. / Vj[cell_id];
+
+                for (size_t i = 0; i < value.dimension(); ++i) {
+                  uh[cell_id][i] = value[i];
+                }
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  for (size_t l = 0; l < dpk_uh.numberOfComponents(); ++l) {
+                    error += std::abs(qf.weight(i_q) * T.jacobianDeterminant() *
+                                      (u_exact(T(qf.point(i_q)))[l] - dpk_uh(cell_id, l)(T(qf.point(i_q)))));
+                  }
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+        }
+      }
+
+      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 f_exact = [](const R1& x) { return 2.3 + 1.7 * x[0] - 3.2 * x[0] * x[0]; };
+
+            auto u_exact = [](const R1& X) -> R3 {
+              const double x = X[0];
+
+              return R3{2.3 + 1.7 * x - 3.2 * x * x,   //
+                        7 + 5 * x + 3 * x * x,         //
+                        -4 + 3.5 * x + 2.7 * x * x};
+            };
+
+            auto A_exact = [](const R1& X) -> R3x3 {
+              const double x  = X[0];
+              const double x2 = x * x;
+              return R3x3{+2.3 + 1.7 * x + 2.9 * x2,   //
+                          -1.7 + 2.1 * x + 1.7 * x2,   //
+                          +1.4 - 0.6 * x - 0.5 * x2,   //
+                                                       //
+                          +2.4 - 2.3 * x + 2.3 * x2,   //
+                          -0.2 + 3.1 * x - 1.9 * x2,   //
+                          -3.2 - 3.6 * x - 0.3 * x2,   //
+                                                       //
+                          -4.1 + 3.1 * x - 1.3 * x2,   //
+                          +0.8 + 2.9 * x + 2.1 * x2,   //
+                          -1.6 + 2.3 * x + 0.8 * x2};
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0Vector<double> vh{p_mesh, 3};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R3 value            = zero;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * T.jacobianDeterminant() * u_exact(T(qf.point(i_q)));
+                }
+
+                value *= 1. / Vj[cell_id];
+
+                for (size_t i = 0; i < value.dimension(); ++i) {
+                  vh[cell_id][i] = value[i];
+                }
+              });
+
+            DiscreteFunctionP0<double> fh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                double value        = 0;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * f_exact(T(qf.point(i_q))) * T.jacobianDeterminant();
+                }
+
+                fh[cell_id] = value / Vj[cell_id];
+              });
+
+            DiscreteFunctionP0<R3x3> Ah{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R3x3 value          = zero;
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  value += qf.weight(i_q) * T.jacobianDeterminant() * A_exact(T(qf.point(i_q)));
+                }
+
+                Ah[cell_id] = 1. / Vj[cell_id] * value;
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah, vh, fh);
+
+            auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3x3>>();
+            auto dpk_vh = reconstructions[1]->get<DiscreteFunctionDPkVector<1, const double>>();
+            auto dpk_fh = reconstructions[2]->get<DiscreteFunctionDPk<1, const double>>();
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  for (size_t l = 0; l < dpk_vh.numberOfComponents(); ++l) {
+                    error += std::abs(qf.weight(i_q) * T.jacobianDeterminant() *
+                                      (u_exact(T(qf.point(i_q)))[l] - dpk_vh(cell_id, l)(T(qf.point(i_q)))));
+                  }
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+                const auto dpkj           = dpk_fh[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  error += std::abs(qf.weight(i_q) * (f_exact(T(qf.point(i_q))) - dpkj(T(qf.point(i_q)))) *
+                                    T.jacobianDeterminant());
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+                const auto dpkj           = dpk_Ah[cell_id];
+
+                LineTransformation<1> T{xr[cell_node_list[0]], xr[cell_node_list[1]]};
+
+                double error = 0;
+
+                for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                  R3x3 diff =
+                    qf.weight(i_q) * T.jacobianDeterminant() * (A_exact(T(qf.point(i_q))) - dpkj(T(qf.point(i_q))));
+                  for (size_t i = 0; i < diff.numberOfRows(); ++i) {
+                    for (size_t j = 0; j < diff.numberOfColumns(); ++j) {
+                      error += std::abs(diff(i, j));
+                    }
+                  }
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      using R2 = TinyVector<2>;
+
+      auto integrate_in_cell = [](const CellType& cell_type, const auto& cell_node_list, const auto& xr,
+                                  const auto& exact, auto& value) {
+        switch (cell_type) {
+        case CellType::Triangle: {
+          TriangleTransformation<2> T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]]};
+          QuadratureFormula<2> qf = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{2});
+          for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+            value += qf.weight(i_q) * T.jacobianDeterminant() * exact(T(qf.point(i_q)));
+          }
+          break;
+        }
+        case CellType::Quadrangle: {
+          SquareTransformation<2> T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                    xr[cell_node_list[3]]};
+          QuadratureFormula<2> qf =
+            QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{3});
+          for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+            const R2 x_hat = qf.point(i_q);
+            const R2 x     = T(x_hat);
+            value += qf.weight(i_q) * T.jacobianDeterminant(x_hat) * exact(x);
+          }
+          break;
+        }
+        default: {
+          throw UnexpectedError("invalid cell type");
+        }
+        }
+      };
+
+      auto compute_L1_error = [](const auto& cell_type, const auto& mesh, const auto& exact, const auto& dpk) {
+        auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+        const auto& xr           = mesh.xr();
+
+        using DataType = typename std::decay_t<decltype(dpk)>::data_type;
+
+        auto sum_abs = [](const DataType& diff) -> double {
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            return std::abs(diff);
+          } else if constexpr (is_tiny_vector_v<DataType>) {
+            double sum = 0;
+            for (size_t i = 0; i < diff.dimension(); ++i) {
+              sum += std::abs(diff[i]);
+            }
+            return sum;
+          } else if constexpr (is_tiny_matrix_v<DataType>) {
+            double sum = 0;
+            for (size_t i = 0; i < diff.numberOfRows(); ++i) {
+              for (size_t j = 0; j < diff.numberOfRows(); ++j) {
+                sum += std::abs(diff(i, j));
+              }
+            }
+            return sum;
+          } else {
+            throw UnexpectedError("unexpected value type");
+          }
+        };
+
+        double L1_error = 0;
+        for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          const auto cell_node_list = cell_to_node_matrix[cell_id];
+          const auto dpkj           = dpk[cell_id];
+
+          double error = 0;
+
+          switch (cell_type[cell_id]) {
+          case CellType::Triangle: {
+            TriangleTransformation<2> T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]]};
+            QuadratureFormula<2> qf = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{2});
+            for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+              const R2 x    = T(qf.point(i_q));
+              DataType diff = qf.weight(i_q) * T.jacobianDeterminant() * (exact(x) - dpkj(x));
+              error += sum_abs(diff);
+            }
+            break;
+          }
+          case CellType::Quadrangle: {
+            SquareTransformation<2> T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                      xr[cell_node_list[3]]};
+            QuadratureFormula<2> qf =
+              QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{3});
+            for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+              const R2 x_hat = qf.point(i_q);
+              const R2 x     = T(x_hat);
+              DataType diff  = qf.weight(i_q) * T.jacobianDeterminant(x_hat) * (exact(x) - dpkj(x));
+              error += sum_abs(diff);
+            }
+            break;
+          }
+          default: {
+            throw UnexpectedError("invalid cell type");
+          }
+          }
+
+          L1_error += error;
+        }
+        return L1_error;
+      };
+
+      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 f_exact = [](const R2& x) {
+              return 2.3 + 1.7 * x[0] + 0.2 * x[1] - 3.2 * x[0] * x[0] + 1.3 * x[0] * x[1] - 1.4 * x[1] * x[1];
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_type           = mesh.connectivity().cellType();
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0<double> fh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                double value        = 0;
+                integrate_in_cell(cell_type[cell_id], cell_node_list, xr, f_exact, value);
+                fh[cell_id] = value / Vj[cell_id];
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
+
+            double L1_error = compute_L1_error(cell_type, mesh, f_exact, dpk_fh);
+            REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      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 u_exact = [](const R2& X) -> R3 {
+              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;
+
+              return R3{+2.3 + 1.7 * x + 1.3 * y - 3.2 * x2 + 1.6 * xy + 0.9 * y2,   //
+                        +7.0 + 5.0 * x - 2.4 * y + 3.0 * x2 - 0.8 * xy + 1.1 * y2,   //
+                        -4.0 + 3.5 * x - 0.7 * y + 2.7 * x2 - 2.1 * xy - 1.8 * y2};
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_type           = mesh.connectivity().cellType();
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0<R3> uh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R3 value            = zero;
+                integrate_in_cell(cell_type[cell_id], cell_node_list, xr, u_exact, value);
+                uh[cell_id] = 1. / Vj[cell_id] * value;
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+            auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R3>>();
+
+            double L1_error = compute_L1_error(cell_type, mesh, u_exact, dpk_uh);
+            REQUIRE(parallel::allReduceMax(L1_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 A_exact = [](const R2& X) -> R2x2 {
+              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;
+
+              return R2x2{+2.3 + 1.7 * x + 1.2 * y + 1.1 * x2 + 0.7 * xy - 0.8 * y2,   //
+                          -1.7 + 2.1 * x - 2.2 * y - 1.9 * x2 + 0.2 * xy + 1.2 * y2,   //
+                          +1.4 - 0.6 * x - 2.1 * y + 0.3 * x2 - 3.1 * xy + 1.3 * y2,   //
+                          +2.4 - 2.3 * x + 1.3 * y - 0.8 * x2 + 1.2 * xy - 2.0 * y2};
+            };
+
+            auto xr = mesh.xr();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_type           = mesh.connectivity().cellType();
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0<R2x2> Ah{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                R2x2 value          = zero;
+                integrate_in_cell(cell_type[cell_id], cell_node_list, xr, A_exact, value);
+                Ah[cell_id] = 1. / Vj[cell_id] * value;
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+            auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
+
+            double L1_error = compute_L1_error(cell_type, mesh, A_exact, dpk_Ah);
+            REQUIRE(parallel::allReduceMax(L1_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));
+      //       }
+      //     }
+      //   }
+      // }
+    }
+
+    SECTION("3D")
+    {
+      using R3 = TinyVector<3>;
+
+      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 f_exact = [](const R3& X) {
+              const double& x = X[0];
+              const double& y = X[1];
+              const double& z = X[2];
+
+              return 2.3 + 1.7 * x + 0.2 * y + 1.4 * z - 3.2 * x * x + 1.3 * x * y - 1.6 * x * z - 1.4 * y * y +
+                     2 * y * z - 1.8 * z * z;
+            };
+
+            auto xr = mesh.xr();
+            auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+            auto Vj = MeshDataManager::instance().getMeshData(mesh).Vj();
+
+            auto cell_type           = mesh.connectivity().cellType();
+            auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+            DiscreteFunctionP0<double> fh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                auto cell_node_list = cell_to_node_matrix[cell_id];
+                double value        = 0;
+
+                switch (cell_type[cell_id]) {
+                case CellType::Tetrahedron: {
+                  TetrahedronTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                              xr[cell_node_list[3]]};
+                  QuadratureFormula<3> qf =
+                    QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{2});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    value += qf.weight(i_q) * f_exact(x) * T.jacobianDeterminant();
+                  }
+                  break;
+                }
+                case CellType::Pyramid: {
+                  PyramidTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                          xr[cell_node_list[3]], xr[cell_node_list[4]]};
+                  QuadratureFormula<3> qf =
+                    QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{3});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    value += qf.weight(i_q) * f_exact(x) * T.jacobianDeterminant(x_hat);
+                  }
+                  break;
+                }
+                case CellType::Prism: {
+                  PrismTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                        xr[cell_node_list[3]], xr[cell_node_list[4]], xr[cell_node_list[5]]};
+                  QuadratureFormula<3> qf = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{3});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    value += qf.weight(i_q) * f_exact(x) * T.jacobianDeterminant(x_hat);
+                  }
+                  break;
+                }
+                case CellType::Hexahedron: {
+                  CubeTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                       xr[cell_node_list[3]], xr[cell_node_list[4]], xr[cell_node_list[5]],
+                                       xr[cell_node_list[6]], xr[cell_node_list[7]]};
+                  QuadratureFormula<3> qf =
+                    QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{3});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    value += qf.weight(i_q) * f_exact(x) * T.jacobianDeterminant(x_hat);
+                  }
+                  break;
+                }
+                default: {
+                  throw UnexpectedError("invalid cell type");
+                }
+                }
+
+                fh[cell_id] = value / Vj[cell_id];
+              });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+
+            {
+              double L1_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const auto cell_node_list = cell_to_node_matrix[cell_id];
+                const auto dpkj           = dpk_fh[cell_id];
+
+                double error = 0;
+
+                switch (cell_type[cell_id]) {
+                case CellType::Tetrahedron: {
+                  TetrahedronTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                              xr[cell_node_list[3]]};
+                  QuadratureFormula<3> qf =
+                    QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{2});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    error += std::abs(qf.weight(i_q) * (f_exact(x) - dpkj(x)) * T.jacobianDeterminant());
+                  }
+                  break;
+                }
+                case CellType::Pyramid: {
+                  PyramidTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                          xr[cell_node_list[3]], xr[cell_node_list[4]]};
+                  QuadratureFormula<3> qf =
+                    QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{3});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    error += std::abs(qf.weight(i_q) * (f_exact(x) - dpkj(x)) * T.jacobianDeterminant(x_hat));
+                  }
+                  break;
+                }
+                case CellType::Prism: {
+                  PrismTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                        xr[cell_node_list[3]], xr[cell_node_list[4]], xr[cell_node_list[5]]};
+                  QuadratureFormula<3> qf = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{3});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    error += std::abs(qf.weight(i_q) * (f_exact(x) - dpkj(x)) * T.jacobianDeterminant(x_hat));
+                  }
+                  break;
+                }
+                case CellType::Hexahedron: {
+                  CubeTransformation T{xr[cell_node_list[0]], xr[cell_node_list[1]], xr[cell_node_list[2]],
+                                       xr[cell_node_list[3]], xr[cell_node_list[4]], xr[cell_node_list[5]],
+                                       xr[cell_node_list[6]], xr[cell_node_list[7]]};
+                  QuadratureFormula<3> qf =
+                    QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{3});
+                  for (size_t i_q = 0; i_q < qf.numberOfPoints(); ++i_q) {
+                    const R3 x_hat = qf.point(i_q);
+                    const R3 x     = T(x_hat);
+                    error += std::abs(qf.weight(i_q) * (f_exact(x) - dpkj(x)) * T.jacobianDeterminant(x_hat));
+                  }
+                  break;
+                }
+                default: {
+                  throw UnexpectedError("invalid cell type");
+                }
+                }
+
+                L1_error += error;
+              }
+              REQUIRE(parallel::allReduceMax(L1_error) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+        }
+      }
+
+      // 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));
+      //       }
+      //     }
+      //   }
+      // }
+    }
+  }
+}