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/algebra/Givens.hpp b/src/algebra/Givens.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..dc69a453764659cffa085810846338fc401647a9
--- /dev/null
+++ b/src/algebra/Givens.hpp
@@ -0,0 +1,161 @@
+#ifndef GIVENS_HPP
+#define GIVENS_HPP
+#include <utils/Exceptions.hpp>
+#include <utils/PugsAssert.hpp>
+
+#include <cmath>
+#include <iomanip>
+
+class Givens
+{
+ private:
+  static void
+  _givens(const double a, const double b, double& c, double& s)
+  {
+    if (b == 0) {
+      c = 1;
+      s = 0;
+    } else {
+      if (std::abs(b) > std::abs(a)) {
+        const double tau = -a / b;
+        s                = 1. / sqrt(1 + tau * tau);
+        c                = s * tau;
+      } else {
+        Assert(a != 0);
+        const double tau = -b / a;
+        c                = 1 / sqrt(1 + tau * tau);
+        s                = c * tau;
+      }
+    }
+  }
+
+  static void
+  _rotate(const double Ai_1j, const double Aij, const double c, const double s, double& ui_1, double& ui)
+  {
+    ui_1 = c * Ai_1j - s * Aij;
+    ui   = s * Ai_1j + c * Aij;
+  }
+
+  template <typename MatrixType, typename VectorType, typename RHSVectorType>
+  static void
+  _lift(const MatrixType& A, const RHSVectorType& b, VectorType& x)
+  {
+    for (ssize_t i = x.dimension() - 1; i >= 0; --i) {
+      const double inv_Aii = 1 / A(i, i);
+      double sum           = 0;
+      for (size_t j = i + 1; j < x.dimension(); ++j) {
+        sum += A(i, j) * x[j];
+      }
+      x[i] = (b[i] - sum) * inv_Aii;
+    }
+  }
+
+  template <typename MatrixType, typename UnknownMatrixType, typename RHSMatrixType>
+  static void
+  _liftCollection(const MatrixType& A, const RHSMatrixType& B, UnknownMatrixType& X)
+  {
+    for (ssize_t i = X.numberOfRows() - 1; i >= 0; --i) {
+      const double inv_Aii = 1 / A(i, i);
+      for (size_t k = 0; k < X.numberOfColumns(); ++k) {
+        double sum = 0;
+        for (size_t j = i + 1; j < X.numberOfRows(); ++j) {
+          sum += A(i, j) * X(j, k);
+        }
+        X(i, k) = (B(i, k) - sum) * inv_Aii;
+      }
+    }
+  }
+
+ public:
+  template <typename MatrixType, typename VectorType, typename RHSVectorType>
+  static void
+  solveInPlace(MatrixType& A, VectorType& x, RHSVectorType& b)
+  {
+    Assert(x.dimension() == A.numberOfColumns(), "The number of columns of A must be the size of x");
+    Assert(b.dimension() == A.numberOfRows(), "The number of rows of A must be the size of b");
+
+    for (size_t j = 0; j < A.numberOfColumns(); ++j) {
+      for (size_t i = A.numberOfRows() - 1; i > j; --i) {
+        double c;
+        double s;
+        _givens(A(i - 1, j), A(i, j), c, s);
+        for (size_t k = j; k < A.numberOfColumns(); ++k) {
+          double xi_1(0), xi(0);
+          _rotate(A(i - 1, k), A(i, k), c, s, xi_1, xi);
+          A(i - 1, k) = xi_1;
+          A(i, k)     = xi;
+        }
+        double xi_1(0), xi(0);
+        _rotate(b[i - 1], b[i], c, s, xi_1, xi);
+        b[i - 1] = xi_1;
+        b[i]     = xi;
+      }
+    }
+
+    _lift(A, b, x);
+  }
+
+  template <typename MatrixType, typename VectorType, typename RHSVectorType>
+  static void
+  solve(const MatrixType& A, VectorType& x, const RHSVectorType& b)
+  {
+    Assert(x.dimension() == A.numberOfColumns(), "The number of columns of A must be the size of x");
+    Assert(b.dimension() == A.numberOfRows(), "The number of rows of A must be the size of b");
+
+    MatrixType rotateA    = copy(A);
+    RHSVectorType rotateb = copy(b);
+
+    solveInPlace(rotateA, x, rotateb);
+  }
+
+  template <typename MatrixType, typename UnknownMatrixType, typename RHSMatrixType>
+  static void
+  solveCollectionInPlace(MatrixType& A, UnknownMatrixType& X, RHSMatrixType& B)
+  {
+    Assert(X.numberOfRows() == A.numberOfColumns(), "The number of columns of A must be the number of rows of X");
+    Assert(B.numberOfRows() == A.numberOfRows(), "The number of rows of A must be the rows of B");
+    Assert(X.numberOfColumns() == B.numberOfColumns(), "The number of columns of X and B must be the same");
+
+    for (size_t j = 0; j < A.numberOfColumns(); ++j) {
+      for (size_t i = A.numberOfRows() - 1; i > j; --i) {
+        double c;
+        double s;
+        _givens(A(i - 1, j), A(i, j), c, s);
+        for (size_t k = j; k < A.numberOfColumns(); ++k) {
+          double xi_1(0), xi(0);
+          _rotate(A(i - 1, k), A(i, k), c, s, xi_1, xi);
+          A(i - 1, k) = xi_1;
+          A(i, k)     = xi;
+        }
+
+        for (size_t l = 0; l < B.numberOfColumns(); ++l) {
+          double xi_1(0), xi(0);
+          _rotate(B(i - 1, l), B(i, l), c, s, xi_1, xi);
+          B(i - 1, l) = xi_1;
+          B(i, l)     = xi;
+        }
+      }
+    }
+
+    _liftCollection(A, B, X);
+  }
+
+  template <typename MatrixType, typename UnknownMatrixType, typename RHSMatrixType>
+  static void
+  solveCollection(const MatrixType& A, UnknownMatrixType& X, const RHSMatrixType& B)
+  {
+    Assert(X.numberOfRows() == A.numberOfColumns(), "The number of columns of A must be the number of rows of X");
+    Assert(B.numberOfRows() == A.numberOfRows(), "The number of rows of A must be the rows of B");
+    Assert(X.numberOfColumns() == B.numberOfColumns(), "The number of columns of X and B must be the same");
+
+    MatrixType rotateA    = copy(A);
+    RHSMatrixType rotateB = copy(B);
+
+    solveCollectionInPlace(rotateA, X, rotateB);
+  }
+
+  Givens()  = delete;
+  ~Givens() = delete;
+};
+
+#endif   // GIVENS_HPP
diff --git a/src/algebra/ShrinkMatrixView.hpp b/src/algebra/ShrinkMatrixView.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a41089a0af510c088d57a85c3f4606e1709dc2a6
--- /dev/null
+++ b/src/algebra/ShrinkMatrixView.hpp
@@ -0,0 +1,67 @@
+#ifndef SHRINK_MATRIX_VIEW_HPP
+#define SHRINK_MATRIX_VIEW_HPP
+
+#include <utils/PugsAssert.hpp>
+#include <utils/PugsMacros.hpp>
+
+#include <cstddef>
+#include <iostream>
+#include <utils/NaNHelper.hpp>
+
+template <typename MatrixType>
+class ShrinkMatrixView
+{
+ public:
+  using index_type = typename MatrixType::index_type;
+  using data_type  = typename MatrixType::data_type;
+
+ private:
+  MatrixType& m_matrix;
+  const size_t m_nb_rows;
+
+ public:
+  friend std::ostream&
+  operator<<(std::ostream& os, const ShrinkMatrixView& A)
+  {
+    for (size_t i = 0; i < A.numberOfRows(); ++i) {
+      os << i << '|';
+      for (size_t j = 0; j < A.numberOfColumns(); ++j) {
+        os << ' ' << j << ':' << NaNHelper(A(i, j));
+      }
+      os << '\n';
+    }
+    return os;
+  }
+
+  PUGS_INLINE size_t
+  numberOfRows() const noexcept
+  {
+    return m_nb_rows;
+  }
+
+  PUGS_INLINE size_t
+  numberOfColumns() const noexcept
+  {
+    return m_matrix.numberOfColumns();
+  }
+
+  PUGS_INLINE
+  data_type&
+  operator()(index_type i, index_type j) const noexcept(NO_ASSERT)
+  {
+    Assert(i < m_nb_rows and j < m_matrix.numberOfColumns(), "cannot access element: invalid indices");
+    return m_matrix(i, j);
+  }
+
+  ShrinkMatrixView(MatrixType& matrix, size_t nb_rows) noexcept(NO_ASSERT) : m_matrix{matrix}, m_nb_rows{nb_rows}
+  {
+    Assert(m_nb_rows <= matrix.numberOfRows(), "shrink number of rows must be smaller than original matrix's");
+  }
+
+  ShrinkMatrixView(const ShrinkMatrixView&) = delete;
+  ShrinkMatrixView(ShrinkMatrixView&&)      = delete;
+
+  ~ShrinkMatrixView() noexcept = default;
+};
+
+#endif   // SHRINK_MATRIX_VIEW_HPP
diff --git a/src/algebra/ShrinkVectorView.hpp b/src/algebra/ShrinkVectorView.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..83aed7cb2295ee4d20a044f7ba1039f34e7b5ffa
--- /dev/null
+++ b/src/algebra/ShrinkVectorView.hpp
@@ -0,0 +1,60 @@
+#ifndef SHRINK_VECTOR_VIEW_HPP
+#define SHRINK_VECTOR_VIEW_HPP
+
+#include <utils/PugsAssert.hpp>
+#include <utils/PugsMacros.hpp>
+
+#include <cstddef>
+#include <iostream>
+#include <utils/NaNHelper.hpp>
+
+template <typename VectorType>
+class ShrinkVectorView
+{
+ public:
+  using index_type = typename VectorType::index_type;
+  using data_type  = typename VectorType::data_type;
+
+ private:
+  VectorType& m_vector;
+  const size_t m_dimension;
+
+ public:
+  friend std::ostream&
+  operator<<(std::ostream& os, const ShrinkVectorView& x)
+  {
+    if (x.dimension() > 0) {
+      os << 0 << ':' << NaNHelper(x[0]);
+    }
+    for (size_t i = 1; i < x.dimension(); ++i) {
+      os << ' ' << i << ':' << NaNHelper(x[i]);
+    }
+    return os;
+  }
+
+  PUGS_INLINE size_t
+  dimension() const noexcept
+  {
+    return m_dimension;
+  }
+
+  PUGS_INLINE
+  data_type&
+  operator[](index_type i) const noexcept(NO_ASSERT)
+  {
+    Assert(i < m_dimension, "cannot access element: invalid indices");
+    return m_vector[i];
+  }
+
+  ShrinkVectorView(VectorType& vector, size_t dimension) noexcept(NO_ASSERT) : m_vector{vector}, m_dimension{dimension}
+  {
+    Assert(m_dimension <= vector.dimension(), "shrink number of rows must be smaller than original vector's");
+  }
+
+  ShrinkVectorView(const ShrinkVectorView&) = delete;
+  ShrinkVectorView(ShrinkVectorView&&)      = delete;
+
+  ~ShrinkVectorView() noexcept = default;
+};
+
+#endif   // SHRINK_VECTOR_VIEW_HPP
diff --git a/src/algebra/SmallMatrix.hpp b/src/algebra/SmallMatrix.hpp
index 718000c2799067e0084d5bb3460acd688a8642ea..f6bb90e2b9ce0044aa971c991ba3cb740d0761bf 100644
--- a/src/algebra/SmallMatrix.hpp
+++ b/src/algebra/SmallMatrix.hpp
@@ -28,6 +28,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
 
   // Allows const version to access our data
   friend SmallMatrix<std::add_const_t<DataType>>;
+  // Allows non-const version to access our data
+  friend SmallMatrix<std::remove_const_t<DataType>>;
 
  public:
   PUGS_INLINE
diff --git a/src/algebra/SmallVector.hpp b/src/algebra/SmallVector.hpp
index 91093bc67203f29cad05c475e88203b99b743b82..5ffdc106317fb5c2b4300a7a9c606f84d0db4929 100644
--- a/src/algebra/SmallVector.hpp
+++ b/src/algebra/SmallVector.hpp
@@ -160,6 +160,13 @@ class SmallVector   // LCOV_EXCL_LINE
     return m_values.size();
   }
 
+  PUGS_INLINE
+  size_t
+  dimension() const noexcept
+  {
+    return m_values.size();
+  }
+
   PUGS_INLINE SmallVector&
   fill(const DataType& value) noexcept
   {
diff --git a/src/algebra/TinyMatrix.hpp b/src/algebra/TinyMatrix.hpp
index 212e5d948315fa07f4c056cbb863d1b575699ac0..fc57d41ae78776aa6c977ae6f0895e1cd9fc0388 100644
--- a/src/algebra/TinyMatrix.hpp
+++ b/src/algebra/TinyMatrix.hpp
@@ -63,7 +63,7 @@ class [[nodiscard]] TinyMatrix
   }
 
   [[nodiscard]] PUGS_INLINE constexpr friend TinyMatrix<N, M, T>
-  transpose(const TinyMatrix& A)
+  transpose(const TinyMatrix& A) noexcept
   {
     TinyMatrix<N, M, T> tA;
     for (size_t i = 0; i < M; ++i) {
@@ -75,31 +75,31 @@ class [[nodiscard]] TinyMatrix
   }
 
   [[nodiscard]] PUGS_INLINE constexpr size_t
-  dimension() const
+  dimension() const noexcept
   {
     return M * N;
   }
 
   [[nodiscard]] PUGS_INLINE constexpr size_t
-  numberOfValues() const
+  numberOfValues() const noexcept
   {
     return this->dimension();
   }
 
   [[nodiscard]] PUGS_INLINE constexpr size_t
-  numberOfRows() const
+  numberOfRows() const noexcept
   {
     return M;
   }
 
   [[nodiscard]] PUGS_INLINE constexpr size_t
-  numberOfColumns() const
+  numberOfColumns() const noexcept
   {
     return N;
   }
 
-  PUGS_INLINE constexpr TinyMatrix
-  operator-() const
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix
+  operator-() const noexcept
   {
     TinyMatrix opposite;
     for (size_t i = 0; i < M * N; ++i) {
@@ -108,9 +108,8 @@ class [[nodiscard]] TinyMatrix
     return opposite;
   }
 
-  PUGS_INLINE
-  constexpr friend TinyMatrix
-  operator*(const T& t, const TinyMatrix& A)
+  [[nodiscard]] PUGS_INLINE constexpr friend TinyMatrix
+  operator*(const T& t, const TinyMatrix& A) noexcept
   {
     TinyMatrix B = A;
     return B *= t;
@@ -118,14 +117,14 @@ class [[nodiscard]] TinyMatrix
 
   PUGS_INLINE
   constexpr friend TinyMatrix
-  operator*(const T& t, TinyMatrix&& A)
+  operator*(const T& t, TinyMatrix&& A) noexcept
   {
     return std::move(A *= t);
   }
 
   PUGS_INLINE
   constexpr TinyMatrix&
-  operator*=(const T& t)
+  operator*=(const T& t) noexcept
   {
     for (size_t i = 0; i < M * N; ++i) {
       m_values[i] *= t;
@@ -144,8 +143,8 @@ class [[nodiscard]] TinyMatrix
   }
 
   template <size_t P>
-  PUGS_INLINE constexpr TinyMatrix<M, P, T>
-  operator*(const TinyMatrix<N, P, T>& B) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix<M, P, T>
+  operator*(const TinyMatrix<N, P, T>& B) const noexcept
   {
     const TinyMatrix& A = *this;
     TinyMatrix<M, P, T> AB;
@@ -161,9 +160,8 @@ class [[nodiscard]] TinyMatrix
     return AB;
   }
 
-  PUGS_INLINE
-  constexpr TinyVector<M, T>
-  operator*(const TinyVector<N, T>& x) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyVector<M, T>
+  operator*(const TinyVector<N, T>& x) const noexcept
   {
     const TinyMatrix& A = *this;
     TinyVector<M, T> Ax;
@@ -198,7 +196,7 @@ class [[nodiscard]] TinyMatrix
   }
 
   [[nodiscard]] PUGS_INLINE constexpr bool
-  operator==(const TinyMatrix& A) const
+  operator==(const TinyMatrix& A) const noexcept
   {
     for (size_t i = 0; i < M * N; ++i) {
       if (m_values[i] != A.m_values[i])
@@ -208,14 +206,13 @@ class [[nodiscard]] TinyMatrix
   }
 
   [[nodiscard]] PUGS_INLINE constexpr bool
-  operator!=(const TinyMatrix& A) const
+  operator!=(const TinyMatrix& A) const noexcept
   {
     return not this->operator==(A);
   }
 
-  PUGS_INLINE
-  constexpr TinyMatrix
-  operator+(const TinyMatrix& A) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix
+  operator+(const TinyMatrix& A) const noexcept
   {
     TinyMatrix sum;
     for (size_t i = 0; i < M * N; ++i) {
@@ -224,17 +221,15 @@ class [[nodiscard]] TinyMatrix
     return sum;
   }
 
-  PUGS_INLINE
-  constexpr TinyMatrix
-  operator+(TinyMatrix&& A) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix
+  operator+(TinyMatrix&& A) const noexcept
   {
     A += *this;
     return std::move(A);
   }
 
-  PUGS_INLINE
-  constexpr TinyMatrix
-  operator-(const TinyMatrix& A) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix
+  operator-(const TinyMatrix& A) const noexcept
   {
     TinyMatrix difference;
     for (size_t i = 0; i < M * N; ++i) {
@@ -243,9 +238,8 @@ class [[nodiscard]] TinyMatrix
     return difference;
   }
 
-  PUGS_INLINE
-  constexpr TinyMatrix
-  operator-(TinyMatrix&& A) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix
+  operator-(TinyMatrix&& A) const noexcept
   {
     for (size_t i = 0; i < M * N; ++i) {
       A.m_values[i] = m_values[i] - A.m_values[i];
@@ -255,7 +249,7 @@ class [[nodiscard]] TinyMatrix
 
   PUGS_INLINE
   constexpr TinyMatrix&
-  operator+=(const TinyMatrix& A)
+  operator+=(const TinyMatrix& A) noexcept
   {
     for (size_t i = 0; i < M * N; ++i) {
       m_values[i] += A.m_values[i];
@@ -265,7 +259,7 @@ class [[nodiscard]] TinyMatrix
 
   PUGS_INLINE
   constexpr TinyMatrix&
-  operator-=(const TinyMatrix& A)
+  operator-=(const TinyMatrix& A) noexcept
   {
     for (size_t i = 0; i < M * N; ++i) {
       m_values[i] -= A.m_values[i];
@@ -376,7 +370,7 @@ class [[nodiscard]] TinyMatrix
 
 template <size_t M, size_t N, typename T>
 [[nodiscard]] PUGS_INLINE constexpr TinyMatrix<M, N, T>
-tensorProduct(const TinyVector<M, T>& x, const TinyVector<N, T>& y)
+tensorProduct(const TinyVector<M, T>& x, const TinyVector<N, T>& y) noexcept
 {
   TinyMatrix<M, N, T> A;
   for (size_t i = 0; i < M; ++i) {
@@ -389,7 +383,8 @@ tensorProduct(const TinyVector<M, T>& x, const TinyVector<N, T>& y)
 
 template <size_t N, typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-det(const TinyMatrix<N, N, T>& A)
+
+det(const TinyMatrix<N, N, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "determinant is not defined for non-arithmetic types");
   static_assert(std::is_floating_point<T>::value, "determinant for arbitrary dimension N is defined for floating "
@@ -440,7 +435,7 @@ det(const TinyMatrix<N, N, T>& A)
 
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-det(const TinyMatrix<1, 1, T>& A)
+det(const TinyMatrix<1, 1, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "determinent is not defined for non-arithmetic types");
   return A(0, 0);
@@ -448,7 +443,7 @@ det(const TinyMatrix<1, 1, T>& A)
 
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-det(const TinyMatrix<2, 2, T>& A)
+det(const TinyMatrix<2, 2, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "determinent is not defined for non-arithmetic types");
   return A(0, 0) * A(1, 1) - A(1, 0) * A(0, 1);
@@ -456,7 +451,7 @@ det(const TinyMatrix<2, 2, T>& A)
 
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-det(const TinyMatrix<3, 3, T>& A)
+det(const TinyMatrix<3, 3, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "determinent is not defined for non-arithmetic types");
   return A(0, 0) * (A(1, 1) * A(2, 2) - A(2, 1) * A(1, 2)) - A(1, 0) * (A(0, 1) * A(2, 2) - A(2, 1) * A(0, 2)) +
@@ -465,7 +460,7 @@ det(const TinyMatrix<3, 3, T>& A)
 
 template <size_t M, size_t N, typename T>
 [[nodiscard]] PUGS_INLINE constexpr TinyMatrix<M - 1, N - 1, T>
-getMinor(const TinyMatrix<M, N, T>& A, size_t I, size_t J)
+getMinor(const TinyMatrix<M, N, T>& A, size_t I, size_t J) noexcept(NO_ASSERT)
 {
   static_assert(M >= 2 and N >= 2, "minor calculation requires at least 2x2 matrices");
   Assert((I < M) and (J < N));
@@ -491,7 +486,7 @@ getMinor(const TinyMatrix<M, N, T>& A, size_t I, size_t J)
 
 template <size_t N, typename T>
 [[nodiscard]] PUGS_INLINE T
-trace(const TinyMatrix<N, N, T>& A)
+trace(const TinyMatrix<N, N, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "trace is not defined for non-arithmetic types");
 
@@ -517,7 +512,7 @@ template <size_t N, typename T>
 
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr TinyMatrix<1, 1, T>
-inverse(const TinyMatrix<1, 1, T>& A)
+inverse(const TinyMatrix<1, 1, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "inverse is not defined for non-arithmetic types");
   static_assert(std::is_floating_point<T>::value, "inverse is defined for floating point types only");
@@ -528,7 +523,7 @@ inverse(const TinyMatrix<1, 1, T>& A)
 
 template <size_t N, typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-cofactor(const TinyMatrix<N, N, T>& A, size_t i, size_t j)
+cofactor(const TinyMatrix<N, N, T>& A, size_t i, size_t j) noexcept(NO_ASSERT)
 {
   static_assert(std::is_arithmetic<T>::value, "cofactor is not defined for non-arithmetic types");
   const T sign = ((i + j) % 2) ? -1 : 1;
@@ -538,7 +533,7 @@ cofactor(const TinyMatrix<N, N, T>& A, size_t i, size_t j)
 
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr TinyMatrix<2, 2, T>
-inverse(const TinyMatrix<2, 2, T>& A)
+inverse(const TinyMatrix<2, 2, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "inverse is not defined for non-arithmetic types");
   static_assert(std::is_floating_point<T>::value, "inverse is defined for floating point types only");
@@ -552,7 +547,7 @@ inverse(const TinyMatrix<2, 2, T>& A)
 
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr TinyMatrix<3, 3, T>
-inverse(const TinyMatrix<3, 3, T>& A)
+inverse(const TinyMatrix<3, 3, T>& A) noexcept(NO_ASSERT)
 {
   static_assert(std::is_arithmetic<T>::value, "inverse is not defined for non-arithmetic types");
   static_assert(std::is_floating_point<T>::value, "inverse is defined for floating point types only");
diff --git a/src/algebra/TinyVector.hpp b/src/algebra/TinyVector.hpp
index be0efac46ede1211f1519d0e5b9760bcfa20b72d..e6d33fe26c46bdbcef4283dfd318ace9d990c40e 100644
--- a/src/algebra/TinyVector.hpp
+++ b/src/algebra/TinyVector.hpp
@@ -34,7 +34,7 @@ class [[nodiscard]] TinyVector
 
  public:
   [[nodiscard]] PUGS_INLINE constexpr TinyVector
-  operator-() const
+  operator-() const noexcept
   {
     TinyVector opposite;
     for (size_t i = 0; i < N; ++i) {
@@ -44,13 +44,13 @@ class [[nodiscard]] TinyVector
   }
 
   [[nodiscard]] PUGS_INLINE constexpr size_t
-  dimension() const
+  dimension() const noexcept
   {
     return N;
   }
 
   [[nodiscard]] PUGS_INLINE constexpr bool
-  operator==(const TinyVector& v) const
+  operator==(const TinyVector& v) const noexcept
   {
     for (size_t i = 0; i < N; ++i) {
       if (m_values[i] != v.m_values[i])
@@ -60,13 +60,13 @@ class [[nodiscard]] TinyVector
   }
 
   [[nodiscard]] PUGS_INLINE constexpr bool
-  operator!=(const TinyVector& v) const
+  operator!=(const TinyVector& v) const noexcept
   {
     return not this->operator==(v);
   }
 
   [[nodiscard]] PUGS_INLINE constexpr friend T
-  dot(const TinyVector& u, const TinyVector& v)
+  dot(const TinyVector& u, const TinyVector& v) noexcept
   {
     T t = u.m_values[0] * v.m_values[0];
     for (size_t i = 1; i < N; ++i) {
@@ -77,7 +77,7 @@ class [[nodiscard]] TinyVector
 
   PUGS_INLINE
   constexpr TinyVector&
-  operator*=(const T& t)
+  operator*=(const T& t) noexcept
   {
     for (size_t i = 0; i < N; ++i) {
       m_values[i] *= t;
@@ -85,17 +85,15 @@ class [[nodiscard]] TinyVector
     return *this;
   }
 
-  PUGS_INLINE
-  constexpr friend TinyVector
-  operator*(const T& t, const TinyVector& v)
+  [[nodiscard]] PUGS_INLINE constexpr friend TinyVector
+  operator*(const T& t, const TinyVector& v) noexcept
   {
     TinyVector w = v;
     return w *= t;
   }
 
-  PUGS_INLINE
-  constexpr friend TinyVector
-  operator*(const T& t, TinyVector&& v)
+  [[nodiscard]] PUGS_INLINE constexpr friend TinyVector
+  operator*(const T& t, TinyVector&& v) noexcept
   {
     v *= t;
     return std::move(v);
@@ -113,9 +111,8 @@ class [[nodiscard]] TinyVector
     return os;
   }
 
-  PUGS_INLINE
-  constexpr TinyVector
-  operator+(const TinyVector& v) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyVector
+  operator+(const TinyVector& v) const noexcept
   {
     TinyVector sum;
     for (size_t i = 0; i < N; ++i) {
@@ -124,9 +121,8 @@ class [[nodiscard]] TinyVector
     return sum;
   }
 
-  PUGS_INLINE
-  constexpr TinyVector
-  operator+(TinyVector&& v) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyVector
+  operator+(TinyVector&& v) const noexcept
   {
     for (size_t i = 0; i < N; ++i) {
       v.m_values[i] += m_values[i];
@@ -134,9 +130,8 @@ class [[nodiscard]] TinyVector
     return std::move(v);
   }
 
-  PUGS_INLINE
-  constexpr TinyVector
-  operator-(const TinyVector& v) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyVector
+  operator-(const TinyVector& v) const noexcept
   {
     TinyVector difference;
     for (size_t i = 0; i < N; ++i) {
@@ -145,9 +140,8 @@ class [[nodiscard]] TinyVector
     return difference;
   }
 
-  PUGS_INLINE
-  constexpr TinyVector
-  operator-(TinyVector&& v) const
+  [[nodiscard]] PUGS_INLINE constexpr TinyVector
+  operator-(TinyVector&& v) const noexcept
   {
     for (size_t i = 0; i < N; ++i) {
       v.m_values[i] = m_values[i] - v.m_values[i];
@@ -155,9 +149,8 @@ class [[nodiscard]] TinyVector
     return std::move(v);
   }
 
-  PUGS_INLINE
-  constexpr TinyVector&
-  operator+=(const TinyVector& v)
+  PUGS_INLINE constexpr TinyVector&
+  operator+=(const TinyVector& v) noexcept
   {
     for (size_t i = 0; i < N; ++i) {
       m_values[i] += v.m_values[i];
@@ -167,7 +160,7 @@ class [[nodiscard]] TinyVector
 
   PUGS_INLINE
   constexpr TinyVector&
-  operator-=(const TinyVector& v)
+  operator-=(const TinyVector& v) noexcept
   {
     for (size_t i = 0; i < N; ++i) {
       m_values[i] -= v.m_values[i];
@@ -175,16 +168,14 @@ class [[nodiscard]] TinyVector
     return *this;
   }
 
-  PUGS_INLINE
-  constexpr T&
+  [[nodiscard]] PUGS_INLINE constexpr T&
   operator[](size_t i) noexcept(NO_ASSERT)
   {
     Assert(i < N);
     return m_values[i];
   }
 
-  PUGS_INLINE
-  constexpr const T&
+  [[nodiscard]] PUGS_INLINE constexpr const T&
   operator[](size_t i) const noexcept(NO_ASSERT)
   {
     Assert(i < N);
@@ -248,8 +239,8 @@ class [[nodiscard]] TinyVector
 };
 
 template <size_t N, typename T>
-[[nodiscard]] PUGS_INLINE constexpr decltype(std::sqrt(std::declval<T>()))
-l2Norm(const TinyVector<N, T>& x)
+[[nodiscard]] PUGS_INLINE constexpr auto
+l2Norm(const TinyVector<N, T>& x) noexcept
 {
   static_assert(std::is_arithmetic<T>(), "Cannot compute L2 norm for non-arithmetic types");
   static_assert(std::is_floating_point<T>::value, "L2 norm is defined for floating point types only");
@@ -258,7 +249,7 @@ l2Norm(const TinyVector<N, T>& x)
 
 template <size_t N, typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-min(const TinyVector<N, T>& x)
+min(const TinyVector<N, T>& x) noexcept
 {
   T m = x[0];
   for (size_t i = 1; i < N; ++i) {
@@ -271,7 +262,7 @@ min(const TinyVector<N, T>& x)
 
 template <size_t N, typename T>
 [[nodiscard]] PUGS_INLINE constexpr T
-max(const TinyVector<N, T>& x)
+max(const TinyVector<N, T>& x) noexcept
 {
   T m = x[0];
   for (size_t i = 1; i < N; ++i) {
@@ -285,7 +276,7 @@ max(const TinyVector<N, T>& x)
 // Cross product is only defined for dimension 3 vectors
 template <typename T>
 [[nodiscard]] PUGS_INLINE constexpr TinyVector<3, T>
-crossProduct(const TinyVector<3, T>& u, const TinyVector<3, T>& v)
+crossProduct(const TinyVector<3, T>& u, const TinyVector<3, T>& v) noexcept
 {
   TinyVector<3, T> cross_product(u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]);
   return cross_product;
diff --git a/src/geometry/LineTransformation.hpp b/src/geometry/LineTransformation.hpp
index ae0b00a8a1dce7e4b6b6bb13a737ee1114a202f1..4576c0b64904ecb489caeb456b10271be2a076cc 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)
   {
@@ -59,6 +67,13 @@ class LineTransformation
     return x[0] * m_velocity + m_shift;
   }
 
+  PUGS_INLINE
+  const TinyVector<Dimension>&
+  velocity() const
+  {
+    return m_velocity;
+  }
+
   double
   velocityNorm() const
   {
diff --git a/src/geometry/SquareTransformation.hpp b/src/geometry/SquareTransformation.hpp
index d6474f33e4ecbaa4bcfba525dadc9fd38e95962d..b0e970c6eda673b48c5cf5c0d0a1ad7e73f8ef74 100644
--- a/src/geometry/SquareTransformation.hpp
+++ b/src/geometry/SquareTransformation.hpp
@@ -84,6 +84,25 @@ class SquareTransformation
     return m_coefficients * X + m_shift;
   }
 
+  PUGS_INLINE
+  TinyVector<Dimension>
+  areaVariation(const TinyVector<2>& X) const
+  {
+    const auto& T   = m_coefficients;
+    const double& x = X[0];
+    const double& y = X[1];
+
+    const TinyVector<Dimension> dxT{T(0, 0) + T(0, 2) * y,   //
+                                    T(1, 0) + T(1, 2) * y,   //
+                                    T(2, 0) + T(2, 2) * y};
+
+    const TinyVector<Dimension> dyT{T(0, 1) + T(0, 2) * x,   //
+                                    T(1, 1) + T(1, 2) * x,   //
+                                    T(2, 1) + T(2, 2) * x};
+
+    return crossProduct(dxT, dyT);
+  }
+
   PUGS_INLINE double
   areaVariationNorm(const TinyVector<2>& X) const
   {
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..c49db368684d8c96aeb5061041828799e8509fac 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)
   {
@@ -58,6 +66,7 @@ class TriangleTransformation
  private:
   TinyMatrix<Dimension, 2> m_jacobian;
   TinyVector<Dimension> m_shift;
+  TinyVector<Dimension> m_area_variation;
   double m_area_variation_norm;
 
  public:
@@ -68,6 +77,21 @@ class TriangleTransformation
     return m_jacobian * x + m_shift;
   }
 
+  PUGS_INLINE
+  TinyVector<Dimension>
+  areaVariation() const
+  {
+    return m_area_variation;
+  }
+
+  PUGS_INLINE
+  TinyVector<Dimension>
+  areaVariation(const TinyVector<2>&) const
+  {
+    return m_area_variation;
+  }
+
+  PUGS_INLINE
   double
   areaVariationNorm() const
   {
@@ -84,7 +108,8 @@ class TriangleTransformation
 
     m_shift = a;
 
-    m_area_variation_norm = l2Norm(crossProduct(b - a, c - a));
+    m_area_variation      = crossProduct(b - a, c - a);
+    m_area_variation_norm = l2Norm(m_area_variation);
   }
 
   ~TriangleTransformation() = default;
diff --git a/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp b/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp
index 5d000813aeff1e670a2f547a69827a679cbfa699..892037219c389e6ff7ebd28e8b5afd586845c14f 100644
--- a/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp
+++ b/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp
@@ -234,10 +234,14 @@ dot(const std::shared_ptr<const DiscreteFunctionVariant>& f_v,
             throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
           }
         } else if constexpr (is_discrete_function_P0_vector_v<TypeOfF>) {
-          if (f.size() == g.size()) {
-            return std::make_shared<DiscreteFunctionVariant>(dot(f, g));
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            if (f.size() == g.size()) {
+              return std::make_shared<DiscreteFunctionVariant>(dot(f, g));
+            } else {
+              throw NormalError("operands have different dimension");
+            }
           } else {
-            throw NormalError("operands have different dimension");
+            throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
           }
         } else {
           throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
@@ -691,8 +695,6 @@ sum_of_Vh_components(const std::shared_ptr<const DiscreteFunctionVariant>& f)
     [&](auto&& discrete_function) -> std::shared_ptr<const DiscreteFunctionVariant> {
       using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
       if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-        using DataType = std::decay_t<typename DiscreteFunctionT::data_type>;
-        static_assert(std::is_same_v<DataType, double>);
         return std::make_shared<DiscreteFunctionVariant>(sumOfComponents(discrete_function));
       } else {
         throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
diff --git a/src/main.cpp b/src/main.cpp
index cd576a9f63a8273b366a76a298b508e2fbb1ff54..cbaf6277e5ee0c1c7d689403e8d78cae54c3ed5f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,6 +5,7 @@
 #include <mesh/DualConnectivityManager.hpp>
 #include <mesh/DualMeshManager.hpp>
 #include <mesh/MeshDataManager.hpp>
+#include <mesh/StencilManager.hpp>
 #include <mesh/SynchronizerManager.hpp>
 #include <utils/ExecutionStatManager.hpp>
 #include <utils/GlobalVariableManager.hpp>
@@ -21,6 +22,8 @@ main(int argc, char* argv[])
   ResumingManager::create();
   ParallelChecker::create();
 
+  GlobalVariableManager::create();
+
   std::string filename = initialize(argc, argv);
 
   PluginsLoader plugins_loader;
@@ -31,16 +34,14 @@ main(int argc, char* argv[])
   MeshDataManager::create();
   DualConnectivityManager::create();
   DualMeshManager::create();
-
-  GlobalVariableManager::create();
+  StencilManager::create();
 
   parser(filename);
   ExecutionStatManager::printInfo();
 
   ModuleRepository::destroy();
 
-  GlobalVariableManager::destroy();
-
+  StencilManager::destroy();
   DualMeshManager::destroy();
   DualConnectivityManager::destroy();
   MeshDataManager::destroy();
@@ -50,6 +51,8 @@ main(int argc, char* argv[])
 
   finalize();
 
+  GlobalVariableManager::destroy();
+
   ParallelChecker::destroy();
   ResumingManager::destroy();
 
diff --git a/src/mesh/CMakeLists.txt b/src/mesh/CMakeLists.txt
index bd45f495eaf08c8b9e000649a4500ddb146aac95..68c6de46daaac99375fcb6b1d3d4e61bffdd83c8 100644
--- a/src/mesh/CMakeLists.txt
+++ b/src/mesh/CMakeLists.txt
@@ -44,6 +44,8 @@ add_library(
   MeshTransformer.cpp
   MeshUtils.cpp
   MeshVariant.cpp
+  StencilBuilder.cpp
+  StencilManager.cpp
   SynchronizerManager.cpp
 )
 
diff --git a/src/mesh/Connectivity.cpp b/src/mesh/Connectivity.cpp
index 44c07b62877451940e975fbc929b6da1530a36f4..029b590f3ddb03225a45de45ddc732c6814d3603 100644
--- a/src/mesh/Connectivity.cpp
+++ b/src/mesh/Connectivity.cpp
@@ -2,6 +2,7 @@
 
 #include <mesh/ConnectivityDescriptor.hpp>
 #include <mesh/ItemValueUtils.hpp>
+#include <mesh/StencilManager.hpp>
 #include <utils/GlobalVariableManager.hpp>
 #include <utils/Messenger.hpp>
 
@@ -51,8 +52,7 @@ Connectivity<Dimension>::_buildFrom(const ConnectivityDescriptor& descriptor)
   {
     const int rank = parallel::rank();
     WeakCellValue<bool> cell_is_owned(*this);
-    parallel_for(
-      this->numberOfCells(), PUGS_CLASS_LAMBDA(CellId j) { cell_is_owned[j] = (m_cell_owner[j] == rank); });
+    parallel_for(this->numberOfCells(), PUGS_CLASS_LAMBDA(CellId j) { cell_is_owned[j] = (m_cell_owner[j] == rank); });
     m_cell_is_owned = cell_is_owned;
   }
 
@@ -62,8 +62,7 @@ Connectivity<Dimension>::_buildFrom(const ConnectivityDescriptor& descriptor)
   {
     const int rank = parallel::rank();
     WeakNodeValue<bool> node_is_owned(*this, node_is_owned_array);
-    parallel_for(
-      this->numberOfNodes(), PUGS_CLASS_LAMBDA(NodeId r) { node_is_owned[r] = (m_node_owner[r] == rank); });
+    parallel_for(this->numberOfNodes(), PUGS_CLASS_LAMBDA(NodeId r) { node_is_owned[r] = (m_node_owner[r] == rank); });
     m_node_is_owned = node_is_owned;
   }
 
@@ -376,6 +375,12 @@ Connectivity<Dimension>::ownCellToCellGraph() const
   return CRSGraph(entries, neighbors);
 }
 
+template <size_t Dimension>
+Connectivity<Dimension>::~Connectivity()
+{
+  StencilManager::instance().deleteConnectivity(this->m_id);
+}
+
 template std::ostream& Connectivity<1>::_write(std::ostream&) const;
 template std::ostream& Connectivity<2>::_write(std::ostream&) const;
 template std::ostream& Connectivity<3>::_write(std::ostream&) const;
@@ -396,6 +401,10 @@ template Connectivity<1>::Connectivity();
 template Connectivity<2>::Connectivity();
 template Connectivity<3>::Connectivity();
 
+template Connectivity<1>::~Connectivity();
+template Connectivity<2>::~Connectivity();
+template Connectivity<3>::~Connectivity();
+
 template void Connectivity<1>::_buildFrom(const ConnectivityDescriptor&);
 template void Connectivity<2>::_buildFrom(const ConnectivityDescriptor&);
 template void Connectivity<3>::_buildFrom(const ConnectivityDescriptor&);
diff --git a/src/mesh/Connectivity.hpp b/src/mesh/Connectivity.hpp
index 6d89a820fc25f2ed662f0e4f277a7f44fb136830..d7b8a608464aef2ffe4ab870520488114948d12a 100644
--- a/src/mesh/Connectivity.hpp
+++ b/src/mesh/Connectivity.hpp
@@ -707,7 +707,7 @@ class Connectivity final : public IConnectivity
   void _buildFrom(const ConnectivityDescriptor& descriptor);
 
  public:
-  ~Connectivity() = default;
+  ~Connectivity();
 };
 
 template <size_t Dimension>
diff --git a/src/mesh/ConnectivityDispatcher.cpp b/src/mesh/ConnectivityDispatcher.cpp
index 1270dd78f39183d198e7155198e904799203398a..d87d7f640de7df65882b9880dbe12f2646372096 100644
--- a/src/mesh/ConnectivityDispatcher.cpp
+++ b/src/mesh/ConnectivityDispatcher.cpp
@@ -2,9 +2,9 @@
 
 #include <mesh/ItemOfItemType.hpp>
 #include <utils/CRSGraph.hpp>
+#include <utils/GlobalVariableManager.hpp>
 #include <utils/Partitioner.hpp>
 
-#include <iostream>
 #include <unordered_set>
 
 template <size_t Dimension>
@@ -86,11 +86,69 @@ ConnectivityDispatcher<Dimension>::_buildItemListToSend()
     std::vector<std::vector<CellId>> cell_vector_to_send_by_proc(parallel::size());
     Array<bool> send_to_rank(parallel::size());
 
+    NodeValue<bool> node_tag{m_connectivity};
+    node_tag.fill(false);
+    std::vector<NodeId> layer_node_id_list;
+    std::vector<NodeId> tagged_node_id_list;
+
+    const size_t nb_layers = GlobalVariableManager::instance().getNumberOfGhostLayers();
+
     for (CellId cell_id = 0; cell_id < m_connectivity.numberOfCells(); ++cell_id) {
+      layer_node_id_list.clear();
       send_to_rank.fill(false);
       const auto& cell_to_node = cell_to_node_matrix[cell_id];
 
       if (cell_is_owned[cell_id]) {
+        for (size_t i_node = 0; i_node < cell_to_node.size(); ++i_node) {
+          const NodeId& node_id    = cell_to_node[i_node];
+          const auto& node_to_cell = node_to_cell_matrix[node_id];
+          for (size_t i_node_cell = 0; i_node_cell < node_to_cell.size(); ++i_node_cell) {
+            const CellId& node_cell_id                 = node_to_cell[i_node_cell];
+            send_to_rank[cell_new_owner[node_cell_id]] = true;
+            const NodeId cell_node_id                  = cell_to_node[i_node];
+            layer_node_id_list.push_back(cell_node_id);
+            node_tag[cell_node_id] = true;
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < nb_layers; ++i_layer) {
+          for (const auto node_id : layer_node_id_list) {
+            tagged_node_id_list.push_back(node_id);
+            const auto& node_to_cell = node_to_cell_matrix[node_id];
+            for (size_t i_node_cell = 0; i_node_cell < node_to_cell.size(); ++i_node_cell) {
+              const CellId& node_cell_id                 = node_to_cell[i_node_cell];
+              send_to_rank[cell_new_owner[node_cell_id]] = true;
+            }
+          }
+
+          if (i_layer + 1 < nb_layers) {
+            std::vector<NodeId> old_layer_node_id_list;
+            std::swap(layer_node_id_list, old_layer_node_id_list);
+
+            for (const auto node_id : old_layer_node_id_list) {
+              const auto& node_to_cell = node_to_cell_matrix[node_id];
+              for (size_t i_node_cell = 0; i_node_cell < node_to_cell.size(); ++i_node_cell) {
+                const CellId& node_cell_id    = node_to_cell[i_node_cell];
+                const auto& node_cell_to_node = cell_to_node_matrix[node_cell_id];
+                for (size_t i_node = 0; i_node < node_cell_to_node.size(); ++i_node) {
+                  const NodeId cell_node_id = node_cell_to_node[i_node];
+                  if (not node_tag[cell_node_id]) {
+                    layer_node_id_list.push_back(cell_node_id);
+                    node_tag[cell_node_id] = true;
+                  }
+                }
+              }
+            }
+          } else {
+            layer_node_id_list.clear();
+          }
+        }
+
+        for (auto node_id : tagged_node_id_list) {
+          node_tag[node_id] = false;
+        }
+        tagged_node_id_list.clear();
+
         for (size_t i_node = 0; i_node < cell_to_node.size(); ++i_node) {
           const NodeId& node_id    = cell_to_node[i_node];
           const auto& node_to_cell = node_to_cell_matrix[node_id];
diff --git a/src/mesh/ConnectivityMatrix.hpp b/src/mesh/ConnectivityMatrix.hpp
index eadc85347cb756932616e655d7b625a2a15505b7..aabd8de949d3e1d2b6c3532876ea3da059d30701 100644
--- a/src/mesh/ConnectivityMatrix.hpp
+++ b/src/mesh/ConnectivityMatrix.hpp
@@ -65,7 +65,7 @@ class ConnectivityMatrix
     Assert(m_row_map[0] == 0, "row map should start with 0");
 #ifndef NDEBUG
     for (size_t i = 1; i < m_row_map.size(); ++i) {
-      Assert(m_row_map[i] > m_row_map[i - 1], "row map values must be strictly increasing");
+      Assert(m_row_map[i] >= m_row_map[i - 1], "row map values must be increasing");
     }
 #endif   // NDEBUG
   }
diff --git a/src/mesh/ItemToItemMatrix.hpp b/src/mesh/ItemToItemMatrix.hpp
index 6cd2d798b6eaa2844d4eae0d33f8405d344593b7..5d9c23d6fee898b87808dcd9361018cf275130ce 100644
--- a/src/mesh/ItemToItemMatrix.hpp
+++ b/src/mesh/ItemToItemMatrix.hpp
@@ -20,8 +20,8 @@ class ItemToItemMatrix
    private:
     using IndexType = typename ConnectivityMatrix::IndexType;
 
-    const IndexType* const m_values;
     const size_t m_size;
+    const IndexType* const m_values;
 
    public:
     PUGS_INLINE
@@ -40,8 +40,9 @@ class ItemToItemMatrix
 
     PUGS_INLINE
     UnsafeSubItemArray(const ConnectivityMatrix& connectivity_matrix, SourceItemId source_item_id)
-      : m_values{&(connectivity_matrix.values()[connectivity_matrix.rowsMap()[source_item_id]])},
-        m_size(connectivity_matrix.rowsMap()[source_item_id + 1] - connectivity_matrix.rowsMap()[source_item_id])
+      : m_size(connectivity_matrix.rowsMap()[source_item_id + 1] - connectivity_matrix.rowsMap()[source_item_id]),
+        m_values{(m_size == 0) ? nullptr
+                               : (&(connectivity_matrix.values()[connectivity_matrix.rowsMap()[source_item_id]]))}
     {}
 
     PUGS_INLINE
diff --git a/src/mesh/MeshFlatEdgeBoundary.cpp b/src/mesh/MeshFlatEdgeBoundary.cpp
index 7a2d4918d9c822303ba6debee91b5f4d707569c4..9c93f2bc796fd9770440cdbf0c88e435aa55ec07 100644
--- a/src/mesh/MeshFlatEdgeBoundary.cpp
+++ b/src/mesh/MeshFlatEdgeBoundary.cpp
@@ -11,7 +11,7 @@ getMeshFlatEdgeBoundary(const MeshType& mesh, const IBoundaryDescriptor& boundar
   MeshEdgeBoundary mesh_edge_boundary          = getMeshEdgeBoundary(mesh, boundary_descriptor);
   MeshFlatNodeBoundary mesh_flat_node_boundary = getMeshFlatNodeBoundary(mesh, boundary_descriptor);
 
-  return MeshFlatEdgeBoundary<MeshType>{mesh, mesh_edge_boundary.refEdgeList(),
+  return MeshFlatEdgeBoundary<MeshType>{mesh, mesh_edge_boundary.refEdgeList(), mesh_flat_node_boundary.origin(),
                                         mesh_flat_node_boundary.outgoingNormal()};
 }
 
diff --git a/src/mesh/MeshFlatEdgeBoundary.hpp b/src/mesh/MeshFlatEdgeBoundary.hpp
index 70b51bc982222b7006b21e3a825cfeeab3fe6c93..ce3d1ded159e5db0d75b7bffbabaecec9b8485c9 100644
--- a/src/mesh/MeshFlatEdgeBoundary.hpp
+++ b/src/mesh/MeshFlatEdgeBoundary.hpp
@@ -12,9 +12,16 @@ class MeshFlatEdgeBoundary final : public MeshEdgeBoundary   // clazy:exclude=co
   using Rd = TinyVector<MeshType::Dimension, double>;
 
  private:
+  const Rd m_origin;
   const Rd m_outgoing_normal;
 
  public:
+  const Rd&
+  origin() const
+  {
+    return m_origin;
+  }
+
   const Rd&
   outgoingNormal() const
   {
@@ -29,8 +36,11 @@ class MeshFlatEdgeBoundary final : public MeshEdgeBoundary   // clazy:exclude=co
                                                                  const IBoundaryDescriptor& boundary_descriptor);
 
  private:
-  MeshFlatEdgeBoundary(const MeshType& mesh, const RefEdgeList& ref_edge_list, const Rd& outgoing_normal)
-    : MeshEdgeBoundary(mesh, ref_edge_list), m_outgoing_normal(outgoing_normal)
+  MeshFlatEdgeBoundary(const MeshType& mesh,
+                       const RefEdgeList& ref_edge_list,
+                       const Rd& origin,
+                       const Rd& outgoing_normal)
+    : MeshEdgeBoundary(mesh, ref_edge_list), m_origin(origin), m_outgoing_normal(outgoing_normal)
   {}
 
  public:
diff --git a/src/mesh/MeshFlatFaceBoundary.cpp b/src/mesh/MeshFlatFaceBoundary.cpp
index 2ecdf6768eb865413386b10a1a419c6e30799def..46aa7063fae5899c2786e6cece6edfd284ddcb90 100644
--- a/src/mesh/MeshFlatFaceBoundary.cpp
+++ b/src/mesh/MeshFlatFaceBoundary.cpp
@@ -11,7 +11,7 @@ getMeshFlatFaceBoundary(const MeshType& mesh, const IBoundaryDescriptor& boundar
   MeshFaceBoundary mesh_face_boundary          = getMeshFaceBoundary(mesh, boundary_descriptor);
   MeshFlatNodeBoundary mesh_flat_node_boundary = getMeshFlatNodeBoundary(mesh, boundary_descriptor);
 
-  return MeshFlatFaceBoundary<MeshType>{mesh, mesh_face_boundary.refFaceList(),
+  return MeshFlatFaceBoundary<MeshType>{mesh, mesh_face_boundary.refFaceList(), mesh_flat_node_boundary.origin(),
                                         mesh_flat_node_boundary.outgoingNormal()};
 }
 
diff --git a/src/mesh/MeshFlatFaceBoundary.hpp b/src/mesh/MeshFlatFaceBoundary.hpp
index fc4d9d0f82b15a0d02dea4efdff3a13a6cceeb7a..2142202682f91059d1a19b80d90f76ef93a354ff 100644
--- a/src/mesh/MeshFlatFaceBoundary.hpp
+++ b/src/mesh/MeshFlatFaceBoundary.hpp
@@ -12,9 +12,16 @@ class MeshFlatFaceBoundary final : public MeshFaceBoundary   // clazy:exclude=co
   using Rd = TinyVector<MeshType::Dimension, double>;
 
  private:
+  const Rd m_origin;
   const Rd m_outgoing_normal;
 
  public:
+  const Rd&
+  origin() const
+  {
+    return m_origin;
+  }
+
   const Rd&
   outgoingNormal() const
   {
@@ -29,8 +36,11 @@ class MeshFlatFaceBoundary final : public MeshFaceBoundary   // clazy:exclude=co
                                                                  const IBoundaryDescriptor& boundary_descriptor);
 
  private:
-  MeshFlatFaceBoundary(const MeshType& mesh, const RefFaceList& ref_face_list, const Rd& outgoing_normal)
-    : MeshFaceBoundary(mesh, ref_face_list), m_outgoing_normal(outgoing_normal)
+  MeshFlatFaceBoundary(const MeshType& mesh,
+                       const RefFaceList& ref_face_list,
+                       const Rd& origin,
+                       const Rd& outgoing_normal)
+    : MeshFaceBoundary(mesh, ref_face_list), m_origin(origin), m_outgoing_normal(outgoing_normal)
   {}
 
  public:
diff --git a/src/mesh/MeshFlatNodeBoundary.cpp b/src/mesh/MeshFlatNodeBoundary.cpp
index c3ca1dbd4ce980e6290b99af8a7bca878ca4520f..647298e97ef3ea2333dfeac62a7065234c343d1a 100644
--- a/src/mesh/MeshFlatNodeBoundary.cpp
+++ b/src/mesh/MeshFlatNodeBoundary.cpp
@@ -34,14 +34,13 @@ MeshFlatNodeBoundary<MeshType>::_checkBoundaryIsFlat(const TinyVector<MeshType::
 
 template <>
 TinyVector<1, double>
-MeshFlatNodeBoundary<Mesh<1>>::_getNormal(const Mesh<1>& mesh)
+MeshFlatNodeBoundary<Mesh<1>>::_getOrigin(const Mesh<1>& mesh)
 {
-  using R = TinyVector<1, double>;
+  auto node_is_owned = mesh.connectivity().nodeIsOwned();
+  auto node_list     = m_ref_node_list.list();
 
   const size_t number_of_bc_nodes = [&]() {
     size_t nb_bc_nodes = 0;
-    auto node_is_owned = mesh.connectivity().nodeIsOwned();
-    auto node_list     = m_ref_node_list.list();
     for (size_t i_node = 0; i_node < node_list.size(); ++i_node) {
       nb_bc_nodes += (node_is_owned[node_list[i_node]]);
     }
@@ -55,7 +54,43 @@ MeshFlatNodeBoundary<Mesh<1>>::_getNormal(const Mesh<1>& mesh)
     throw NormalError(ost.str());
   }
 
-  return R{1};
+  auto xr = mesh.xr();
+
+  Array<Rd> origin;
+  for (size_t i_node = 0; i_node < node_list.size(); ++i_node) {
+    const NodeId node_id = node_list[i_node];
+    if (node_is_owned[node_list[i_node]]) {
+      origin    = Array<Rd>(1);
+      origin[0] = xr[node_id];
+    }
+  }
+  origin = parallel::allGatherVariable(origin);
+  Assert(origin.size() == 1);
+  return origin[0];
+}
+
+template <>
+TinyVector<1, double>
+MeshFlatNodeBoundary<Mesh<1>>::_getNormal(const Mesh<1>&)
+{
+  using R1 = TinyVector<1, double>;
+
+  // The verification of the unicity of the boundary node is performed by _getOrigin()
+  return R1{1};
+}
+
+template <>
+TinyVector<2, double>
+MeshFlatNodeBoundary<Mesh<2>>::_getOrigin(const Mesh<2>& mesh)
+{
+  using R2 = TinyVector<2, double>;
+
+  std::array<R2, 2> bounds = getBounds(mesh, m_ref_node_list);
+
+  const R2& xmin = bounds[0];
+  const R2& xmax = bounds[1];
+
+  return 0.5 * (xmin + xmax);
 }
 
 template <>
@@ -137,6 +172,46 @@ MeshFlatNodeBoundary<Mesh<3>>::_getFarestNode(const Mesh<3>& mesh, const Rd& x0,
   return farest_x;
 }
 
+template <>
+TinyVector<3, double>
+MeshFlatNodeBoundary<Mesh<3>>::_getOrigin(const Mesh<3>& mesh)
+{
+  using R3 = TinyVector<3, double>;
+
+  std::array<R3, 2> diagonal = [](const std::array<R3, 6>& bounds) {
+    size_t max_i      = 0;
+    size_t max_j      = 0;
+    double max_length = 0;
+
+    for (size_t i = 0; i < bounds.size(); ++i) {
+      for (size_t j = i + 1; j < bounds.size(); ++j) {
+        double length = l2Norm(bounds[i] - bounds[j]);
+        if (length > max_length) {
+          max_i      = i;
+          max_j      = j;
+          max_length = length;
+        }
+      }
+    }
+
+    return std::array<R3, 2>{bounds[max_i], bounds[max_j]};
+  }(getBounds(mesh, m_ref_node_list));
+
+  const R3& x0 = diagonal[0];
+  const R3& x1 = diagonal[1];
+
+  if (x0 == x1) {
+    std::ostringstream ost;
+    ost << "invalid boundary \"" << rang::fgB::yellow << m_ref_node_list.refId() << rang::style::reset
+        << "\": unable to compute normal";
+    throw NormalError(ost.str());
+  }
+
+  const R3 x2 = this->_getFarestNode(mesh, x0, x1);
+
+  return 1. / 3. * (x0 + x1 + x2);
+}
+
 template <>
 TinyVector<3, double>
 MeshFlatNodeBoundary<Mesh<3>>::_getNormal(const Mesh<3>& mesh)
diff --git a/src/mesh/MeshFlatNodeBoundary.hpp b/src/mesh/MeshFlatNodeBoundary.hpp
index 66c610ea72a3aed5e6876516890c0d168bc9aac4..2ef2a22da8cd0541c1050aab2f63bebf7c7b8869 100644
--- a/src/mesh/MeshFlatNodeBoundary.hpp
+++ b/src/mesh/MeshFlatNodeBoundary.hpp
@@ -13,6 +13,7 @@ class [[nodiscard]] MeshFlatNodeBoundary final : public MeshNodeBoundary   // cl
   using Rd = TinyVector<MeshType::Dimension, double>;
 
  private:
+  const Rd m_origin;
   const Rd m_outgoing_normal;
 
   Rd _getFarestNode(const MeshType& mesh, const Rd& x0, const Rd& x1);
@@ -24,9 +25,17 @@ class [[nodiscard]] MeshFlatNodeBoundary final : public MeshNodeBoundary   // cl
                             const double length,
                             const MeshType& mesh) const;
 
+  Rd _getOrigin(const MeshType& mesh);
+
   Rd _getOutgoingNormal(const MeshType& mesh);
 
  public:
+  const Rd&
+  origin() const
+  {
+    return m_origin;
+  }
+
   const Rd&
   outgoingNormal() const
   {
@@ -42,11 +51,11 @@ class [[nodiscard]] MeshFlatNodeBoundary final : public MeshNodeBoundary   // cl
 
  private:
   MeshFlatNodeBoundary(const MeshType& mesh, const RefFaceList& ref_face_list)
-    : MeshNodeBoundary(mesh, ref_face_list), m_outgoing_normal(_getOutgoingNormal(mesh))
+    : MeshNodeBoundary(mesh, ref_face_list), m_origin{_getOrigin(mesh)}, m_outgoing_normal(_getOutgoingNormal(mesh))
   {}
 
   MeshFlatNodeBoundary(const MeshType& mesh, const RefNodeList& ref_node_list)
-    : MeshNodeBoundary(mesh, ref_node_list), m_outgoing_normal(_getOutgoingNormal(mesh))
+    : MeshNodeBoundary(mesh, ref_node_list), m_origin{_getOrigin(mesh)}, m_outgoing_normal(_getOutgoingNormal(mesh))
   {}
 
  public:
diff --git a/src/mesh/StencilArray.hpp b/src/mesh/StencilArray.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ffe8638bf0d8fdb472780150603c7fab21ca1eb9
--- /dev/null
+++ b/src/mesh/StencilArray.hpp
@@ -0,0 +1,108 @@
+#ifndef STENCIL_ARRAY_HPP
+#define STENCIL_ARRAY_HPP
+
+#include <mesh/ConnectivityMatrix.hpp>
+#include <mesh/IBoundaryDescriptor.hpp>
+#include <mesh/ItemId.hpp>
+#include <mesh/ItemToItemMatrix.hpp>
+
+template <ItemType source_item_type, ItemType target_item_type>
+class StencilArray
+{
+ public:
+  using ItemToItemMatrixT = ItemToItemMatrix<source_item_type, target_item_type>;
+
+  using SourceItemId = ItemIdT<source_item_type>;
+  using TargetItemId = ItemIdT<target_item_type>;
+
+  class BoundaryDescriptorStencilArray
+  {
+   private:
+    std::shared_ptr<const IBoundaryDescriptor> m_iboundary_descriptor;
+    const ConnectivityMatrix m_connectivity_matrix;
+    const ItemToItemMatrixT m_stencil_array;
+
+   public:
+    const IBoundaryDescriptor&
+    boundaryDescriptor() const
+    {
+      return *m_iboundary_descriptor;
+    }
+
+    const auto&
+    stencilArray() const
+    {
+      return m_stencil_array;
+    }
+
+    BoundaryDescriptorStencilArray(const std::shared_ptr<const IBoundaryDescriptor>& iboundary_descriptor,
+                                   const ConnectivityMatrix& connectivity_matrix)
+      : m_iboundary_descriptor{iboundary_descriptor},
+        m_connectivity_matrix{connectivity_matrix},
+        m_stencil_array{m_connectivity_matrix}
+    {}
+
+    BoundaryDescriptorStencilArray(const BoundaryDescriptorStencilArray& bdsa)
+      : m_iboundary_descriptor{bdsa.m_iboundary_descriptor},
+        m_connectivity_matrix{bdsa.m_connectivity_matrix},
+        m_stencil_array{m_connectivity_matrix}
+    {}
+
+    BoundaryDescriptorStencilArray(BoundaryDescriptorStencilArray&& bdsa)
+      : m_iboundary_descriptor{std::move(bdsa.m_iboundary_descriptor)},
+        m_connectivity_matrix{std::move(bdsa.m_connectivity_matrix)},
+        m_stencil_array{m_connectivity_matrix}
+    {}
+
+    ~BoundaryDescriptorStencilArray() = default;
+  };
+
+  using BoundaryDescriptorStencilArrayList = std::vector<BoundaryDescriptorStencilArray>;
+
+ private:
+  const ConnectivityMatrix m_connectivity_matrix;
+  const ItemToItemMatrixT m_stencil_array;
+  BoundaryDescriptorStencilArrayList m_symmetry_boundary_stencil_array_list;
+
+ public:
+  PUGS_INLINE
+  const auto&
+  symmetryBoundaryStencilArrayList() const
+  {
+    return m_symmetry_boundary_stencil_array_list;
+  }
+
+  PUGS_INLINE
+  auto
+  operator[](SourceItemId source_item_id) const
+  {
+    return m_stencil_array[source_item_id];
+  }
+
+  StencilArray(const ConnectivityMatrix& connectivity_matrix,
+               const BoundaryDescriptorStencilArrayList& symmetry_boundary_descriptor_stencil_array_list)
+    : m_connectivity_matrix{connectivity_matrix},
+      m_stencil_array{m_connectivity_matrix},
+      m_symmetry_boundary_stencil_array_list{symmetry_boundary_descriptor_stencil_array_list}
+  {}
+
+  StencilArray(const StencilArray& stencil_array)
+    : m_connectivity_matrix{stencil_array.m_connectivity_matrix},
+      m_stencil_array{m_connectivity_matrix},
+      m_symmetry_boundary_stencil_array_list{stencil_array.m_symmetry_boundary_stencil_array_list}
+  {}
+
+  StencilArray(StencilArray&& stencil_array)
+    : m_connectivity_matrix{std::move(stencil_array.m_connectivity_matrix)},
+      m_stencil_array{m_connectivity_matrix},
+      m_symmetry_boundary_stencil_array_list{std::move(stencil_array.m_symmetry_boundary_stencil_array_list)}
+  {}
+
+  ~StencilArray() = default;
+};
+
+using CellToCellStencilArray = StencilArray<ItemType::cell, ItemType::cell>;
+using CellToFaceStencilArray = StencilArray<ItemType::cell, ItemType::face>;
+using NodeToCellStencilArray = StencilArray<ItemType::node, ItemType::cell>;
+
+#endif   // STENCIL_ARRAY_HPP
diff --git a/src/mesh/StencilBuilder.cpp b/src/mesh/StencilBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..de7501d44b5b0aef6e50f830460e9be110456331
--- /dev/null
+++ b/src/mesh/StencilBuilder.cpp
@@ -0,0 +1,895 @@
+#include <mesh/StencilBuilder.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/ItemArray.hpp>
+#include <utils/GlobalVariableManager.hpp>
+#include <utils/Messenger.hpp>
+
+template <ItemType item_type>
+class StencilBuilder::Layer
+{
+  using ItemId = ItemIdT<item_type>;
+  ItemValue<const int, item_type> m_number_of;
+
+  std::vector<ItemId> m_item_id_vector;
+  std::vector<int> m_item_number_vector;
+
+ public:
+  void
+  clear()
+  {
+    m_item_id_vector.clear();
+    m_item_number_vector.clear();
+  }
+
+  size_t
+  size() const
+  {
+    Assert(m_item_id_vector.size() == m_item_number_vector.size());
+    return m_item_id_vector.size();
+  }
+
+  const std::vector<ItemId>&
+  itemIdList() const
+  {
+    return m_item_id_vector;
+  }
+
+  bool
+  hasItemNumber(const ItemId item_id) const
+  {
+    const int item_number = m_number_of[item_id];
+
+    ssize_t begin = 0;
+    ssize_t end   = m_item_number_vector.size();
+
+    while (begin < end) {
+      const ssize_t mid      = (begin + end) / 2;
+      const auto& mid_number = m_item_number_vector[mid];
+      if (mid_number == item_number) {
+        return true;   // We found the value
+      } else if (mid_number < item_number) {
+        if (begin == mid) {
+          break;
+        }
+        begin = mid;
+      } else {
+        if (end == mid) {
+          break;
+        }
+        end = mid;
+      }
+    }
+    return false;
+  }
+
+  void
+  add(const ItemId item_id)
+  {
+    const int item_number = m_number_of[item_id];
+
+    ssize_t begin = 0;
+    ssize_t end   = m_item_number_vector.size();
+
+    while (begin < end) {
+      const ssize_t mid      = (begin + end) / 2;
+      const auto& mid_number = m_item_number_vector[mid];
+
+      if (mid_number == item_number) {
+        return;   // We found the value
+      } else if (mid_number < item_number) {
+        if (begin == mid) {
+          break;
+        }
+        begin = mid;
+      } else {
+        if (end == mid) {
+          break;
+        }
+        end = mid;
+      }
+    }
+
+    m_item_id_vector.push_back(item_id);
+    m_item_number_vector.push_back(item_number);
+
+    const auto& begin_number = m_item_number_vector[begin];
+
+    if (begin_number > item_number) {
+      for (ssize_t i = m_item_number_vector.size() - 2; i >= begin; --i) {
+        std::swap(m_item_number_vector[i], m_item_number_vector[i + 1]);
+        std::swap(m_item_id_vector[i], m_item_id_vector[i + 1]);
+      }
+    } else if (begin_number < item_number) {
+      for (ssize_t i = m_item_number_vector.size() - 2; i > begin; --i) {
+        std::swap(m_item_number_vector[i], m_item_number_vector[i + 1]);
+        std::swap(m_item_id_vector[i], m_item_id_vector[i + 1]);
+      }
+    }
+  }
+
+  Layer& operator=(const Layer&) = default;
+  Layer& operator=(Layer&&)      = default;
+
+  template <size_t Dimension>
+  Layer(const Connectivity<Dimension>& connectivity) : m_number_of{connectivity.template number<item_type>()}
+  {}
+
+  //  Layer() = default;
+
+  Layer(const Layer&) = default;
+  Layer(Layer&&)      = default;
+
+  ~Layer() = default;
+};
+
+template <ItemType connecting_item_type, typename ConnectivityType>
+auto
+StencilBuilder::_buildSymmetryConnectingItemList(const ConnectivityType& connectivity,
+                                                 const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  static_assert(connecting_item_type != ItemType::cell, "cells cannot be used to define symmetry boundaries");
+
+  using ConnectingItemId = ItemIdT<connecting_item_type>;
+  ItemArray<bool, connecting_item_type> symmetry_connecting_item_list{connectivity,
+                                                                      symmetry_boundary_descriptor_list.size()};
+  symmetry_connecting_item_list.fill(false);
+
+  if constexpr (ItemTypeId<ConnectivityType::Dimension>::itemTId(connecting_item_type) ==
+                ItemTypeId<ConnectivityType::Dimension>::itemTId(ItemType::face)) {
+    size_t i_symmetry_boundary = 0;
+    for (auto p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+      const IBoundaryDescriptor& boundary_descriptor = *p_boundary_descriptor;
+
+      bool found = false;
+      for (size_t i_ref_connecting_item_list = 0;
+           i_ref_connecting_item_list < connectivity.template numberOfRefItemList<connecting_item_type>();
+           ++i_ref_connecting_item_list) {
+        const auto& ref_connecting_item_list =
+          connectivity.template refItemList<connecting_item_type>(i_ref_connecting_item_list);
+        if (ref_connecting_item_list.refId() == boundary_descriptor) {
+          found                     = true;
+          auto connecting_item_list = ref_connecting_item_list.list();
+          for (size_t i_connecting_item = 0; i_connecting_item < connecting_item_list.size(); ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = connecting_item_list[i_connecting_item];
+
+            symmetry_connecting_item_list[connecting_item_id][i_symmetry_boundary] = true;
+          }
+          break;
+        }
+      }
+      ++i_symmetry_boundary;
+      if (not found) {
+        std::ostringstream error_msg;
+        error_msg << "cannot find boundary '" << rang::fgB::yellow << boundary_descriptor << rang::fg::reset << '\'';
+        throw NormalError(error_msg.str());
+      }
+    }
+  } else {
+    auto face_to_connecting_item_matrix =
+      connectivity.template getItemToItemMatrix<ItemType::face, connecting_item_type>();
+    size_t i_symmetry_boundary = 0;
+    for (auto p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+      const IBoundaryDescriptor& boundary_descriptor = *p_boundary_descriptor;
+
+      bool found = false;
+      for (size_t i_ref_face_list = 0; i_ref_face_list < connectivity.template numberOfRefItemList<ItemType::face>();
+           ++i_ref_face_list) {
+        const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i_ref_face_list);
+        if (ref_face_list.refId() == boundary_descriptor) {
+          found = true;
+          for (size_t i_face = 0; i_face < ref_face_list.list().size(); ++i_face) {
+            const FaceId face_id      = ref_face_list.list()[i_face];
+            auto connecting_item_list = face_to_connecting_item_matrix[face_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < connecting_item_list.size(); ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = connecting_item_list[i_connecting_item];
+
+              symmetry_connecting_item_list[connecting_item_id][i_symmetry_boundary] = true;
+            }
+          }
+          break;
+        }
+      }
+      ++i_symmetry_boundary;
+      if (not found) {
+        std::ostringstream error_msg;
+        error_msg << "cannot find boundary '" << rang::fgB::yellow << boundary_descriptor << rang::fg::reset << '\'';
+        throw NormalError(error_msg.str());
+      }
+    }
+  }
+
+  return symmetry_connecting_item_list;
+}
+
+template <ItemType item_type, ItemType connecting_item_type, typename ConnectivityType>
+StencilArray<item_type, item_type>
+StencilBuilder::_build_for_same_source_and_target(const ConnectivityType& connectivity,
+                                                  const size_t& number_of_layers,
+                                                  const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  if (number_of_layers == 0) {
+    throw NormalError("number of layers must be greater than 0 to build stencils");
+  }
+
+  auto item_to_connecting_item_matrix = connectivity.template getItemToItemMatrix<item_type, connecting_item_type>();
+  auto connecting_item_to_item_matrix = connectivity.template getItemToItemMatrix<connecting_item_type, item_type>();
+
+  auto item_is_owned          = connectivity.template isOwned<item_type>();
+  auto item_number            = connectivity.template number<item_type>();
+  auto connecting_item_number = connectivity.template number<connecting_item_type>();
+
+  using ItemId           = ItemIdT<item_type>;
+  using ConnectingItemId = ItemIdT<connecting_item_type>;
+
+  ItemArray<bool, connecting_item_type> symmetry_item_list =
+    this->_buildSymmetryConnectingItemList<connecting_item_type>(connectivity, symmetry_boundary_descriptor_list);
+
+  Array<uint32_t> row_map{connectivity.template numberOf<item_type>() + 1};
+  row_map[0] = 0;
+  std::vector<Array<uint32_t>> symmetry_row_map_list(symmetry_boundary_descriptor_list.size());
+  for (auto&& symmetry_row_map : symmetry_row_map_list) {
+    symmetry_row_map    = Array<uint32_t>{connectivity.template numberOf<item_type>() + 1};
+    symmetry_row_map[0] = 0;
+  }
+
+  std::vector<ItemId> column_indices_vector;
+  std::vector<std::vector<uint32_t>> symmetry_column_indices_vector(symmetry_boundary_descriptor_list.size());
+
+  std::vector<Layer<item_type>> item_layer_list;
+  std::vector<Layer<connecting_item_type>> connecting_layer_list;
+
+  std::vector<Layer<item_type>> symmetry_item_layer_list;
+  std::vector<Layer<connecting_item_type>> symmetry_connecting_layer_list;
+
+  for (size_t i = 0; i < number_of_layers; ++i) {
+    item_layer_list.emplace_back(Layer<item_type>{connectivity});
+    connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+
+    if (symmetry_boundary_descriptor_list.size() > 0) {
+      symmetry_item_layer_list.emplace_back(Layer<item_type>{connectivity});
+      symmetry_connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+    }
+  }
+
+  for (ItemId item_id = 0; item_id < connectivity.template numberOf<item_type>(); ++item_id) {
+    if (item_is_owned[item_id]) {
+      for (auto&& item_layer : item_layer_list) {
+        item_layer.clear();
+      }
+      for (auto&& connecting_layer : connecting_layer_list) {
+        connecting_layer.clear();
+      }
+
+      // First layer is a special case
+      {
+        auto& item_layer       = item_layer_list[0];
+        auto& connecting_layer = connecting_layer_list[0];
+
+        for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_matrix[item_id].size();
+             ++i_connecting_item) {
+          const ConnectingItemId connecting_item_id = item_to_connecting_item_matrix[item_id][i_connecting_item];
+
+          connecting_layer.add(connecting_item_id);
+        }
+
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          for (size_t i_item = 0; i_item < connecting_item_to_item_matrix[connecting_item_id].size(); ++i_item) {
+            const ItemId layer_item_id = connecting_item_to_item_matrix[connecting_item_id][i_item];
+            if (layer_item_id != item_id) {
+              item_layer.add(layer_item_id);
+            }
+          }
+        }
+      }
+
+      for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+        auto& connecting_layer = connecting_layer_list[i_layer];
+
+        auto has_connecting_item = [&i_layer, &connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+              return true;
+            }
+          }
+
+          return false;
+        };
+
+        for (auto&& previous_layer_item_id_list : item_layer_list[i_layer - 1].itemIdList()) {
+          const auto item_to_connecting_item_list = item_to_connecting_item_matrix[previous_layer_item_id_list];
+          for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_list.size();
+               ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = item_to_connecting_item_list[i_connecting_item];
+
+            if (not has_connecting_item(connecting_item_id)) {
+              connecting_layer.add(connecting_item_id);
+            }
+          }
+        }
+
+        auto& item_layer = item_layer_list[i_layer];
+
+        auto has_layer_item = [&i_layer, &item_layer_list](ItemId layer_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (item_layer_list[i].hasItemNumber(layer_item_id)) {
+              return true;
+            }
+          }
+
+          return false;
+        };
+
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          const auto& connecting_item_to_item_list = connecting_item_to_item_matrix[connecting_item_id];
+          for (size_t i_item = 0; i_item < connecting_item_to_item_list.size(); ++i_item) {
+            const ItemId layer_item_id = connecting_item_to_item_list[i_item];
+            if ((layer_item_id != item_id) and (not has_layer_item(layer_item_id))) {
+              item_layer.add(layer_item_id);
+            }
+          }
+        }
+      }
+
+      for (size_t i_symmetry = 0; i_symmetry < symmetry_boundary_descriptor_list.size(); ++i_symmetry) {
+        for (auto&& symmetry_item_layer : symmetry_item_layer_list) {
+          symmetry_item_layer.clear();
+        }
+        for (auto&& symmetry_connecting_layer : symmetry_connecting_layer_list) {
+          symmetry_connecting_layer.clear();
+        }
+
+        // First layer is a special case
+        for (auto&& connecting_item_id : connecting_layer_list[0].itemIdList()) {
+          if (symmetry_item_list[connecting_item_id][i_symmetry]) {
+            symmetry_connecting_layer_list[0].add(connecting_item_id);
+          }
+        }
+
+        for (auto&& connecting_item_id : symmetry_connecting_layer_list[0].itemIdList()) {
+          auto item_of_connecting_item_list = connecting_item_to_item_matrix[connecting_item_id];
+          for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < item_of_connecting_item_list.size();
+               ++i_item_of_connecting_item) {
+            const ItemId item_id_of_connecting_item = item_of_connecting_item_list[i_item_of_connecting_item];
+            symmetry_item_layer_list[0].add(item_id_of_connecting_item);
+          }
+        }
+
+        for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+          auto has_connecting_item = [&i_layer,
+                                      &symmetry_connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& symmetry_connecting_item_id : connecting_layer_list[i_layer].itemIdList()) {
+            if (symmetry_item_list[symmetry_connecting_item_id][i_symmetry]) {
+              if (not has_connecting_item(symmetry_connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(symmetry_connecting_item_id);
+              }
+            }
+          }
+
+          for (auto&& previous_layer_item_id_list : symmetry_item_layer_list[i_layer - 1].itemIdList()) {
+            const auto item_to_connecting_item_list = item_to_connecting_item_matrix[previous_layer_item_id_list];
+            for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = item_to_connecting_item_list[i_connecting_item];
+
+              if (not has_connecting_item(connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(connecting_item_id);
+              }
+            }
+          }
+
+          auto has_symmetry_layer_item = [&i_layer, &symmetry_item_layer_list](ItemId layer_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_item_layer_list[i].hasItemNumber(layer_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& connecting_item_id : symmetry_connecting_layer_list[i_layer].itemIdList()) {
+            auto item_of_connecting_item_list = connecting_item_to_item_matrix[connecting_item_id];
+            for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < item_of_connecting_item_list.size();
+                 ++i_item_of_connecting_item) {
+              const ItemId item_id_of_connecting_item = item_of_connecting_item_list[i_item_of_connecting_item];
+              if (not has_symmetry_layer_item(item_id_of_connecting_item)) {
+                symmetry_item_layer_list[i_layer].add(item_id_of_connecting_item);
+              }
+            }
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          for (auto symmetry_layer_item_id : symmetry_item_layer_list[i_layer].itemIdList()) {
+            symmetry_column_indices_vector[i_symmetry].push_back(symmetry_layer_item_id);
+          }
+        }
+
+        {
+          size_t stencil_size = 0;
+          for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+            auto& item_layer = symmetry_item_layer_list[i_layer];
+            stencil_size += item_layer.size();
+          }
+
+          symmetry_row_map_list[i_symmetry][item_id + 1] = symmetry_row_map_list[i_symmetry][item_id] + stencil_size;
+        }
+      }
+
+      {
+        size_t stencil_size = 0;
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          auto& item_layer = item_layer_list[i_layer];
+          for (auto stencil_item_id : item_layer.itemIdList()) {
+            column_indices_vector.push_back(stencil_item_id);
+          }
+          stencil_size += item_layer.size();
+        }
+        row_map[item_id + 1] = row_map[item_id] + stencil_size;
+      }
+
+    } else {
+      row_map[item_id + 1] = row_map[item_id];
+      for (size_t i = 0; i < symmetry_row_map_list.size(); ++i) {
+        symmetry_row_map_list[i][item_id + 1] = symmetry_row_map_list[i][item_id];
+      }
+    }
+  }
+
+  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]; });
+
+  ConnectivityMatrix primal_stencil{row_map, column_indices};
+
+  typename StencilArray<item_type, item_type>::BoundaryDescriptorStencilArrayList symmetry_boundary_stencil_list;
+  {
+    size_t i = 0;
+    for (auto&& p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+      symmetry_boundary_stencil_list.emplace_back(
+        typename StencilArray<item_type, item_type>::
+          BoundaryDescriptorStencilArray{p_boundary_descriptor,
+                                         ConnectivityMatrix{symmetry_row_map_list[i],
+                                                            convert_to_array(symmetry_column_indices_vector[i])}});
+      ++i;
+    }
+  }
+
+  return {{primal_stencil}, {symmetry_boundary_stencil_list}};
+}
+
+template <ItemType source_item_type,
+          ItemType connecting_item_type,
+          ItemType target_item_type,
+          typename ConnectivityType>
+StencilArray<source_item_type, target_item_type>
+StencilBuilder::_build_for_different_source_and_target(
+  const ConnectivityType& connectivity,
+  const size_t& number_of_layers,
+  const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  constexpr size_t Dimension = ConnectivityType::Dimension;
+
+  if (number_of_layers == 0) {
+    throw NormalError("number of layers must be greater than 0 to build stencils");
+  }
+
+  auto connecting_item_to_target_item_matrix =
+    connectivity.template getItemToItemMatrix<connecting_item_type, target_item_type>();
+  auto target_item_to_connecting_item_matrix =
+    connectivity.template getItemToItemMatrix<target_item_type, connecting_item_type>();
+
+  auto source_item_to_connecting_item_matrix =
+    [](const auto& given_connectivity) -> ItemToItemMatrix<source_item_type, connecting_item_type> {
+    if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                  ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+      return {ConnectivityMatrix{}};
+    } else {
+      return given_connectivity.template getItemToItemMatrix<source_item_type, connecting_item_type>();
+    }
+  }(connectivity);
+
+  auto source_item_is_owned   = connectivity.template isOwned<source_item_type>();
+  auto target_item_number     = connectivity.template number<target_item_type>();
+  auto connecting_item_number = connectivity.template number<connecting_item_type>();
+
+  using SourceItemId     = ItemIdT<source_item_type>;
+  using TargetItemId     = ItemIdT<target_item_type>;
+  using ConnectingItemId = ItemIdT<connecting_item_type>;
+
+  ItemArray<bool, connecting_item_type> symmetry_item_list =
+    this->_buildSymmetryConnectingItemList<connecting_item_type>(connectivity, symmetry_boundary_descriptor_list);
+
+  Array<uint32_t> row_map{connectivity.template numberOf<source_item_type>() + 1};
+  row_map[0] = 0;
+  std::vector<Array<uint32_t>> symmetry_row_map_list(symmetry_boundary_descriptor_list.size());
+  for (auto&& symmetry_row_map : symmetry_row_map_list) {
+    symmetry_row_map    = Array<uint32_t>{connectivity.template numberOf<source_item_type>() + 1};
+    symmetry_row_map[0] = 0;
+  }
+
+  std::vector<TargetItemId> column_indices_vector;
+  std::vector<std::vector<uint32_t>> symmetry_column_indices_vector(symmetry_boundary_descriptor_list.size());
+
+  std::vector<Layer<target_item_type>> target_item_layer_list;
+  std::vector<Layer<connecting_item_type>> connecting_layer_list;
+
+  std::vector<Layer<target_item_type>> symmetry_target_item_layer_list;
+  std::vector<Layer<connecting_item_type>> symmetry_connecting_layer_list;
+
+  for (size_t i = 0; i < number_of_layers; ++i) {
+    target_item_layer_list.emplace_back(Layer<target_item_type>{connectivity});
+    connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+
+    if (symmetry_boundary_descriptor_list.size() > 0) {
+      symmetry_target_item_layer_list.emplace_back(Layer<target_item_type>{connectivity});
+      symmetry_connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+    }
+  }
+
+  for (SourceItemId source_item_id = 0; source_item_id < connectivity.template numberOf<source_item_type>();
+       ++source_item_id) {
+    if (source_item_is_owned[source_item_id]) {
+      for (auto&& target_item_layer : target_item_layer_list) {
+        target_item_layer.clear();
+      }
+      for (auto&& connecting_layer : connecting_layer_list) {
+        connecting_layer.clear();
+      }
+
+      // First layer is a special case
+      {
+        auto& target_item_layer = target_item_layer_list[0];
+        auto& connecting_layer  = connecting_layer_list[0];
+
+        if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                      ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+          connecting_layer.add(ConnectingItemId{static_cast<typename ConnectingItemId::base_type>(source_item_id)});
+        } else {
+          for (size_t i_connecting_item = 0;
+               i_connecting_item < source_item_to_connecting_item_matrix[source_item_id].size(); ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id =
+              source_item_to_connecting_item_matrix[source_item_id][i_connecting_item];
+
+            connecting_layer.add(connecting_item_id);
+          }
+        }
+
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          for (size_t i_item = 0; i_item < connecting_item_to_target_item_matrix[connecting_item_id].size(); ++i_item) {
+            const TargetItemId layer_item_id = connecting_item_to_target_item_matrix[connecting_item_id][i_item];
+            target_item_layer.add(layer_item_id);
+          }
+        }
+      }
+
+      for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+        auto& connecting_layer = connecting_layer_list[i_layer];
+
+        auto has_connecting_item = [&i_layer, &connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+              return true;
+            }
+          }
+
+          return false;
+        };
+
+        for (auto&& previous_layer_item_id_list : target_item_layer_list[i_layer - 1].itemIdList()) {
+          const auto target_item_to_connecting_item_list =
+            target_item_to_connecting_item_matrix[previous_layer_item_id_list];
+          for (size_t i_connecting_item = 0; i_connecting_item < target_item_to_connecting_item_list.size();
+               ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = target_item_to_connecting_item_list[i_connecting_item];
+
+            if (not has_connecting_item(connecting_item_id)) {
+              connecting_layer.add(connecting_item_id);
+            }
+          }
+        }
+
+        auto& target_item_layer = target_item_layer_list[i_layer];
+
+        auto has_layer_item = [&i_layer, &target_item_layer_list](TargetItemId layer_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (target_item_layer_list[i].hasItemNumber(layer_item_id)) {
+              return true;
+            }
+          }
+
+          return false;
+        };
+
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          const auto& connecting_item_to_target_item_list = connecting_item_to_target_item_matrix[connecting_item_id];
+          for (size_t i_item = 0; i_item < connecting_item_to_target_item_list.size(); ++i_item) {
+            const TargetItemId layer_item_id = connecting_item_to_target_item_list[i_item];
+            if (not has_layer_item(layer_item_id)) {
+              target_item_layer.add(layer_item_id);
+            }
+          }
+        }
+      }
+
+      for (size_t i_symmetry = 0; i_symmetry < symmetry_boundary_descriptor_list.size(); ++i_symmetry) {
+        for (auto&& symmetry_target_item_layer : symmetry_target_item_layer_list) {
+          symmetry_target_item_layer.clear();
+        }
+        for (auto&& symmetry_connecting_layer : symmetry_connecting_layer_list) {
+          symmetry_connecting_layer.clear();
+        }
+
+        // First layer is a special case
+        for (auto&& connecting_item_id : connecting_layer_list[0].itemIdList()) {
+          if (symmetry_item_list[connecting_item_id][i_symmetry]) {
+            symmetry_connecting_layer_list[0].add(connecting_item_id);
+          }
+        }
+
+        for (auto&& connecting_item_id : symmetry_connecting_layer_list[0].itemIdList()) {
+          auto item_of_connecting_item_list = connecting_item_to_target_item_matrix[connecting_item_id];
+          for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < item_of_connecting_item_list.size();
+               ++i_item_of_connecting_item) {
+            const TargetItemId item_id_of_connecting_item = item_of_connecting_item_list[i_item_of_connecting_item];
+            symmetry_target_item_layer_list[0].add(item_id_of_connecting_item);
+          }
+        }
+
+        for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+          auto has_connecting_item = [&i_layer,
+                                      &symmetry_connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& symmetry_connecting_item_id : connecting_layer_list[i_layer].itemIdList()) {
+            if (symmetry_item_list[symmetry_connecting_item_id][i_symmetry]) {
+              if (not has_connecting_item(symmetry_connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(symmetry_connecting_item_id);
+              }
+            }
+          }
+
+          for (auto&& previous_layer_target_item_id_list : symmetry_target_item_layer_list[i_layer - 1].itemIdList()) {
+            const auto item_to_connecting_item_list =
+              target_item_to_connecting_item_matrix[previous_layer_target_item_id_list];
+            for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = item_to_connecting_item_list[i_connecting_item];
+
+              if (not has_connecting_item(connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(connecting_item_id);
+              }
+            }
+          }
+
+          auto has_symmetry_layer_item = [&i_layer,
+                                          &symmetry_target_item_layer_list](TargetItemId layer_target_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_target_item_layer_list[i].hasItemNumber(layer_target_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& connecting_item_id : symmetry_connecting_layer_list[i_layer].itemIdList()) {
+            auto item_of_connecting_item_list = connecting_item_to_target_item_matrix[connecting_item_id];
+            for (size_t i_target_item_of_connecting_item = 0;
+                 i_target_item_of_connecting_item < item_of_connecting_item_list.size();
+                 ++i_target_item_of_connecting_item) {
+              const TargetItemId target_item_id_of_connecting_item =
+                item_of_connecting_item_list[i_target_item_of_connecting_item];
+              if (not has_symmetry_layer_item(target_item_id_of_connecting_item)) {
+                symmetry_target_item_layer_list[i_layer].add(target_item_id_of_connecting_item);
+              }
+            }
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          for (auto symmetry_layer_target_item_id : symmetry_target_item_layer_list[i_layer].itemIdList()) {
+            symmetry_column_indices_vector[i_symmetry].push_back(symmetry_layer_target_item_id);
+          }
+        }
+
+        {
+          size_t stencil_size = 0;
+          for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+            auto& target_item_layer = symmetry_target_item_layer_list[i_layer];
+            stencil_size += target_item_layer.size();
+          }
+
+          symmetry_row_map_list[i_symmetry][source_item_id + 1] =
+            symmetry_row_map_list[i_symmetry][source_item_id] + stencil_size;
+        }
+      }
+
+      {
+        size_t stencil_size = 0;
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          auto& item_layer = target_item_layer_list[i_layer];
+          for (auto stencil_item_id : item_layer.itemIdList()) {
+            column_indices_vector.push_back(stencil_item_id);
+          }
+          stencil_size += item_layer.size();
+        }
+        row_map[source_item_id + 1] = row_map[source_item_id] + stencil_size;
+      }
+
+    } else {
+      row_map[source_item_id + 1] = row_map[source_item_id];
+      for (size_t i = 0; i < symmetry_row_map_list.size(); ++i) {
+        symmetry_row_map_list[i][source_item_id + 1] = symmetry_row_map_list[i][source_item_id];
+      }
+    }
+  }
+
+  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]; });
+
+  ConnectivityMatrix primal_stencil{row_map, column_indices};
+
+  typename StencilArray<source_item_type, target_item_type>::BoundaryDescriptorStencilArrayList
+    symmetry_boundary_stencil_list;
+  {
+    size_t i = 0;
+    for (auto&& p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+      symmetry_boundary_stencil_list.emplace_back(
+        typename StencilArray<source_item_type, target_item_type>::
+          BoundaryDescriptorStencilArray{p_boundary_descriptor,
+                                         ConnectivityMatrix{symmetry_row_map_list[i],
+                                                            convert_to_array(symmetry_column_indices_vector[i])}});
+      ++i;
+    }
+  }
+
+  return {{primal_stencil}, {symmetry_boundary_stencil_list}};
+}
+
+template <ItemType source_item_type,
+          ItemType connecting_item_type,
+          ItemType target_item_type,
+          typename ConnectivityType>
+StencilArray<source_item_type, target_item_type>
+StencilBuilder::_build(const ConnectivityType& connectivity,
+                       const size_t& number_of_layers,
+                       const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  if constexpr (connecting_item_type != target_item_type) {
+    if constexpr (source_item_type == target_item_type) {
+      return this
+        ->_build_for_same_source_and_target<source_item_type, connecting_item_type>(connectivity, number_of_layers,
+                                                                                    symmetry_boundary_descriptor_list);
+    } else {
+      return this->_build_for_different_source_and_target<source_item_type, connecting_item_type,
+                                                          target_item_type>(connectivity, number_of_layers,
+                                                                            symmetry_boundary_descriptor_list);
+    }
+  } else {
+    std::ostringstream error_msg;
+    error_msg << "cannot build stencil of " << rang::fgB::yellow << itemName(target_item_type) << rang::fg::reset
+              << " using " << rang::fgB::yellow << itemName(connecting_item_type) << rang::fg::reset
+              << " for connectivity";
+    throw UnexpectedError(error_msg.str());
+  }
+}
+
+template <ItemType source_item_type, ItemType target_item_type, typename ConnectivityType>
+StencilArray<source_item_type, target_item_type>
+StencilBuilder::_build(const ConnectivityType& connectivity,
+                       const StencilDescriptor& stencil_descriptor,
+                       const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  switch (stencil_descriptor.connectionType()) {
+  case StencilDescriptor::ConnectionType::by_nodes: {
+    return this->_build<source_item_type, ItemType::node, target_item_type>(connectivity,
+                                                                            stencil_descriptor.numberOfLayers(),
+                                                                            symmetry_boundary_descriptor_list);
+  }
+  case StencilDescriptor::ConnectionType::by_edges: {
+    return this->_build<source_item_type, ItemType::edge, target_item_type>(connectivity,
+                                                                            stencil_descriptor.numberOfLayers(),
+                                                                            symmetry_boundary_descriptor_list);
+  }
+  case StencilDescriptor::ConnectionType::by_faces: {
+    return this->_build<source_item_type, ItemType::face, target_item_type>(connectivity,
+                                                                            stencil_descriptor.numberOfLayers(),
+                                                                            symmetry_boundary_descriptor_list);
+  }
+  case StencilDescriptor::ConnectionType::by_cells: {
+    return this->_build<source_item_type, ItemType::cell, target_item_type>(connectivity,
+                                                                            stencil_descriptor.numberOfLayers(),
+                                                                            symmetry_boundary_descriptor_list);
+  }
+    // LCOV_EXCL_START
+  default: {
+    throw UnexpectedError("invalid connection type");
+  }
+    // LCOV_EXCL_STOP
+  }
+}
+
+CellToCellStencilArray
+StencilBuilder::buildC2C(const IConnectivity& connectivity,
+                         const StencilDescriptor& stencil_descriptor,
+                         const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  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()
+              << 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).";
+    throw NormalError(error_msg.str());
+  }
+
+  switch (connectivity.dimension()) {
+  case 1: {
+    return this->_build<ItemType::cell, ItemType::cell>(dynamic_cast<const Connectivity<1>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  case 2: {
+    return this->_build<ItemType::cell, ItemType::cell>(dynamic_cast<const Connectivity<2>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  case 3: {
+    return this->_build<ItemType::cell, ItemType::cell>(dynamic_cast<const Connectivity<3>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  default: {
+    throw UnexpectedError("invalid connectivity dimension");
+  }
+  }
+}
+
+NodeToCellStencilArray
+StencilBuilder::buildN2C(const IConnectivity& connectivity,
+                         const StencilDescriptor& stencil_descriptor,
+                         const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+{
+  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()
+              << 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).";
+    throw NormalError(error_msg.str());
+  }
+
+  switch (connectivity.dimension()) {
+  case 1: {
+    return this->_build<ItemType::node, ItemType::cell>(dynamic_cast<const Connectivity<1>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  case 2: {
+    return this->_build<ItemType::node, ItemType::cell>(dynamic_cast<const Connectivity<2>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  case 3: {
+    return this->_build<ItemType::node, ItemType::cell>(dynamic_cast<const Connectivity<3>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  default: {
+    throw UnexpectedError("invalid connectivity dimension");
+  }
+  }
+}
diff --git a/src/mesh/StencilBuilder.hpp b/src/mesh/StencilBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2ed1d3a6c45fdac651ce62ba05690532c506cf26
--- /dev/null
+++ b/src/mesh/StencilBuilder.hpp
@@ -0,0 +1,93 @@
+#ifndef STENCIL_BUILDER_HPP
+#define STENCIL_BUILDER_HPP
+
+#include <mesh/IBoundaryDescriptor.hpp>
+#include <mesh/StencilArray.hpp>
+#include <mesh/StencilDescriptor.hpp>
+
+#include <string>
+#include <vector>
+
+class IConnectivity;
+
+class StencilBuilder
+{
+ public:
+  using BoundaryDescriptorList = std::vector<std::shared_ptr<const IBoundaryDescriptor>>;
+
+ private:
+  template <ItemType item_type>
+  class Layer;
+
+  template <ItemType connecting_item, typename ConnectivityType>
+  auto _buildSymmetryConnectingItemList(const ConnectivityType& connectivity,
+                                        const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  template <ItemType item_type, ItemType connecting_item_type, typename ConnectivityType>
+  StencilArray<item_type, item_type> _build_for_same_source_and_target(
+    const ConnectivityType& connectivity,
+    const size_t& number_of_layers,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  template <ItemType source_item_type,
+            ItemType connecting_item_type,
+            ItemType target_item_type,
+            typename ConnectivityType>
+  StencilArray<source_item_type, target_item_type> _build_for_different_source_and_target(
+    const ConnectivityType& connectivity,
+    const size_t& number_of_layers,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  template <ItemType source_item_type,
+            ItemType connecting_item_type,
+            ItemType target_item_type,
+            typename ConnectivityType>
+  StencilArray<source_item_type, target_item_type> _build(
+    const ConnectivityType& connectivity,
+    const size_t& number_of_layers,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  template <ItemType source_item_type, ItemType target_item_type, typename ConnectivityType>
+  StencilArray<source_item_type, target_item_type> _build(
+    const ConnectivityType& connectivity,
+    const StencilDescriptor& stencil_descriptor,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  template <typename ConnectivityType>
+  NodeToCellStencilArray _buildN2C(const ConnectivityType& connectivity,
+                                   size_t number_of_layers,
+                                   const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  CellToCellStencilArray buildC2C(const IConnectivity& connectivity,
+                                  const StencilDescriptor& stencil_descriptor,
+                                  const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  NodeToCellStencilArray buildN2C(const IConnectivity& connectivity,
+                                  const StencilDescriptor& stencil_descriptor,
+                                  const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const;
+
+  friend class StencilManager;
+
+  template <ItemType source_item_type, ItemType target_item_type>
+  StencilArray<source_item_type, target_item_type>
+  _build(const IConnectivity& connectivity,
+         const StencilDescriptor& stencil_descriptor,
+         const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
+  {
+    if constexpr ((source_item_type == ItemType::cell) and (target_item_type == ItemType::cell)) {
+      return buildC2C(connectivity, stencil_descriptor, symmetry_boundary_descriptor_list);
+    } else if constexpr ((source_item_type == ItemType::node) and (target_item_type == ItemType::cell)) {
+      return buildN2C(connectivity, stencil_descriptor, symmetry_boundary_descriptor_list);
+    } else {
+      static_assert(is_false_item_type_v<source_item_type>, "invalid stencil type");
+    }
+  }
+
+ public:
+  StencilBuilder()                      = default;
+  StencilBuilder(const StencilBuilder&) = default;
+  StencilBuilder(StencilBuilder&&)      = default;
+  ~StencilBuilder()                     = default;
+};
+
+#endif   // STENCIL_BUILDER_HPP
diff --git a/src/mesh/StencilDescriptor.hpp b/src/mesh/StencilDescriptor.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d099f00864ea7f2485664fc4ed6928a69bf95bfc
--- /dev/null
+++ b/src/mesh/StencilDescriptor.hpp
@@ -0,0 +1,62 @@
+#ifndef STENCIL_DESCRIPTOR_HPP
+#define STENCIL_DESCRIPTOR_HPP
+
+#include <utils/PugsMacros.hpp>
+
+#include <cstddef>
+
+class StencilDescriptor
+{
+ public:
+  enum class ConnectionType : int
+  {
+    by_nodes = 1,
+    by_edges = 2,
+    by_faces = 3,
+    by_cells = 4
+  };
+
+ private:
+  size_t m_number_of_layers;
+  ConnectionType m_connection_type;
+
+ public:
+  PUGS_INLINE
+  const size_t&
+  numberOfLayers() const
+  {
+    return m_number_of_layers;
+  };
+
+  PUGS_INLINE
+  const ConnectionType&
+  connectionType() const
+  {
+    return m_connection_type;
+  };
+
+  PUGS_INLINE
+  friend bool
+  operator==(const StencilDescriptor& sd0, const StencilDescriptor& sd1)
+  {
+    return (sd0.m_number_of_layers == sd1.m_number_of_layers) and (sd0.m_connection_type == sd1.m_connection_type);
+  }
+
+  PUGS_INLINE
+  friend bool
+  operator!=(const StencilDescriptor& sd0, const StencilDescriptor& sd1)
+  {
+    return not(sd0 == sd1);
+  }
+
+  StencilDescriptor(const size_t number_of_layers, const ConnectionType type)
+    : m_number_of_layers{number_of_layers}, m_connection_type{type}
+  {}
+
+  StencilDescriptor(const StencilDescriptor&) = default;
+  StencilDescriptor(StencilDescriptor&&)      = default;
+
+  ~StencilDescriptor() = default;
+};
+
+#endif   // STENCIL_DESCRIPTOR_HPP
diff --git a/src/mesh/StencilManager.cpp b/src/mesh/StencilManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..04a8e32a1d50d872d95896e8e7a58e76e32e2189
--- /dev/null
+++ b/src/mesh/StencilManager.cpp
@@ -0,0 +1,99 @@
+#include <mesh/StencilManager.hpp>
+
+#include <mesh/StencilBuilder.hpp>
+#include <utils/Exceptions.hpp>
+
+StencilManager* StencilManager::m_instance = nullptr;
+
+void
+StencilManager::create()
+{
+  Assert(m_instance == nullptr, "StencilManager is already created");
+  m_instance = new StencilManager;
+}
+
+void
+StencilManager::destroy()
+{
+  Assert(m_instance != nullptr, "StencilManager was not created");
+
+  // LCOV_EXCL_START
+  auto check_still_registered = [](auto& stored_stencil_map) {
+    if (stored_stencil_map.size() > 0) {
+      std::stringstream error;
+      error << ": some connectivities are still registered\n";
+      for (const auto& [key, stencil_p] : stored_stencil_map) {
+        error << " - connectivity of id " << rang::fgB::magenta << key.connectivity_id << rang::style::reset << '\n';
+      }
+      throw UnexpectedError(error.str());
+    }
+  };
+
+  check_still_registered(m_instance->m_stored_cell_to_cell_stencil_map);
+  check_still_registered(m_instance->m_stored_node_to_cell_stencil_map);
+  // LCOV_EXCL_STOP
+
+  delete m_instance;
+  m_instance = nullptr;
+}
+
+template <ItemType source_item_type, ItemType target_item_type>
+const StencilArray<source_item_type, target_item_type>&
+StencilManager::_getStencilArray(
+  const IConnectivity& connectivity,
+  const StencilDescriptor& stencil_descriptor,
+  const BoundaryDescriptorList& symmetry_boundary_descriptor_list,
+  StoredStencilTMap<source_item_type, target_item_type>& stored_source_to_target_stencil_map)
+{
+  if (not stored_source_to_target_stencil_map.contains(
+        Key{connectivity.id(), stencil_descriptor, symmetry_boundary_descriptor_list})) {
+    stored_source_to_target_stencil_map[Key{connectivity.id(), stencil_descriptor, symmetry_boundary_descriptor_list}] =
+      std::make_shared<StencilArray<source_item_type, target_item_type>>(
+        StencilBuilder{}.template _build<source_item_type, target_item_type>(connectivity, stencil_descriptor,
+                                                                             symmetry_boundary_descriptor_list));
+  }
+
+  return *stored_source_to_target_stencil_map.at(
+    Key{connectivity.id(), stencil_descriptor, symmetry_boundary_descriptor_list});
+}
+
+const CellToCellStencilArray&
+StencilManager::getCellToCellStencilArray(const IConnectivity& connectivity,
+                                          const StencilDescriptor& stencil_descriptor,
+                                          const BoundaryDescriptorList& symmetry_boundary_descriptor_list)
+{
+  return this->_getStencilArray<ItemType::cell, ItemType::cell>(connectivity, stencil_descriptor,
+                                                                symmetry_boundary_descriptor_list,
+                                                                m_stored_cell_to_cell_stencil_map);
+}
+
+const NodeToCellStencilArray&
+StencilManager::getNodeToCellStencilArray(const IConnectivity& connectivity,
+                                          const StencilDescriptor& stencil_descriptor,
+                                          const BoundaryDescriptorList& symmetry_boundary_descriptor_list)
+{
+  return this->_getStencilArray<ItemType::node, ItemType::cell>(connectivity, stencil_descriptor,
+                                                                symmetry_boundary_descriptor_list,
+                                                                m_stored_node_to_cell_stencil_map);
+}
+
+void
+StencilManager::deleteConnectivity(const size_t connectivity_id)
+{
+  auto delete_connectivity_stencil = [&connectivity_id](auto& stored_stencil_map) {
+    bool has_removed = false;
+    do {
+      has_removed = false;
+      for (const auto& [key, p_stencil] : stored_stencil_map) {
+        if (connectivity_id == key.connectivity_id) {
+          stored_stencil_map.erase(key);
+          has_removed = true;
+          break;
+        }
+      }
+    } while (has_removed);
+  };
+
+  delete_connectivity_stencil(m_stored_cell_to_cell_stencil_map);
+  delete_connectivity_stencil(m_stored_node_to_cell_stencil_map);
+}
diff --git a/src/mesh/StencilManager.hpp b/src/mesh/StencilManager.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8f07e282848af23bca8604e5bbc68754c63251a7
--- /dev/null
+++ b/src/mesh/StencilManager.hpp
@@ -0,0 +1,110 @@
+#ifndef STENCIL_MANAGER_HPP
+#define STENCIL_MANAGER_HPP
+
+#include <mesh/IBoundaryDescriptor.hpp>
+#include <mesh/IConnectivity.hpp>
+#include <mesh/StencilArray.hpp>
+#include <mesh/StencilDescriptor.hpp>
+
+#include <memory>
+#include <set>
+#include <unordered_map>
+#include <vector>
+
+class StencilManager
+{
+ public:
+  using BoundaryDescriptorList = std::vector<std::shared_ptr<const IBoundaryDescriptor>>;
+
+ private:
+  StencilManager()  = default;
+  ~StencilManager() = default;
+
+  static StencilManager* m_instance;
+
+  struct Key
+  {
+    size_t connectivity_id;
+    StencilDescriptor stencil_descriptor;
+    BoundaryDescriptorList symmetry_boundary_descriptor_list;
+
+    PUGS_INLINE bool
+    operator==(const Key& k) const
+    {
+      if ((connectivity_id != k.connectivity_id) or (stencil_descriptor != k.stencil_descriptor) or
+          (symmetry_boundary_descriptor_list.size() != k.symmetry_boundary_descriptor_list.size())) {
+        return false;
+      }
+
+      std::set<std::string> boundary_descriptor_set;
+      for (auto&& p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+        const std::string name = stringify(*p_boundary_descriptor);
+        boundary_descriptor_set.insert(name);
+      }
+
+      std::set<std::string> k_boundary_descriptor_set;
+      for (auto&& p_boundary_descriptor : k.symmetry_boundary_descriptor_list) {
+        const std::string name = stringify(*p_boundary_descriptor);
+        k_boundary_descriptor_set.insert(name);
+      }
+
+      return boundary_descriptor_set == k_boundary_descriptor_set;
+    }
+  };
+
+  struct HashKey
+  {
+    size_t
+    operator()(const Key& key) const
+    {
+      // We do not use the symmetry boundary set by now
+      return ((std::hash<decltype(Key::connectivity_id)>()(key.connectivity_id)) ^
+              (std::hash<std::decay_t<decltype(Key::stencil_descriptor.numberOfLayers())>>()(
+                 key.stencil_descriptor.numberOfLayers())
+               << 1));
+    }
+  };
+
+  template <ItemType source_item_type, ItemType target_item_type>
+  using StoredStencilTMap =
+    std::unordered_map<Key, std::shared_ptr<const StencilArray<source_item_type, target_item_type>>, HashKey>;
+
+  StoredStencilTMap<ItemType::cell, ItemType::cell> m_stored_cell_to_cell_stencil_map;
+  StoredStencilTMap<ItemType::node, ItemType::cell> m_stored_node_to_cell_stencil_map;
+
+  template <ItemType source_item_type, ItemType target_item_type>
+  const StencilArray<source_item_type, target_item_type>& _getStencilArray(
+    const IConnectivity& connectivity,
+    const StencilDescriptor& stencil_descriptor,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list,
+    StoredStencilTMap<source_item_type, target_item_type>& stored_source_to_target_stencil_map);
+
+ public:
+  static void create();
+  static void destroy();
+
+  PUGS_INLINE
+  static StencilManager&
+  instance()
+  {
+    Assert(m_instance != nullptr, "StencilManager was not created!");
+    return *m_instance;
+  }
+
+  void deleteConnectivity(const size_t connectivity_id);
+
+  const CellToCellStencilArray& getCellToCellStencilArray(
+    const IConnectivity& i_connectivity,
+    const StencilDescriptor& stencil_descriptor,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list = {});
+
+  const NodeToCellStencilArray& getNodeToCellStencilArray(
+    const IConnectivity& i_connectivity,
+    const StencilDescriptor& stencil_descriptor,
+    const BoundaryDescriptorList& symmetry_boundary_descriptor_list = {});
+
+  StencilManager(const StencilManager&) = delete;
+  StencilManager(StencilManager&&)      = delete;
+};
+
+#endif   // STENCIL_MANAGER_HPP
diff --git a/src/output/OutputNamedItemValueSet.hpp b/src/output/OutputNamedItemValueSet.hpp
index aa2c5e2ed67140d47c7d4ddf1f57caecc2f28360..8c43330420c91417e0c339228b4bfb41f9dc908b 100644
--- a/src/output/OutputNamedItemValueSet.hpp
+++ b/src/output/OutputNamedItemValueSet.hpp
@@ -38,7 +38,7 @@ class NamedItemData
   }
 
   NamedItemData& operator=(const NamedItemData&) = default;
-  NamedItemData& operator=(NamedItemData&&) = default;
+  NamedItemData& operator=(NamedItemData&&)      = default;
 
   NamedItemData(const std::string& name, const ItemDataT<DataType, item_type, ConnectivityPtr>& item_data)
     : m_name(name), m_item_data(item_data)
@@ -113,24 +113,48 @@ class OutputNamedItemDataSet
                                        NodeArray<const long int>,
                                        NodeArray<const unsigned long int>,
                                        NodeArray<const double>,
+                                       NodeArray<const TinyVector<1, double>>,
+                                       NodeArray<const TinyVector<2, double>>,
+                                       NodeArray<const TinyVector<3, double>>,
+                                       NodeArray<const TinyMatrix<1, 1, double>>,
+                                       NodeArray<const TinyMatrix<2, 2, double>>,
+                                       NodeArray<const TinyMatrix<3, 3, double>>,
 
                                        EdgeArray<const bool>,
                                        EdgeArray<const int>,
                                        EdgeArray<const long int>,
                                        EdgeArray<const unsigned long int>,
                                        EdgeArray<const double>,
+                                       EdgeArray<const TinyVector<1, double>>,
+                                       EdgeArray<const TinyVector<2, double>>,
+                                       EdgeArray<const TinyVector<3, double>>,
+                                       EdgeArray<const TinyMatrix<1, 1, double>>,
+                                       EdgeArray<const TinyMatrix<2, 2, double>>,
+                                       EdgeArray<const TinyMatrix<3, 3, double>>,
 
                                        FaceArray<const bool>,
                                        FaceArray<const int>,
                                        FaceArray<const long int>,
                                        FaceArray<const unsigned long int>,
                                        FaceArray<const double>,
+                                       FaceArray<const TinyVector<1, double>>,
+                                       FaceArray<const TinyVector<2, double>>,
+                                       FaceArray<const TinyVector<3, double>>,
+                                       FaceArray<const TinyMatrix<1, 1, double>>,
+                                       FaceArray<const TinyMatrix<2, 2, double>>,
+                                       FaceArray<const TinyMatrix<3, 3, double>>,
 
                                        CellArray<const bool>,
                                        CellArray<const int>,
                                        CellArray<const long int>,
                                        CellArray<const unsigned long int>,
-                                       CellArray<const double>>;
+                                       CellArray<const double>,
+                                       CellArray<const TinyVector<1, double>>,
+                                       CellArray<const TinyVector<2, double>>,
+                                       CellArray<const TinyVector<3, double>>,
+                                       CellArray<const TinyMatrix<1, 1, double>>,
+                                       CellArray<const TinyMatrix<2, 2, double>>,
+                                       CellArray<const TinyMatrix<3, 3, double>>>;
 
  private:
   // We do not use a map, we want variables to be written in the
diff --git a/src/output/VTKWriter.cpp b/src/output/VTKWriter.cpp
index 03e6337f572e494e94d1a31b48991c9444c881bd..b5771c3abe4b3edb21cb318c5c6dcd04d7b68a79 100644
--- a/src/output/VTKWriter.cpp
+++ b/src/output/VTKWriter.cpp
@@ -398,10 +398,17 @@ VTKWriter::_write(const MeshType& mesh,
          << "\">\n";
     fout << "<CellData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](
-                   auto&&
-                     item_value) { return this->_write_cell_data(fout, var_name, item_value, serialize_data_list); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_cell_data(fout, var_name, item_value, serialize_data_list);
+          }
+        },
+        item_value_variant);
     }
     if (parallel::size() > 1) {
       CellValue<uint8_t> vtk_ghost_type{mesh.connectivity()};
@@ -415,10 +422,17 @@ VTKWriter::_write(const MeshType& mesh,
     fout << "</CellData>\n";
     fout << "<PointData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](
-                   auto&&
-                     item_value) { return this->_write_node_data(fout, var_name, item_value, serialize_data_list); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_node_data(fout, var_name, item_value, serialize_data_list);
+          }
+        },
+        item_value_variant);
     }
     fout << "</PointData>\n";
     fout << "<Points>\n";
@@ -646,15 +660,33 @@ VTKWriter::_write(const MeshType& mesh,
 
     fout << "<PPointData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](auto&& item_value) { return this->_write_node_pvtu(fout, var_name, item_value); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_node_pvtu(fout, var_name, item_value);
+          }
+        },
+        item_value_variant);
     }
     fout << "</PPointData>\n";
 
     fout << "<PCellData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](auto&& item_value) { return this->_write_cell_pvtu(fout, var_name, item_value); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_cell_pvtu(fout, var_name, item_value);
+          }
+        },
+        item_value_variant);
     }
     if (parallel::size() > 1) {
       fout << "<PDataArray type=\"UInt8\" Name=\"vtkGhostType\" NumberOfComponents=\"1\"/>\n";
diff --git a/src/scheme/AcousticSolver.cpp b/src/scheme/AcousticSolver.cpp
index e4db63bd7ec57502c08a1e3f212386ebe8215859..5c8e894c0e2d804813a727f18fd5de44ecdc9cd8 100644
--- a/src/scheme/AcousticSolver.cpp
+++ b/src/scheme/AcousticSolver.cpp
@@ -17,6 +17,7 @@
 #include <scheme/IBoundaryConditionDescriptor.hpp>
 #include <scheme/IDiscreteFunctionDescriptor.hpp>
 #include <scheme/SymmetryBoundaryConditionDescriptor.hpp>
+#include <utils/GlobalVariableManager.hpp>
 #include <utils/Socket.hpp>
 
 #include <variant>
@@ -371,6 +372,10 @@ class AcousticSolverHandler::AcousticSolver final : public AcousticSolverHandler
                  const std::shared_ptr<const DiscreteFunctionVariant>& p_v,
                  const std::vector<std::shared_ptr<const IBoundaryConditionDescriptor>>& bc_descriptor_list) const
   {
+    if ((parallel::size() > 1) and (GlobalVariableManager::instance().getNumberOfGhostLayers() == 0)) {
+      throw NormalError("Acoustic solver requires 1 layer of ghost cells in parallel");
+    }
+
     std::shared_ptr mesh_v = getCommonMesh({rho_v, c_v, u_v, p_v});
     if (not mesh_v) {
       throw NormalError("discrete functions are not defined on the same mesh");
diff --git a/src/scheme/CMakeLists.txt b/src/scheme/CMakeLists.txt
index a4642f38ffbc8a07e6e341ada1ce63e54a2137cf..852a6cd751d092a7ab1c59437c1e549ae2f8094f 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
@@ -11,9 +13,16 @@ add_library(
   FluxingAdvectionSolver.cpp
   HyperelasticSolver.cpp
   LoadBalancer.cpp
+  PolynomialReconstruction.cpp
 )
 
 target_link_libraries(
   PugsScheme
   ${HIGHFIVE_TARGET}
 )
+
+# Additional dependencies
+add_dependencies(
+  PugsScheme
+  PugsSchemeReconstructionUtils
+)
diff --git a/src/scheme/CellIntegrator.hpp b/src/scheme/CellIntegrator.hpp
index 33241f9b8683700d24507000a50ed0f0ae507517..068e9cebe078916c686d9dbe0d7e57874df86e14 100644
--- a/src/scheme/CellIntegrator.hpp
+++ b/src/scheme/CellIntegrator.hpp
@@ -89,9 +89,6 @@ class CellIntegrator
     static_assert(std::is_same_v<std::remove_const_t<typename OutputArrayT::data_type>, OutputType>,
                   "invalid output data type");
 
-    using execution_space = typename Kokkos::DefaultExecutionSpace::execution_space;
-    Kokkos::Experimental::UniqueToken<execution_space, Kokkos::Experimental::UniqueTokenScope::Global> tokens;
-
     if constexpr (std::is_arithmetic_v<OutputType>) {
       value.fill(0);
     } else if constexpr (is_tiny_vector_v<OutputType> or is_tiny_matrix_v<OutputType>) {
diff --git a/src/scheme/DiscreteFunctionDPk.hpp b/src/scheme/DiscreteFunctionDPk.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..aacac5380deb5896ea5f502ecac6f7a53dc46a82
--- /dev/null
+++ b/src/scheme/DiscreteFunctionDPk.hpp
@@ -0,0 +1,148 @@
+#ifndef DISCRETE_FUNCTION_DPK_HPP
+#define DISCRETE_FUNCTION_DPK_HPP
+
+#include <language/utils/ASTNodeDataTypeTraits.hpp>
+
+#include <mesh/ItemArray.hpp>
+#include <mesh/ItemArrayUtils.hpp>
+#include <mesh/MeshData.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <scheme/DiscreteFunctionDescriptorP0.hpp>
+#include <scheme/PolynomialCenteredCanonicalBasisView.hpp>
+
+template <size_t Dimension,
+          typename DataType,
+          typename BasisView = PolynomialCenteredCanonicalBasisView<Dimension, DataType>>
+class DiscreteFunctionDPk
+{
+ public:
+  using BasisViewType = BasisView;
+  using data_type     = DataType;
+
+  friend class DiscreteFunctionDPk<Dimension, std::add_const_t<DataType>>;
+  friend class DiscreteFunctionDPk<Dimension, std::remove_const_t<DataType>>;
+
+ private:
+  std::shared_ptr<const MeshVariant> m_mesh_v;
+  size_t m_degree;
+  CellArray<DataType> m_by_cell_coefficients;
+  CellValue<const TinyVector<Dimension>> m_xj;
+
+ public:
+  PUGS_INLINE
+  ASTNodeDataType
+  dataType() const
+  {
+    return ast_node_data_type_from<std::remove_const_t<DataType>>;
+  }
+
+  PUGS_INLINE size_t
+  degree() const
+  {
+    return m_degree;
+  }
+
+  PUGS_INLINE
+  const CellArray<DataType>&
+  cellArrays() const
+  {
+    return m_by_cell_coefficients;
+  }
+
+  PUGS_INLINE std::shared_ptr<const MeshVariant>
+  meshVariant() const
+  {
+    return m_mesh_v;
+  }
+
+  PUGS_FORCEINLINE
+  operator DiscreteFunctionDPk<Dimension, const DataType>() const
+  {
+    return DiscreteFunctionDPk<Dimension, const DataType>(m_mesh_v, m_by_cell_coefficients);
+  }
+
+  PUGS_INLINE
+  void
+  fill(const DataType& data) const noexcept
+  {
+    static_assert(not std::is_const_v<DataType>, "cannot modify DiscreteFunctionDPk of const");
+    m_by_cell_coefficients.fill(data);
+  }
+
+  friend PUGS_INLINE DiscreteFunctionDPk<Dimension, std::remove_const_t<DataType>>
+  copy(const DiscreteFunctionDPk& source)
+  {
+    return DiscreteFunctionDPk<Dimension, std::remove_const_t<DataType>>{source.m_mesh_v, copy(source.cellArrays())};
+  }
+
+  friend PUGS_INLINE void
+  copy_to(const DiscreteFunctionDPk<Dimension, DataType>& source,
+          DiscreteFunctionDPk<Dimension, std::remove_const_t<DataType>>& destination)
+  {
+    Assert(source.m_mesh_v->id() == destination.m_mesh_v->id(), "copy_to target must use the same mesh");
+    Assert(source.m_degree == destination.m_degree, "copy_to target must have the same degree");
+    copy_to(source.m_by_cell_coefficients, destination.m_by_cell_coefficients);
+  }
+
+  PUGS_INLINE
+  auto
+  coefficients(const CellId cell_id) const
+  {
+    return m_by_cell_coefficients[cell_id];
+  }
+
+  PUGS_FORCEINLINE BasisView
+  operator[](const CellId cell_id) const noexcept(NO_ASSERT)
+  {
+    Assert(m_mesh_v.use_count() > 0, "DiscreteFunctionDPk is not built");
+    return BasisView{m_degree, m_by_cell_coefficients[cell_id], m_xj[cell_id]};
+  }
+
+  PUGS_INLINE DiscreteFunctionDPk
+  operator=(const DiscreteFunctionDPk& f)
+  {
+    m_mesh_v               = f.m_mesh_v;
+    m_degree               = f.m_degree;
+    m_by_cell_coefficients = f.m_by_cell_coefficients;
+    m_xj                   = f.m_xj;
+
+    return *this;
+  }
+
+  DiscreteFunctionDPk(const std::shared_ptr<const MeshVariant>& mesh_v, size_t degree)
+    : m_mesh_v{mesh_v},
+      m_degree{degree},
+      m_by_cell_coefficients{mesh_v->connectivity(), BasisView::dimensionFromDegree(degree)},
+      m_xj{MeshDataManager::instance().getMeshData(*m_mesh_v->get<Mesh<Dimension>>()).xj()}
+  {}
+
+  DiscreteFunctionDPk(const std::shared_ptr<const MeshVariant>& mesh_v, const CellArray<DataType>& cell_array)
+    : m_mesh_v{mesh_v},
+      m_degree{BasisView::degreeFromDimension(cell_array.sizeOfArrays())},
+      m_by_cell_coefficients{cell_array},
+      m_xj{MeshDataManager::instance().getMeshData(*m_mesh_v->get<Mesh<Dimension>>()).xj()}
+  {
+    Assert(mesh_v->connectivity().id() == cell_array.connectivity_ptr()->id(),
+           "cell_array is built on different connectivity");
+  }
+
+  template <MeshConcept MeshType>
+  DiscreteFunctionDPk(const std::shared_ptr<const MeshType>& p_mesh, size_t degree)
+    : DiscreteFunctionDPk{p_mesh->meshVariant(), degree}
+  {}
+
+  template <MeshConcept MeshType>
+  DiscreteFunctionDPk(const std::shared_ptr<const MeshType>& p_mesh, const CellArray<DataType>& cell_array)
+    : DiscreteFunctionDPk{p_mesh->meshVariant(), cell_array}
+  {}
+
+  DiscreteFunctionDPk() noexcept = default;
+
+  DiscreteFunctionDPk(const DiscreteFunctionDPk&) noexcept = default;
+  DiscreteFunctionDPk(DiscreteFunctionDPk&&) noexcept      = default;
+
+  ~DiscreteFunctionDPk() = default;
+};
+
+#endif   // DISCRETE_FUNCTION_DPK_HPP
diff --git a/src/scheme/DiscreteFunctionDPkVariant.hpp b/src/scheme/DiscreteFunctionDPkVariant.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2942f141f38452faed9e95a5f95ca675bc7d8c29
--- /dev/null
+++ b/src/scheme/DiscreteFunctionDPkVariant.hpp
@@ -0,0 +1,134 @@
+#ifndef DISCRETE_FUNCTION_DPK_VARIANT_HPP
+#define DISCRETE_FUNCTION_DPK_VARIANT_HPP
+
+#include <scheme/DiscreteFunctionDPk.hpp>
+#include <scheme/DiscreteFunctionDPkVector.hpp>
+
+#include <memory>
+#include <variant>
+
+class DiscreteFunctionDPkVariant
+{
+ public:
+  using Variant = std::variant<DiscreteFunctionDPk<1, const double>,
+                               DiscreteFunctionDPk<1, const TinyVector<1>>,
+                               DiscreteFunctionDPk<1, const TinyVector<2>>,
+                               DiscreteFunctionDPk<1, const TinyVector<3>>,
+                               DiscreteFunctionDPk<1, const TinyMatrix<1>>,
+                               DiscreteFunctionDPk<1, const TinyMatrix<2>>,
+                               DiscreteFunctionDPk<1, const TinyMatrix<3>>,
+
+                               DiscreteFunctionDPk<2, const double>,
+                               DiscreteFunctionDPk<2, const TinyVector<1>>,
+                               DiscreteFunctionDPk<2, const TinyVector<2>>,
+                               DiscreteFunctionDPk<2, const TinyVector<3>>,
+                               DiscreteFunctionDPk<2, const TinyMatrix<1>>,
+                               DiscreteFunctionDPk<2, const TinyMatrix<2>>,
+                               DiscreteFunctionDPk<2, const TinyMatrix<3>>,
+
+                               DiscreteFunctionDPk<3, const double>,
+                               DiscreteFunctionDPk<3, const TinyVector<1>>,
+                               DiscreteFunctionDPk<3, const TinyVector<2>>,
+                               DiscreteFunctionDPk<3, const TinyVector<3>>,
+                               DiscreteFunctionDPk<3, const TinyMatrix<1>>,
+                               DiscreteFunctionDPk<3, const TinyMatrix<2>>,
+                               DiscreteFunctionDPk<3, const TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<1, const double>,
+                               DiscreteFunctionDPkVector<1, const TinyVector<1>>,
+                               DiscreteFunctionDPkVector<1, const TinyVector<2>>,
+                               DiscreteFunctionDPkVector<1, const TinyVector<3>>,
+                               DiscreteFunctionDPkVector<1, const TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<1, const TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<1, const TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<2, const double>,
+                               DiscreteFunctionDPkVector<2, const TinyVector<1>>,
+                               DiscreteFunctionDPkVector<2, const TinyVector<2>>,
+                               DiscreteFunctionDPkVector<2, const TinyVector<3>>,
+                               DiscreteFunctionDPkVector<2, const TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<2, const TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<2, const TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<3, const double>,
+                               DiscreteFunctionDPkVector<3, const TinyVector<1>>,
+                               DiscreteFunctionDPkVector<3, const TinyVector<2>>,
+                               DiscreteFunctionDPkVector<3, const TinyVector<3>>,
+                               DiscreteFunctionDPkVector<3, const TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<3, const TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<3, const TinyMatrix<3>>>;
+
+ private:
+  Variant m_discrete_function_dpk;
+
+ public:
+  PUGS_INLINE
+  const Variant&
+  discreteFunctionDPk() const
+  {
+    return m_discrete_function_dpk;
+  }
+
+  template <typename DiscreteFunctionDPkT>
+  PUGS_INLINE auto
+  get() const
+  {
+    static_assert(is_discrete_function_dpk_v<DiscreteFunctionDPkT>, "invalid template argument");
+    using DataType = typename DiscreteFunctionDPkT::data_type;
+    static_assert(std::is_const_v<DataType>, "data type of extracted discrete function must be const");
+
+#ifndef NDEBUG
+    if (not std::holds_alternative<DiscreteFunctionDPkT>(this->m_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_discrete_function_dpk)
+                << rang::fg::reset;
+      throw NormalError(error_msg.str());
+    }
+#endif   // NDEBUG
+
+    return std::get<DiscreteFunctionDPkT>(this->discreteFunctionDPk());
+  }
+
+  template <size_t Dimension, typename DataType>
+  DiscreteFunctionDPkVariant(const DiscreteFunctionDPk<Dimension, DataType>& discrete_function_dpk)
+    : m_discrete_function_dpk{DiscreteFunctionDPk<Dimension, const DataType>{discrete_function_dpk}}
+  {
+    static_assert(std::is_same_v<std::remove_const_t<DataType>, double> or                       //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<1, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<2, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<3, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<3, 3, double>>,
+                  "DiscreteFunctionDPk with this DataType is not allowed in variant");
+  }
+
+  template <size_t Dimension, typename DataType>
+  DiscreteFunctionDPkVariant(const DiscreteFunctionDPkVector<Dimension, DataType>& discrete_function_dpk)
+    : m_discrete_function_dpk{DiscreteFunctionDPkVector<Dimension, const DataType>{discrete_function_dpk}}
+  {
+    static_assert(std::is_same_v<std::remove_const_t<DataType>, double> or                       //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<1, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<2, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<3, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<3, 3, double>>,
+                  "DiscreteFunctionDPkVector with this DataType is not allowed in variant");
+  }
+
+  DiscreteFunctionDPkVariant& operator=(DiscreteFunctionDPkVariant&&)      = default;
+  DiscreteFunctionDPkVariant& operator=(const DiscreteFunctionDPkVariant&) = default;
+
+  DiscreteFunctionDPkVariant(const DiscreteFunctionDPkVariant&) = default;
+  DiscreteFunctionDPkVariant(DiscreteFunctionDPkVariant&&)      = default;
+
+  DiscreteFunctionDPkVariant()  = delete;
+  ~DiscreteFunctionDPkVariant() = default;
+};
+
+#endif   // DISCRETE_FUNCTION_DPK_VARIANT_HPP
diff --git a/src/scheme/DiscreteFunctionDPkVector.hpp b/src/scheme/DiscreteFunctionDPkVector.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e8ac729f4b3e002f8c1c3b2c1cdcbd5f4aef3f77
--- /dev/null
+++ b/src/scheme/DiscreteFunctionDPkVector.hpp
@@ -0,0 +1,221 @@
+#ifndef DISCRETE_FUNCTION_DPK_VECTOR_HPP
+#define DISCRETE_FUNCTION_DPK_VECTOR_HPP
+
+#include <language/utils/ASTNodeDataTypeTraits.hpp>
+
+#include <mesh/ItemArray.hpp>
+#include <mesh/ItemArrayUtils.hpp>
+#include <mesh/MeshData.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <scheme/DiscreteFunctionDescriptorP0.hpp>
+#include <scheme/PolynomialCenteredCanonicalBasisView.hpp>
+
+template <size_t Dimension,
+          typename DataType,
+          typename BasisView = PolynomialCenteredCanonicalBasisView<Dimension, DataType>>
+class DiscreteFunctionDPkVector
+{
+ public:
+  using data_type = DataType;
+
+  friend class DiscreteFunctionDPkVector<Dimension, std::add_const_t<DataType>>;
+  friend class DiscreteFunctionDPkVector<Dimension, std::remove_const_t<DataType>>;
+
+  class ComponentCoefficientView
+  {
+   private:
+    DataType* const m_data;
+    const size_t m_size;
+
+   public:
+    size_t
+    size() const
+    {
+      return m_size;
+    }
+
+    DataType&
+    operator[](size_t i) const
+    {
+      Assert(i < m_size);
+      return m_data[i];
+    }
+
+    ComponentCoefficientView(DataType* data, size_t size) : m_data{data}, m_size{size} {}
+
+    ComponentCoefficientView(const ComponentCoefficientView&) = delete;
+    ComponentCoefficientView(ComponentCoefficientView&&)      = delete;
+
+    ~ComponentCoefficientView() = default;
+  };
+
+ private:
+  std::shared_ptr<const MeshVariant> m_mesh_v;
+  size_t m_degree;
+  size_t m_number_of_components;
+  size_t m_nb_coefficients_per_component;
+  CellArray<DataType> m_by_cell_coefficients;
+  CellValue<const TinyVector<Dimension>> m_xj;
+
+ public:
+  PUGS_INLINE
+  ASTNodeDataType
+  dataType() const
+  {
+    return ast_node_data_type_from<std::remove_const_t<DataType>>;
+  }
+
+  PUGS_INLINE size_t
+  degree() const
+  {
+    return m_degree;
+  }
+
+  PUGS_INLINE size_t
+  numberOfComponents() const
+  {
+    return m_number_of_components;
+  }
+
+  PUGS_INLINE
+  size_t
+  numberOfCoefficientsPerComponent() const
+  {
+    return m_nb_coefficients_per_component;
+  }
+
+  PUGS_INLINE
+  const CellArray<DataType>&
+  cellArrays() const
+  {
+    return m_by_cell_coefficients;
+  }
+
+  PUGS_INLINE std::shared_ptr<const MeshVariant>
+  meshVariant() const
+  {
+    return m_mesh_v;
+  }
+
+  PUGS_FORCEINLINE
+  operator DiscreteFunctionDPkVector<Dimension, const DataType>() const
+  {
+    return DiscreteFunctionDPkVector<Dimension, const DataType>(m_mesh_v, m_degree, m_number_of_components,
+                                                                m_by_cell_coefficients);
+  }
+
+  PUGS_INLINE
+  void
+  fill(const DataType& data) const noexcept
+  {
+    static_assert(not std::is_const_v<DataType>, "Cannot modify ItemValue of const");
+    m_by_cell_coefficients.fill(data);
+  }
+
+  friend PUGS_INLINE DiscreteFunctionDPkVector<Dimension, std::remove_const_t<DataType>>
+  copy(const DiscreteFunctionDPkVector& source)
+  {
+    return DiscreteFunctionDPkVector<Dimension, std::remove_const_t<DataType>>{source.m_mesh_v, source.m_degree,
+                                                                               source.m_number_of_components,
+                                                                               copy(source.cellArrays())};
+  }
+
+  friend PUGS_INLINE void
+  copy_to(const DiscreteFunctionDPkVector<Dimension, DataType>& source,
+          DiscreteFunctionDPkVector<Dimension, std::remove_const_t<DataType>>& destination)
+  {
+    Assert(source.m_mesh_v->id() == destination.m_mesh_v->id(), "copy_to target must use the same mesh");
+    Assert(source.m_degree == destination.m_degree, "copy_to target must have the same degree");
+    Assert(source.m_number_of_components == destination.m_number_of_components,
+           "copy_to target must have the same number of components");
+    copy_to(source.m_by_cell_coefficients, destination.m_by_cell_coefficients);
+  }
+
+  PUGS_INLINE
+  auto
+  coefficients(const CellId cell_id) const
+  {
+    return m_by_cell_coefficients[cell_id];
+  }
+
+  PUGS_INLINE auto
+  componentCoefficients(const CellId cell_id, size_t i_component) const noexcept(NO_ASSERT)
+  {
+    Assert(m_mesh_v.use_count() > 0, "DiscreteFunctionDPkVector is not built");
+    Assert(i_component < m_number_of_components, "incorrect component number");
+
+    return ComponentCoefficientView{&m_by_cell_coefficients[cell_id][i_component * m_nb_coefficients_per_component],
+                                    m_nb_coefficients_per_component};
+  }
+
+  PUGS_FORCEINLINE BasisView
+  operator()(const CellId cell_id, size_t i_component) const noexcept(NO_ASSERT)
+  {
+    Assert(m_mesh_v.use_count() > 0, "DiscreteFunctionDPkVector is not built");
+    Assert(i_component < m_number_of_components, "incorrect component number");
+    ComponentCoefficientView
+      component_coefficient_view{&m_by_cell_coefficients[cell_id][i_component * m_nb_coefficients_per_component],
+                                 m_nb_coefficients_per_component};
+    return BasisView{m_degree, component_coefficient_view, m_xj[cell_id]};
+  }
+
+  PUGS_INLINE DiscreteFunctionDPkVector
+  operator=(const DiscreteFunctionDPkVector& f)
+  {
+    m_mesh_v                        = f.m_mesh_v;
+    m_degree                        = f.m_degree;
+    m_number_of_components          = f.m_number_of_components;
+    m_nb_coefficients_per_component = f.m_nb_coefficients_per_component;
+    m_by_cell_coefficients          = f.m_by_cell_coefficients;
+    return *this;
+  }
+
+  DiscreteFunctionDPkVector(const std::shared_ptr<const MeshVariant>& mesh_v,
+                            size_t degree,
+                            size_t number_of_components)
+    : m_mesh_v{mesh_v},
+      m_degree{degree},
+      m_number_of_components{number_of_components},
+      m_nb_coefficients_per_component(BasisView::dimensionFromDegree(degree)),
+      m_by_cell_coefficients{mesh_v->connectivity(), m_number_of_components * BasisView::dimensionFromDegree(degree)},
+      m_xj{MeshDataManager::instance().getMeshData(*m_mesh_v->get<Mesh<Dimension>>()).xj()}
+  {}
+
+  DiscreteFunctionDPkVector(const std::shared_ptr<const MeshVariant>& mesh_v,
+                            size_t degree,
+                            size_t number_of_components,
+                            CellArray<DataType> by_cell_coefficients)
+    : m_mesh_v{mesh_v},
+      m_degree{degree},
+      m_number_of_components{number_of_components},
+      m_nb_coefficients_per_component(BasisView::dimensionFromDegree(degree)),
+      m_by_cell_coefficients{by_cell_coefficients},
+      m_xj{MeshDataManager::instance().getMeshData(*m_mesh_v->get<Mesh<Dimension>>()).xj()}
+  {
+    Assert(mesh_v->connectivity().id() == by_cell_coefficients.connectivity_ptr()->id(),
+           "cell_array is built on different connectivity");
+  }
+
+  template <MeshConcept MeshType>
+  DiscreteFunctionDPkVector(const std::shared_ptr<const MeshType>& p_mesh, size_t degree, size_t number_of_components)
+    : DiscreteFunctionDPkVector{p_mesh->meshVariant(), degree, number_of_components}
+  {}
+
+  template <MeshConcept MeshType>
+  DiscreteFunctionDPkVector(const std::shared_ptr<const MeshType>& p_mesh,
+                            size_t degree,
+                            size_t number_of_components,
+                            CellArray<DataType> by_cell_coefficients)
+    : DiscreteFunctionDPkVector{p_mesh->meshVariant(), degree, number_of_components, by_cell_coefficients}
+  {}
+
+  DiscreteFunctionDPkVector() noexcept = default;
+
+  DiscreteFunctionDPkVector(const DiscreteFunctionDPkVector&) noexcept = default;
+  DiscreteFunctionDPkVector(DiscreteFunctionDPkVector&&) noexcept      = default;
+
+  ~DiscreteFunctionDPkVector() = default;
+};
+
+#endif   // DISCRETE_FUNCTION_DPK_VECTOR_HPP
diff --git a/src/scheme/DiscreteFunctionP0Vector.hpp b/src/scheme/DiscreteFunctionP0Vector.hpp
index da3904b2ad9853a05a521920c3a3a9434aac14a9..bc08cb2a7f27286b9bafdd8bb15d0c761b892d1b 100644
--- a/src/scheme/DiscreteFunctionP0Vector.hpp
+++ b/src/scheme/DiscreteFunctionP0Vector.hpp
@@ -19,8 +19,6 @@ class DiscreteFunctionP0Vector
   friend class DiscreteFunctionP0Vector<std::add_const_t<DataType>>;
   friend class DiscreteFunctionP0Vector<std::remove_const_t<DataType>>;
 
-  static_assert(std::is_arithmetic_v<DataType>, "DiscreteFunctionP0Vector are only defined for arithmetic data type");
-
  private:
   std::shared_ptr<const MeshVariant> m_mesh;
   CellArray<DataType> m_cell_arrays;
@@ -188,16 +186,23 @@ class DiscreteFunctionP0Vector
     return product;
   }
 
-  PUGS_INLINE friend DiscreteFunctionP0<double>
+  PUGS_INLINE friend DiscreteFunctionP0<std::remove_const_t<DataType>>
   sumOfComponents(const DiscreteFunctionP0Vector& f)
   {
-    DiscreteFunctionP0<double> result{f.m_mesh};
+    DiscreteFunctionP0<std::remove_const_t<DataType>> result{f.m_mesh};
 
     parallel_for(
       f.m_mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
         const auto& f_cell_id = f[cell_id];
 
-        double sum = 0;
+        std::remove_const_t<DataType> sum = [] {
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            return 0;
+          } else {
+            return zero;
+          }
+        }();
+
         for (size_t i = 0; i < f.size(); ++i) {
           sum += f_cell_id[i];
         }
@@ -213,6 +218,7 @@ class DiscreteFunctionP0Vector
   {
     Assert(f.meshVariant()->id() == g.meshVariant()->id(), "functions are nor defined on the same mesh");
     Assert(f.size() == g.size());
+    static_assert(std::is_arithmetic_v<std::decay_t<DataType>>);
     DiscreteFunctionP0<double> result{f.m_mesh};
     parallel_for(
       f.m_mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
diff --git a/src/scheme/DiscreteFunctionVariant.hpp b/src/scheme/DiscreteFunctionVariant.hpp
index 901b7989084249ca0e437f9eef24e0371dd723a8..616184298a212c10fe4865b451d0a7879a18b2ea 100644
--- a/src/scheme/DiscreteFunctionVariant.hpp
+++ b/src/scheme/DiscreteFunctionVariant.hpp
@@ -19,7 +19,13 @@ class DiscreteFunctionVariant
                                DiscreteFunctionP0<const TinyMatrix<2>>,
                                DiscreteFunctionP0<const TinyMatrix<3>>,
 
-                               DiscreteFunctionP0Vector<const double>>;
+                               DiscreteFunctionP0Vector<const double>,
+                               DiscreteFunctionP0Vector<const TinyVector<1>>,
+                               DiscreteFunctionP0Vector<const TinyVector<2>>,
+                               DiscreteFunctionP0Vector<const TinyVector<3>>,
+                               DiscreteFunctionP0Vector<const TinyMatrix<1>>,
+                               DiscreteFunctionP0Vector<const TinyMatrix<2>>,
+                               DiscreteFunctionP0Vector<const TinyMatrix<3>>>;
 
   Variant m_discrete_function;
 
@@ -70,7 +76,13 @@ class DiscreteFunctionVariant
   DiscreteFunctionVariant(const DiscreteFunctionP0Vector<DataType>& discrete_function)
     : m_discrete_function{DiscreteFunctionP0Vector<const DataType>{discrete_function}}
   {
-    static_assert(std::is_same_v<std::remove_const_t<DataType>, double>,
+    static_assert(std::is_same_v<std::remove_const_t<DataType>, double> or                       //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<1, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<2, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<3, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<3, 3, double>>,
                   "DiscreteFunctionP0Vector with this DataType is not allowed in variant");
   }
 
diff --git a/src/scheme/EdgeIntegrator.hpp b/src/scheme/EdgeIntegrator.hpp
index 7557a7ed851fa999e129e99f68bdd7812dbafd3d..d1a8bfec14bd76f3b63e2f25710ae40975b4d07b 100644
--- a/src/scheme/EdgeIntegrator.hpp
+++ b/src/scheme/EdgeIntegrator.hpp
@@ -82,9 +82,6 @@ class EdgeIntegrator
     static_assert(std::is_same_v<std::remove_const_t<typename OutputArrayT::data_type>, OutputType>,
                   "invalid output data type");
 
-    using execution_space = typename Kokkos::DefaultExecutionSpace::execution_space;
-    Kokkos::Experimental::UniqueToken<execution_space, Kokkos::Experimental::UniqueTokenScope::Global> tokens;
-
     if constexpr (std::is_arithmetic_v<OutputType>) {
       value.fill(0);
     } else if constexpr (is_tiny_vector_v<OutputType> or is_tiny_matrix_v<OutputType>) {
diff --git a/src/scheme/FaceIntegrator.hpp b/src/scheme/FaceIntegrator.hpp
index 3fc6b25f7b1bf99419e99e22c3873083832b8a7c..4b73a50d73322688ce12d495672775a1bd8ed22d 100644
--- a/src/scheme/FaceIntegrator.hpp
+++ b/src/scheme/FaceIntegrator.hpp
@@ -84,9 +84,6 @@ class FaceIntegrator
     static_assert(std::is_same_v<std::remove_const_t<typename OutputArrayT::data_type>, OutputType>,
                   "invalid output data type");
 
-    using execution_space = typename Kokkos::DefaultExecutionSpace::execution_space;
-    Kokkos::Experimental::UniqueToken<execution_space, Kokkos::Experimental::UniqueTokenScope::Global> tokens;
-
     if constexpr (std::is_arithmetic_v<OutputType>) {
       value.fill(0);
     } else if constexpr (is_tiny_vector_v<OutputType> or is_tiny_matrix_v<OutputType>) {
diff --git a/src/scheme/FluxingAdvectionSolver.cpp b/src/scheme/FluxingAdvectionSolver.cpp
index 1dec92a7dcc8e5e0c281d5d0aaa33bce9872b128..6f85610e0f1f87d2bf77ba516ddc9211f0b93fe6 100644
--- a/src/scheme/FluxingAdvectionSolver.cpp
+++ b/src/scheme/FluxingAdvectionSolver.cpp
@@ -23,6 +23,7 @@
 #include <scheme/InflowBoundaryConditionDescriptor.hpp>
 #include <scheme/OutflowBoundaryConditionDescriptor.hpp>
 #include <scheme/SymmetryBoundaryConditionDescriptor.hpp>
+#include <utils/GlobalVariableManager.hpp>
 
 #include <variant>
 #include <vector>
@@ -85,10 +86,15 @@ class FluxingAdvectionSolver
     m_remapped_list.emplace_back(copy(old_q.cellValues()));
   }
 
+  template <typename DataType>
   void
-  _storeValues(const DiscreteFunctionP0Vector<const double>& old_q)
+  _storeValues(const DiscreteFunctionP0Vector<const DataType>& old_q)
   {
-    m_remapped_list.emplace_back(copy(old_q.cellArrays()));
+    if constexpr (std::is_arithmetic_v<DataType>) {
+      m_remapped_list.emplace_back(copy(old_q.cellArrays()));
+    } else {
+      throw NormalError("remapping DiscreteFunctionP0Vector of non arithmetic data type is not supported");
+    }
   }
 
   template <typename DataType>
@@ -201,6 +207,10 @@ class FluxingAdvectionSolver
       throw NormalError("old and new meshes must share the same connectivity");
     }
 
+    if ((parallel::size() > 1) and (GlobalVariableManager::instance().getNumberOfGhostLayers() == 0)) {
+      throw NormalError("Fluxing advection requires 1 layer of ghost cells in parallel");
+    }
+
     this->_computeGeometricalData();
   }
 
@@ -736,8 +746,12 @@ FluxingAdvectionSolver<MeshType>::remap(
           new_variables.push_back(std::make_shared<DiscreteFunctionVariant>(
             DiscreteFunctionT(m_new_mesh, std::get<CellValue<DataType>>(m_remapped_list[i]))));
         } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-          new_variables.push_back(std::make_shared<DiscreteFunctionVariant>(
-            DiscreteFunctionT(m_new_mesh, std::get<CellArray<DataType>>(m_remapped_list[i]))));
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            new_variables.push_back(std::make_shared<DiscreteFunctionVariant>(
+              DiscreteFunctionT(m_new_mesh, std::get<CellArray<DataType>>(m_remapped_list[i]))));
+          } else {
+            throw NormalError("remapping DiscreteFunctionP0Vector of non arithmetic data type is not supported");
+          }
         } else {
           throw UnexpectedError("invalid discrete function type");
         }
diff --git a/src/scheme/IntegrationMethodType.hpp b/src/scheme/IntegrationMethodType.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..33264f1dce9953bb9fdfbf471ec8f428ae60a5d4
--- /dev/null
+++ b/src/scheme/IntegrationMethodType.hpp
@@ -0,0 +1,38 @@
+#ifndef INTEGRATION_METHOD_TYPE_HPP
+#define INTEGRATION_METHOD_TYPE_HPP
+
+#include <utils/PugsMacros.hpp>
+
+#include <string>
+
+enum class IntegrationMethodType
+{
+  boundary,      // use divergence theorem to compute polynomial
+                 // integrals
+  cell_center,   // use exact integrals for degree 1 polynomials
+                 // using evaluation at mass center
+  element        // use element based quadrature to compute
+                 // polynomial integrals
+};
+
+std::string PUGS_INLINE
+name(const IntegrationMethodType& method_type)
+{
+  std::string method_name;
+  switch (method_type) {
+  case IntegrationMethodType::boundary: {
+    method_name = "boundary";
+    break;
+  }
+  case IntegrationMethodType::cell_center: {
+    method_name = "cell center";
+    break;
+  }
+  case IntegrationMethodType::element: {
+    method_name = "element";
+  }
+  }
+  return method_name;
+}
+
+#endif   // INTEGRATION_METHOD_TYPE_HPP
diff --git a/src/scheme/PolynomialCenteredCanonicalBasisView.hpp b/src/scheme/PolynomialCenteredCanonicalBasisView.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2417ec1596e42f572e0e9f624f06f27e89ee4607
--- /dev/null
+++ b/src/scheme/PolynomialCenteredCanonicalBasisView.hpp
@@ -0,0 +1,205 @@
+#ifndef POLYNOMIAL_CENTERED_CANONICAL_BASIS_VIEW_HPP
+#define POLYNOMIAL_CENTERED_CANONICAL_BASIS_VIEW_HPP
+
+#include <algebra/TinyVector.hpp>
+#include <utils/Array.hpp>
+#include <utils/Exceptions.hpp>
+
+template <size_t Dimension, typename DataType>
+class PolynomialCenteredCanonicalBasisView
+{
+ public:
+  class CoefficientsView
+  {
+   private:
+    DataType* const m_values;
+    const size_t m_size;
+
+   public:
+    [[nodiscard]] PUGS_INLINE size_t
+    size() const
+    {
+      return m_size;
+    }
+
+    [[nodiscard]] PUGS_INLINE DataType&
+    operator[](size_t i) const
+    {
+      Assert(i < m_size, "invalid index");
+      return m_values[i];
+    }
+
+    CoefficientsView& operator=(const CoefficientsView&) = delete;
+    CoefficientsView& operator=(CoefficientsView&&)      = delete;
+
+    CoefficientsView(DataType* values, size_t size) : m_values{values}, m_size{size}
+    {
+      ;
+    }
+
+    // To try to keep these views close to the initial array one
+    // forbids copy constructor and take benefits of C++-17 copy
+    // elisions.
+    CoefficientsView(const CoefficientsView&) = delete;
+
+    CoefficientsView()  = delete;
+    ~CoefficientsView() = default;
+  };
+
+ private:
+  static_assert((Dimension > 0) and (Dimension <= 3), "invalid dimension");
+
+  const size_t m_degree;
+
+  // Coefficients are stored in the following order given the shifting
+  // value is omitted here for simplicity (ie: xj=0).
+  //
+  // ----------------------------------------------------
+  // For a polynomial of degree n depending on x, one has
+  // 1, x, x^2, ..., x^n
+  //
+  // ----------------------------------------------------
+  // For a polynomial of degree n depending on x and y, one has
+  // 1,   x,   x^2, ...,   x^{n-1}, x^n
+  // y, x y, x^2 y, ..., x^{n-1} y
+  // ...
+  // y^{n-1}, x y^{n-1}
+  // y^n
+  //
+  // ----------------------------------------------------
+  // For a polynomial of degree n depending on x, y and z, one has
+  // 1,   x,   x^2, ...,   x^{n-1}, x^n
+  // y, x y, x^2 y, ..., x^{n-1} y
+  // ...
+  // y^{n-1}, x y^{n-1}
+  // y^n
+  //
+  //   z,   x z,   x^2 z, ...,   x^{n-2} z, x^{n-1} z
+  // y z, x y z, x^2 y z, ..., x^{n-2} y z
+  // ...
+  // y^{n-2}  z, x y^{n-2} z
+  // y^{n -1} z
+  // ...
+  // ...
+  // z^{n-2},      x z^{n-2}, x^2 z^{n-2}
+  // y z^{n-2},  x y z^{n-2}
+  // y^2 z^{n-2}
+  //
+  // z^{n-1},      x z^{n-1}
+  // y z^{n-1}
+  //
+  // z^n
+
+  CoefficientsView m_coefficients;
+  const TinyVector<Dimension>& m_xj;
+
+ public:
+  static size_t
+  dimensionFromDegree(size_t degree)
+  {
+    if constexpr (Dimension == 1) {
+      return degree + 1;
+    } else if constexpr (Dimension == 2) {
+      return ((degree + 2) * (degree + 1)) / 2;
+    } else {   // Dimension == 3
+      return ((degree + 3) * (degree + 2) * (degree + 1)) / 6;
+    }
+  }
+
+  static size_t
+  degreeFromDimension(size_t polynomial_basis_dimension)
+  {
+    Assert(polynomial_basis_dimension > 0);
+    if constexpr (Dimension == 1) {
+      return polynomial_basis_dimension - 1;
+    } else {
+      // No need fir an explicit formula
+      // - degree is small and integer
+      // - do not need the use of sqrt
+      for (size_t degree = 0; degree < polynomial_basis_dimension; ++degree) {
+        size_t dimension_from_degree = dimensionFromDegree(degree);
+        if (dimension_from_degree == polynomial_basis_dimension) {
+          return degree;
+        } else if (dimension_from_degree > polynomial_basis_dimension) {
+          break;
+        }
+      }
+      throw NormalError("incorrect polynomial basis dimension");
+    }
+  }
+
+  DataType
+  operator()(const TinyVector<Dimension>& x) const
+  {
+    if constexpr (Dimension == 1) {
+      const double x_xj = (x - m_xj)[0];
+
+      std::remove_const_t<DataType> result = m_coefficients[m_degree];
+      for (ssize_t i = m_degree - 1; i >= 0; --i) {
+        result = x_xj * result + m_coefficients[i];
+      }
+
+      return result;
+    } else if constexpr (Dimension == 2) {
+      const TinyVector X_Xj = x - m_xj;
+      const double& x_xj    = X_Xj[0];
+      const double& y_yj    = X_Xj[1];
+
+      size_t i = m_coefficients.size() - 1;
+
+      std::remove_const_t<DataType> result = m_coefficients[i--];
+      for (ssize_t i_y = m_degree - 1; i_y >= 0; --i_y) {
+        std::remove_const_t<DataType> x_result = m_coefficients[i--];
+        for (ssize_t i_x = m_degree - i_y - 1; i_x >= 0; --i_x) {
+          x_result = x_xj * x_result + m_coefficients[i--];
+        }
+        result = y_yj * result + x_result;
+      }
+
+      return result;
+    } else {   // Dimension == 3
+      const TinyVector X_Xj = x - m_xj;
+      const double& x_xj    = X_Xj[0];
+      const double& y_yj    = X_Xj[1];
+      const double& z_zj    = X_Xj[2];
+
+      size_t i = m_coefficients.size() - 1;
+
+      std::remove_const_t<DataType> result = m_coefficients[i--];
+      for (ssize_t i_z = m_degree - 1; i_z >= 0; --i_z) {
+        std::remove_const_t<DataType> y_result = m_coefficients[i--];
+        for (ssize_t i_y = m_degree - i_z - 1; i_y >= 0; --i_y) {
+          std::remove_const_t<DataType> x_result = m_coefficients[i--];
+          for (ssize_t i_x = m_degree - i_z - i_y - 1; i_x >= 0; --i_x) {
+            x_result = x_xj * x_result + m_coefficients[i--];
+          }
+          y_result = y_yj * y_result + x_result;
+        }
+        result = z_zj * result + y_result;
+      }
+
+      return result;
+    }
+  }
+
+  template <typename ArrayType>
+  PolynomialCenteredCanonicalBasisView(const size_t degree,
+                                       ArrayType& coefficient_list,
+                                       const TinyVector<Dimension>& xj)
+    : m_degree{degree}, m_coefficients{&(coefficient_list[0]), coefficient_list.size()}, m_xj{xj}
+  {}
+
+  template <typename ArrayType>
+  PolynomialCenteredCanonicalBasisView(const size_t degree,
+                                       const ArrayType& coefficient_list,
+                                       const TinyVector<Dimension>& xj)
+    : m_degree{degree}, m_coefficients{&(coefficient_list[0]), coefficient_list.size()}, m_xj{xj}
+  {}
+
+  PolynomialCenteredCanonicalBasisView(const PolynomialCenteredCanonicalBasisView&) = delete;
+  PolynomialCenteredCanonicalBasisView(PolynomialCenteredCanonicalBasisView&&)      = default;
+  PolynomialCenteredCanonicalBasisView()                                            = delete;
+  ~PolynomialCenteredCanonicalBasisView()                                           = default;
+};
+
+#endif   // POLYNOMIAL_CENTERED_CANONICAL_BASIS_VIEW_HPP
diff --git a/src/scheme/PolynomialReconstruction.cpp b/src/scheme/PolynomialReconstruction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3d3768d9dd4a880386c66c20ca69fd5847be8e2
--- /dev/null
+++ b/src/scheme/PolynomialReconstruction.cpp
@@ -0,0 +1,852 @@
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <algebra/Givens.hpp>
+#include <algebra/ShrinkMatrixView.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <geometry/SymmetryUtils.hpp>
+#include <mesh/MeshData.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <mesh/MeshFlatFaceBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/StencilDescriptor.hpp>
+#include <mesh/StencilManager.hpp>
+#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 <MeshConcept MeshType>
+class PolynomialReconstruction::Internal
+{
+ private:
+  using Rd = TinyVector<MeshType::Dimension>;
+
+  friend PolynomialReconstruction;
+
+  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)
+  {
+    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);
+                  }
+                }
+              }
+            }
+
+            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 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
+                  }
+                }
+              }
+            }
+
+            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>) {
+              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 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];
+
+                  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_matrix(normal, 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);
+                        }
+                      }
+                    }
+                  }
+                } 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
+                }
+              }
+            }
+
+            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());
+    }
+  }
+
+  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 l = 0; l < B.numberOfColumns(); ++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 < A.numberOfColumns(); ++l) {
+          A(index, l) *= weight;
+        }
+        for (size_t l = 0; l < B.numberOfColumns(); ++l) {
+          B(index, l) *= weight;
+        }
+      }
+    }
+  }
+
+  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);
+
+        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;
+      }
+    }
+  }
+
+  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& 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 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];
+                      }
+                    }
+                  }
+
+                  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];
+                        }
+                      }
+                    }
+                  }
+
+                  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];
+                          }
+                        }
+                      }
+                    }
+                  }
+
+                  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];
+                        }
+                      }
+                    }
+
+                    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];
+                          }
+                        }
+                      }
+                    }
+
+                    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];
+                            }
+                          }
+                        }
+                      }
+                    }
+
+                    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(
+  const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
+{
+  size_t number_of_columns = 0;
+  for (auto discrete_function_variant_p : discrete_function_variant_list) {
+    number_of_columns += std::visit(
+      [](auto&& discrete_function) -> size_t {
+        using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
+        if constexpr (is_discrete_function_P0_v<DiscreteFunctionT>) {
+          using data_type = std::decay_t<typename DiscreteFunctionT::data_type>;
+          if constexpr (std::is_arithmetic_v<data_type>) {
+            return 1;
+          } else if constexpr (is_tiny_vector_v<data_type> or is_tiny_matrix_v<data_type>) {
+            return data_type::Dimension;
+          } else {
+            // LCOV_EXCL_START
+            throw UnexpectedError("unexpected data type " + demangle<data_type>());
+            // LCOV_EXCL_STOP
+          }
+        } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
+          using data_type = std::decay_t<typename DiscreteFunctionT::data_type>;
+          if constexpr (std::is_arithmetic_v<data_type>) {
+            return discrete_function.size();
+          } else if constexpr (is_tiny_vector_v<data_type> or is_tiny_matrix_v<data_type>) {
+            return discrete_function.size() * data_type::Dimension;
+          } else {
+            // LCOV_EXCL_START
+            throw UnexpectedError("unexpected data type " + demangle<data_type>());
+            // LCOV_EXCL_STOP
+          }
+        } else {
+          // LCOV_EXCL_START
+          throw UnexpectedError("unexpected discrete function type");
+          // LCOV_EXCL_STOP
+        }
+      },
+      discrete_function_variant_p->discreteFunction());
+  }
+  return number_of_columns;
+}
+
+template <MeshConcept MeshType>
+std::vector<PolynomialReconstruction::MutableDiscreteFunctionDPkVariant>
+PolynomialReconstruction::_createMutableDiscreteFunctionDPKVariantList(
+  const std::shared_ptr<const MeshType>& p_mesh,
+  const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
+{
+  std::vector<MutableDiscreteFunctionDPkVariant> mutable_discrete_function_dpk_variant_list;
+  for (size_t i_discrete_function_variant = 0; i_discrete_function_variant < discrete_function_variant_list.size();
+       ++i_discrete_function_variant) {
+    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::remove_const_t<std::decay_t<typename DiscreteFunctionT::data_type>>;
+          mutable_discrete_function_dpk_variant_list.push_back(
+            DiscreteFunctionDPk<MeshType::Dimension, DataType>(p_mesh, m_descriptor.degree()));
+        } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
+          using DataType = std::remove_const_t<std::decay_t<typename DiscreteFunctionT::data_type>>;
+          mutable_discrete_function_dpk_variant_list.push_back(
+            DiscreteFunctionDPkVector<MeshType::Dimension, DataType>(p_mesh, m_descriptor.degree(),
+                                                                     discrete_function.size()));
+        } else {
+          // LCOV_EXCL_START
+          throw UnexpectedError("unexpected discrete function type");
+          // LCOV_EXCL_STOP
+        }
+      },
+      discrete_function_variant->discreteFunction());
+  }
+
+  return mutable_discrete_function_dpk_variant_list;
+}
+
+template <MeshConcept MeshType>
+void
+PolynomialReconstruction::_checkDataAndSymmetriesCompatibility(
+  const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
+{
+  for (auto&& discrete_function_variant : discrete_function_variant_list) {
+    std::visit(
+      [&](auto&& discrete_function) {
+        using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
+        if constexpr (is_discrete_function_P0_v<DiscreteFunctionT> or
+                      is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
+          using DataType = std::decay_t<typename DiscreteFunctionT::data_type>;
+          if constexpr (is_tiny_vector_v<DataType>) {
+            if constexpr (DataType::Dimension != MeshType::Dimension) {
+              std::stringstream error_msg;
+              error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                        << " using a mesh of dimension " << MeshType::Dimension;
+              throw NormalError(error_msg.str());
+            }
+          } else if constexpr (is_tiny_matrix_v<DataType>) {
+            if constexpr ((DataType::NumberOfRows != MeshType::Dimension) or
+                          (DataType::NumberOfColumns != MeshType::Dimension)) {
+              std::stringstream error_msg;
+              error_msg << "cannot symmetrize matrices of dimensions " << DataType::NumberOfRows << 'x'
+                        << DataType::NumberOfColumns << " using a mesh of dimension " << MeshType::Dimension;
+              throw NormalError(error_msg.str());
+            }
+          }
+        } else {
+          // LCOV_EXCL_START
+          throw UnexpectedError("invalid discrete function type");
+          // LCOV_EXCL_STOP
+        }
+      },
+      discrete_function_variant->discreteFunction());
+  }
+}
+
+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>;
+
+  if (m_descriptor.symmetryBoundaryDescriptorList().size() > 0) {
+    this->_checkDataAndSymmetriesCompatibility<MeshType>(discrete_function_variant_list);
+  }
+
+  const size_t number_of_columns = this->_getNumberOfColumns(discrete_function_variant_list);
+
+  const size_t basis_dimension =
+    DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(m_descriptor.degree());
+
+  const auto& stencil_array =
+    StencilManager::instance().getCellToCellStencilArray(mesh.connectivity(), m_descriptor.stencilDescriptor(),
+                                                         m_descriptor.symmetryBoundaryDescriptorList());
+
+  auto xr = mesh.xr();
+  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 full_stencil_size = [&](const CellId cell_id) {
+    auto stencil_cell_list = stencil_array[cell_id];
+    size_t stencil_size    = stencil_cell_list.size();
+    for (size_t i = 0; i < m_descriptor.symmetryBoundaryDescriptorList().size(); ++i) {
+      auto& ghost_stencil = stencil_array.symmetryBoundaryStencilArrayList()[i].stencilArray();
+      stencil_size += ghost_stencil[cell_id].size();
+    }
+
+    return stencil_size;
+  };
+
+  const size_t max_stencil_size = [&]() {
+    size_t max_size = 0;
+    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+      const size_t stencil_size = full_stencil_size(cell_id);
+      if (cell_is_owned[cell_id] and stencil_size > max_size) {
+        max_size = stencil_size;
+      }
+    }
+    return max_size;
+  }();
+
+  SmallArray<const Rd> symmetry_normal_list = [&] {
+    SmallArray<Rd> normal_list(m_descriptor.symmetryBoundaryDescriptorList().size());
+    size_t i_symmetry_boundary = 0;
+    for (auto p_boundary_descriptor : m_descriptor.symmetryBoundaryDescriptorList()) {
+      const IBoundaryDescriptor& boundary_descriptor = *p_boundary_descriptor;
+
+      auto symmetry_boundary             = getMeshFlatFaceBoundary(mesh, boundary_descriptor);
+      normal_list[i_symmetry_boundary++] = symmetry_boundary.outgoingNormal();
+    }
+    return normal_list;
+  }();
+
+  SmallArray<const Rd> symmetry_origin_list = [&] {
+    SmallArray<Rd> origin_list(m_descriptor.symmetryBoundaryDescriptorList().size());
+    size_t i_symmetry_boundary = 0;
+    for (auto p_boundary_descriptor : m_descriptor.symmetryBoundaryDescriptorList()) {
+      const IBoundaryDescriptor& boundary_descriptor = *p_boundary_descriptor;
+
+      auto symmetry_boundary             = getMeshFlatFaceBoundary(mesh, boundary_descriptor);
+      origin_list[i_symmetry_boundary++] = symmetry_boundary.origin();
+    }
+    return origin_list;
+  }();
+
+  Kokkos::Experimental::UniqueToken<Kokkos::DefaultExecutionSpace::execution_space,
+                                    Kokkos::Experimental::UniqueTokenScope::Global>
+    tokens;
+
+  auto mutable_discrete_function_dpk_variant_list =
+    this->_createMutableDiscreteFunctionDPKVariantList(p_mesh, discrete_function_variant_list);
+
+  SmallArray<SmallMatrix<double>> A_pool(Kokkos::DefaultExecutionSpace::concurrency());
+  SmallArray<SmallMatrix<double>> B_pool(Kokkos::DefaultExecutionSpace::concurrency());
+  SmallArray<SmallVector<double>> G_pool(Kokkos::DefaultExecutionSpace::concurrency());
+  SmallArray<SmallMatrix<double>> X_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);
+  }
+
+  SmallArray<std::shared_ptr<ReconstructionMatrixBuilderType>> reconstruction_matrix_builder_pool(A_pool.size());
+
+  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(
+    mesh.numberOfCells(), PUGS_CLASS_LAMBDA(const CellId cell_j_id) {
+      if (cell_is_owned[cell_j_id]) {
+        const int32_t t = tokens.acquire();
+
+        ShrinkMatrixView A(A_pool[t], full_stencil_size(cell_j_id));
+        ShrinkMatrixView B(B_pool[t], full_stencil_size(cell_j_id));
+
+        Internal<MeshType>::buildB(cell_j_id, stencil_array, discrete_function_variant_list, symmetry_normal_list, B);
+
+        ReconstructionMatrixBuilderType& reconstruction_matrix_builder = *reconstruction_matrix_builder_pool[t];
+        reconstruction_matrix_builder.build(cell_j_id, A);
+
+        if (m_descriptor.rowWeighting()) {
+          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 precision)
+          SmallVector<double>& G = G_pool[t];
+
+          Internal<MeshType>::solveCollectionInPlaceWithPreconditionner(A, X, B, G);
+        } else {
+          Givens::solveCollectionInPlace(A, X, B);
+        }
+
+        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);
+      }
+    });
+
+  std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>> discrete_function_dpk_variant_list;
+
+  for (auto discrete_function_dpk_variant_p : mutable_discrete_function_dpk_variant_list) {
+    std::visit(
+      [&](auto&& mutable_function_dpk) {
+        synchronize(mutable_function_dpk.cellArrays());
+        discrete_function_dpk_variant_list.push_back(
+          std::make_shared<DiscreteFunctionDPkVariant>(mutable_function_dpk));
+      },
+      discrete_function_dpk_variant_p.mutableDiscreteFunctionDPk());
+  }
+
+  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: {
+    return this->_build<BoundaryIntegralReconstructionMatrixBuilder<MeshType>>(p_mesh, discrete_function_variant_list);
+  }
+  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
+{
+  if (not hasSameMesh(discrete_function_variant_list)) {
+    throw NormalError("cannot reconstruct functions living of different meshes simultaneously");
+  }
+
+  auto mesh_v = getCommonMesh(discrete_function_variant_list);
+
+  return std::visit([&](auto&& p_mesh) { return this->_build(p_mesh, discrete_function_variant_list); },
+                    mesh_v->variant());
+}
diff --git a/src/scheme/PolynomialReconstruction.hpp b/src/scheme/PolynomialReconstruction.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f76844711ada4641b69234c5ac787c472247a37
--- /dev/null
+++ b/src/scheme/PolynomialReconstruction.hpp
@@ -0,0 +1,84 @@
+#ifndef POLYNOMIAL_RECONSTRUCTION_HPP
+#define POLYNOMIAL_RECONSTRUCTION_HPP
+
+#include <mesh/MeshTraits.hpp>
+#include <scheme/PolynomialReconstructionDescriptor.hpp>
+
+class DiscreteFunctionDPkVariant;
+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(
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
+
+  template <MeshConcept MeshType>
+  void _checkDataAndSymmetriesCompatibility(
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
+
+  template <MeshConcept MeshType>
+  std::vector<MutableDiscreteFunctionDPkVariant> _createMutableDiscreteFunctionDPKVariantList(
+    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,
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
+
+ public:
+  [[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>> build(
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
+
+  template <typename... DiscreteFunctionT>
+  [[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>>
+  build(DiscreteFunctionT... input) const
+  {
+    std::vector<std::shared_ptr<const DiscreteFunctionVariant>> variant_vector;
+    auto convert = [&variant_vector](auto&& df) {
+      using DF_T = std::decay_t<decltype(df)>;
+      if constexpr (is_discrete_function_v<DF_T> or std::is_same_v<DiscreteFunctionVariant, DF_T>) {
+        variant_vector.push_back(std::make_shared<DiscreteFunctionVariant>(df));
+      } else if constexpr (is_shared_ptr_v<DF_T>) {
+        using DF_Value_T = std::decay_t<typename DF_T::element_type>;
+        if constexpr (is_discrete_function_v<DF_Value_T> or std::is_same_v<DiscreteFunctionVariant, DF_Value_T>) {
+          variant_vector.push_back(std::make_shared<DiscreteFunctionVariant>(*df));
+        } else {
+          static_assert(is_false_v<DF_T>, "unexpected type");
+        }
+      } else {
+        static_assert(is_false_v<DF_T>, "unexpected type");
+      }
+    };
+
+    (convert(std::forward<DiscreteFunctionT>(input)), ...);
+    return this->build(variant_vector);
+  }
+
+  PolynomialReconstruction(const PolynomialReconstructionDescriptor& descriptor) : m_descriptor{descriptor} {}
+  ~PolynomialReconstruction() = default;
+};
+
+#endif   // POLYNOMIAL_RECONSTRUCTION_HPP
diff --git a/src/scheme/PolynomialReconstructionDescriptor.hpp b/src/scheme/PolynomialReconstructionDescriptor.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..44fdd2082e0efdf550bc1490a6df18d5658ff9bd
--- /dev/null
+++ b/src/scheme/PolynomialReconstructionDescriptor.hpp
@@ -0,0 +1,123 @@
+#ifndef POLYNOMIAL_RECONSTRUCTION_DESCRIPTOR_HPP
+#define POLYNOMIAL_RECONSTRUCTION_DESCRIPTOR_HPP
+
+#include <mesh/IBoundaryDescriptor.hpp>
+#include <mesh/StencilDescriptor.hpp>
+#include <scheme/IntegrationMethodType.hpp>
+#include <utils/PugsMacros.hpp>
+
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+class PolynomialReconstructionDescriptor
+{
+ public:
+  using BoundaryDescriptorList = std::vector<std::shared_ptr<const IBoundaryDescriptor>>;
+
+ private:
+  IntegrationMethodType m_integration_method;
+  size_t m_degree;
+  StencilDescriptor m_stencil_descriptor;
+
+  BoundaryDescriptorList m_symmetry_boundary_descriptor_list;
+
+  bool m_preconditioning = true;
+  bool m_row_weighting   = true;
+
+ public:
+  PUGS_INLINE IntegrationMethodType
+  integrationMethodType() const
+  {
+    return m_integration_method;
+  }
+
+  PUGS_INLINE
+  size_t
+  degree() const
+  {
+    return m_degree;
+  }
+
+  PUGS_INLINE
+  const StencilDescriptor&
+  stencilDescriptor() const
+  {
+    return m_stencil_descriptor;
+  }
+
+  PUGS_INLINE
+  const BoundaryDescriptorList&
+  symmetryBoundaryDescriptorList() const
+  {
+    return m_symmetry_boundary_descriptor_list;
+  }
+
+  PUGS_INLINE
+  bool
+  preconditioning() const
+  {
+    return m_preconditioning;
+  }
+
+  PUGS_INLINE
+  bool
+  rowWeighting() const
+  {
+    return m_row_weighting;
+  }
+
+  PUGS_INLINE
+  void
+  setPreconditioning(const bool preconditioning)
+  {
+    m_preconditioning = preconditioning;
+  }
+
+  PUGS_INLINE
+  void
+  setRowWeighting(const bool row_weighting)
+  {
+    m_row_weighting = row_weighting;
+  }
+
+  PolynomialReconstructionDescriptor(const IntegrationMethodType integration_method, const size_t degree)
+    : m_integration_method{integration_method},
+      m_degree{degree},
+      m_stencil_descriptor(degree, StencilDescriptor::ConnectionType::by_nodes)
+  {}
+
+  PolynomialReconstructionDescriptor(const IntegrationMethodType integration_method,
+                                     const size_t degree,
+                                     const BoundaryDescriptorList& symmetry_boundary_descriptor_list)
+    : m_integration_method{integration_method},
+      m_degree{degree},
+      m_stencil_descriptor(degree, StencilDescriptor::ConnectionType::by_nodes),
+      m_symmetry_boundary_descriptor_list(symmetry_boundary_descriptor_list)
+  {}
+
+  PolynomialReconstructionDescriptor(const IntegrationMethodType integration_method,
+                                     const size_t degree,
+                                     const StencilDescriptor& stencil_descriptor)
+    : m_integration_method{integration_method}, m_degree{degree}, m_stencil_descriptor{stencil_descriptor}
+  {}
+
+  PolynomialReconstructionDescriptor(const IntegrationMethodType integration_method,
+                                     const size_t degree,
+                                     const StencilDescriptor& stencil_descriptor,
+                                     const BoundaryDescriptorList& symmetry_boundary_descriptor_list)
+    : m_integration_method{integration_method},
+      m_degree{degree},
+      m_stencil_descriptor{stencil_descriptor},
+      m_symmetry_boundary_descriptor_list(symmetry_boundary_descriptor_list)
+  {}
+
+  PolynomialReconstructionDescriptor() = delete;
+
+  PolynomialReconstructionDescriptor(const PolynomialReconstructionDescriptor&) = default;
+  PolynomialReconstructionDescriptor(PolynomialReconstructionDescriptor&&)      = default;
+
+  ~PolynomialReconstructionDescriptor() = default;
+};
+
+#endif   // POLYNOMIAL_RECONSTRUCTION_DESCRIPTOR_HPP
diff --git a/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.cpp b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..58b8fcd5ea34a64bd8d5808c41ea95079c2fcf84
--- /dev/null
+++ b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.cpp
@@ -0,0 +1,553 @@
+#include <scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp>
+
+#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
+#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <analysis/QuadratureFormula.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <geometry/SquareTransformation.hpp>
+#include <geometry/SymmetryUtils.hpp>
+#include <geometry/TriangleTransformation.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <scheme/DiscreteFunctionDPk.hpp>
+
+template <MeshConcept MeshTypeT>
+class PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<MeshTypeT>::SpecificDimensionalData
+{
+ private:
+  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;
+
+ public:
+  PUGS_INLINE const auto&
+  cellToFaceMatrix() const noexcept
+  {
+    return m_cell_to_face_matrix;
+  }
+
+  PUGS_INLINE
+  const auto&
+  faceToNodeMatrix() const noexcept
+  {
+    return m_face_to_node_matrix;
+  }
+
+  PUGS_INLINE
+  const auto&
+  cellFaceIsReversed() const noexcept
+  {
+    return m_cell_face_is_reversed;
+  }
+
+  SpecificDimensionalData(const Connectivity<MeshType::Dimension>& connectivity)
+    : m_cell_to_face_matrix{connectivity.cellToFaceMatrix()},
+      m_face_to_node_matrix{connectivity.faceToNodeMatrix()},
+      m_cell_face_is_reversed{connectivity.cellFaceIsReversed()}
+  {}
+
+  ~SpecificDimensionalData() = default;
+};
+
+template <>
+class PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<1>>::SpecificDimensionalData
+{
+ private:
+  const ItemToItemMatrix<ItemType::cell, ItemType::node> m_cell_to_node_matrix;
+
+ public:
+  PUGS_INLINE
+  const auto&
+  cellToNodeMatrix() const noexcept
+  {
+    return m_cell_to_node_matrix;
+  }
+
+  SpecificDimensionalData(const Connectivity<1>& connectivity) : m_cell_to_node_matrix{connectivity.cellToNodeMatrix()}
+  {}
+
+  ~SpecificDimensionalData() = default;
+};
+
+template <>
+template <typename ConformTransformationT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<3>>::_computeEjkBoundaryMean(
+  const QuadratureFormula<MeshType::Dimension - 1>& quadrature,
+  const ConformTransformationT& T,
+  const Rd& Xj,
+  const double inv_Vi,
+  SmallArray<double>& mean_of_ejk)
+{
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq          = quadrature.weight(i_q);
+    const TinyVector<2> xi_q = quadrature.point(i_q);
+
+    const double area_variation_e1 = T.areaVariation(xi_q)[0] * inv_Vi;
+
+    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;
+      m_tmp_array[k++] = x_xj * wq * area_variation_e1;
+      for (; k <= m_polynomial_degree; ++k) {
+        m_tmp_array[k]                                                         //
+          = x_xj * m_tmp_array[k - 1] * m_antiderivative_correction_coef[k];   // ((1. * k) / (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_tmp_array[k] = y_yj * m_tmp_array[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_tmp_array[k] = z_zj * m_tmp_array[begin_i_yz_1 + l];
+          }
+        }
+      }
+    }
+
+    for (size_t k = 1; k < m_basis_dimension; ++k) {
+      mean_of_ejk[k - 1] += m_tmp_array[k];
+    }
+  }
+}
+
+template <>
+template <typename ConformTransformationT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<2>>::_computeEjkBoundaryMean(
+  const QuadratureFormula<MeshType::Dimension - 1>& quadrature,
+  const ConformTransformationT& T,
+  const Rd& Xj,
+  const double inv_Vi,
+  SmallArray<double>& mean_of_ejk)
+{
+  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_tmp_array[k++] = x_xj * wq * velocity_perp_e1;
+      for (; k <= m_polynomial_degree; ++k) {
+        m_tmp_array[k]                                                         //
+          = x_xj * m_tmp_array[k - 1] * m_antiderivative_correction_coef[k];   // ((1. * k) / (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) {
+          m_tmp_array[k++] = y_yj * m_tmp_array[begin_i_y_1 + l];
+        }
+      }
+    }
+
+    for (size_t k = 1; k < m_basis_dimension; ++k) {
+      mean_of_ejk[k - 1] += m_tmp_array[k];
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMeanByBoundary(
+  const Rd& Xj,
+  const CellId& cell_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  const double inv_Vi = 1. / m_Vj[cell_id];
+
+  const auto& face_is_reversed    = m_dimensional_data_ptr->cellFaceIsReversed()[cell_id];
+  const auto& cell_face_list      = m_dimensional_data_ptr->cellToFaceMatrix()[cell_id];
+  const auto& face_to_node_matrix = m_dimensional_data_ptr->faceToNodeMatrix();
+
+  mean_of_ejk.fill(0);
+
+  if constexpr (MeshType::Dimension == 2) {
+    const auto& quadrature =
+      QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(m_polynomial_degree + 1));
+
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = face_to_node_matrix[face_id];
+      if (face_is_reversed[i_face]) {
+        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(MeshType::Dimension == 3);
+
+    const auto& triangle_quadrature =
+      QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor(m_polynomial_degree + 1));
+    const auto& square_quadrature =
+      QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor(m_polynomial_degree + 1));
+
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = face_to_node_matrix[face_id];
+      switch (face_node_list.size()) {
+      case 3: {
+        if (face_is_reversed[i_face]) {
+          const TriangleTransformation<3> T{m_xr[face_node_list[2]], m_xr[face_node_list[1]], m_xr[face_node_list[0]]};
+          _computeEjkBoundaryMean(triangle_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const TriangleTransformation<3> T{m_xr[face_node_list[0]], m_xr[face_node_list[1]], m_xr[face_node_list[2]]};
+          _computeEjkBoundaryMean(triangle_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      case 4: {
+        if (face_is_reversed[i_face]) {
+          const SquareTransformation<3> T{m_xr[face_node_list[3]], m_xr[face_node_list[2]], m_xr[face_node_list[1]],
+                                          m_xr[face_node_list[0]]};
+          _computeEjkBoundaryMean(square_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const SquareTransformation<3> T{m_xr[face_node_list[0]], m_xr[face_node_list[1]], m_xr[face_node_list[2]],
+                                          m_xr[face_node_list[3]]};
+          _computeEjkBoundaryMean(square_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      default: {
+        throw NotImplementedError("invalid face type");
+      }
+      }
+    }
+  }
+}
+
+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)
+{
+  const double inv_Vi = 1. / m_Vj[cell_id];
+
+  const auto& face_is_reversed    = m_dimensional_data_ptr->cellFaceIsReversed()[cell_id];
+  const auto& cell_face_list      = m_dimensional_data_ptr->cellToFaceMatrix()[cell_id];
+  const auto& face_to_node_matrix = m_dimensional_data_ptr->faceToNodeMatrix();
+
+  mean_of_ejk.fill(0);
+  if constexpr (MeshType::Dimension == 2) {
+    const auto& quadrature =
+      QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(m_polynomial_degree + 1));
+
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++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, m_xr[face_node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[0]]);
+
+      if (face_is_reversed[i_face]) {
+        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(MeshType::Dimension == 3);
+
+    const auto& triangle_quadrature =
+      QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor(m_polynomial_degree + 1));
+    const auto& square_quadrature =
+      QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor(m_polynomial_degree + 1));
+
+    for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+      const FaceId face_id = cell_face_list[i_face];
+      auto face_node_list  = face_to_node_matrix[face_id];
+      switch (face_node_list.size()) {
+      case 3: {
+        const auto x0 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[2]]);
+        const auto x1 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[1]]);
+        const auto x2 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[0]]);
+
+        if (face_is_reversed[i_face]) {
+          const TriangleTransformation<3> T{x2, x1, x0};
+          _computeEjkBoundaryMean(triangle_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const TriangleTransformation<3> T{x0, x1, x2};
+          _computeEjkBoundaryMean(triangle_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      case 4: {
+        const auto x0 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[3]]);
+        const auto x1 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[2]]);
+        const auto x2 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[1]]);
+        const auto x3 = symmetrize_coordinates(origin, normal, m_xr[face_node_list[0]]);
+
+        if (face_is_reversed[i_face]) {
+          const SquareTransformation<3> T{x3, x2, x1, x0};
+          _computeEjkBoundaryMean(square_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        } else {
+          const SquareTransformation<3> T{x0, x1, x2, x3};
+          _computeEjkBoundaryMean(square_quadrature, T, Xj, inv_Vi, mean_of_ejk);
+        }
+        break;
+      }
+      default: {
+        throw NotImplementedError("invalid face type");
+      }
+      }
+    }
+  }
+}
+
+template <>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<1>>::_computeEjkMeanByBoundary(
+  const Rd& Xj,
+  const CellId& cell_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  const double inv_Vi = 1. / m_Vj[cell_id];
+
+  const auto& cell_node_list = m_dimensional_data_ptr->cellToNodeMatrix()[cell_id];
+
+  const double xr1_xj = (m_xr[cell_node_list[1]] - Xj)[0];
+
+  m_tmp_array[0] = xr1_xj * inv_Vi;
+  for (size_t k = 1; k <= m_polynomial_degree; ++k) {
+    m_tmp_array[k]                                                           //
+      = xr1_xj * m_tmp_array[k - 1] * m_antiderivative_correction_coef[k];   // ((1. * k) / (k + 1));
+  }
+
+  for (size_t k = 1; k < m_basis_dimension; ++k) {
+    mean_of_ejk[k - 1] = m_tmp_array[k];
+  }
+
+  const double xr0_xj = (m_xr[cell_node_list[0]] - Xj)[0];
+
+  m_tmp_array[0] = xr0_xj * inv_Vi;
+  for (size_t k = 1; k <= m_polynomial_degree; ++k) {
+    m_tmp_array[k]                                                           //
+      = xr0_xj * m_tmp_array[k - 1] * m_antiderivative_correction_coef[k];   // ((1. * k) / (k + 1));
+  }
+
+  for (size_t k = 1; k < m_basis_dimension; ++k) {
+    mean_of_ejk[k - 1] -= m_tmp_array[k];
+  }
+}
+
+template <>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  Mesh<1>>::_computeEjkMeanByBoundaryInSymmetricCell(const Rd& origin,
+                                                     const Rd& normal,
+                                                     const Rd& Xj,
+                                                     const CellId& cell_id,
+                                                     SmallArray<double>& mean_of_ejk)
+{
+  const double inv_Vi = 1. / m_Vj[cell_id];
+
+  const auto& cell_node_list = m_dimensional_data_ptr->cellToNodeMatrix()[cell_id];
+
+  const double xr1_xj = (symmetrize_coordinates(origin, normal, m_xr[cell_node_list[0]]) - Xj)[0];
+
+  m_tmp_array[0] = xr1_xj * inv_Vi;
+  for (size_t k = 1; k <= m_polynomial_degree; ++k) {
+    m_tmp_array[k]                                                           //
+      = xr1_xj * m_tmp_array[k - 1] * m_antiderivative_correction_coef[k];   // ((1. * k) / (k + 1));
+  }
+
+  for (size_t k = 1; k < m_basis_dimension; ++k) {
+    mean_of_ejk[k - 1] = m_tmp_array[k];
+  }
+
+  const double xr0_xj = (symmetrize_coordinates(origin, normal, m_xr[cell_node_list[1]]) - Xj)[0];
+
+  m_tmp_array[0] = xr0_xj * inv_Vi;
+  for (size_t k = 1; k <= m_polynomial_degree; ++k) {
+    m_tmp_array[k]                                                           //
+      = xr0_xj * m_tmp_array[k - 1] * m_antiderivative_correction_coef[k];   // ((1. * k) / (k + 1));
+  }
+
+  for (size_t k = 1; k < m_basis_dimension; ++k) {
+    mean_of_ejk[k - 1] -= m_tmp_array[k];
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<MeshTypeT>::build(
+  const CellId cell_j_id,
+  ShrinkMatrixView<SmallMatrix<double>>& A)
+{
+  if constexpr (MeshType::Dimension <= 3) {
+    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_tmp_array{m_basis_dimension},
+    m_mean_j_of_ejk{m_basis_dimension - 1},
+    m_mean_i_of_ejk{m_basis_dimension - 1},
+
+    m_dimensional_data_ptr{std::make_shared<SpecificDimensionalData>(mesh.connectivity())},
+
+    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()}
+{
+  SmallArray<double> antiderivative_correction_coef(m_polynomial_degree + 1);
+  for (size_t k = 0; k < antiderivative_correction_coef.size(); ++k) {
+    // The antiderivative of x^k is k/(k+1) times the antiderivative of x^(k-1)
+    antiderivative_correction_coef[k] = ((1. * k) / (k + 1));
+  }
+
+  m_antiderivative_correction_coef = antiderivative_correction_coef;
+
+  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::BoundaryIntegralReconstructionMatrixBuilder<Mesh<1>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<Mesh<3>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  Mesh<1>>::BoundaryIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                        const size_t,
+                                                        const SmallArray<const Rd>&,
+                                                        const SmallArray<const Rd>&,
+                                                        const CellToCellStencilArray&);
+
+template PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  Mesh<2>>::BoundaryIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                        const size_t,
+                                                        const SmallArray<const Rd>&,
+                                                        const SmallArray<const Rd>&,
+                                                        const CellToCellStencilArray&);
+
+template PolynomialReconstruction::BoundaryIntegralReconstructionMatrixBuilder<
+  Mesh<3>>::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..60707189b909e9d5f17ae2852be655cbfb877351
--- /dev/null
+++ b/src/scheme/reconstruction_utils/BoundaryIntegralReconstructionMatrixBuilder.hpp
@@ -0,0 +1,89 @@
+#ifndef BOUNDARY_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+#define BOUNDARY_INTEGRAL_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 <size_t Dimension>
+class QuadratureFormula;
+
+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_tmp_array;
+  SmallArray<double> m_mean_j_of_ejk;
+  SmallArray<double> m_mean_i_of_ejk;
+
+  class SpecificDimensionalData;
+  std::shared_ptr<SpecificDimensionalData> m_dimensional_data_ptr;
+
+  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;
+
+  // 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;
+
+  SmallArray<const double> m_antiderivative_correction_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;
+};
+
+#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..94d1ef9a79ac1fb8f7e3cc0ece4c8eb21672bef7
--- /dev/null
+++ b/src/scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.cpp
@@ -0,0 +1,92 @@
+#include <scheme/reconstruction_utils/CellCenterReconstructionMatrixBuilder.hpp>
+
+#include <geometry/SymmetryUtils.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.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 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&);
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..09e410811ed203610926bd605be3618e4219f396
--- /dev/null
+++ b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp
@@ -0,0 +1,501 @@
+#include <scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.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/SymmetryUtils.hpp>
+#include <geometry/TetrahedronTransformation.hpp>
+#include <geometry/TriangleTransformation.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.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 = 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 (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 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&);
diff --git a/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7a3e8f7fc04d6d4ba21d4a36ed02ba24dd0d67d0
--- /dev/null
+++ b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
@@ -0,0 +1,88 @@
+#ifndef ELEMENT_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+#define ELEMENT_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
+
+#include <algebra/ShrinkMatrixView.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <mesh/CellType.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/StencilArray.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+#include <utils/SmallArray.hpp>
+
+template <size_t Dimension>
+class QuadratureFormula;
+
+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/src/utils/Array.hpp b/src/utils/Array.hpp
index a08453ba766450909f3bcdf0f0fd678bdb06456c..94c6bcce4a1ee72869e9b666c45dce5eaf615638 100644
--- a/src/utils/Array.hpp
+++ b/src/utils/Array.hpp
@@ -63,9 +63,9 @@ class [[nodiscard]] Array
     UnsafeArrayView& operator=(UnsafeArrayView&&)      = delete;
 
     UnsafeArrayView(const Array<DataType>& array, index_type begin, index_type size)
-      : m_values{&array[begin]}, m_size{size}
+      : m_values{(size == 0) ? nullptr : &array[begin]}, m_size{size}
     {
-      Assert((begin < array.size()) and (begin + size <= array.size()), "required view is not contained in the Array");
+      Assert((size == 0) or (begin + size <= array.size()), "required view is not contained in the Array");
     }
 
     // To try to keep these views close to the initial array one
diff --git a/src/utils/GlobalVariableManager.hpp b/src/utils/GlobalVariableManager.hpp
index 0768a2ab2beb28a9060cadd44ee705f3aba598e2..cdc007677f44bbe64ab7f6a65a9fd5de7d0898e2 100644
--- a/src/utils/GlobalVariableManager.hpp
+++ b/src/utils/GlobalVariableManager.hpp
@@ -1,15 +1,23 @@
 #ifndef GLOBAL_VARIABLE_MANAGER_HPP
 #define GLOBAL_VARIABLE_MANAGER_HPP
 
+#include <utils/Exceptions.hpp>
 #include <utils/PugsAssert.hpp>
 #include <utils/PugsMacros.hpp>
 
+#include <optional>
+
 class GlobalVariableManager
 {
  private:
+  // Give some special access for testing
+  friend class NbGhostLayersTester;
+
   size_t m_connectivity_id = 0;
   size_t m_mesh_id         = 0;
 
+  std::optional<size_t> m_number_of_ghost_layers;
+
   static GlobalVariableManager* m_instance;
 
   explicit GlobalVariableManager()                    = default;
@@ -53,6 +61,24 @@ class GlobalVariableManager
     return m_mesh_id++;
   }
 
+  PUGS_INLINE
+  void
+  setNumberOfGhostLayers(const size_t number)
+  {
+    if (m_number_of_ghost_layers.has_value()) {
+      throw UnexpectedError("changing number of ghost layers is forbidden");
+    }
+    m_number_of_ghost_layers = number;
+  }
+
+  PUGS_INLINE
+  size_t
+  getNumberOfGhostLayers()
+  {
+    Assert(m_number_of_ghost_layers.has_value());
+    return m_number_of_ghost_layers.value();
+  }
+
   PUGS_INLINE
   void
   setMeshId(size_t mesh_id)
diff --git a/src/utils/PugsTraits.hpp b/src/utils/PugsTraits.hpp
index 3c7a4e40a217d957dea388223d99574e6b1ecf74..e0008a7a4a0ecf30998e8a739bbf91511d73e3d4 100644
--- a/src/utils/PugsTraits.hpp
+++ b/src/utils/PugsTraits.hpp
@@ -32,6 +32,12 @@ class DiscreteFunctionP0;
 template <typename DataType>
 class DiscreteFunctionP0Vector;
 
+template <size_t Dimension, typename DataType, typename BasisView>
+class DiscreteFunctionDPk;
+
+template <size_t Dimension, typename DataType, typename BasisView>
+class DiscreteFunctionDPkVector;
+
 // Traits is_trivially_castable
 
 template <typename T>
@@ -112,6 +118,12 @@ inline constexpr bool is_tiny_vector_v = false;
 template <size_t N, typename T>
 inline constexpr bool is_tiny_vector_v<TinyVector<N, T>> = true;
 
+template <typename T>
+inline constexpr bool is_tiny_vector_v<const T> = is_tiny_vector_v<std::remove_cvref_t<T>>;
+
+template <typename T>
+inline constexpr bool is_tiny_vector_v<T&> = is_tiny_vector_v<std::remove_cvref_t<T>>;
+
 // Traits is_tiny_matrix
 
 template <typename T>
@@ -120,6 +132,12 @@ inline constexpr bool is_tiny_matrix_v = false;
 template <size_t M, size_t N, typename T>
 inline constexpr bool is_tiny_matrix_v<TinyMatrix<M, N, T>> = true;
 
+template <typename T>
+inline constexpr bool is_tiny_matrix_v<const T> = is_tiny_matrix_v<std::remove_cvref_t<T>>;
+
+template <typename T>
+inline constexpr bool is_tiny_matrix_v<T&> = is_tiny_matrix_v<std::remove_cvref_t<T>>;
+
 // Traits is_small_matrix
 
 template <typename T>
@@ -128,6 +146,12 @@ inline constexpr bool is_small_matrix_v = false;
 template <typename DataType>
 inline constexpr bool is_small_matrix_v<SmallMatrix<DataType>> = true;
 
+template <typename T>
+inline constexpr bool is_small_matrix_v<const T> = is_small_matrix_v<std::remove_cvref_t<T>>;
+
+template <typename T>
+inline constexpr bool is_small_matrix_v<T&> = is_small_matrix_v<std::remove_cvref_t<T>>;
+
 // Traits is_crs_matrix
 
 template <typename T>
@@ -136,6 +160,12 @@ inline constexpr bool is_crs_matrix_v = false;
 template <typename DataType, typename IndexType>
 inline constexpr bool is_crs_matrix_v<CRSMatrix<DataType, IndexType>> = true;
 
+template <typename T>
+inline constexpr bool is_crs_matrix_v<const T> = is_crs_matrix_v<std::remove_cvref_t<T>>;
+
+template <typename T>
+inline constexpr bool is_crs_matrix_v<T&> = is_crs_matrix_v<std::remove_cvref_t<T>>;
+
 // Trais is ItemValue
 
 template <typename T>
@@ -173,6 +203,29 @@ constexpr inline bool is_discrete_function_P0_vector_v<DiscreteFunctionP0Vector<
 template <typename T>
 constexpr inline bool is_discrete_function_v = is_discrete_function_P0_v<T> or is_discrete_function_P0_vector_v<T>;
 
+// Trais is DiscreteFunctionDPk
+
+template <typename T>
+constexpr inline bool is_discrete_function_dpk_scalar_v = false;
+
+template <size_t Dimension, typename DataType, typename BasisView>
+constexpr inline bool is_discrete_function_dpk_scalar_v<DiscreteFunctionDPk<Dimension, DataType, BasisView>> = true;
+
+// Trais is DiscreteFunctionDPkVector
+
+template <typename T>
+constexpr inline bool is_discrete_function_dpk_vector_v = false;
+
+template <size_t Dimension, typename DataType, typename BasisView>
+constexpr inline bool is_discrete_function_dpk_vector_v<DiscreteFunctionDPkVector<Dimension, DataType, BasisView>> =
+  true;
+
+// Trais is DiscreteFunction
+
+template <typename T>
+constexpr inline bool is_discrete_function_dpk_v =
+  is_discrete_function_dpk_scalar_v<T> or is_discrete_function_dpk_vector_v<T>;
+
 // helper to check if a type is part of a variant
 
 template <typename T, typename V>
diff --git a/src/utils/PugsUtils.cpp b/src/utils/PugsUtils.cpp
index 356869699a7d76f04648dce9a95d4d091a619df2..5766160347d055a59017cf6ccb9cec43bcecd628 100644
--- a/src/utils/PugsUtils.cpp
+++ b/src/utils/PugsUtils.cpp
@@ -134,6 +134,10 @@ initialize(int& argc, char* argv[])
     bool pause_on_error = false;
     app.add_flag("-p,--pause-on-error", pause_on_error, "Pause for debugging on unexpected error [default: false]");
 
+    int nb_ghost_layers = 1;
+    app.add_option("--number-of-ghost-layers", nb_ghost_layers, "Number of ghost layers of cells [default: 1]")
+      ->check(CLI::Range(0, std::numeric_limits<decltype(nb_ghost_layers)>::max()));
+
     bool reproducible_sums = true;
     app.add_flag("--reproducible-sums,!--no-reproducible-sums", reproducible_sums,
                  "Special treatment of array sums to ensure reproducibility [default: true]");
@@ -173,6 +177,8 @@ initialize(int& argc, char* argv[])
       CommunicatorManager::setSplitColor(mpi_split_color);
     }
 
+    GlobalVariableManager::instance().setNumberOfGhostLayers(nb_ghost_layers);
+
     ResumingManager::getInstance().setIsResuming(is_resuming);
     if (is_resuming) {
       ResumingManager::getInstance().setFilename(filename);
diff --git a/src/utils/SignalManager.cpp b/src/utils/SignalManager.cpp
index 4b2046a98796fc72cef406f77e7498930089f533..5054622c0d9959c089ddfa3f3a323820614c5806 100644
--- a/src/utils/SignalManager.cpp
+++ b/src/utils/SignalManager.cpp
@@ -52,17 +52,27 @@ SignalManager::signalName(int signal)
 void
 SignalManager::pauseForDebug(int signal)
 {
-  if (std::string(PUGS_BUILD_TYPE) != "Release") {
-    if (s_pause_on_error) {
-      // Each failing process must write
-      std::cerr.clear();
+  if (s_pause_on_error) {
+    // Each failing process must write
+    std::cerr.clear();
+    if (std::string(PUGS_BUILD_TYPE) != "Release") {
+      char hostname[HOST_NAME_MAX + 1];
+      gethostname(hostname, HOST_NAME_MAX + 1);
       std::cerr << "\n======================================\n"
-                << rang::style::reset << rang::fg::reset << rang::style::bold << "to attach gdb to this process run\n"
+                << rang::style::reset << rang::fg::reset << rang::style::bold << "Process paused on host \""
+                << rang::fg::yellow << hostname << rang::fg::reset << "\"\n"
+                << "to attach gdb to this process run\n"
                 << "\tgdb -pid " << rang::fg::red << getpid() << rang::fg::reset << '\n'
                 << "else press Control-C to exit\n"
                 << rang::style::reset << "======================================\n"
                 << std::flush;
       pause();
+    } else {
+      std::cerr << '\n'
+                << rang::style::bold
+                << "Pausing is useless for Release version.\n"
+                   "To attach debugger use Debug built type."
+                << rang::style::reset << '\n';
     }
   }
   std::exit(signal);
@@ -73,56 +83,52 @@ SignalManager::handler(int signal)
 {
   static std::mutex mutex;
 
-  if (mutex.try_lock()) {
-    std::signal(SIGTERM, SIG_DFL);
-    std::signal(SIGINT, SIG_DFL);
-    std::signal(SIGABRT, SIG_DFL);
-
-    // Each failing process must write
-    std::cerr.clear();
+  std::lock_guard<std::mutex> lock(mutex);
 
-    std::cerr << BacktraceManager{} << '\n';
+  std::signal(SIGINT, SIG_BLOCK);
 
-    std::cerr << "\n *** " << rang::style::reset << rang::fg::reset << rang::style::bold << "Signal " << rang::fgB::red
-              << signalName(signal) << rang::fg::reset << " caught" << rang::style::reset << " ***\n\n";
+  // Each failing process must write
+  std::cerr.clear();
 
-    std::exception_ptr eptr = std::current_exception();
-    try {
-      if (eptr) {
-        std::rethrow_exception(eptr);
-      } else {
-        std::ostringstream error_msg;
-        error_msg << "received " << signalName(signal);
-        std::cerr << ASTExecutionStack::getInstance().errorMessageAt(error_msg.str()) << '\n';
-      }
-    }
-    catch (const IBacktraceError& backtrace_error) {
-      auto source_location = backtrace_error.sourceLocation();
-      std::cerr << rang::fgB::cyan << source_location.file_name() << ':' << source_location.line() << ':'
-                << source_location.column() << ':' << rang::fg::reset << rang::fgB::yellow
-                << " threw the following exception" << rang::fg::reset << "\n\n";
-      std::cerr << ASTExecutionStack::getInstance().errorMessageAt(backtrace_error.what()) << '\n';
-    }
-    catch (const ParseError& parse_error) {
-      auto p = parse_error.positions().front();
-      std::cerr << rang::style::bold << p.source << ':' << p.line << ':' << p.column << ':' << rang::style::reset
-                << rang::fgB::red << " error: " << rang::fg::reset << rang::style::bold << parse_error.what()
-                << rang::style::reset << '\n';
-    }
-    catch (const IExitError& exit_error) {
-      std::cerr << ASTExecutionStack::getInstance().errorMessageAt(exit_error.what()) << '\n';
-    }
-    catch (const AssertError& assert_error) {
-      std::cerr << assert_error << '\n';
-    }
-    catch (...) {
-      std::cerr << "Unknown exception!\n";
-    }
+  std::cerr << BacktraceManager{} << '\n';
 
-    SignalManager::pauseForDebug(signal);
+  std::cerr << "\n *** " << rang::style::reset << rang::fg::reset << rang::style::bold << "Signal " << rang::fgB::red
+            << signalName(signal) << rang::fg::reset << " caught" << rang::style::reset << " ***\n\n";
 
-    mutex.unlock();
+  std::exception_ptr eptr = std::current_exception();
+  try {
+    if (eptr) {
+      std::rethrow_exception(eptr);
+    } else {
+      std::ostringstream error_msg;
+      error_msg << "received " << signalName(signal);
+      std::cerr << ASTExecutionStack::getInstance().errorMessageAt(error_msg.str()) << '\n';
+    }
+  }
+  catch (const IBacktraceError& backtrace_error) {
+    auto source_location = backtrace_error.sourceLocation();
+    std::cerr << rang::fgB::cyan << source_location.file_name() << ':' << source_location.line() << ':'
+              << source_location.column() << ':' << rang::fg::reset << rang::fgB::yellow
+              << " threw the following exception" << rang::fg::reset << "\n\n";
+    std::cerr << ASTExecutionStack::getInstance().errorMessageAt(backtrace_error.what()) << '\n';
   }
+  catch (const ParseError& parse_error) {
+    auto p = parse_error.positions().front();
+    std::cerr << rang::style::bold << p.source << ':' << p.line << ':' << p.column << ':' << rang::style::reset
+              << rang::fgB::red << " error: " << rang::fg::reset << rang::style::bold << parse_error.what()
+              << rang::style::reset << '\n';
+  }
+  catch (const IExitError& exit_error) {
+    std::cerr << ASTExecutionStack::getInstance().errorMessageAt(exit_error.what()) << '\n';
+  }
+  catch (const AssertError& assert_error) {
+    std::cerr << assert_error << '\n';
+  }
+  catch (...) {
+    std::cerr << "Unknown exception!\n";
+  }
+
+  SignalManager::pauseForDebug(signal);
 }
 
 void
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 169d375538eae053ff85a70919e11c24620d8c8a..8c05b9ed0ae319b803f053dda1bda8246c9339be 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -122,6 +122,7 @@ add_executable (unit_tests
   test_GaussLegendreQuadratureDescriptor.cpp
   test_GaussLobattoQuadratureDescriptor.cpp
   test_GaussQuadratureDescriptor.cpp
+  test_Givens.cpp
   test_IfProcessor.cpp
   test_IncDecExpressionProcessor.cpp
   test_IntegrateCellArray.cpp
@@ -155,6 +156,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
@@ -229,6 +231,9 @@ endif(PUGS_HAS_HDF5)
 add_executable (mpi_unit_tests
   mpi_test_main.cpp
   test_Connectivity.cpp
+  test_ConnectivityDispatcher.cpp
+  test_DiscreteFunctionDPk.cpp
+  test_DiscreteFunctionDPkVector.cpp
   test_DiscreteFunctionIntegrator.cpp
   test_DiscreteFunctionIntegratorByZone.cpp
   test_DiscreteFunctionInterpoler.cpp
@@ -274,7 +279,13 @@ add_executable (mpi_unit_tests
   test_OFStream.cpp
   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
+  test_StencilBuilder_node2cell.cpp
   test_SubItemArrayPerItemVariant.cpp
   test_SubItemValuePerItem.cpp
   test_SubItemValuePerItemVariant.cpp
@@ -306,6 +317,7 @@ target_link_libraries (unit_tests
   PugsAlgebra
   PugsAnalysis
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsOutput
   PugsUtils
   PugsCheckpointing
@@ -336,6 +348,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/NbGhostLayersTester.hpp b/tests/NbGhostLayersTester.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0468fe18305367834c9cbd0959ba5b3f443fe870
--- /dev/null
+++ b/tests/NbGhostLayersTester.hpp
@@ -0,0 +1,27 @@
+#ifndef NB_GHOST_LAYERS_TESTER_HPP
+#define NB_GHOST_LAYERS_TESTER_HPP
+
+#include <cstddef>
+#include <utils/GlobalVariableManager.hpp>
+
+class NbGhostLayersTester
+{
+ private:
+  const size_t m_original_number_of_ghost_layers;
+
+ public:
+  PUGS_INLINE
+  NbGhostLayersTester(const size_t number_of_ghost_layers)
+    : m_original_number_of_ghost_layers{GlobalVariableManager::instance().getNumberOfGhostLayers()}
+  {
+    GlobalVariableManager::instance().m_number_of_ghost_layers = number_of_ghost_layers;
+  }
+
+  PUGS_INLINE
+  ~NbGhostLayersTester()
+  {
+    GlobalVariableManager::instance().m_number_of_ghost_layers = m_original_number_of_ghost_layers;
+  }
+};
+
+#endif   // NB_GHOST_LAYERS_TESTER_HPP
diff --git a/tests/mpi_test_main.cpp b/tests/mpi_test_main.cpp
index cd74479774dbf255460e5afe1b056c73f3e80821..cabb10386d4e8e99ab0dd9b1946bc7dba818502f 100644
--- a/tests/mpi_test_main.cpp
+++ b/tests/mpi_test_main.cpp
@@ -7,6 +7,7 @@
 #include <mesh/DualConnectivityManager.hpp>
 #include <mesh/DualMeshManager.hpp>
 #include <mesh/MeshDataManager.hpp>
+#include <mesh/StencilManager.hpp>
 #include <mesh/SynchronizerManager.hpp>
 #include <utils/GlobalVariableManager.hpp>
 #include <utils/Messenger.hpp>
@@ -112,7 +113,10 @@ main(int argc, char* argv[])
       MeshDataManager::create();
       DualConnectivityManager::create();
       DualMeshManager::create();
+      StencilManager::create();
+
       GlobalVariableManager::create();
+      GlobalVariableManager::instance().setNumberOfGhostLayers(1);
 
       MeshDataBaseForTests::create();
 
@@ -124,6 +128,7 @@ main(int argc, char* argv[])
 
       MeshDataBaseForTests::destroy();
 
+      StencilManager::destroy();
       GlobalVariableManager::destroy();
       DualMeshManager::destroy();
       DualConnectivityManager::destroy();
diff --git a/tests/test_ConnectivityDispatcher.cpp b/tests/test_ConnectivityDispatcher.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7cc747588e9004a410f2a44453374eb81d499b8a
--- /dev/null
+++ b/tests/test_ConnectivityDispatcher.cpp
@@ -0,0 +1,236 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <mesh/CartesianMeshBuilder.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ConnectivityDispatcher.hpp>
+#include <mesh/GmshReader.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <utils/Messenger.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <NbGhostLayersTester.hpp>
+
+#include <filesystem>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("ConnectivityDispatcher", "[mesh]")
+{
+  auto check_number_of_ghost_layers = [](const auto& connectivity, const size_t number_of_layers) {
+    // We assume that the specified number of layers can be built
+    // (there are enough non owned layer of cells in the connectivity)
+    const auto cell_is_owned = connectivity.cellIsOwned();
+
+    CellValue<size_t> cell_layer{connectivity};
+    cell_layer.fill(number_of_layers + 1);
+
+    NodeValue<size_t> node_layer{connectivity};
+    node_layer.fill(number_of_layers + 1);
+
+    auto node_to_cell_matrix = connectivity.nodeToCellMatrix();
+    auto cell_to_node_matrix = connectivity.cellToNodeMatrix();
+
+    parallel_for(
+      connectivity.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+        if (cell_is_owned[cell_id]) {
+          cell_layer[cell_id] = 0;
+        }
+      });
+
+    for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+      parallel_for(
+        connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) {
+          auto node_cell_list = node_to_cell_matrix[node_id];
+          size_t min_layer    = cell_layer[node_cell_list[0]];
+          for (size_t i_cell = 1; i_cell < node_cell_list.size(); ++i_cell) {
+            min_layer = std::min(min_layer, cell_layer[node_cell_list[i_cell]]);
+          }
+          if (min_layer < number_of_layers + 1) {
+            node_layer[node_id] = min_layer;
+          }
+        });
+
+      parallel_for(
+        connectivity.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+          auto cell_node_list = cell_to_node_matrix[cell_id];
+          size_t min_layer    = node_layer[cell_node_list[0]];
+          size_t max_layer    = min_layer;
+          for (size_t i_node = 1; i_node < cell_node_list.size(); ++i_node) {
+            min_layer = std::min(min_layer, node_layer[cell_node_list[i_node]]);
+            max_layer = std::max(max_layer, node_layer[cell_node_list[i_node]]);
+          }
+          if ((min_layer != max_layer) or
+              ((min_layer < number_of_layers + 1) and (cell_layer[cell_id] == number_of_layers + 1))) {
+            cell_layer[cell_id] = min_layer + 1;
+          }
+        });
+    }
+
+    auto is_boundary_face    = connectivity.isBoundaryFace();
+    auto face_to_cell_matrix = connectivity.faceToCellMatrix();
+
+    bool has_required_number_of_ghost_layers = true;
+    for (FaceId face_id = 0; face_id < connectivity.numberOfFaces(); ++face_id) {
+      auto face_cell_list = face_to_cell_matrix[face_id];
+      if ((face_cell_list.size() == 1) and (not is_boundary_face[face_id])) {
+        const CellId face_cell_id = face_cell_list[0];
+        has_required_number_of_ghost_layers &= (cell_layer[face_cell_id] == number_of_layers);
+      }
+    }
+
+    REQUIRE(parallel::allReduceAnd(has_required_number_of_ghost_layers));
+    bool first_ghost_layer_is_1 = true;
+    for (FaceId face_id = 0; face_id < connectivity.numberOfFaces(); ++face_id) {
+      auto face_cell_list = face_to_cell_matrix[face_id];
+      if (face_cell_list.size() == 2) {
+        const CellId face_cell0_id = face_cell_list[0];
+        const CellId face_cell1_id = face_cell_list[1];
+        if (cell_is_owned[face_cell0_id] xor cell_is_owned[face_cell1_id]) {
+          for (size_t i_cell = 0; i_cell < face_cell_list.size(); ++i_cell) {
+            const CellId face_cell_id = face_cell_list[i_cell];
+            if (not cell_is_owned[face_cell_id]) {
+              first_ghost_layer_is_1 &= (cell_layer[face_cell_id] == 1);
+            }
+          }
+        }
+      }
+    }
+
+    REQUIRE(parallel::allReduceAnd(first_ghost_layer_is_1));
+  };
+
+  SECTION("1 layer meshes")
+  {
+    for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+      SECTION(named_mesh.name())
+      {
+        const std::shared_ptr p_mesh = named_mesh.mesh()->get<const Mesh<1>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), 1);
+      }
+    }
+    for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+      SECTION(named_mesh.name())
+      {
+        const std::shared_ptr p_mesh = named_mesh.mesh()->get<const Mesh<2>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), 1);
+      }
+    }
+    for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+      SECTION(named_mesh.name())
+      {
+        const std::shared_ptr p_mesh = named_mesh.mesh()->get<const Mesh<3>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), 1);
+      }
+    }
+  }
+
+  constexpr bool has_partitioner = []() {
+#if defined(PUGS_HAS_PARMETIS) || defined(PUGS_HAS_PTSCOTCH)
+    return true;
+#else
+    return false;
+#endif
+  }();
+
+  for (size_t nb_ghost_layers = 2; nb_ghost_layers < 5; ++nb_ghost_layers) {
+    std::stringstream os;
+    os << nb_ghost_layers << " layer meshes";
+
+    SECTION(os.str())
+    {
+      REQUIRE(GlobalVariableManager::instance().getNumberOfGhostLayers() == 1);
+
+      NbGhostLayersTester nb_ghost_layers_tester(nb_ghost_layers);
+
+      REQUIRE(GlobalVariableManager::instance().getNumberOfGhostLayers() == nb_ghost_layers);
+
+      SECTION("Cartesian 1D mesh")
+      {
+        auto cartesian_1d_mesh =
+          CartesianMeshBuilder{TinyVector<1>{-1}, TinyVector<1>{3}, TinyVector<1, size_t>{23}}.mesh();
+        const std::shared_ptr p_mesh = cartesian_1d_mesh->get<const Mesh<1>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), nb_ghost_layers);
+
+        if (has_partitioner) {
+          ConnectivityDispatcher cd{p_mesh->connectivity()};
+          check_number_of_ghost_layers(*cd.dispatchedConnectivity(), nb_ghost_layers);
+        }
+      }
+
+      SECTION("Cartesian 2D mesh")
+      {
+        auto cartesian_2d_mesh =
+          CartesianMeshBuilder{TinyVector<2>{0, -1}, TinyVector<2>{3, 2}, TinyVector<2, size_t>{6, 7}}.mesh();
+        const std::shared_ptr p_mesh = cartesian_2d_mesh->get<const Mesh<2>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), nb_ghost_layers);
+
+        if (has_partitioner) {
+          ConnectivityDispatcher cd{p_mesh->connectivity()};
+          check_number_of_ghost_layers(*cd.dispatchedConnectivity(), nb_ghost_layers);
+        }
+      }
+
+      SECTION("Cartesian 3D mesh")
+      {
+        auto cartesian_3d_mesh =
+          CartesianMeshBuilder{TinyVector<3>{0, 1, 0}, TinyVector<3>{2, -1, 3}, TinyVector<3, size_t>{6, 7, 4}}.mesh();
+        const std::shared_ptr p_mesh = cartesian_3d_mesh->get<const Mesh<3>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), nb_ghost_layers);
+
+        if (has_partitioner) {
+          ConnectivityDispatcher cd{p_mesh->connectivity()};
+          check_number_of_ghost_layers(*cd.dispatchedConnectivity(), nb_ghost_layers);
+        }
+      }
+
+      SECTION("unordered 1d mesh")
+      {
+        const std::string filename = std::filesystem::path{PUGS_BINARY_DIR}.append("tests").append("unordered-1d.msh");
+
+        auto mesh_v = GmshReader{filename}.mesh();
+
+        const std::shared_ptr p_mesh = mesh_v->get<const Mesh<1>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), nb_ghost_layers);
+
+        if (has_partitioner) {
+          ConnectivityDispatcher cd{p_mesh->connectivity()};
+          check_number_of_ghost_layers(*cd.dispatchedConnectivity(), nb_ghost_layers);
+        }
+      }
+
+      SECTION("hybrid 2d mesh")
+      {
+        const std::string filename = std::filesystem::path{PUGS_BINARY_DIR}.append("tests").append("hybrid-2d.msh");
+
+        auto mesh_v = GmshReader{filename}.mesh();
+
+        const std::shared_ptr p_mesh = mesh_v->get<const Mesh<2>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), nb_ghost_layers);
+
+        if (has_partitioner) {
+          ConnectivityDispatcher cd{p_mesh->connectivity()};
+          check_number_of_ghost_layers(*cd.dispatchedConnectivity(), nb_ghost_layers);
+        }
+      }
+
+      SECTION("hybrid 3d mesh")
+      {
+        const std::string filename = std::filesystem::path{PUGS_BINARY_DIR}.append("tests").append("hybrid-3d.msh");
+
+        auto mesh_v = GmshReader{filename}.mesh();
+
+        const std::shared_ptr p_mesh = mesh_v->get<const Mesh<3>>();
+        check_number_of_ghost_layers(p_mesh->connectivity(), nb_ghost_layers);
+
+        if (has_partitioner) {
+          ConnectivityDispatcher cd{p_mesh->connectivity()};
+          check_number_of_ghost_layers(*cd.dispatchedConnectivity(), nb_ghost_layers);
+        }
+      }
+    }
+
+    REQUIRE(GlobalVariableManager::instance().getNumberOfGhostLayers() == 1);
+  }
+}
diff --git a/tests/test_DiscreteFunctionDPk.cpp b/tests/test_DiscreteFunctionDPk.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..60120f894a1c3fb4de851e243f593efc7f41a700
--- /dev/null
+++ b/tests/test_DiscreteFunctionDPk.cpp
@@ -0,0 +1,670 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/Mesh.hpp>
+#include <scheme/DiscreteFunctionDPk.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("DiscreteFunctionDPk", "[scheme]")
+{
+  SECTION("Basic interface")
+  {
+    const std::shared_ptr mesh_2d_v = MeshDataBaseForTests::get().cartesian2DMesh();
+    const std::shared_ptr mesh_2d   = mesh_2d_v->get<Mesh<2>>();
+
+    const std::shared_ptr mesh_2d_2_v = MeshDataBaseForTests::get().hybrid2DMesh();
+    const std::shared_ptr mesh_2d_2   = mesh_2d_2_v->get<Mesh<2>>();
+
+    DiscreteFunctionDPk<2, const double> R_dkp;
+
+#ifndef NDEBUG
+    REQUIRE_THROWS_WITH(R_dkp[CellId(0)], "DiscreteFunctionDPk is not built");
+#endif   // NDEBUG
+
+    {
+      DiscreteFunctionDPk<2, double> tmp_R_dkp(mesh_2d_v, 2);
+      tmp_R_dkp.fill(2);
+      R_dkp = tmp_R_dkp;
+    }
+
+    REQUIRE(R_dkp.degree() == 2);
+
+    REQUIRE(min(R_dkp.cellArrays()) == 2);
+    REQUIRE(max(R_dkp.cellArrays()) == 2);
+
+    REQUIRE(R_dkp.dataType() == ast_node_data_type_from<double>);
+    REQUIRE(R_dkp.meshVariant()->id() == mesh_2d->id());
+
+    DiscreteFunctionDPk<2, double> R_dkp2;
+    R_dkp2 = copy(R_dkp);
+
+    REQUIRE(R_dkp2.degree() == 2);
+
+    REQUIRE(min(R_dkp2.cellArrays()) == 2);
+    REQUIRE(max(R_dkp2.cellArrays()) == 2);
+
+    REQUIRE(R_dkp2.dataType() == ast_node_data_type_from<double>);
+    REQUIRE(R_dkp2.meshVariant()->id() == mesh_2d_v->id());
+
+    DiscreteFunctionDPk<2, double> R_dkp3(mesh_2d_2_v, 1);
+    R_dkp3.fill(3);
+
+    REQUIRE(R_dkp3.degree() == 1);
+
+    REQUIRE(min(R_dkp3.cellArrays()) == 3);
+    REQUIRE(max(R_dkp3.cellArrays()) == 3);
+
+    REQUIRE(R_dkp3.dataType() == ast_node_data_type_from<double>);
+    REQUIRE(R_dkp3.meshVariant()->id() == mesh_2d_2_v->id());
+
+    DiscreteFunctionDPk<2, double> R_dkp4(mesh_2d, R_dkp2.cellArrays());
+
+    REQUIRE(min(R_dkp4.cellArrays()) == 2);
+    REQUIRE(max(R_dkp4.cellArrays()) == 2);
+
+    R_dkp4.fill(5);
+    REQUIRE(min(R_dkp4.cellArrays()) == 5);
+    REQUIRE(max(R_dkp4.cellArrays()) == 5);
+    REQUIRE(min(R_dkp2.cellArrays()) == 5);
+    REQUIRE(max(R_dkp2.cellArrays()) == 5);
+
+    copy_to(R_dkp, R_dkp4);
+    REQUIRE(min(R_dkp4.cellArrays()) == 2);
+    REQUIRE(max(R_dkp4.cellArrays()) == 2);
+    REQUIRE(min(R_dkp2.cellArrays()) == 2);
+    REQUIRE(max(R_dkp2.cellArrays()) == 2);
+
+#ifndef NDEBUG
+    REQUIRE_THROWS_WITH(copy_to(R_dkp, R_dkp3), "copy_to target must use the same mesh");
+
+    DiscreteFunctionDPk<2, double> R_dkp5(mesh_2d, 3);
+    REQUIRE_THROWS_WITH(copy_to(R_dkp, R_dkp5), "copy_to target must have the same degree");
+
+    REQUIRE_THROWS_WITH((DiscreteFunctionDPk<2, double>{mesh_2d_2, R_dkp2.cellArrays()}),
+                        "cell_array is built on different connectivity");
+#endif   // NDEBUG
+  }
+
+  SECTION("R data")
+  {
+    SECTION("1D")
+    {
+      constexpr size_t Dimension = 1;
+
+      using R1 = TinyVector<1>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian1DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 4;
+
+      DiscreteFunctionDPk<Dimension, double> pk(mesh_v, degree);
+
+      std::vector<double> a = {1, 1.4, -6.2, 2.7, 3.1};
+
+      for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < a.size(); ++i) {
+          coefficients[i] = a[i];
+        }
+      };
+
+      DiscreteFunctionP0<double> p_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) { p_xj[cell_id] = pk[cell_id](xj[cell_id]); });
+
+      REQUIRE(max(p_xj) == 1);
+      REQUIRE(min(p_xj) == 1);
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.3 * Vj[cell_id];
+          delta[cell_id]  = pk[cell_id](xj[cell_id] + R1{x0})   //
+                           - (a[0] + x0 * a[1] + x0 * x0 * a[2] + x0 * x0 * x0 * a[3] + x0 * x0 * x0 * x0 * a[4]);
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+      REQUIRE(min(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+
+    SECTION("2D")
+    {
+      constexpr size_t Dimension = 2;
+
+      using R2 = TinyVector<2>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian2DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 3;
+
+      DiscreteFunctionDPk<Dimension, double> pk(mesh_v, degree);
+
+      const std::vector<double> a = {1, 1.4, -6.2, 3.5, -2.3, 5.2, 6.1, 2.3, 0.5, -1.3};
+
+      for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < coefficients.size(); ++i) {
+          coefficients[i] = a[i];
+        }
+      };
+
+      DiscreteFunctionP0<double> p_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) { p_xj[cell_id] = pk[cell_id](xj[cell_id]); });
+
+      REQUIRE(max(p_xj) == 1);
+      REQUIRE(min(p_xj) == 1);
+
+      DiscreteFunctionP0<double> error(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.2 * Vj[cell_id];
+          const double y0 = 0.3 * Vj[cell_id];
+          error[cell_id]                                       //
+            = std::abs(pk[cell_id](xj[cell_id] + R2{x0, y0})   //
+                       - (a[0]                                 //
+                          + x0 * a[1]                          //
+                          + x0 * x0 * a[2]                     //
+                          + x0 * x0 * x0 * a[3]                //
+                          + y0 * a[4]                          //
+                          + y0 * x0 * a[5]                     //
+                          + y0 * x0 * x0 * a[6]                //
+                          + y0 * y0 * a[7]                     //
+                          + y0 * y0 * x0 * a[8]                //
+                          + y0 * y0 * y0 * a[9]));
+        });
+
+      REQUIRE(max(error) == Catch::Approx(0.).margin(1E-14));
+    }
+
+    SECTION("3D")
+    {
+      constexpr size_t Dimension = 3;
+
+      using R3 = TinyVector<3>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian3DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 3;
+
+      DiscreteFunctionDPk<Dimension, double> pk(mesh_v, degree);
+
+      const std::vector<double> a = {+1.0, +1.4, -6.2, +3.5, -2.3, +5.2, +6.1, +2.3, +0.5, -1.3,   //
+                                     +2.8, -8.4, +9.5, +4.0, +4.3, +7.2, -9.1, +6.8, +6.7, +9.2};
+
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          auto coefficients = pk.coefficients(cell_id);
+          for (size_t i = 0; i < coefficients.size(); ++i) {
+            coefficients[i] = a[i];
+          }
+        });
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(CellId cell_id) { delta_xj[cell_id] = std::abs(pk[cell_id](xj[cell_id]) - a[0]); });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0.).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = +0.5 * Vj[cell_id];
+          const double y0 = +0.3 * Vj[cell_id];
+          const double z0 = -0.4 * Vj[cell_id];
+
+          delta[cell_id] = pk[cell_id](xj[cell_id] + R3{x0, y0, z0})   //
+                           - (a[0]                                     //
+                              + x0 * a[1]                              //
+                              + x0 * x0 * a[2]                         //
+                              + x0 * x0 * x0 * a[3]                    //
+                              + y0 * a[4]                              //
+                              + y0 * x0 * a[5]                         //
+                              + y0 * x0 * x0 * a[6]                    //
+                              + y0 * y0 * a[7]                         //
+                              + y0 * y0 * x0 * a[8]                    //
+                              + y0 * y0 * y0 * a[9]                    //
+                              + z0 * a[10]                             //
+                              + z0 * x0 * a[11]                        //
+                              + z0 * x0 * x0 * a[12]                   //
+                              + z0 * y0 * a[13]                        //
+                              + z0 * y0 * x0 * a[14]                   //
+                              + z0 * y0 * y0 * a[15]                   //
+                              + z0 * z0 * a[16]                        //
+                              + z0 * z0 * x0 * a[17]                   //
+                              + z0 * z0 * y0 * a[18]                   //
+                              + z0 * z0 * z0 * a[19]                   //
+                             );
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+      REQUIRE(min(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+  }
+
+  SECTION("R^d data")
+  {
+    SECTION("1D")
+    {
+      constexpr size_t Dimension = 1;
+
+      using R1 = TinyVector<1>;
+      using R2 = TinyVector<2>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian1DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 4;
+
+      DiscreteFunctionDPk<Dimension, R2> pk(mesh_v, degree);
+
+      const std::vector<R2> a = {R2{-1.0, +3.0}, R2{+1.4, +1.9}, R2{-6.2, -1.0}, R2{+2.7, +1.6}, R2{+3.1, -1.3}};
+
+      for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < a.size(); ++i) {
+          coefficients[i] = a[i];
+        }
+      }
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(CellId cell_id) { delta_xj[cell_id] = l2Norm(pk[cell_id](xj[cell_id]) - a[0]); });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.3 * Vj[cell_id];
+          delta[cell_id] =
+            l2Norm(pk[cell_id](xj[cell_id] + R1{x0})   //
+                   - (a[0] + x0 * a[1] + x0 * x0 * a[2] + x0 * x0 * x0 * a[3] + x0 * x0 * x0 * x0 * a[4]));
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+
+    SECTION("2D")
+    {
+      constexpr size_t Dimension = 2;
+
+      using R2 = TinyVector<2>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().hybrid2DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 3;
+
+      DiscreteFunctionDPk<Dimension, R2> pk(mesh_v, degree);
+
+      const std::vector<R2> a = {R2{-1.0, +3.0}, R2{+1.4, +1.9}, R2{-6.2, -1.0}, R2{+2.7, +1.6}, R2{+3.1, -1.3},
+                                 R2{+2.5, -4.2}, R2{+2.1, -1.7}, R2{-3.2, +1.0}, R2{-2.3, +1.3}, R2{-2.9, -3.2}};
+
+      for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < a.size(); ++i) {
+          coefficients[i] = a[i];
+        }
+      }
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(CellId cell_id) { delta_xj[cell_id] = l2Norm(pk[cell_id](xj[cell_id]) - a[0]); });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.2 * Vj[cell_id];
+          const double y0 = 0.3 * Vj[cell_id];
+          delta[cell_id]                                     //
+            = l2Norm(pk[cell_id](xj[cell_id] + R2{x0, y0})   //
+                     - (a[0]                                 //
+                        + x0 * a[1]                          //
+                        + x0 * x0 * a[2]                     //
+                        + x0 * x0 * x0 * a[3]                //
+                        + y0 * a[4]                          //
+                        + y0 * x0 * a[5]                     //
+                        + y0 * x0 * x0 * a[6]                //
+                        + y0 * y0 * a[7]                     //
+                        + y0 * y0 * x0 * a[8]                //
+                        + y0 * y0 * y0 * a[9]));
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+
+    SECTION("3D")
+    {
+      constexpr size_t Dimension = 3;
+
+      using R2 = TinyVector<2>;
+      using R3 = TinyVector<3>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().hybrid3DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 3;
+
+      const std::vector<R2> a = {R2{+9.2, +7.2}, R2{+0.0, -2.7}, R2{+4.8, -5.8},  R2{+6.0, +3.6}, R2{-7.0, -9.7},
+                                 R2{+5.3, -1.2}, R2{-1.2, +3.7}, R2{-0.4, +6.1},  R2{+8.4, +9.5}, R2{-9.7, +3.3},
+                                 R2{+0.5, +4.2}, R2{+3.8, +3.3}, R2{+8.0, -10.0}, R2{-5.1, -4.1}, R2{+2.6, -2.5},
+                                 R2{-3.4, -2.7}, R2{+0.7, +4.9}, R2{+6.0, +6.4},  R2{+3.5, +5.0}, R2{+1.7, +4.8}};
+
+      DiscreteFunctionDPk<Dimension, R2> pk(mesh_v, degree);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          auto coefficients = pk.coefficients(cell_id);
+          for (size_t i = 0; i < coefficients.size(); ++i) {
+            coefficients[i] = a[i];
+          }
+        });
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(CellId cell_id) { delta_xj[cell_id] = l2Norm(pk[cell_id](xj[cell_id]) - a[0]); });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0.).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = +0.5 * Vj[cell_id];
+          const double y0 = +0.3 * Vj[cell_id];
+          const double z0 = -0.4 * Vj[cell_id];
+
+          delta[cell_id] = l2Norm(pk[cell_id](xj[cell_id] + R3{x0, y0, z0})   //
+                                  - (a[0]                                     //
+                                     + x0 * a[1]                              //
+                                     + x0 * x0 * a[2]                         //
+                                     + x0 * x0 * x0 * a[3]                    //
+                                     + y0 * a[4]                              //
+                                     + y0 * x0 * a[5]                         //
+                                     + y0 * x0 * x0 * a[6]                    //
+                                     + y0 * y0 * a[7]                         //
+                                     + y0 * y0 * x0 * a[8]                    //
+                                     + y0 * y0 * y0 * a[9]                    //
+                                     + z0 * a[10]                             //
+                                     + z0 * x0 * a[11]                        //
+                                     + z0 * x0 * x0 * a[12]                   //
+                                     + z0 * y0 * a[13]                        //
+                                     + z0 * y0 * x0 * a[14]                   //
+                                     + z0 * y0 * y0 * a[15]                   //
+                                     + z0 * z0 * a[16]                        //
+                                     + z0 * z0 * x0 * a[17]                   //
+                                     + z0 * z0 * y0 * a[18]                   //
+                                     + z0 * z0 * z0 * a[19]                   //
+                                     ));
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+  }
+
+  SECTION("R^d1xd2 data")
+  {
+    SECTION("1D")
+    {
+      constexpr size_t Dimension = 1;
+
+      using R1   = TinyVector<1>;
+      using R2x3 = TinyMatrix<2, 3>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian1DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 4;
+
+      DiscreteFunctionDPk<Dimension, R2x3> pk(mesh_v, degree);
+
+      std::vector<R2x3> A = {R2x3{+1.0, +2.0, +3.0, +4.0, +5.0, +6.0},   //
+                             R2x3{-1.2, -2.3, +7.2, +8.4, -5.0, +0.7},   //
+                             R2x3{-2.1, -3.3, -2.7, -3.4, -0.5, -2.7},   //
+                             R2x3{+6.2, -2.9, +3.1, -2.6, +1.5, +2.1},   //
+                             R2x3{-2.6, -2.2, +4.2, -1.7, +8.5, -1.4}};
+
+      for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < coefficients.size(); ++i) {
+          coefficients[i] = A[i];
+        }
+      };
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const R2x3 Diff = pk[cell_id](xj[cell_id]) - A[0];
+
+          delta_xj[cell_id] = l2Norm((transpose(Diff) * Diff) * TinyVector<3>(1, 1, 1));
+        });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.3 * Vj[cell_id];
+          const R2x3 Diff = pk[cell_id](xj[cell_id] + R1{x0}) -
+                            (A[0] + x0 * A[1] + x0 * x0 * A[2] + x0 * x0 * x0 * A[3] + x0 * x0 * x0 * x0 * A[4]);
+
+          delta[cell_id] = l2Norm((transpose(Diff) * Diff) * TinyVector<3>(1, 1, 1));
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+
+    SECTION("2D")
+    {
+      constexpr size_t Dimension = 2;
+
+      using R2   = TinyVector<2>;
+      using R2x3 = TinyMatrix<2, 3>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian2DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 3;
+
+      DiscreteFunctionDPk<Dimension, R2x3> pk(mesh_v, degree);
+
+      std::vector<R2x3> A = {R2x3{+1.0, +2.0, +3.0, +4.0, +5.0, +6.0},   //
+                             R2x3{-1.2, -2.3, +7.2, +8.4, -5.0, +0.7},   //
+                             R2x3{-2.1, -3.3, -2.7, -3.4, -0.5, -2.7},   //
+                             R2x3{+6.2, -2.9, +3.1, -2.6, +1.5, +2.1},   //
+                             R2x3{-2.6, -2.2, +4.2, -1.7, +8.5, -1.4},   //
+                             R2x3{+1.7, +3.1, +3.0, +0.4, +3.4, +4.3},   //
+                             R2x3{+2.5, +2.3, +4.7, -8.7, -5.0, +2.4},   //
+                             R2x3{-3.6, -1.3, -1.3, -4.1, -0.5, -6.2},   //
+                             R2x3{+6.4, -2.9, +2.3, -6.1, +1.5, -1.9},   //
+                             R2x3{-7.3, -3.2, +4.1, +2.7, +8.5, -6.9}};
+
+      for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < coefficients.size(); ++i) {
+          coefficients[i] = A[i];
+        }
+      };
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const R2x3 Diff   = pk[cell_id](xj[cell_id]) - A[0];
+          delta_xj[cell_id] = l2Norm((transpose(Diff) * Diff) * TinyVector<3>(1, 1, 1));
+        });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.2 * Vj[cell_id];
+          const double y0 = 0.3 * Vj[cell_id];
+          const R2x3 Diff = pk[cell_id](xj[cell_id] + R2{x0, y0})   //
+                            - (A[0]                                 //
+                               + x0 * A[1]                          //
+                               + x0 * x0 * A[2]                     //
+                               + x0 * x0 * x0 * A[3]                //
+                               + y0 * A[4]                          //
+                               + y0 * x0 * A[5]                     //
+                               + y0 * x0 * x0 * A[6]                //
+                               + y0 * y0 * A[7]                     //
+                               + y0 * y0 * x0 * A[8]                //
+                               + y0 * y0 * y0 * A[9]);
+
+          delta[cell_id] = l2Norm((transpose(Diff) * Diff) * TinyVector<3>(1, 1, 1));
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+
+    SECTION("3D")
+    {
+      constexpr size_t Dimension = 3;
+
+      using R2x3 = TinyMatrix<2, 3>;
+      using R3   = TinyVector<3>;
+
+      const std::shared_ptr mesh_v = MeshDataBaseForTests::get().hybrid3DMesh();
+      const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+      auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+      const size_t degree = 3;
+
+      const std::vector<R2x3> A = {R2x3{-3.1, +0.7, -4.2, -6.6, -2.3, +7.2}, R2x3{-5.1, -4.5, +9.6, +0.8, -3.2, +3.7},
+                                   R2x3{-2.4, +3.8, -2.5, +0.7, -6.5, -7.7}, R2x3{+3.2, +7.6, +7.8, -4.1, +1.6, +9.9},
+                                   R2x3{-0.8, +6.1, -2.4, -1.1, -4.1, +0.6}, R2x3{+6.3, +1.0, +0.0, -9.4, +4.4, +1.2},
+                                   R2x3{-1.1, +9.8, +10., -4.9, -2.4, -4.3}, R2x3{+6.9, +4.2, +8.8, +2.0, +3.4, +6.4},
+                                   R2x3{+6.5, -6.3, +7.1, +8.7, -1.9, -9.7}, R2x3{+3.3, +1.5, -8.2, +8.1, +2.9, +3.3},
+                                   R2x3{+6.9, -9.8, -2.5, -6.8, +0.9, +9.4}, R2x3{+4.3, +3.0, +2.2, -8.8, +3.0, -3.2},
+                                   R2x3{-4.7, +9.0, +2.7, -0.3, -8.1, -8.6}, R2x3{+1.0, +1.7, -3.9, +7.8, -1.2, +5.6},
+                                   R2x3{-2.1, +6.1, -8.7, +4.3, -1.9, -9.3}, R2x3{-0.3, -8.3, -9.7, +9.4, -9.7, +3.8},
+                                   R2x3{+9.2, +7.1, +9.1, -9.1, +6.8, -5.2}, R2x3{-9.1, +4.8, +5.3, +9.4, -1.2, -9.2},
+                                   R2x3{+1.3, -8.7, -1.2, +2.7, -1.8, -1.6}, R2x3{+1.0, -9.7, +1.0, +9.2, -0.1, -4.9}};
+
+      DiscreteFunctionDPk<Dimension, R2x3> pk(mesh_v, degree);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          auto coefficients = pk.coefficients(cell_id);
+          for (size_t i = 0; i < coefficients.size(); ++i) {
+            coefficients[i] = A[i];
+          }
+        });
+
+      DiscreteFunctionP0<double> delta_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const R2x3 Diff   = pk[cell_id](xj[cell_id]) - A[0];
+          delta_xj[cell_id] = l2Norm(transpose(Diff) * Diff * TinyVector<3>{1, 1, 1});
+        });
+
+      REQUIRE(max(delta_xj) == Catch::Approx(0.).margin(1E-14));
+
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = +0.5 * Vj[cell_id];
+          const double y0 = +0.3 * Vj[cell_id];
+          const double z0 = -0.4 * Vj[cell_id];
+
+          const R2x3 Diff = pk[cell_id](xj[cell_id] + R3{x0, y0, z0})   //
+                            - (A[0]                                     //
+                               + x0 * A[1]                              //
+                               + x0 * x0 * A[2]                         //
+                               + x0 * x0 * x0 * A[3]                    //
+                               + y0 * A[4]                              //
+                               + y0 * x0 * A[5]                         //
+                               + y0 * x0 * x0 * A[6]                    //
+                               + y0 * y0 * A[7]                         //
+                               + y0 * y0 * x0 * A[8]                    //
+                               + y0 * y0 * y0 * A[9]                    //
+                               + z0 * A[10]                             //
+                               + z0 * x0 * A[11]                        //
+                               + z0 * x0 * x0 * A[12]                   //
+                               + z0 * y0 * A[13]                        //
+                               + z0 * y0 * x0 * A[14]                   //
+                               + z0 * y0 * y0 * A[15]                   //
+                               + z0 * z0 * A[16]                        //
+                               + z0 * z0 * x0 * A[17]                   //
+                               + z0 * z0 * y0 * A[18]                   //
+                               + z0 * z0 * z0 * A[19]                   //
+                              );
+
+          delta[cell_id] = l2Norm(transpose(Diff) * Diff * TinyVector<3>{1, 1, 1});
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+  }
+
+  SECTION("degree and polynomial basis size")
+  {
+    SECTION("1D")
+    {
+      for (size_t degree = 0; degree < 10; ++degree) {
+        size_t polynomial_basis_dimension =
+          PolynomialCenteredCanonicalBasisView<1, double>::dimensionFromDegree(degree);
+        REQUIRE(polynomial_basis_dimension == degree + 1);
+        REQUIRE(PolynomialCenteredCanonicalBasisView<1, double>::degreeFromDimension(polynomial_basis_dimension) ==
+                degree);
+      }
+    }
+
+    SECTION("2D")
+    {
+      for (size_t degree = 0; degree < 10; ++degree) {
+        size_t polynomial_basis_dimension =
+          PolynomialCenteredCanonicalBasisView<2, double>::dimensionFromDegree(degree);
+        REQUIRE(2 * polynomial_basis_dimension == (degree + 1) * (degree + 2));
+        REQUIRE(PolynomialCenteredCanonicalBasisView<2, double>::degreeFromDimension(polynomial_basis_dimension) ==
+                degree);
+      }
+
+      REQUIRE_THROWS_WITH((PolynomialCenteredCanonicalBasisView<2, double>::degreeFromDimension(2)),
+                          "error: incorrect polynomial basis dimension");
+      REQUIRE_THROWS_WITH((PolynomialCenteredCanonicalBasisView<2, double>::degreeFromDimension(4)),
+                          "error: incorrect polynomial basis dimension");
+      REQUIRE_THROWS_WITH((PolynomialCenteredCanonicalBasisView<2, double>::degreeFromDimension(5)),
+                          "error: incorrect polynomial basis dimension");
+      REQUIRE_THROWS_WITH((PolynomialCenteredCanonicalBasisView<2, double>::degreeFromDimension(7)),
+                          "error: incorrect polynomial basis dimension");
+    }
+  }
+}
diff --git a/tests/test_DiscreteFunctionDPkVector.cpp b/tests/test_DiscreteFunctionDPkVector.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ea028072b05716d5dec5bfada758cb9539982b2
--- /dev/null
+++ b/tests/test_DiscreteFunctionDPkVector.cpp
@@ -0,0 +1,291 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/Mesh.hpp>
+#include <scheme/DiscreteFunctionDPkVector.hpp>
+#include <scheme/DiscreteFunctionP0Vector.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("DiscreteFunctionDPkVector", "[scheme]")
+{
+  SECTION("Basic interface")
+  {
+    const std::shared_ptr mesh_2d_v = MeshDataBaseForTests::get().cartesian2DMesh();
+    const std::shared_ptr mesh_2d   = mesh_2d_v->get<Mesh<2>>();
+
+    const std::shared_ptr mesh_2d_2_v = MeshDataBaseForTests::get().hybrid2DMesh();
+    const std::shared_ptr mesh_2d_2   = mesh_2d_2_v->get<Mesh<2>>();
+
+    DiscreteFunctionDPkVector<2, const double> R_dkp;
+
+#ifndef NDEBUG
+    REQUIRE_THROWS_WITH(R_dkp(CellId(0), 0), "DiscreteFunctionDPkVector is not built");
+#endif   // NDEBUG
+
+    {
+      DiscreteFunctionDPkVector<2, double> tmp0{mesh_2d_v, 2, 3};
+      DiscreteFunctionDPkVector<2, double> tmp_R_dkp(std::move(tmp0));
+      tmp_R_dkp.fill(2);
+      R_dkp = tmp_R_dkp;
+    }
+
+#ifndef NDEBUG
+    REQUIRE_THROWS_WITH(R_dkp(CellId(0), 10), "incorrect component number");
+#endif   // NDEBUG
+
+    REQUIRE(R_dkp.degree() == 2);
+    REQUIRE(R_dkp.numberOfComponents() == 3);
+    REQUIRE(R_dkp.numberOfCoefficientsPerComponent() ==
+            PolynomialCenteredCanonicalBasisView<2, double>::dimensionFromDegree(2));
+
+    REQUIRE(min(R_dkp.cellArrays()) == 2);
+    REQUIRE(max(R_dkp.cellArrays()) == 2);
+
+    REQUIRE(R_dkp.dataType() == ast_node_data_type_from<double>);
+    REQUIRE(R_dkp.meshVariant()->id() == mesh_2d->id());
+
+    DiscreteFunctionDPkVector<2, double> R_dkp2;
+    R_dkp2 = copy(R_dkp);
+
+    REQUIRE(R_dkp2.degree() == 2);
+    REQUIRE(R_dkp2.numberOfComponents() == 3);
+    REQUIRE(R_dkp2.numberOfCoefficientsPerComponent() ==
+            PolynomialCenteredCanonicalBasisView<2, double>::dimensionFromDegree(2));
+
+    REQUIRE(min(R_dkp2.cellArrays()) == 2);
+    REQUIRE(max(R_dkp2.cellArrays()) == 2);
+
+    REQUIRE(R_dkp2.dataType() == ast_node_data_type_from<double>);
+    REQUIRE(R_dkp2.meshVariant()->id() == mesh_2d_v->id());
+
+    DiscreteFunctionDPkVector<2, double> R_dkp3(mesh_2d_2_v, 1, 2);
+    R_dkp3.fill(3);
+
+    REQUIRE(R_dkp3.degree() == 1);
+    REQUIRE(R_dkp3.numberOfComponents() == 2);
+
+    REQUIRE(min(R_dkp3.cellArrays()) == 3);
+    REQUIRE(max(R_dkp3.cellArrays()) == 3);
+
+    REQUIRE(R_dkp3.dataType() == ast_node_data_type_from<double>);
+    REQUIRE(R_dkp3.meshVariant()->id() == mesh_2d_2_v->id());
+
+    DiscreteFunctionDPkVector<2, double> R_dkp4(mesh_2d_v, R_dkp2.degree(), R_dkp2.numberOfComponents(),
+                                                R_dkp2.cellArrays());
+
+    REQUIRE(min(R_dkp4.cellArrays()) == 2);
+    REQUIRE(max(R_dkp4.cellArrays()) == 2);
+
+    R_dkp4.fill(5);
+    REQUIRE(min(R_dkp4.cellArrays()) == 5);
+    REQUIRE(max(R_dkp4.cellArrays()) == 5);
+    REQUIRE(min(R_dkp2.cellArrays()) == 5);
+    REQUIRE(max(R_dkp2.cellArrays()) == 5);
+
+    copy_to(R_dkp, R_dkp4);
+    REQUIRE(min(R_dkp4.cellArrays()) == 2);
+    REQUIRE(max(R_dkp4.cellArrays()) == 2);
+    REQUIRE(min(R_dkp2.cellArrays()) == 2);
+    REQUIRE(max(R_dkp2.cellArrays()) == 2);
+
+#ifndef NDEBUG
+    REQUIRE_THROWS_WITH(copy_to(R_dkp, R_dkp3), "copy_to target must use the same mesh");
+
+    DiscreteFunctionDPkVector<2, double> R_dkp5(mesh_2d, 3, 3);
+    REQUIRE_THROWS_WITH(copy_to(R_dkp, R_dkp5), "copy_to target must have the same degree");
+    R_dkp5 = DiscreteFunctionDPkVector<2, double>{mesh_2d, 2, 1};
+    REQUIRE_THROWS_WITH(copy_to(R_dkp, R_dkp5), "copy_to target must have the same number of components");
+
+    REQUIRE_THROWS_WITH((DiscreteFunctionDPkVector<2, double>{mesh_2d_2, 2, 3, R_dkp2.cellArrays()}),
+                        "cell_array is built on different connectivity");
+#endif   // NDEBUG
+  }
+
+  SECTION("1D")
+  {
+    constexpr size_t Dimension = 1;
+
+    using R1 = TinyVector<1>;
+
+    const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian1DMesh();
+    const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+    auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+    auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+    const size_t degree = 4;
+
+    DiscreteFunctionDPkVector<Dimension, double> pk(mesh_v, degree, 3);
+
+    std::vector<double> a = {+1.0, +1.4, -6.2, +2.7, +3.1,   //
+                             +2.0, -2.3, +5.4, -2.7, -1.3,   //
+                             -1.2, +2.3, +3.1, +1.6, +2.3};
+
+    for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+      auto coefficients = pk.coefficients(cell_id);
+      for (size_t i = 0; i < a.size(); ++i) {
+        coefficients[i] = a[i];
+      }
+    };
+
+    const size_t number_of_coefs = pk.numberOfCoefficientsPerComponent();
+
+    for (size_t i = 0; i < pk.numberOfComponents(); ++i) {
+      DiscreteFunctionP0<double> p_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { p_xj[cell_id] = pk(cell_id, i)(xj[cell_id]); });
+
+      REQUIRE(max(p_xj) == a[i * number_of_coefs]);
+      REQUIRE(min(p_xj) == a[i * number_of_coefs]);
+    }
+
+    for (size_t i = 0; i < pk.numberOfComponents(); ++i) {
+      DiscreteFunctionP0<double> delta(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.3 * Vj[cell_id];
+          delta[cell_id]  = pk(cell_id, i)(xj[cell_id] + R1{x0})            //
+                           - (a[i * number_of_coefs] +                      //
+                              x0 * a[i * number_of_coefs + 1] +             //
+                              x0 * x0 * a[i * number_of_coefs + 2] +        //
+                              x0 * x0 * x0 * a[i * number_of_coefs + 3] +   //
+                              x0 * x0 * x0 * x0 * a[i * number_of_coefs + 4]);
+        });
+
+      REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+      REQUIRE(min(delta) == Catch::Approx(0.).margin(1E-14));
+    }
+  }
+
+  SECTION("2D")
+  {
+    constexpr size_t Dimension = 2;
+
+    using R2 = TinyVector<2>;
+
+    const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian2DMesh();
+    const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+    auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+    auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+    const size_t degree = 3;
+
+    DiscreteFunctionDPkVector<Dimension, double> pk(mesh_v, degree, 2);
+
+    const std::vector<double> a = {+1.0, +1.4, -6.2, +3.5, -2.3, +5.2, +6.1, +2.3, +0.5, -1.3,   //
+                                   -1.3, +2.3, +2.7, +1.7, +2.1, -2.7, -5.3, +1.2, +1.3, +3.2};
+
+    for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+      auto coefficients = pk.coefficients(cell_id);
+      for (size_t i = 0; i < coefficients.size(); ++i) {
+        coefficients[i] = a[i];
+      }
+    }
+
+    const size_t number_of_coefs = pk.numberOfCoefficientsPerComponent();
+
+    for (size_t i = 0; i < pk.numberOfComponents(); ++i) {
+      DiscreteFunctionP0<double> p_xj(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) { p_xj[cell_id] = pk(cell_id, i)(xj[cell_id]); });
+
+      REQUIRE(max(p_xj) == a[i * number_of_coefs]);
+      REQUIRE(min(p_xj) == a[i * number_of_coefs]);
+    }
+
+    for (size_t i = 0; i < pk.numberOfComponents(); ++i) {
+      DiscreteFunctionP0<double> error(mesh_v);
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+          const double x0 = 0.2 * Vj[cell_id];
+          const double y0 = 0.3 * Vj[cell_id];
+          error[cell_id]                                                //
+            = std::abs(pk(cell_id, i)(xj[cell_id] + R2{x0, y0})         //
+                       - (a[i * number_of_coefs]                        //
+                          + x0 * a[i * number_of_coefs + 1]             //
+                          + x0 * x0 * a[i * number_of_coefs + 2]        //
+                          + x0 * x0 * x0 * a[i * number_of_coefs + 3]   //
+                          + y0 * a[i * number_of_coefs + 4]             //
+                          + y0 * x0 * a[i * number_of_coefs + 5]        //
+                          + y0 * x0 * x0 * a[i * number_of_coefs + 6]   //
+                          + y0 * y0 * a[i * number_of_coefs + 7]        //
+                          + y0 * y0 * x0 * a[i * number_of_coefs + 8]   //
+                          + y0 * y0 * y0 * a[i * number_of_coefs + 9]));
+        });
+
+      REQUIRE(max(error) == Catch::Approx(0.).margin(1E-14));
+    }
+  }
+
+  SECTION("3D")
+  {
+    constexpr size_t Dimension = 3;
+
+    using R3 = TinyVector<3>;
+
+    const std::shared_ptr mesh_v = MeshDataBaseForTests::get().cartesian3DMesh();
+    const std::shared_ptr mesh   = mesh_v->get<Mesh<Dimension>>();
+
+    auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+    auto Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+    const size_t degree = 3;
+
+    DiscreteFunctionDPkVector<Dimension, double> pk(mesh_v, degree, 1);
+
+    const std::vector<double> a = {+1.0, +1.4, -6.2, +3.5, -2.3, +5.2, +6.1, +2.3, +0.5, -1.3,   //
+                                   +2.8, -8.4, +9.5, +4.0, +4.3, +7.2, -9.1, +6.8, +6.7, +9.2};
+
+    parallel_for(
+      mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+        auto coefficients = pk.coefficients(cell_id);
+        for (size_t i = 0; i < coefficients.size(); ++i) {
+          coefficients[i] = a[i];
+        }
+      });
+
+    DiscreteFunctionP0<double> delta_xj(mesh_v);
+    parallel_for(
+      mesh->numberOfCells(),
+      PUGS_LAMBDA(CellId cell_id) { delta_xj[cell_id] = std::abs(pk(cell_id, 0)(xj[cell_id]) - a[0]); });
+
+    REQUIRE(max(delta_xj) == Catch::Approx(0.).margin(1E-14));
+
+    DiscreteFunctionP0<double> delta(mesh_v);
+    parallel_for(
+      mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
+        const double x0 = +0.5 * Vj[cell_id];
+        const double y0 = +0.3 * Vj[cell_id];
+        const double z0 = -0.4 * Vj[cell_id];
+
+        delta[cell_id] = pk(cell_id, 0)(xj[cell_id] + R3{x0, y0, z0})   //
+                         - (a[0]                                        //
+                            + x0 * a[1]                                 //
+                            + x0 * x0 * a[2]                            //
+                            + x0 * x0 * x0 * a[3]                       //
+                            + y0 * a[4]                                 //
+                            + y0 * x0 * a[5]                            //
+                            + y0 * x0 * x0 * a[6]                       //
+                            + y0 * y0 * a[7]                            //
+                            + y0 * y0 * x0 * a[8]                       //
+                            + y0 * y0 * y0 * a[9]                       //
+                            + z0 * a[10]                                //
+                            + z0 * x0 * a[11]                           //
+                            + z0 * x0 * x0 * a[12]                      //
+                            + z0 * y0 * a[13]                           //
+                            + z0 * y0 * x0 * a[14]                      //
+                            + z0 * y0 * y0 * a[15]                      //
+                            + z0 * z0 * a[16]                           //
+                            + z0 * z0 * x0 * a[17]                      //
+                            + z0 * z0 * y0 * a[18]                      //
+                            + z0 * z0 * z0 * a[19]                      //
+                           );
+      });
+
+    REQUIRE(max(delta) == Catch::Approx(0.).margin(1E-14));
+    REQUIRE(min(delta) == Catch::Approx(0.).margin(1E-14));
+  }
+}
diff --git a/tests/test_Givens.cpp b/tests/test_Givens.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..aec60a82b5b944706eb5ec1e0ab02dac4d8c407f
--- /dev/null
+++ b/tests/test_Givens.cpp
@@ -0,0 +1,216 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <algebra/Givens.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <algebra/SmallVector.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("Givens", "[algebra]")
+{
+  SECTION("classic (single vector)")
+  {
+    SECTION("square matrix")
+    {
+      SmallMatrix<double> A{5, 5};
+      A.fill(0);
+      A(0, 0) = 2;
+      A(0, 1) = -1;
+
+      A(1, 0) = -0.2;
+      A(1, 1) = 2;
+      A(1, 2) = -1;
+
+      A(2, 1) = -1;
+      A(2, 2) = 4;
+      A(2, 3) = -2;
+
+      A(3, 2) = -1;
+      A(3, 3) = 2;
+      A(3, 4) = -0.1;
+
+      A(4, 3) = 1;
+      A(4, 4) = 3;
+
+      SmallVector<const double> x_exact = [] {
+        SmallVector<double> y{5};
+        y[0] = 1;
+        y[1] = 3;
+        y[2] = 2;
+        y[3] = 4;
+        y[4] = 5;
+        return y;
+      }();
+
+      SmallVector<double> b = A * x_exact;
+
+      SmallVector<double> x{5};
+      x = zero;
+
+      Givens::solve(A, x, b);
+      SmallVector<double> error = x - x_exact;
+
+      REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x, x)));
+    }
+
+    SECTION("rectangular matrix")
+    {
+      SmallMatrix<double> A{5, 3};
+      A.fill(0);
+      A(0, 0) = 2;
+      A(0, 1) = -1;
+
+      A(1, 0) = -0.2;
+      A(1, 1) = 2;
+      A(1, 2) = -1;
+
+      A(2, 1) = -1;
+      A(2, 2) = 4;
+
+      A(3, 2) = -1;
+      A(4, 0) = 0.5;
+      A(4, 1) = 1;
+      A(4, 2) = 1;
+
+      SmallVector<const double> x_exact = [] {
+        SmallVector<double> y{3};
+        y[0] = 1;
+        y[1] = 3;
+        y[2] = 2;
+        return y;
+      }();
+
+      SmallVector<double> b = A * x_exact;
+
+      SmallVector<double> x{3};
+      x = zero;
+
+      Givens::solve(A, x, b);
+      SmallVector<double> error = x - x_exact;
+
+      REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x, x)));
+    }
+  }
+
+  SECTION("generalized (vector collection)")
+  {
+    SECTION("square matrix")
+    {
+      SmallMatrix<double> A{5, 5};
+      A.fill(0);
+      A(0, 0) = 2;
+      A(0, 1) = -1;
+
+      A(1, 0) = -0.2;
+      A(1, 1) = 2;
+      A(1, 2) = -1;
+
+      A(2, 1) = -1;
+      A(2, 2) = 4;
+      A(2, 3) = -2;
+
+      A(3, 2) = -1;
+      A(3, 3) = 2;
+      A(3, 4) = -0.1;
+
+      A(4, 3) = 1;
+      A(4, 4) = 3;
+
+      SmallMatrix<const double> X_exact = [] {
+        SmallMatrix<double> Y{5, 3};
+        Y(0, 0) = 1;
+        Y(1, 0) = 3;
+        Y(2, 0) = 2;
+        Y(3, 0) = 4;
+        Y(4, 0) = 5;
+
+        Y(0, 1) = -3;
+        Y(1, 1) = 6;
+        Y(2, 1) = 1;
+        Y(3, 1) = -2;
+        Y(4, 1) = 4;
+
+        Y(0, 2) = -2;
+        Y(1, 2) = -4;
+        Y(2, 2) = 2;
+        Y(3, 2) = 6;
+        Y(4, 2) = 7;
+
+        return Y;
+      }();
+
+      SmallMatrix<double> b = A * X_exact;
+
+      SmallMatrix<double> X{5, 3};
+
+      Givens::solveCollection(A, X, b);
+      SmallMatrix<double> error = X - X_exact;
+
+      double max_error = 0;
+      for (size_t i = 0; i < error.numberOfRows(); ++i) {
+        for (size_t j = 0; j < error.numberOfColumns(); ++j) {
+          max_error = std::max(max_error, std::abs(error(i, j)));
+        }
+      }
+      REQUIRE(max_error < 1E-10);
+    }
+
+    SECTION("rectangular matrix")
+    {
+      SmallMatrix<double> A{5, 3};
+      A.fill(0);
+      A(0, 0) = 2;
+      A(0, 1) = -1;
+
+      A(1, 0) = -0.2;
+      A(1, 1) = 2;
+      A(1, 2) = -1;
+
+      A(2, 1) = -1;
+      A(2, 2) = 4;
+
+      A(3, 2) = -1;
+      A(4, 0) = 0.5;
+      A(4, 1) = 1;
+      A(4, 2) = 1;
+
+      SmallMatrix<const double> X_exact = [] {
+        SmallMatrix<double> Y{3, 4};
+        Y(0, 0) = 1;
+        Y(1, 0) = 3;
+        Y(2, 0) = 2;
+
+        Y(0, 1) = -1;
+        Y(1, 1) = 1.5;
+        Y(2, 1) = 5;
+
+        Y(0, 2) = -3;
+        Y(1, 2) = 1;
+        Y(2, 2) = 4;
+
+        Y(0, 3) = 0.7;
+        Y(1, 3) = -3.2;
+        Y(2, 3) = 2.5;
+
+        return Y;
+      }();
+
+      SmallMatrix<double> B = A * X_exact;
+
+      SmallMatrix<double> X{3, 4};
+      X = zero;
+
+      Givens::solveCollection(A, X, B);
+      SmallMatrix<double> error = X - X_exact;
+
+      double max_error = 0;
+      for (size_t i = 0; i < error.numberOfRows(); ++i) {
+        for (size_t j = 0; j < error.numberOfColumns(); ++j) {
+          max_error = std::max(max_error, std::abs(error(i, j)));
+        }
+      }
+      REQUIRE(max_error < 1E-10);
+    }
+  }
+}
diff --git a/tests/test_PolynomialReconstructionDescriptor.cpp b/tests/test_PolynomialReconstructionDescriptor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..32d3c4af12c48d6c7f69b2b250838bea7e3cf19e
--- /dev/null
+++ b/tests/test_PolynomialReconstructionDescriptor.cpp
@@ -0,0 +1,138 @@
+#include <catch2/catch_approx.hpp>
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+#include <scheme/PolynomialReconstructionDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("PolynomialReconstructionDescriptor", "[scheme]")
+{
+  SECTION("degree 1")
+  {
+    PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::cell_center, 1};
+
+    REQUIRE(descriptor.degree() == 1);
+    REQUIRE(descriptor.stencilDescriptor().numberOfLayers() == 1);
+    REQUIRE(descriptor.stencilDescriptor().connectionType() == StencilDescriptor::ConnectionType::by_nodes);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList().size() == 0);
+
+    REQUIRE(descriptor.preconditioning() == true);
+    REQUIRE(descriptor.rowWeighting() == true);
+  }
+
+  SECTION("degree 4")
+  {
+    PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::cell_center, 4};
+
+    REQUIRE(descriptor.degree() == 4);
+    REQUIRE(descriptor.stencilDescriptor().numberOfLayers() == 4);
+    REQUIRE(descriptor.stencilDescriptor().connectionType() == StencilDescriptor::ConnectionType::by_nodes);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList().size() == 0);
+
+    REQUIRE(descriptor.preconditioning() == true);
+    REQUIRE(descriptor.rowWeighting() == true);
+  }
+
+  SECTION("degree and stencil")
+  {
+    StencilDescriptor sd{2, StencilDescriptor::ConnectionType::by_faces};
+
+    PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::cell_center, 1, sd};
+
+    REQUIRE(descriptor.degree() == 1);
+    REQUIRE(descriptor.stencilDescriptor().numberOfLayers() == 2);
+    REQUIRE(descriptor.stencilDescriptor().connectionType() == StencilDescriptor::ConnectionType::by_faces);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList().size() == 0);
+
+    REQUIRE(descriptor.preconditioning() == true);
+    REQUIRE(descriptor.rowWeighting() == true);
+  }
+
+  SECTION("degree and symmetries")
+  {
+    std::vector<std::shared_ptr<const IBoundaryDescriptor>> bc_list;
+    bc_list.push_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+    bc_list.push_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+    bc_list.push_back(std::make_shared<NumberedBoundaryDescriptor>(2));
+
+    PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::cell_center, 2, bc_list};
+
+    REQUIRE(descriptor.degree() == 2);
+    REQUIRE(descriptor.stencilDescriptor().numberOfLayers() == 2);
+    REQUIRE(descriptor.stencilDescriptor().connectionType() == StencilDescriptor::ConnectionType::by_nodes);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList().size() == 3);
+
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList()[0]->type() == IBoundaryDescriptor::Type::named);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList()[1]->type() == IBoundaryDescriptor::Type::named);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList()[2]->type() == IBoundaryDescriptor::Type::numbered);
+
+    REQUIRE(descriptor.preconditioning() == true);
+    REQUIRE(descriptor.rowWeighting() == true);
+  }
+
+  SECTION("degree, stencil and symmetries")
+  {
+    StencilDescriptor sd{3, StencilDescriptor::ConnectionType::by_edges};
+
+    std::vector<std::shared_ptr<const IBoundaryDescriptor>> bc_list;
+    bc_list.push_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+    bc_list.push_back(std::make_shared<NumberedBoundaryDescriptor>(2));
+    bc_list.push_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+
+    PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::cell_center, 1, sd, bc_list};
+
+    REQUIRE(descriptor.degree() == 1);
+    REQUIRE(descriptor.stencilDescriptor().numberOfLayers() == 3);
+    REQUIRE(descriptor.stencilDescriptor().connectionType() == StencilDescriptor::ConnectionType::by_edges);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList().size() == 3);
+
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList()[0]->type() == IBoundaryDescriptor::Type::named);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList()[1]->type() == IBoundaryDescriptor::Type::numbered);
+    REQUIRE(descriptor.symmetryBoundaryDescriptorList()[2]->type() == IBoundaryDescriptor::Type::named);
+
+    REQUIRE(descriptor.preconditioning() == true);
+    REQUIRE(descriptor.rowWeighting() == true);
+  }
+
+  SECTION("utlities")
+  {
+    PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::cell_center, 3};
+
+    REQUIRE(descriptor.degree() == 3);
+    REQUIRE(descriptor.preconditioning() == true);
+    REQUIRE(descriptor.rowWeighting() == true);
+
+    SECTION("set preconditioning")
+    {
+      descriptor.setPreconditioning(false);
+
+      REQUIRE(descriptor.degree() == 3);
+      REQUIRE(descriptor.preconditioning() == false);
+      REQUIRE(descriptor.rowWeighting() == true);
+
+      descriptor.setPreconditioning(true);
+
+      REQUIRE(descriptor.degree() == 3);
+      REQUIRE(descriptor.preconditioning() == true);
+      REQUIRE(descriptor.rowWeighting() == true);
+    }
+
+    SECTION("set row weighting")
+    {
+      descriptor.setRowWeighting(false);
+
+      REQUIRE(descriptor.degree() == 3);
+      REQUIRE(descriptor.preconditioning() == true);
+      REQUIRE(descriptor.rowWeighting() == false);
+
+      descriptor.setRowWeighting(true);
+
+      REQUIRE(descriptor.degree() == 3);
+      REQUIRE(descriptor.preconditioning() == true);
+      REQUIRE(descriptor.rowWeighting() == true);
+    }
+  }
+}
diff --git a/tests/test_PolynomialReconstruction_degree_1.cpp b/tests/test_PolynomialReconstruction_degree_1.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fb2c7b2ec4a56239e37314704b2cc30122182e8c
--- /dev/null
+++ b/tests/test_PolynomialReconstruction_degree_1.cpp
@@ -0,0 +1,800 @@
+#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>
+
+// 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, degree},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::element, degree},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, degree}};
+
+    for (auto descriptor : descriptor_list) {
+      SECTION(name(descriptor.integrationMethodType()))
+      {
+        SECTION("1D")
+        {
+          using R1 = TinyVector<1>;
+
+          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 = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
+
+                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{+2.3 + 1.7 * x[0],   //
+                            +1.4 - 0.6 * x[0],   //
+                            -0.2 + 3.1 * x[0]};
+                };
+
+                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{
+                    +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],
+                  };
+                };
+
+                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 =
+                  {[](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]; }};
+
+                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&)>, 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]};
+                   }};
+
+                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 = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
+
+                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_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],
+                  };
+                };
+
+                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 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>;
+
+          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 = [](const R2& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1]; };
+
+                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-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_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]};
+                };
+
+                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-14));
+              }
+            }
+          }
+
+          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{+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]};
+                };
+
+                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-14));
+              }
+            }
+          }
+
+          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 =
+                  {[](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]; }};
+
+                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-14));
+              }
+            }
+          }
+        }
+
+        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 R_exact = [](const R3& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]; };
+
+                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{+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]};
+                };
+
+                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{+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]};
+                };
+
+                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 =
+                  {[](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]; }};
+
+                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("errors")
+        {
+          auto p_mesh1 = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+          DiscreteFunctionP0<double> f1{p_mesh1};
+
+          auto p_mesh2 = MeshDataBaseForTests::get().cartesian1DMesh()->get<Mesh<1>>();
+          DiscreteFunctionP0<double> f2{p_mesh2};
+
+          REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(f1, f2),
+                              "error: cannot reconstruct functions living of different meshes simultaneously");
+        }
+      }
+    }
+  }
+
+  SECTION("with symmetries")
+  {
+    SECTION("errors")
+    {
+      SECTION("1D")
+      {
+        auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, degree,
+                                                      std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                        std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<2>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<3>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<2>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<3>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 1");
+      }
+
+      SECTION("2D")
+      {
+        auto p_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, degree,
+                                                      std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                        std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<1>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<3>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<1>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<3>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 2");
+      }
+
+      SECTION("3D")
+      {
+        auto p_mesh = MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, degree,
+                                                      std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                        std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<1>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<2>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<1>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<2>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 3");
+      }
+    }
+
+    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()))
+        {
+          SECTION("R^1 data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+            auto& mesh = *p_mesh;
+
+            auto R1_exact = [](const R1& x) { return R1{1.7 * (x[0] + 1)}; };
+
+            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")
+          {
+            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<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)}; }};
+
+                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::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()))
+        {
+          SECTION("R^2 data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+            auto& mesh  = *p_mesh;
+
+            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")
+    {
+      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()))
+        {
+          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..db13612c1d8c5efad94a6f890251e366826d014b
--- /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-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-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-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-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-12));
+                }
+              }
+            }
+          }
+        }
+
+        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));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_QuadraticPolynomialReconstruction.cpp b/tests/test_QuadraticPolynomialReconstruction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c9b1e722294627e19ce2d5cadcfbcb067ef7c881
--- /dev/null
+++ b/tests/test_QuadraticPolynomialReconstruction.cpp
@@ -0,0 +1,1041 @@
+#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")
+  {
+    SECTION("1D")
+    {
+      PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 2};
+      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;
+      };
+
+      std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+        {PolynomialReconstructionDescriptor{IntegrationMethodType::element, 2},
+         PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, 2}};
+
+      for (auto descriptor : descriptor_list) {
+        SECTION(name(descriptor.integrationMethodType()))
+        {
+          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("3D")
+    {
+      PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 2};
+
+      using R3 = TinyVector<3>;
+
+      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::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) * T.jacobianDeterminant() * exact(x);
+          }
+          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) * T.jacobianDeterminant(x_hat) * exact(x);
+          }
+          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) * T.jacobianDeterminant(x_hat) * exact(x);
+          }
+          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) * 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::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);
+              DataType diff  = qf.weight(i_q) * T.jacobianDeterminant() * (exact(x) - dpkj(x));
+              error += sum_abs(diff);
+            }
+            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);
+              DataType diff  = qf.weight(i_q) * T.jacobianDeterminant(x_hat) * (exact(x) - dpkj(x));
+              error += sum_abs(diff);
+            }
+            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);
+              DataType diff  = qf.weight(i_q) * T.jacobianDeterminant(x_hat) * (exact(x) - dpkj(x));
+              error += sum_abs(diff);
+            }
+            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);
+              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().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];
+              const double x2 = x * x;
+              const double xy = x * y;
+              const double xz = x * z;
+              const double y2 = y * y;
+              const double yz = y * z;
+              const double z2 = z * z;
+
+              return 2.3 + 1.7 * x + 0.2 * y + 1.4 * z - 3.2 * x2 + 1.3 * xy - 1.6 * xz - 1.4 * y2 + 2 * yz - 1.8 * z2;
+            };
+
+            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<3, 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")
+      {
+        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 u_exact = [](const R3& X) -> R3 {
+              const double x  = X[0];
+              const double y  = X[1];
+              const double z  = X[2];
+              const double x2 = x * x;
+              const double xy = x * y;
+              const double xz = x * z;
+              const double y2 = y * y;
+              const double yz = y * z;
+              const double z2 = z * z;
+
+              return R3{+2.3 + 1.7 * x - 2.2 * y + 1.8 * z                                     //
+                          + 0.3 * x2 - 1.3 * xy + 2.7 * xz + 0.7 * y2 + 2.0 * yz - 1.2 * z2,   //
+                        +1.4 - 0.6 * x + 1.3 * y - 3.7 * z                                     //
+                          + 3.1 * x2 - 2.4 * xy - 1.5 * xz - 1.9 * y2 + 0.2 * yz + 0.9 * z2,   //
+                        -0.2 + 3.1 * x - 1.1 * y + 1.9 * z                                     //
+                          - 1.3 * x2 + 2.8 * xy - 2.1 * xz + 3.2 * y2 - 2.1 * yz + 1.6 * z2};
+            };
+
+            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<3, 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().all3DMeshes()) {
+          SECTION(named_mesh.name())
+          {
+            auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+            auto& mesh  = *p_mesh;
+
+            auto A_exact = [](const R3& X) -> R2x2 {
+              const double x  = X[0];
+              const double y  = X[1];
+              const double z  = X[2];
+              const double x2 = x * x;
+              const double xy = x * y;
+              const double xz = x * z;
+              const double y2 = y * y;
+              const double yz = y * z;
+              const double z2 = z * z;
+
+              return R2x2{+2.3 + 1.7 * x - 2.2 * y + 1.8 * z                                     //
+                            + 0.3 * x2 - 1.3 * xy + 2.7 * xz + 0.7 * y2 + 2.0 * yz - 1.2 * z2,   //
+                          +1.4 - 0.6 * x + 1.3 * y - 3.7 * z                                     //
+                            + 3.1 * x2 - 2.4 * xy - 1.5 * xz - 1.9 * y2 + 0.2 * yz + 0.9 * z2,   //
+                                                                                                 //
+                          -0.2 + 3.1 * x - 1.1 * y + 1.9 * z                                     //
+                            - 1.3 * x2 + 2.8 * xy - 2.1 * xz + 3.2 * y2 - 2.1 * yz + 1.6 * z2,   //
+                          +0.9 - 1.3 * x + 2.1 * y + 3.1 * z                                     //
+                            + 1.1 * x2 + 1.8 * xy + 1.2 * xz - 2.3 * y2 + 3.3 * yz - 1.2 * z2};
+            };
+
+            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;
+              });
+
+            descriptor.setRowWeighting(false);
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+            auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_StencilBuilder_cell2cell.cpp b/tests/test_StencilBuilder_cell2cell.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..42ceb6a5da583a9869035db09f5239d008c6f780
--- /dev/null
+++ b/tests/test_StencilBuilder_cell2cell.cpp
@@ -0,0 +1,1081 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/CartesianMeshBuilder.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ConnectivityUtils.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/ItemValueUtils.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFaceBoundary.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/StencilManager.hpp>
+#include <utils/Messenger.hpp>
+
+#include <NbGhostLayersTester.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("StencilBuilder cell2cell", "[mesh]")
+{
+  auto is_valid = []<ItemType connecting_item_type>(const auto& connectivity, const auto& stencil_array,
+                                                    const size_t number_of_layers) {
+    auto cell_to_connecting_item_matrix =
+      connectivity.template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+    auto connecting_to_cell_matrix = connectivity.template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+    auto cell_is_owned             = connectivity.cellIsOwned();
+    auto cell_number               = connectivity.cellNumber();
+
+    using ConnectingItemId = ItemIdT<connecting_item_type>;
+
+    for (CellId cell_id = 0; cell_id < connectivity.numberOfCells(); ++cell_id) {
+      if (cell_is_owned[cell_id]) {
+        std::vector<CellId> expected_stencil;
+
+        std::set<CellId> marked_cell_set;
+        marked_cell_set.insert(cell_id);
+
+        std::set<ConnectingItemId> marked_connecting_item_set;
+        std::set<ConnectingItemId> layer_connecting_item_set;
+        {
+          auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[cell_id];
+          for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+               ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+            layer_connecting_item_set.insert(connecting_item_id);
+            marked_connecting_item_set.insert(connecting_item_id);
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          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 (auto&& connecting_item_id : layer_connecting_item_set) {
+            auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+            for (size_t i_connecting_item_of_cell = 0; i_connecting_item_of_cell < connecting_item_to_cell_list.size();
+                 ++i_connecting_item_of_cell) {
+              const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+              if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                cell_set.insert(connecting_item_id_of_cell);
+                marked_cell_set.insert(connecting_item_id_of_cell);
+              }
+            }
+          }
+
+          layer_connecting_item_set.clear();
+          for (auto layer_cell_id : cell_set) {
+            auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+              if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+          }
+
+          for (auto&& set_cell_id : cell_set) {
+            expected_stencil.push_back(set_cell_id);
+          }
+        }
+
+        auto cell_stencil = stencil_array[cell_id];
+
+        auto i_set_cell = expected_stencil.begin();
+        if (cell_stencil.size() != expected_stencil.size()) {
+          return false;
+        }
+
+        for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+          if (*i_set_cell != cell_stencil[index]) {
+            return false;
+          }
+        }
+      }
+    }
+    return true;
+  };
+
+  SECTION("inner stencil")
+  {
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+    }
+  }
+
+  SECTION("Stencil using symmetries")
+  {
+    auto check_ghost_cells_have_empty_stencils = [](const auto& stencil_array, const auto& connecticity) {
+      bool is_empty = true;
+
+      auto cell_is_owned = connecticity.cellIsOwned();
+
+      for (CellId cell_id = 0; cell_id < connecticity.numberOfCells(); ++cell_id) {
+        if (not cell_is_owned[cell_id]) {
+          is_empty &= (stencil_array[cell_id].size() == 0);
+          for (size_t i_symmetry_stencil = 0;
+               i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry_stencil) {
+            is_empty &=
+              (stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray()[cell_id].size() ==
+               0);
+          }
+        }
+      }
+
+      return is_empty;
+    };
+
+    auto are_symmetry_stencils_valid = []<ItemType connecting_item_type>(const auto& stencil_array, const auto& mesh,
+                                                                         const size_t number_of_layers) {
+      bool are_valid_symmetries = true;
+
+      auto connecting_to_cell_matrix =
+        mesh.connectivity().template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+      auto cell_to_connecting_item_matrix =
+        mesh.connectivity().template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+      auto cell_is_owned = mesh.connectivity().cellIsOwned();
+      auto cell_number   = mesh.connectivity().cellNumber();
+
+      auto connecting_number = mesh.connectivity().template number<connecting_item_type>();
+
+      using ConnectingItemId = ItemIdT<connecting_item_type>;
+      using MeshType         = std::decay_t<decltype(mesh)>;
+
+      for (size_t i_symmetry_stencil = 0; i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size();
+           ++i_symmetry_stencil) {
+        const IBoundaryDescriptor& boundary_descriptor =
+          stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].boundaryDescriptor();
+
+        auto boundary_stencil   = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray();
+        auto boundary_face_list = getMeshFaceBoundary(mesh, boundary_descriptor);
+
+        std::set<ConnectingItemId> sym_connecting_item_set;
+
+        if constexpr (ItemTypeId<MeshType::Dimension>::itemTId(connecting_item_type) ==
+                      ItemTypeId<MeshType::Dimension>::itemTId(ItemType::face)) {
+          for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+            const FaceId face_id = boundary_face_list.faceList()[i_face];
+            sym_connecting_item_set.insert(ConnectingItemId{FaceId::base_type{face_id}});
+          }
+
+        } else {
+          auto face_to_connecting_item_matrix =
+            mesh.connectivity().template getItemToItemMatrix<ItemType::face, connecting_item_type>();
+
+          for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+            const FaceId face_id      = boundary_face_list.faceList()[i_face];
+            auto connecting_item_list = face_to_connecting_item_matrix[face_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < connecting_item_list.size(); ++i_connecting_item) {
+              sym_connecting_item_set.insert(connecting_item_list[i_connecting_item]);
+            }
+          }
+        }
+
+        for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          if (not cell_is_owned[cell_id]) {
+            are_valid_symmetries &= (boundary_stencil[cell_id].size() == 0);
+          } else {
+            std::vector<CellId> expected_stencil;
+
+            std::set<CellId> marked_cell_set;
+            marked_cell_set.insert(cell_id);
+
+            std::set<ConnectingItemId> marked_connecting_item_set;
+            std::set<ConnectingItemId> layer_connecting_item_set;
+            {
+              auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[cell_id];
+              for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                   ++i_connecting_item) {
+                const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+
+            std::set<ConnectingItemId> marked_sym_connecting_item_set;
+            std::set<ConnectingItemId> layer_sym_connecting_item_set;
+
+            for (auto&& connecting_item_id : marked_connecting_item_set) {
+              if (sym_connecting_item_set.contains(connecting_item_id)) {
+                marked_sym_connecting_item_set.insert(connecting_item_id);
+                layer_sym_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+
+            std::set<CellId> marked_sym_cell_set;
+
+            for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+              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 (auto&& connecting_item_id : layer_connecting_item_set) {
+                auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                for (size_t i_connecting_item_of_cell = 0;
+                     i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                  const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                  if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                    cell_set.insert(connecting_item_id_of_cell);
+                    marked_cell_set.insert(connecting_item_id_of_cell);
+                  }
+                }
+              }
+
+              std::set<CellId, std::function<bool(CellId, CellId)>> sym_cell_set(
+                [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+              for (auto&& connecting_item_id : layer_sym_connecting_item_set) {
+                auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                for (size_t i_connecting_item_of_cell = 0;
+                     i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                  const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                  if (not marked_sym_cell_set.contains(connecting_item_id_of_cell)) {
+                    sym_cell_set.insert(connecting_item_id_of_cell);
+                    marked_sym_cell_set.insert(connecting_item_id_of_cell);
+                  }
+                }
+              }
+
+              for (auto&& set_sym_cell_id : sym_cell_set) {
+                expected_stencil.push_back(set_sym_cell_id);
+              }
+
+              layer_connecting_item_set.clear();
+              for (auto&& layer_cell_id : cell_set) {
+                auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+                for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                     ++i_connecting_item) {
+                  const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                  if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                    layer_connecting_item_set.insert(connecting_item_id);
+                    marked_connecting_item_set.insert(connecting_item_id);
+                  }
+                }
+              }
+
+              layer_sym_connecting_item_set.clear();
+
+              for (auto&& connecting_item_id : layer_connecting_item_set) {
+                if (sym_connecting_item_set.contains(connecting_item_id)) {
+                  if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                    marked_sym_connecting_item_set.insert(connecting_item_id);
+                    layer_sym_connecting_item_set.insert(connecting_item_id);
+                  }
+                }
+              }
+
+              for (auto layer_sym_cell_id : sym_cell_set) {
+                auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_sym_cell_id];
+                for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                     ++i_connecting_item) {
+                  const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                  if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                    marked_sym_connecting_item_set.insert(connecting_item_id);
+                    layer_sym_connecting_item_set.insert(connecting_item_id);
+                  }
+                }
+              }
+            }
+
+            auto cell_stencil = boundary_stencil[cell_id];
+
+            if (cell_stencil.size() != expected_stencil.size()) {
+              are_valid_symmetries = false;
+            }
+
+            auto i_set_cell = expected_stencil.begin();
+            for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+              if (*i_set_cell != cell_stencil[index]) {
+                are_valid_symmetries = false;
+              }
+            }
+          }
+        }
+      }
+
+      return are_valid_symmetries;
+    };
+
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(not(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1)));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(not(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1)));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(not(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2)));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_StencilBuilder_node2cell.cpp b/tests/test_StencilBuilder_node2cell.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d9c429c6d4ba903185c90a1e6c37d63bf9c9feb
--- /dev/null
+++ b/tests/test_StencilBuilder_node2cell.cpp
@@ -0,0 +1,1184 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/CartesianMeshBuilder.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ConnectivityUtils.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/ItemValueUtils.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFaceBoundary.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/StencilManager.hpp>
+#include <utils/Messenger.hpp>
+
+#include <NbGhostLayersTester.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("StencilBuilder node2cell", "[mesh]")
+{
+  auto is_valid = []<ItemType source_item_type, ItemType connecting_item_type>(const auto& connectivity,
+                                                                               const auto& stencil_array,
+                                                                               const size_t number_of_layers) {
+    constexpr size_t Dimension = std::decay_t<decltype(connectivity)>::Dimension;
+
+    auto cell_to_connecting_item_matrix =
+      connectivity.template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+
+    auto source_item_to_connecting_item_matrix =
+      [](const auto& given_connectivity) -> ItemToItemMatrix<source_item_type, connecting_item_type> {
+      if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                    ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+        return ConnectivityMatrix{};
+      } else {
+        return given_connectivity.template getItemToItemMatrix<source_item_type, connecting_item_type>();
+      }
+    }(connectivity);
+
+    auto connecting_to_cell_matrix = connectivity.template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+    auto cell_is_owned             = connectivity.cellIsOwned();
+    auto cell_number               = connectivity.cellNumber();
+    auto source_item_is_owned      = connectivity.template isOwned<source_item_type>();
+
+    using SourceItemId     = ItemIdT<source_item_type>;
+    using ConnectingItemId = ItemIdT<connecting_item_type>;
+
+    for (SourceItemId source_item_id = 0; source_item_id < connectivity.template numberOf<source_item_type>();
+         ++source_item_id) {
+      if (source_item_is_owned[source_item_id]) {
+        std::vector<CellId> expected_stencil;
+
+        std::set<CellId> marked_cell_set;
+        if constexpr (source_item_type == ItemType::cell) {
+          marked_cell_set.insert(source_item_id);
+        }
+
+        std::set<ConnectingItemId> marked_connecting_item_set;
+        std::set<ConnectingItemId> layer_connecting_item_set;
+        {
+          if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                        ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+            ConnectingItemId connecting_id =
+              ConnectingItemId{static_cast<typename SourceItemId::base_type>(source_item_id)};
+            layer_connecting_item_set.insert(connecting_id);
+            marked_connecting_item_set.insert(connecting_id);
+          } else {
+            auto source_item_to_connecting_item_list = source_item_to_connecting_item_matrix[source_item_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < source_item_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = source_item_to_connecting_item_list[i_connecting_item];
+              layer_connecting_item_set.insert(connecting_item_id);
+              marked_connecting_item_set.insert(connecting_item_id);
+            }
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          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 (auto&& connecting_item_id : layer_connecting_item_set) {
+            auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+            for (size_t i_connecting_item_of_cell = 0; i_connecting_item_of_cell < connecting_item_to_cell_list.size();
+                 ++i_connecting_item_of_cell) {
+              const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+              if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                cell_set.insert(connecting_item_id_of_cell);
+                marked_cell_set.insert(connecting_item_id_of_cell);
+              }
+            }
+          }
+
+          layer_connecting_item_set.clear();
+          for (auto layer_cell_id : cell_set) {
+            auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+              if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+          }
+
+          for (auto&& set_cell_id : cell_set) {
+            expected_stencil.push_back(set_cell_id);
+          }
+        }
+
+        auto cell_stencil = stencil_array[source_item_id];
+
+        auto i_set_cell = expected_stencil.begin();
+        if (cell_stencil.size() != expected_stencil.size()) {
+          return false;
+        }
+
+        for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+          if (*i_set_cell != cell_stencil[index]) {
+            return false;
+          }
+        }
+      }
+    }
+    return true;
+  };
+
+  SECTION("inner stencil")
+  {
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+    }
+  }
+
+  SECTION("Stencil using symmetries")
+  {
+    auto check_ghost_nodes_have_empty_stencils = [](const auto& stencil_array, const auto& connecticity) {
+      bool is_empty = true;
+
+      auto node_is_owned = connecticity.nodeIsOwned();
+
+      for (NodeId node_id = 0; node_id < connecticity.numberOfNodes(); ++node_id) {
+        if (not node_is_owned[node_id]) {
+          is_empty &= (stencil_array[node_id].size() == 0);
+          for (size_t i_symmetry_stencil = 0;
+               i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry_stencil) {
+            is_empty &=
+              (stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray()[node_id].size() ==
+               0);
+          }
+        }
+      }
+
+      return is_empty;
+    };
+
+    auto are_symmetry_stencils_valid =
+      []<ItemType source_item_type, ItemType connecting_item_type>(const auto& stencil_array, const auto& mesh,
+                                                                   const size_t number_of_layers) {
+        bool are_valid_symmetries = true;
+
+        constexpr const size_t Dimension = std::decay_t<decltype(mesh)>::Dimension;
+
+        auto connecting_to_cell_matrix =
+          mesh.connectivity().template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+        auto cell_to_connecting_item_matrix =
+          mesh.connectivity().template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+        //        auto source_item_is_owned        = mesh.connectivity().cellIsOwned();
+        auto cell_number          = mesh.connectivity().cellNumber();
+        auto source_item_is_owned = mesh.connectivity().template isOwned<source_item_type>();
+
+        auto connecting_number = mesh.connectivity().template number<connecting_item_type>();
+
+        auto source_item_to_connecting_item_matrix =
+          [](const auto& given_connectivity) -> ItemToItemMatrix<source_item_type, connecting_item_type> {
+          if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                        ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+            return ConnectivityMatrix{};
+          } else {
+            return given_connectivity.template getItemToItemMatrix<source_item_type, connecting_item_type>();
+          }
+        }(mesh.connectivity());
+
+        using SourceItemId     = ItemIdT<source_item_type>;
+        using ConnectingItemId = ItemIdT<connecting_item_type>;
+        using MeshType         = std::decay_t<decltype(mesh)>;
+
+        for (size_t i_symmetry_stencil = 0;
+             i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry_stencil) {
+          const IBoundaryDescriptor& boundary_descriptor =
+            stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].boundaryDescriptor();
+
+          auto boundary_stencil   = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray();
+          auto boundary_face_list = getMeshFaceBoundary(mesh, boundary_descriptor);
+
+          std::set<ConnectingItemId> sym_connecting_item_set;
+
+          if constexpr (ItemTypeId<MeshType::Dimension>::itemTId(connecting_item_type) ==
+                        ItemTypeId<MeshType::Dimension>::itemTId(ItemType::face)) {
+            for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+              const FaceId face_id = boundary_face_list.faceList()[i_face];
+              sym_connecting_item_set.insert(ConnectingItemId{FaceId::base_type{face_id}});
+            }
+
+          } else {
+            auto face_to_connecting_item_matrix =
+              mesh.connectivity().template getItemToItemMatrix<ItemType::face, connecting_item_type>();
+
+            for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+              const FaceId face_id      = boundary_face_list.faceList()[i_face];
+              auto connecting_item_list = face_to_connecting_item_matrix[face_id];
+              for (size_t i_connecting_item = 0; i_connecting_item < connecting_item_list.size(); ++i_connecting_item) {
+                sym_connecting_item_set.insert(connecting_item_list[i_connecting_item]);
+              }
+            }
+          }
+
+          for (SourceItemId source_item_id = 0; source_item_id < mesh.template numberOf<source_item_type>();
+               ++source_item_id) {
+            if (not source_item_is_owned[source_item_id]) {
+              are_valid_symmetries &= (boundary_stencil[source_item_id].size() == 0);
+            } else {
+              std::vector<CellId> expected_stencil;
+
+              std::set<CellId> marked_cell_set;
+              if constexpr (source_item_type == ItemType::cell) {
+                marked_cell_set.insert(source_item_id);
+              }
+
+              std::set<ConnectingItemId> marked_connecting_item_set;
+              std::set<ConnectingItemId> layer_connecting_item_set;
+              if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                            ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+                const ConnectingItemId connecting_item_id =
+                  static_cast<typename SourceItemId::base_type>(source_item_id);
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              } else {
+                auto source_item_to_connecting_item_list = source_item_to_connecting_item_matrix[source_item_id];
+                for (size_t i_connecting_item = 0; i_connecting_item < source_item_to_connecting_item_list.size();
+                     ++i_connecting_item) {
+                  const ConnectingItemId connecting_item_id = source_item_to_connecting_item_list[i_connecting_item];
+                  layer_connecting_item_set.insert(connecting_item_id);
+                  marked_connecting_item_set.insert(connecting_item_id);
+                }
+              }
+
+              std::set<ConnectingItemId> marked_sym_connecting_item_set;
+              std::set<ConnectingItemId> layer_sym_connecting_item_set;
+
+              for (auto&& connecting_item_id : marked_connecting_item_set) {
+                if (sym_connecting_item_set.contains(connecting_item_id)) {
+                  marked_sym_connecting_item_set.insert(connecting_item_id);
+                  layer_sym_connecting_item_set.insert(connecting_item_id);
+                }
+              }
+
+              std::set<CellId> marked_sym_cell_set;
+
+              for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+                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 (auto&& connecting_item_id : layer_connecting_item_set) {
+                  auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                  for (size_t i_connecting_item_of_cell = 0;
+                       i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                    const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                    if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                      cell_set.insert(connecting_item_id_of_cell);
+                      marked_cell_set.insert(connecting_item_id_of_cell);
+                    }
+                  }
+                }
+
+                std::set<CellId, std::function<bool(CellId, CellId)>> sym_cell_set(
+                  [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+                for (auto&& connecting_item_id : layer_sym_connecting_item_set) {
+                  auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                  for (size_t i_connecting_item_of_cell = 0;
+                       i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                    const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                    if (not marked_sym_cell_set.contains(connecting_item_id_of_cell)) {
+                      sym_cell_set.insert(connecting_item_id_of_cell);
+                      marked_sym_cell_set.insert(connecting_item_id_of_cell);
+                    }
+                  }
+                }
+
+                for (auto&& set_sym_cell_id : sym_cell_set) {
+                  expected_stencil.push_back(set_sym_cell_id);
+                }
+
+                layer_connecting_item_set.clear();
+                for (auto&& layer_cell_id : cell_set) {
+                  auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+                  for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                       ++i_connecting_item) {
+                    const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                    if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                      layer_connecting_item_set.insert(connecting_item_id);
+                      marked_connecting_item_set.insert(connecting_item_id);
+                    }
+                  }
+                }
+
+                layer_sym_connecting_item_set.clear();
+
+                for (auto&& connecting_item_id : layer_connecting_item_set) {
+                  if (sym_connecting_item_set.contains(connecting_item_id)) {
+                    if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                      marked_sym_connecting_item_set.insert(connecting_item_id);
+                      layer_sym_connecting_item_set.insert(connecting_item_id);
+                    }
+                  }
+                }
+
+                for (auto layer_sym_cell_id : sym_cell_set) {
+                  auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_sym_cell_id];
+                  for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                       ++i_connecting_item) {
+                    const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                    if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                      marked_sym_connecting_item_set.insert(connecting_item_id);
+                      layer_sym_connecting_item_set.insert(connecting_item_id);
+                    }
+                  }
+                }
+              }
+
+              auto cell_stencil = boundary_stencil[source_item_id];
+
+              if (cell_stencil.size() != expected_stencil.size()) {
+                are_valid_symmetries = false;
+              }
+
+              auto i_set_cell = expected_stencil.begin();
+              for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+                if (*i_set_cell != cell_stencil[index]) {
+                  are_valid_symmetries = false;
+                }
+              }
+            }
+          }
+        }
+
+        return are_valid_symmetries;
+      };
+
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(not(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2)));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_TinyMatrix.cpp b/tests/test_TinyMatrix.cpp
index 58a96f3acdf690653a4450f6e927febaf027a385..2eaadd46a7cb0780d3d54fe8e7d06fc4e059a9b3 100644
--- a/tests/test_TinyMatrix.cpp
+++ b/tests/test_TinyMatrix.cpp
@@ -9,6 +9,7 @@
 
 #include <algebra/TinyMatrix.hpp>
 
+#include <cmath>
 #include <sstream>
 
 // Instantiate to ensure full coverage is performed
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index c4fca371ef152cba6e319141f3fac6424cb7d4ef..98c01f7879d00fbf33718ab679d05ef3d7c73fde 100644
--- a/tests/test_main.cpp
+++ b/tests/test_main.cpp
@@ -7,6 +7,7 @@
 #include <mesh/DualConnectivityManager.hpp>
 #include <mesh/DualMeshManager.hpp>
 #include <mesh/MeshDataManager.hpp>
+#include <mesh/StencilManager.hpp>
 #include <mesh/SynchronizerManager.hpp>
 #include <utils/GlobalVariableManager.hpp>
 #include <utils/Messenger.hpp>
@@ -67,7 +68,10 @@ main(int argc, char* argv[])
       MeshDataManager::create();
       DualConnectivityManager::create();
       DualMeshManager::create();
+      StencilManager::create();
+
       GlobalVariableManager::create();
+      GlobalVariableManager::instance().setNumberOfGhostLayers(1);
 
       MeshDataBaseForTests::create();
 
@@ -80,6 +84,7 @@ main(int argc, char* argv[])
       MeshDataBaseForTests::destroy();
 
       GlobalVariableManager::destroy();
+      StencilManager::destroy();
       DualMeshManager::destroy();
       DualConnectivityManager::destroy();
       MeshDataManager::destroy();