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..cdf786c5a30970b747b0f540642d5637059198b8
--- /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.size() > 0) {
+      os << 0 << ':' << NaNHelper(x[0]);
+    }
+    for (size_t i = 1; i < x.size(); ++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 b8d3b81b5fad0ac65521a94aa4f94183f4cff333..22ccbf8813c1522900f8dfa06c32e808b01ef9e4 100644
--- a/src/algebra/SmallMatrix.hpp
+++ b/src/algebra/SmallMatrix.hpp
@@ -28,20 +28,25 @@ 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
-  bool isSquare() const noexcept
+  bool
+  isSquare() const noexcept
   {
     return m_nb_rows == m_nb_columns;
   }
 
-  friend PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>> copy(const SmallMatrix& A) noexcept
+  friend PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>>
+  copy(const SmallMatrix& A) noexcept
   {
     return SmallMatrix<std::remove_const_t<DataType>>{A.m_nb_rows, A.m_nb_columns, copy(A.m_values)};
   }
 
-  friend PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>> transpose(const SmallMatrix& A)
+  friend PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>>
+  transpose(const SmallMatrix& A)
   {
     SmallMatrix<std::remove_const_t<DataType>> A_transpose{A.m_nb_columns, A.m_nb_rows};
     for (size_t i = 0; i < A.m_nb_rows; ++i) {
@@ -52,14 +57,16 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
     return A_transpose;
   }
 
-  friend PUGS_INLINE SmallMatrix operator*(const DataType& a, const SmallMatrix& A)
+  friend PUGS_INLINE SmallMatrix
+  operator*(const DataType& a, const SmallMatrix& A)
   {
     SmallMatrix<std::remove_const_t<DataType>> aA = copy(A);
     return aA *= a;
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallVector<std::remove_const_t<DataType>> operator*(const SmallVector<DataType2>& x) const
+  PUGS_INLINE SmallVector<std::remove_const_t<DataType>>
+  operator*(const SmallVector<DataType2>& x) const
   {
     static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                   "incompatible data types");
@@ -77,7 +84,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>> operator*(const SmallMatrix<DataType2>& B) const
+  PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>>
+  operator*(const SmallMatrix<DataType2>& B) const
   {
     static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                   "incompatible data types");
@@ -98,14 +106,16 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix& operator/=(const DataType2& a)
+  PUGS_INLINE SmallMatrix&
+  operator/=(const DataType2& a)
   {
     const auto inv_a = 1. / a;
     return (*this) *= inv_a;
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix& operator*=(const DataType2& a)
+  PUGS_INLINE SmallMatrix&
+  operator*=(const DataType2& a)
   {
     parallel_for(
       m_values.size(), PUGS_LAMBDA(index_type i) { m_values[i] *= a; });
@@ -113,7 +123,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix& operator-=(const SmallMatrix<DataType2>& B)
+  PUGS_INLINE SmallMatrix&
+  operator-=(const SmallMatrix<DataType2>& B)
   {
     static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                   "incompatible data types");
@@ -126,7 +137,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix& operator+=(const SmallMatrix<DataType2>& B)
+  PUGS_INLINE SmallMatrix&
+  operator+=(const SmallMatrix<DataType2>& B)
   {
     static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                   "incompatible data types");
@@ -139,7 +151,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>> operator+(const SmallMatrix<DataType2>& B) const
+  PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>>
+  operator+(const SmallMatrix<DataType2>& B) const
   {
     static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                   "incompatible data types");
@@ -155,7 +168,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>> operator-(const SmallMatrix<DataType2>& B) const
+  PUGS_INLINE SmallMatrix<std::remove_const_t<DataType>>
+  operator-(const SmallMatrix<DataType2>& B) const
   {
     static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                   "incompatible data types");
@@ -171,36 +185,42 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   PUGS_INLINE
-  DataType& operator()(index_type i, index_type j) const noexcept(NO_ASSERT)
+  DataType&
+  operator()(index_type i, index_type j) const noexcept(NO_ASSERT)
   {
     Assert(i < m_nb_rows and j < m_nb_columns, "cannot access element: invalid indices");
     return m_values[i * m_nb_columns + j];
   }
 
   PUGS_INLINE
-  size_t numberOfRows() const noexcept
+  size_t
+  numberOfRows() const noexcept
   {
     return m_nb_rows;
   }
 
   PUGS_INLINE
-  size_t numberOfColumns() const noexcept
+  size_t
+  numberOfColumns() const noexcept
   {
     return m_nb_columns;
   }
 
-  PUGS_INLINE void fill(const DataType& value) noexcept
+  PUGS_INLINE void
+  fill(const DataType& value) noexcept
   {
     m_values.fill(value);
   }
 
-  PUGS_INLINE SmallMatrix& operator=(ZeroType) noexcept
+  PUGS_INLINE SmallMatrix&
+  operator=(ZeroType) noexcept
   {
     m_values.fill(0);
     return *this;
   }
 
-  PUGS_INLINE SmallMatrix& operator=(IdentityType) noexcept(NO_ASSERT)
+  PUGS_INLINE SmallMatrix&
+  operator=(IdentityType) noexcept(NO_ASSERT)
   {
     Assert(m_nb_rows == m_nb_columns, "identity must be a square matrix");
 
@@ -211,7 +231,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   }
 
   template <typename DataType2>
-  PUGS_INLINE SmallMatrix& operator=(const SmallMatrix<DataType2>& A) noexcept
+  PUGS_INLINE SmallMatrix&
+  operator=(const SmallMatrix<DataType2>& A) noexcept
   {
     // ensures that DataType is the same as source DataType2
     static_assert(std::is_same<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>(),
@@ -232,7 +253,8 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
   PUGS_INLINE
   SmallMatrix& operator=(SmallMatrix&&) = default;
 
-  friend std::ostream& operator<<(std::ostream& os, const SmallMatrix& A)
+  friend std::ostream&
+  operator<<(std::ostream& os, const SmallMatrix& A)
   {
     for (size_t i = 0; i < A.numberOfRows(); ++i) {
       os << i << '|';
@@ -259,7 +281,7 @@ class [[nodiscard]] SmallMatrix   // LCOV_EXCL_LINE
 
   SmallMatrix(const SmallMatrix&) = default;
 
-  SmallMatrix(SmallMatrix &&) = default;
+  SmallMatrix(SmallMatrix&&) = default;
 
   explicit SmallMatrix(size_t nb_rows, size_t nb_columns, const SmallArray<DataType>& values)
     : m_nb_rows{nb_rows}, m_nb_columns{nb_columns}, m_values{values}
diff --git a/src/algebra/SmallVector.hpp b/src/algebra/SmallVector.hpp
index d4a0bf84268bdf1d164cf6216153117adafaab56..bc3686c75e227c738129c532f4ce4683b5953e49 100644
--- a/src/algebra/SmallVector.hpp
+++ b/src/algebra/SmallVector.hpp
@@ -166,6 +166,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 94590ff656784d018e9ca57991674da773d64568..e2a87058c7315737a06b55ef32af967e2adcf9d6 100644
--- a/src/algebra/TinyMatrix.hpp
+++ b/src/algebra/TinyMatrix.hpp
@@ -56,16 +56,14 @@ class [[nodiscard]] TinyMatrix
   }
 
  public:
-  PUGS_INLINE
-  constexpr bool
+  [[nodiscard]] PUGS_INLINE constexpr bool
   isSquare() const noexcept
   {
     return M == N;
   }
 
-  PUGS_INLINE
-  constexpr friend TinyMatrix<N, M, T>
-  transpose(const TinyMatrix& A)
+  [[nodiscard]] PUGS_INLINE constexpr friend TinyMatrix<N, M, T>
+  transpose(const TinyMatrix& A) noexcept
   {
     TinyMatrix<N, M, T> tA;
     for (size_t i = 0; i < M; ++i) {
@@ -76,37 +74,32 @@ class [[nodiscard]] TinyMatrix
     return tA;
   }
 
-  PUGS_INLINE
-  constexpr size_t
-  dimension() const
+  [[nodiscard]] PUGS_INLINE constexpr size_t
+  dimension() const noexcept
   {
     return M * N;
   }
 
-  PUGS_INLINE
-  constexpr size_t
-  numberOfValues() const
+  [[nodiscard]] PUGS_INLINE constexpr size_t
+  numberOfValues() const noexcept
   {
     return this->dimension();
   }
 
-  PUGS_INLINE
-  constexpr size_t
-  numberOfRows() const
+  [[nodiscard]] PUGS_INLINE constexpr size_t
+  numberOfRows() const noexcept
   {
     return M;
   }
 
-  PUGS_INLINE
-  constexpr size_t
-  numberOfColumns() const
+  [[nodiscard]] PUGS_INLINE constexpr size_t
+  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) {
@@ -115,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;
@@ -125,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;
@@ -141,8 +133,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;
@@ -158,9 +150,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;
@@ -194,9 +185,8 @@ class [[nodiscard]] TinyMatrix
     return os;
   }
 
-  PUGS_INLINE
-  constexpr bool
-  operator==(const TinyMatrix& A) const
+  [[nodiscard]] PUGS_INLINE constexpr bool
+  operator==(const TinyMatrix& A) const noexcept
   {
     for (size_t i = 0; i < M * N; ++i) {
       if (m_values[i] != A.m_values[i])
@@ -205,16 +195,26 @@ class [[nodiscard]] TinyMatrix
     return true;
   }
 
-  PUGS_INLINE
-  constexpr bool
-  operator!=(const TinyMatrix& A) const
+  [[nodiscard]] PUGS_INLINE constexpr bool
+  operator!=(const TinyMatrix& A) const noexcept
   {
     return not this->operator==(A);
   }
 
-  PUGS_INLINE
-  constexpr TinyMatrix
-  operator+(const TinyMatrix& A) const
+  [[nodiscard]] PUGS_INLINE constexpr friend T
+  dot(const TinyMatrix& A, const TinyMatrix& B) noexcept
+  {
+    T sum = A.m_values[0] * B.m_values[0];
+
+    for (size_t i = 1; i < M * N; ++i) {
+      sum += A.m_values[i] * B.m_values[i];
+    }
+
+    return sum;
+  }
+
+  [[nodiscard]] PUGS_INLINE constexpr TinyMatrix
+  operator+(const TinyMatrix& A) const noexcept
   {
     TinyMatrix sum;
     for (size_t i = 0; i < M * N; ++i) {
@@ -223,17 +223,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) {
@@ -242,9 +240,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];
@@ -254,7 +251,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];
@@ -264,7 +261,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];
@@ -377,8 +374,8 @@ class [[nodiscard]] TinyMatrix
 };
 
 template <size_t M, size_t N, typename T>
-PUGS_INLINE constexpr TinyMatrix<M, N, T>
-tensorProduct(const TinyVector<M, T>& x, const TinyVector<N, T>& y)
+[[nodiscard]] PUGS_INLINE constexpr TinyMatrix<M, N, T>
+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,9 +386,17 @@ tensorProduct(const TinyVector<M, T>& x, const TinyVector<N, T>& y)
   return A;
 }
 
+template <size_t M, size_t N, typename T>
+[[nodiscard]] PUGS_INLINE constexpr auto
+frobeniusNorm(const TinyMatrix<M, N, T>& A) noexcept
+{
+  static_assert(std::is_arithmetic<T>(), "Cannot compute frobeniusNorm value for non-arithmetic types");
+  return std::sqrt(dot(A, A));
+}
+
 template <size_t N, typename T>
-PUGS_INLINE constexpr T
-det(const TinyMatrix<N, N, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr T
+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 "
@@ -441,24 +446,24 @@ det(const TinyMatrix<N, N, T>& A)
 }
 
 template <typename T>
-PUGS_INLINE constexpr T
-det(const TinyMatrix<1, 1, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr T
+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);
 }
 
 template <typename T>
-PUGS_INLINE constexpr T
-det(const TinyMatrix<2, 2, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr T
+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);
 }
 
 template <typename T>
-PUGS_INLINE constexpr T
-det(const TinyMatrix<3, 3, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr T
+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)) +
@@ -466,8 +471,8 @@ det(const TinyMatrix<3, 3, T>& A)
 }
 
 template <size_t M, size_t N, typename T>
-PUGS_INLINE constexpr TinyMatrix<M - 1, N - 1, T>
-getMinor(const TinyMatrix<M, N, T>& A, size_t I, size_t J)
+[[nodiscard]] PUGS_INLINE constexpr TinyMatrix<M - 1, N - 1, T>
+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));
@@ -492,8 +497,8 @@ getMinor(const TinyMatrix<M, N, T>& A, size_t I, size_t J)
 }
 
 template <size_t N, typename T>
-PUGS_INLINE T
-trace(const TinyMatrix<N, N, T>& A)
+[[nodiscard]] PUGS_INLINE T
+trace(const TinyMatrix<N, N, T>& A) noexcept
 {
   static_assert(std::is_arithmetic<T>::value, "trace is not defined for non-arithmetic types");
 
@@ -505,11 +510,11 @@ trace(const TinyMatrix<N, N, T>& A)
 }
 
 template <size_t N, typename T>
-PUGS_INLINE constexpr TinyMatrix<N, N, T> inverse(const TinyMatrix<N, N, T>& A);
+[[nodiscard]] PUGS_INLINE constexpr TinyMatrix<N, N, T> inverse(const TinyMatrix<N, N, T>& A);
 
 template <typename T>
-PUGS_INLINE constexpr TinyMatrix<1, 1, T>
-inverse(const TinyMatrix<1, 1, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr TinyMatrix<1, 1, T>
+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");
@@ -519,8 +524,8 @@ inverse(const TinyMatrix<1, 1, T>& A)
 }
 
 template <size_t N, typename T>
-PUGS_INLINE constexpr T
-cofactor(const TinyMatrix<N, N, T>& A, size_t i, size_t j)
+[[nodiscard]] PUGS_INLINE constexpr T
+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;
@@ -529,8 +534,8 @@ cofactor(const TinyMatrix<N, N, T>& A, size_t i, size_t j)
 }
 
 template <typename T>
-PUGS_INLINE constexpr TinyMatrix<2, 2, T>
-inverse(const TinyMatrix<2, 2, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr TinyMatrix<2, 2, T>
+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");
@@ -543,8 +548,8 @@ inverse(const TinyMatrix<2, 2, T>& A)
 }
 
 template <typename T>
-PUGS_INLINE constexpr TinyMatrix<3, 3, T>
-inverse(const TinyMatrix<3, 3, T>& A)
+[[nodiscard]] PUGS_INLINE constexpr TinyMatrix<3, 3, T>
+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 2bc9c79c36ebe2caef6cf63ac6dc653d3a8b0bcb..e6d33fe26c46bdbcef4283dfd318ace9d990c40e 100644
--- a/src/algebra/TinyVector.hpp
+++ b/src/algebra/TinyVector.hpp
@@ -33,9 +33,8 @@ class [[nodiscard]] TinyVector
   }
 
  public:
-  PUGS_INLINE
-  constexpr TinyVector
-  operator-() const
+  [[nodiscard]] PUGS_INLINE constexpr TinyVector
+  operator-() const noexcept
   {
     TinyVector opposite;
     for (size_t i = 0; i < N; ++i) {
@@ -45,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])
@@ -61,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) {
@@ -78,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;
@@ -86,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);
@@ -114,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) {
@@ -125,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];
@@ -135,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) {
@@ -146,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];
@@ -156,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];
@@ -168,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];
@@ -176,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);
@@ -249,8 +239,8 @@ class [[nodiscard]] TinyVector
 };
 
 template <size_t N, typename T>
-[[nodiscard]] PUGS_INLINE constexpr 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");
@@ -259,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) {
@@ -272,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) {
@@ -286,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/analysis/PyramidGaussQuadrature.cpp b/src/analysis/PyramidGaussQuadrature.cpp
index 679f3ded5b85764bca43ed062f379d8171cd5c65..e85a74ac1af9ec6893d15f5489cd568891e0ca92 100644
--- a/src/analysis/PyramidGaussQuadrature.cpp
+++ b/src/analysis/PyramidGaussQuadrature.cpp
@@ -24,6 +24,11 @@ PyramidGaussQuadrature::_buildPointAndWeightLists(const size_t degree)
 
       const double w = (4. / 3) * unit_weight;
 
+      // gcc's bound checking are messed up due to the use of
+      // std::array and the following dynamic/general switch
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Warray-bounds"
+
       switch (id) {
       case 1: {
         Assert(value_list.size() == 1);
@@ -96,6 +101,8 @@ PyramidGaussQuadrature::_buildPointAndWeightLists(const size_t degree)
       }
         // LCOV_EXCL_STOP
       }
+
+#pragma GCC diagnostic pop
     }
   };
 
diff --git a/src/geometry/LineTransformation.hpp b/src/geometry/LineTransformation.hpp
index ae0b00a8a1dce7e4b6b6bb13a737ee1114a202f1..c9f7def71f9d342cca580debf09aa8e77316c556 100644
--- a/src/geometry/LineTransformation.hpp
+++ b/src/geometry/LineTransformation.hpp
@@ -59,6 +59,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/main.cpp b/src/main.cpp
index 7d0112c80715b3cd0b69ea91b874527134b6a2ed..917addbe58d9fd90d6a5316315dc888f0a6bb97c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -4,6 +4,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>
@@ -16,6 +17,8 @@ main(int argc, char* argv[])
   ExecutionStatManager::create();
   ParallelChecker::create();
 
+  GlobalVariableManager::create();
+
   std::string filename = initialize(argc, argv);
 
   SynchronizerManager::create();
@@ -24,14 +27,12 @@ main(int argc, char* argv[])
   MeshDataManager::create();
   DualConnectivityManager::create();
   DualMeshManager::create();
-
-  GlobalVariableManager::create();
+  StencilManager::create();
 
   parser(filename);
   ExecutionStatManager::printInfo();
 
-  GlobalVariableManager::destroy();
-
+  StencilManager::destroy();
   DualMeshManager::destroy();
   DualConnectivityManager::destroy();
   MeshDataManager::destroy();
@@ -41,6 +42,8 @@ main(int argc, char* argv[])
 
   finalize();
 
+  GlobalVariableManager::destroy();
+
   ParallelChecker::destroy();
 
   int return_code = ExecutionStatManager::getInstance().exitCode();
diff --git a/src/mesh/CMakeLists.txt b/src/mesh/CMakeLists.txt
index 78b72bcafc640ecc888c0997eeb3ff8f3056db58..b4d70359a5a4a7f8a4d2e352ab702ecc26761dbc 100644
--- a/src/mesh/CMakeLists.txt
+++ b/src/mesh/CMakeLists.txt
@@ -43,6 +43,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 bd1e0d85b6f94d1d2d00326857853f4da75eacfe..b28237ded4485de15e2b8c17233e95906752ab89 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>
 
@@ -310,6 +311,12 @@ Connectivity<Dimension>::_write(std::ostream& os) const
   return os;
 }
 
+template <size_t Dim>
+Connectivity<Dim>::~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;
@@ -330,6 +337,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 24d9ef656f0cdd0746db0ee2f27881ce5244c8b0..c04bdbdf9d215ec9324d5545dcf2a079d4590c7f 100644
--- a/src/mesh/Connectivity.hpp
+++ b/src/mesh/Connectivity.hpp
@@ -744,7 +744,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 65326b56bd49c1ce155dbf07fe32da8622c2e66c..e9491ad34c928b5c84e2f090057a46cebfaffba8 100644
--- a/src/mesh/ConnectivityDispatcher.cpp
+++ b/src/mesh/ConnectivityDispatcher.cpp
@@ -2,6 +2,7 @@
 
 #include <mesh/ItemOfItemType.hpp>
 #include <utils/CRSGraph.hpp>
+#include <utils/GlobalVariableManager.hpp>
 #include <utils/Partitioner.hpp>
 
 #include <iostream>
@@ -28,17 +29,17 @@ ConnectivityDispatcher<Dimension>::_buildNewOwner()
     using ItemId = ItemIdT<item_type>;
     ItemValue<int, item_type> item_new_owner(m_connectivity);
     parallel_for(
-      item_new_owner.numberOfItems(), PUGS_LAMBDA(const ItemId& l) {
-        const auto& item_to_cell = item_to_cell_matrix[l];
-        CellId Jmin              = item_to_cell[0];
-
-        for (size_t j = 1; j < item_to_cell.size(); ++j) {
-          const CellId J = item_to_cell[j];
-          if (cell_number[J] < cell_number[Jmin]) {
-            Jmin = J;
+      item_new_owner.numberOfItems(), PUGS_LAMBDA(const ItemId& item_id) {
+        const auto& item_to_cell  = item_to_cell_matrix[item_id];
+        CellId min_number_cell_id = item_to_cell[0];
+
+        for (size_t i_cell = 1; i_cell < item_to_cell.size(); ++i_cell) {
+          const CellId cell_id = item_to_cell[i_cell];
+          if (cell_number[cell_id] < cell_number[min_number_cell_id]) {
+            min_number_cell_id = cell_id;
           }
         }
-        item_new_owner[l] = cell_new_owner[Jmin];
+        item_new_owner[item_id] = cell_new_owner[min_number_cell_id];
       });
 
     synchronize(item_new_owner);
@@ -72,22 +73,65 @@ ConnectivityDispatcher<Dimension>::_buildItemListToSend()
 
     std::vector<std::vector<CellId>> cell_vector_to_send_by_proc(parallel::size());
     Array<bool> send_to_rank(parallel::size());
-    for (CellId j = 0; j < m_connectivity.numberOfCells(); ++j) {
+
+    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[j];
-
-      for (size_t R = 0; R < cell_to_node.size(); ++R) {
-        const NodeId& r          = cell_to_node[R];
-        const auto& node_to_cell = node_to_cell_matrix[r];
-        for (size_t K = 0; K < node_to_cell.size(); ++K) {
-          const CellId& k                 = node_to_cell[K];
-          send_to_rank[cell_new_owner[k]] = true;
+      const auto& cell_to_node = cell_to_node_matrix[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];
+        layer_node_id_list.push_back(node_id);
+        node_tag[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 (size_t k = 0; k < send_to_rank.size(); ++k) {
-        if (send_to_rank[k]) {
-          cell_vector_to_send_by_proc[k].push_back(j);
+      for (auto node_id : tagged_node_id_list) {
+        node_tag[node_id] = false;
+      }
+      tagged_node_id_list.clear();
+
+      for (size_t i_rank = 0; i_rank < send_to_rank.size(); ++i_rank) {
+        if (send_to_rank[i_rank]) {
+          cell_vector_to_send_by_proc[i_rank].push_back(cell_id);
         }
       }
     }
@@ -110,11 +154,11 @@ ConnectivityDispatcher<Dimension>::_buildItemListToSend()
       Array<bool> tag(m_connectivity.template numberOf<item_type>());
       tag.fill(false);
       std::vector<ItemId> item_id_vector;
-      for (size_t j = 0; j < cell_list_to_send_by_proc[i_rank].size(); ++j) {
-        const CellId& cell_id          = cell_list_to_send_by_proc[i_rank][j];
+      for (size_t i_cell = 0; i_cell < cell_list_to_send_by_proc[i_rank].size(); ++i_cell) {
+        const CellId& cell_id          = cell_list_to_send_by_proc[i_rank][i_cell];
         const auto& cell_sub_item_list = cell_to_sub_item_matrix[cell_id];
-        for (size_t r = 0; r < cell_sub_item_list.size(); ++r) {
-          const ItemId& item_id = cell_sub_item_list[r];
+        for (size_t i_item = 0; i_item < cell_sub_item_list.size(); ++i_item) {
+          const ItemId& item_id = cell_sub_item_list[i_item];
           if (not tag[item_id]) {
             item_id_vector.push_back(item_id);
             tag[item_id] = true;
@@ -155,9 +199,9 @@ ConnectivityDispatcher<Dimension>::_gatherFrom(const ItemValue<DataType, item_ty
   gathered_array = Array<std::remove_const_t<DataType>>(this->_dispatchedInfo<item_type>().m_number_to_id_map.size());
   for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
     Assert(recv_id_correspondance_by_proc[i_rank].size() == recv_item_data_by_proc[i_rank].size());
-    for (size_t r = 0; r < recv_id_correspondance_by_proc[i_rank].size(); ++r) {
-      const auto& item_id     = recv_id_correspondance_by_proc[i_rank][r];
-      gathered_array[item_id] = recv_item_data_by_proc[i_rank][r];
+    for (size_t i_item = 0; i_item < recv_id_correspondance_by_proc[i_rank].size(); ++i_item) {
+      const auto& item_id     = recv_id_correspondance_by_proc[i_rank][i_item];
+      gathered_array[item_id] = recv_item_data_by_proc[i_rank][i_item];
     }
   }
 }
@@ -180,11 +224,11 @@ ConnectivityDispatcher<Dimension>::_gatherFrom(
 
   for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
     std::vector<MutableDataType> data_by_item_vector;
-    for (size_t j = 0; j < item_list_to_send_by_proc[i_rank].size(); ++j) {
-      const ItemId& item_id = item_list_to_send_by_proc[i_rank][j];
+    for (size_t i_item = 0; i_item < item_list_to_send_by_proc[i_rank].size(); ++i_item) {
+      const ItemId& item_id = item_list_to_send_by_proc[i_rank][i_item];
       const auto& item_data = data_to_gather.itemArray(item_id);
-      for (size_t l = 0; l < item_data.size(); ++l) {
-        data_by_item_vector.push_back(item_data[l]);
+      for (size_t i_data = 0; i_data < item_data.size(); ++i_data) {
+        data_by_item_vector.push_back(item_data[i_data]);
       }
     }
     data_to_send_by_proc[i_rank] = convert_to_array(data_by_item_vector);
@@ -211,13 +255,13 @@ ConnectivityDispatcher<Dimension>::_gatherFrom(
 
   gathered_array = Array<std::remove_const_t<DataType>>(recv_array_size);
   {
-    size_t l = 0;
+    size_t i_value = 0;
     for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
-      for (size_t j = 0; j < recv_data_to_gather_by_proc[i_rank].size(); ++j) {
-        gathered_array[l++] = recv_data_to_gather_by_proc[i_rank][j];
+      for (size_t i_rank_value = 0; i_rank_value < recv_data_to_gather_by_proc[i_rank].size(); ++i_rank_value) {
+        gathered_array[i_value++] = recv_data_to_gather_by_proc[i_rank][i_rank_value];
       }
     }
-    Assert(gathered_array.size() == l);
+    Assert(gathered_array.size() == i_value);
   }
 }
 
@@ -229,8 +273,8 @@ ConnectivityDispatcher<Dimension>::_buildCellNumberIdMap()
   auto& cell_number_id_map            = this->_dispatchedInfo<ItemType::cell>().m_number_to_id_map;
   for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
     CellId cell_id = 0;
-    for (size_t i = 0; i < recv_cell_number_by_proc[i_rank].size(); ++i) {
-      const int cell_number     = recv_cell_number_by_proc[i_rank][i];
+    for (size_t i_rank_cell = 0; i_rank_cell < recv_cell_number_by_proc[i_rank].size(); ++i_rank_cell) {
+      const int cell_number     = recv_cell_number_by_proc[i_rank][i_rank_cell];
       auto [iterator, inserted] = cell_number_id_map.insert(std::make_pair(cell_number, cell_id));
       if (inserted)
         ++cell_id;
@@ -252,8 +296,9 @@ ConnectivityDispatcher<Dimension>::_buildSubItemNumberToIdMap()
   auto& sub_item_number_id_map = this->_dispatchedInfo<ItemOfItemT::sub_item_type>().m_number_to_id_map;
   for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
     int sub_item_id = 0;
-    for (size_t i = 0; i < cell_sub_item_number_to_recv_by_proc[i_rank].size(); ++i) {
-      int sub_item_number       = cell_sub_item_number_to_recv_by_proc[i_rank][i];
+    for (size_t i_rank_sub_item = 0; i_rank_sub_item < cell_sub_item_number_to_recv_by_proc[i_rank].size();
+         ++i_rank_sub_item) {
+      int sub_item_number       = cell_sub_item_number_to_recv_by_proc[i_rank][i_rank_sub_item];
       auto [iterator, inserted] = sub_item_number_id_map.insert(std::make_pair(sub_item_number, sub_item_id));
       if (inserted)
         sub_item_id++;
@@ -273,8 +318,9 @@ ConnectivityDispatcher<Dimension>::_buildNumberOfSubItemPerItemToRecvByProc()
 
   using ItemId = ItemIdT<SubItemOfItemT::item_type>;
   parallel_for(
-    number_of_sub_item_per_item.numberOfItems(),
-    PUGS_LAMBDA(const ItemId& j) { number_of_sub_item_per_item[j] = item_to_sub_item_matrix[j].size(); });
+    number_of_sub_item_per_item.numberOfItems(), PUGS_LAMBDA(const ItemId& item_id) {
+      number_of_sub_item_per_item[item_id] = item_to_sub_item_matrix[item_id].size();
+    });
 
   this->_dispatchedInfo<SubItemOfItemT>().m_number_of_sub_item_per_item_to_recv_by_proc =
     this->exchange(number_of_sub_item_per_item);
@@ -299,11 +345,11 @@ ConnectivityDispatcher<Dimension>::_buildSubItemNumbersToRecvByProc()
       const auto& item_list_to_send_by_proc = this->_dispatchedInfo<SubItemOfItemT::item_type>().m_list_to_send_by_proc;
 
       std::vector<int> sub_item_numbers_by_item_vector;
-      for (size_t j = 0; j < item_list_to_send_by_proc[i_rank].size(); ++j) {
-        const ItemId& item_id     = item_list_to_send_by_proc[i_rank][j];
+      for (size_t i_rank_item = 0; i_rank_item < item_list_to_send_by_proc[i_rank].size(); ++i_rank_item) {
+        const ItemId& item_id     = item_list_to_send_by_proc[i_rank][i_rank_item];
         const auto& sub_item_list = item_to_sub_item_matrix[item_id];
-        for (size_t r = 0; r < sub_item_list.size(); ++r) {
-          const SubItemId& sub_item_id = sub_item_list[r];
+        for (size_t i_sub_item = 0; i_sub_item < sub_item_list.size(); ++i_sub_item) {
+          const SubItemId& sub_item_id = sub_item_list[i_sub_item];
           sub_item_numbers_by_item_vector.push_back(sub_item_number[sub_item_id]);
         }
       }
@@ -351,11 +397,14 @@ ConnectivityDispatcher<Dimension>::_buildItemToSubItemDescriptor()
   std::vector<std::vector<unsigned int>> item_to_subitem_legacy;
   size_t number_of_node_by_cell = 0;
   for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
-    int l = 0;
-    for (size_t i = 0; i < item_list_to_recv_size_by_proc[i_rank]; ++i) {
+    int i_sub_item = 0;
+    for (size_t i_rank_item = 0; i_rank_item < item_list_to_recv_size_by_proc[i_rank]; ++i_rank_item) {
       std::vector<unsigned int> sub_item_vector;
-      for (int k = 0; k < number_of_sub_item_per_item_to_recv_by_proc[i_rank][i]; ++k) {
-        const auto& searched_sub_item_id = sub_item_number_id_map.find(recv_item_of_item_numbers_by_proc[i_rank][l++]);
+      for (int i_rank_sub_item_per_item = 0;
+           i_rank_sub_item_per_item < number_of_sub_item_per_item_to_recv_by_proc[i_rank][i_rank_item];
+           ++i_rank_sub_item_per_item) {
+        const auto& searched_sub_item_id =
+          sub_item_number_id_map.find(recv_item_of_item_numbers_by_proc[i_rank][i_sub_item++]);
         Assert(searched_sub_item_id != sub_item_number_id_map.end());
         sub_item_vector.push_back(searched_sub_item_id->second);
       }
@@ -371,14 +420,16 @@ ConnectivityDispatcher<Dimension>::_buildItemToSubItemDescriptor()
   item_to_subitem_list.fill(10000000);
 
   item_to_subitem_row_map[0] = 0;
-  for (size_t i = 0; i < item_to_subitem_legacy.size(); ++i) {
-    item_to_subitem_row_map[i + 1] = item_to_subitem_row_map[i] + item_to_subitem_legacy[i].size();
+  for (size_t i_rank = 0; i_rank < item_to_subitem_legacy.size(); ++i_rank) {
+    item_to_subitem_row_map[i_rank + 1] = item_to_subitem_row_map[i_rank] + item_to_subitem_legacy[i_rank].size();
   }
-  size_t l = 0;
-  for (size_t i = 0; i < item_to_subitem_legacy.size(); ++i) {
-    const auto& subitem_list = item_to_subitem_legacy[i];
-    for (size_t j = 0; j < subitem_list.size(); ++j, ++l) {
-      item_to_subitem_list[l] = subitem_list[j];
+  {
+    size_t i_sub_item = 0;
+    for (size_t i_rank = 0; i_rank < item_to_subitem_legacy.size(); ++i_rank) {
+      const auto& subitem_list = item_to_subitem_legacy[i_rank];
+      for (size_t i_rank_sub_item = 0; i_rank_sub_item < subitem_list.size(); ++i_rank_sub_item, ++i_sub_item) {
+        item_to_subitem_list[i_sub_item] = subitem_list[i_rank_sub_item];
+      }
     }
   }
 
@@ -401,7 +452,8 @@ ConnectivityDispatcher<Dimension>::_buildRecvItemIdCorrespondanceByProc()
     Array<int> send_item_number(item_list_to_send_by_proc[i_rank].size());
     const Array<const ItemId> send_item_id = item_list_to_send_by_proc[i_rank];
     parallel_for(
-      send_item_number.size(), PUGS_LAMBDA(size_t j) { send_item_number[j] = item_number[send_item_id[j]]; });
+      send_item_number.size(),
+      PUGS_LAMBDA(size_t i_item) { send_item_number[i_item] = item_number[send_item_id[i_item]]; });
     send_item_number_by_proc[i_rank] = send_item_number;
   }
 
@@ -415,11 +467,11 @@ ConnectivityDispatcher<Dimension>::_buildRecvItemIdCorrespondanceByProc()
   const auto& item_number_to_id_map = this->_dispatchedInfo<item_type>().m_number_to_id_map;
   for (size_t i_rank = 0; i_rank < item_list_to_recv_size_by_proc.size(); ++i_rank) {
     Array<ItemId> item_id_correspondance(item_list_to_recv_size_by_proc[i_rank]);
-    for (size_t l = 0; l < item_list_to_recv_size_by_proc[i_rank]; ++l) {
-      const int& recv_item_number  = recv_item_number_by_proc[i_rank][l];
+    for (size_t i_rank_item = 0; i_rank_item < item_list_to_recv_size_by_proc[i_rank]; ++i_rank_item) {
+      const int& recv_item_number  = recv_item_number_by_proc[i_rank][i_rank_item];
       const auto& searched_item_id = item_number_to_id_map.find(recv_item_number);
       Assert(searched_item_id != item_number_to_id_map.end());
-      item_id_correspondance[l] = searched_item_id->second;
+      item_id_correspondance[i_rank_item] = searched_item_id->second;
     }
     recv_item_id_correspondance_by_proc[i_rank] = item_id_correspondance;
   }
@@ -572,9 +624,9 @@ ConnectivityDispatcher<Dimension>::_buildItemReferenceList()
           this->_dispatchedInfo<item_type>().m_recv_id_correspondance_by_proc;
         std::vector<block_type> item_refs(m_new_descriptor.template itemNumberVector<item_type>().size());
         for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
-          for (size_t r = 0; r < recv_item_refs_by_proc[i_rank].size(); ++r) {
-            const ItemId& item_id = recv_item_id_correspondance_by_proc[i_rank][r];
-            item_refs[item_id]    = recv_item_refs_by_proc[i_rank][r];
+          for (size_t i_item = 0; i_item < recv_item_refs_by_proc[i_rank].size(); ++i_item) {
+            const ItemId& item_id = recv_item_id_correspondance_by_proc[i_rank][i_item];
+            item_refs[item_id]    = recv_item_refs_by_proc[i_rank][i_item];
           }
         }
 
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..9571c346bae6e4e345a16b09188dce15ab73b456
--- /dev/null
+++ b/src/mesh/StencilArray.hpp
@@ -0,0 +1,105 @@
+#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>;
+
+  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[](CellId cell_id) const
+  {
+    return m_stencil_array[cell_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..31db762a1002e3c633afeaabd4f06d6a98dff634
--- /dev/null
+++ b/src/mesh/StencilBuilder.cpp
@@ -0,0 +1,637 @@
+#include <mesh/StencilBuilder.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/ItemArray.hpp>
+#include <utils/GlobalVariableManager.hpp>
+#include <utils/Messenger.hpp>
+
+#include <set>
+
+template <ItemType item_type>
+class StencilBuilder::Layer
+{
+  using ItemId = ItemIdT<item_type>;
+  std::vector<ItemId> m_item_id_vector;
+  std::vector<int> m_item_number_vector;
+
+ public:
+  size_t
+  size() const
+  {
+    Assert(m_item_id_vector.size() == m_item_number_vector.size());
+    return m_item_id_vector.size();
+  }
+
+  bool
+  hasItemNumber(const int item_number) const
+  {
+    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 - 1;
+      } else {
+        if (end == mid) {
+          break;
+        }
+        end = mid + 1;
+      }
+    }
+    return false;
+  }
+
+  const std::vector<ItemId>&
+  itemIdList() const
+  {
+    return m_item_id_vector;
+  }
+
+  void
+  add(const ItemId item_id, const int item_number)
+  {
+    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() = 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 (ConnectivityType::Dimension > 1) {
+    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;
+  } else {
+    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());
+      }
+    }
+
+    return symmetry_connecting_item_list;
+  }
+}
+
+template <typename ConnectivityType>
+Array<const uint32_t>
+StencilBuilder::_getRowMap(const ConnectivityType& connectivity) const
+{
+  auto cell_to_node_matrix = connectivity.cellToNodeMatrix();
+  auto node_to_cell_matrix = connectivity.nodeToCellMatrix();
+
+  auto cell_is_owned = connectivity.cellIsOwned();
+
+  Array<uint32_t> row_map{connectivity.numberOfCells() + 1};
+  row_map[0] = 0;
+  std::vector<CellId> neighbors;
+  for (CellId cell_id = 0; cell_id < connectivity.numberOfCells(); ++cell_id) {
+    neighbors.resize(0);
+    // The stencil is not built for ghost cells
+    if (cell_is_owned[cell_id]) {
+      auto cell_nodes = cell_to_node_matrix[cell_id];
+      for (size_t i_node = 0; i_node < cell_nodes.size(); ++i_node) {
+        const NodeId node_id = cell_nodes[i_node];
+        auto node_cells      = node_to_cell_matrix[node_id];
+        for (size_t i_node_cell = 0; i_node_cell < node_cells.size(); ++i_node_cell) {
+          const CellId node_cell_id = node_cells[i_node_cell];
+          if (node_cell_id != cell_id) {
+            neighbors.push_back(node_cells[i_node_cell]);
+          }
+        }
+      }
+      std::sort(neighbors.begin(), neighbors.end());
+      neighbors.erase(std::unique(neighbors.begin(), neighbors.end()), neighbors.end());
+    }
+    // The cell itself is not counted
+    row_map[cell_id + 1] = row_map[cell_id] + neighbors.size();
+  }
+
+  return row_map;
+}
+
+template <typename ConnectivityType>
+Array<const uint32_t>
+StencilBuilder::_getColumnIndices(const ConnectivityType& connectivity, const Array<const uint32_t>& row_map) const
+{
+  auto cell_number = connectivity.cellNumber();
+
+  Array<uint32_t> max_index(row_map.size() - 1);
+  parallel_for(
+    max_index.size(), PUGS_LAMBDA(size_t i) { max_index[i] = row_map[i]; });
+
+  auto cell_to_node_matrix = connectivity.cellToNodeMatrix();
+  auto node_to_cell_matrix = connectivity.nodeToCellMatrix();
+
+  auto cell_is_owned = connectivity.cellIsOwned();
+
+  Array<uint32_t> column_indices(row_map[row_map.size() - 1]);
+  column_indices.fill(std::numeric_limits<uint32_t>::max());
+
+  for (CellId cell_id = 0; cell_id < connectivity.numberOfCells(); ++cell_id) {
+    // The stencil is not built for ghost cells
+    if (cell_is_owned[cell_id]) {
+      auto cell_nodes = cell_to_node_matrix[cell_id];
+      for (size_t i_node = 0; i_node < cell_nodes.size(); ++i_node) {
+        const NodeId node_id = cell_nodes[i_node];
+        auto node_cells      = node_to_cell_matrix[node_id];
+        for (size_t i_node_cell = 0; i_node_cell < node_cells.size(); ++i_node_cell) {
+          const CellId node_cell_id = node_cells[i_node_cell];
+          if (node_cell_id != cell_id) {
+            bool found = false;
+            for (size_t i_index = row_map[cell_id]; i_index < max_index[cell_id]; ++i_index) {
+              if (column_indices[i_index] == node_cell_id) {
+                found = true;
+                break;
+              }
+            }
+            if (not found) {
+              const auto node_cell_number = cell_number[node_cell_id];
+              size_t i_index              = row_map[cell_id];
+              // search for position for index
+              while ((i_index < max_index[cell_id])) {
+                if (node_cell_number > cell_number[CellId(column_indices[i_index])]) {
+                  ++i_index;
+                } else {
+                  break;
+                }
+              }
+
+              for (size_t i_destination = max_index[cell_id]; i_destination > i_index; --i_destination) {
+                const size_t i_source = i_destination - 1;
+
+                column_indices[i_destination] = column_indices[i_source];
+              }
+              ++max_index[cell_id];
+              column_indices[i_index] = node_cell_id;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return column_indices;
+}
+
+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");
+  }
+  if (number_of_layers > 2) {
+    throw NotImplementedError("number of layers too large");
+  }
+  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>;
+
+  if (symmetry_boundary_descriptor_list.size() == 0) {
+    if (number_of_layers == 1) {
+      Array<uint32_t> row_map{connectivity.template numberOf<item_type>() + 1};
+      row_map[0] = 0;
+
+      std::vector<ItemId> column_indices_vector;
+
+      for (ItemId item_id = 0; item_id < connectivity.template numberOf<item_type>(); ++item_id) {
+        // First layer is a special case
+
+        Layer<item_type> item_layer;
+        Layer<connecting_item_type> connecting_layer;
+
+        if (item_is_owned[item_id]) {
+          for (size_t i_connecting_item_1 = 0; i_connecting_item_1 < item_to_connecting_item_matrix[item_id].size();
+               ++i_connecting_item_1) {
+            const ConnectingItemId layer_1_connecting_item_id =
+              item_to_connecting_item_matrix[item_id][i_connecting_item_1];
+            connecting_layer.add(layer_1_connecting_item_id, connecting_item_number[layer_1_connecting_item_id]);
+          }
+
+          for (auto connecting_item_id : connecting_layer.itemIdList()) {
+            for (size_t i_item_1 = 0; i_item_1 < connecting_item_to_item_matrix[connecting_item_id].size();
+                 ++i_item_1) {
+              const ItemId layer_1_item_id = connecting_item_to_item_matrix[connecting_item_id][i_item_1];
+              if (layer_1_item_id != item_id) {
+                item_layer.add(layer_1_item_id, item_number[layer_1_item_id]);
+              }
+            }
+          }
+        }
+
+        for (auto layer_item_id : item_layer.itemIdList()) {
+          column_indices_vector.push_back(layer_item_id);
+        }
+        row_map[item_id + 1] = row_map[item_id] + item_layer.itemIdList().size();
+      }
+
+      if (row_map[row_map.size() - 1] != column_indices_vector.size()) {
+        throw UnexpectedError("incorrect stencil size");
+      }
+      Array<uint32_t> column_indices(row_map[row_map.size() - 1]);
+      column_indices.fill(std::numeric_limits<uint32_t>::max());
+
+      for (size_t i = 0; i < column_indices.size(); ++i) {
+        column_indices[i] = column_indices_vector[i];
+      }
+
+      return {ConnectivityMatrix{row_map, column_indices}, {}};
+    } else {
+      Array<uint32_t> row_map{connectivity.template numberOf<item_type>() + 1};
+      row_map[0] = 0;
+
+      std::vector<ItemId> column_indices_vector;
+
+      for (ItemId item_id = 0; item_id < connectivity.template numberOf<item_type>(); ++item_id) {
+        if (item_is_owned[item_id]) {
+          std::set<ItemId, std::function<bool(ItemId, ItemId)>> item_set(
+            [=](ItemId item_0, ItemId item_1) { return item_number[item_0] < item_number[item_1]; });
+
+          for (size_t i_connecting_item_1 = 0; i_connecting_item_1 < item_to_connecting_item_matrix[item_id].size();
+               ++i_connecting_item_1) {
+            const ConnectingItemId layer_1_connecting_item_id =
+              item_to_connecting_item_matrix[item_id][i_connecting_item_1];
+
+            for (size_t i_item_1 = 0; i_item_1 < connecting_item_to_item_matrix[layer_1_connecting_item_id].size();
+                 ++i_item_1) {
+              ItemId item_1_id = connecting_item_to_item_matrix[layer_1_connecting_item_id][i_item_1];
+
+              for (size_t i_connecting_item_2 = 0;
+                   i_connecting_item_2 < item_to_connecting_item_matrix[item_1_id].size(); ++i_connecting_item_2) {
+                const ConnectingItemId layer_2_connecting_item_id =
+                  item_to_connecting_item_matrix[item_1_id][i_connecting_item_2];
+
+                for (size_t i_item_2 = 0; i_item_2 < connecting_item_to_item_matrix[layer_2_connecting_item_id].size();
+                     ++i_item_2) {
+                  ItemId item_2_id = connecting_item_to_item_matrix[layer_2_connecting_item_id][i_item_2];
+
+                  if (item_2_id != item_id) {
+                    item_set.insert(item_2_id);
+                  }
+                }
+              }
+            }
+          }
+
+          for (auto stencil_item_id : item_set) {
+            column_indices_vector.push_back(stencil_item_id);
+          }
+          row_map[item_id + 1] = row_map[item_id] + item_set.size();
+        }
+      }
+
+      if (row_map[row_map.size() - 1] != column_indices_vector.size()) {
+        throw UnexpectedError("incorrect stencil size");
+      }
+
+      Array<uint32_t> column_indices(row_map[row_map.size() - 1]);
+      column_indices.fill(std::numeric_limits<uint32_t>::max());
+
+      for (size_t i = 0; i < column_indices.size(); ++i) {
+        column_indices[i] = column_indices_vector[i];
+      }
+      ConnectivityMatrix primal_stencil{row_map, column_indices};
+
+      return {primal_stencil, {}};
+    }
+  } else {
+    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<uint32_t> column_indices_vector;
+    std::vector<std::vector<uint32_t>> symmetry_column_indices_vector(symmetry_boundary_descriptor_list.size());
+
+    for (ItemId item_id = 0; item_id < connectivity.template numberOf<item_type>(); ++item_id) {
+      std::set<ItemId> item_set;
+      std::vector<std::set<ItemId>> by_boundary_symmetry_item(symmetry_boundary_descriptor_list.size());
+
+      if (item_is_owned[item_id]) {
+        auto item_to_connecting_item_list = item_to_connecting_item_matrix[item_id];
+        for (size_t i_connecting_item_of_item = 0; i_connecting_item_of_item < item_to_connecting_item_list.size();
+             ++i_connecting_item_of_item) {
+          const ConnectingItemId connecting_item_id_of_item = item_to_connecting_item_list[i_connecting_item_of_item];
+          auto connecting_item_to_item_list = connecting_item_to_item_matrix[connecting_item_id_of_item];
+          for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < connecting_item_to_item_list.size();
+               ++i_item_of_connecting_item) {
+            const ItemId item_id_of_connecting_item = connecting_item_to_item_list[i_item_of_connecting_item];
+            if (item_id != item_id_of_connecting_item) {
+              item_set.insert(item_id_of_connecting_item);
+            }
+          }
+        }
+
+        {
+          std::vector<ItemId> item_vector;
+          for (auto&& set_item_id : item_set) {
+            item_vector.push_back(set_item_id);
+          }
+          std::sort(item_vector.begin(), item_vector.end(),
+                    [&item_number](const ItemId& item0_id, const ItemId& item1_id) {
+                      return item_number[item0_id] < item_number[item1_id];
+                    });
+
+          for (auto&& vector_item_id : item_vector) {
+            column_indices_vector.push_back(vector_item_id);
+          }
+        }
+
+        for (size_t i = 0; i < symmetry_boundary_descriptor_list.size(); ++i) {
+          std::set<ItemId> symmetry_item_set;
+          for (size_t i_connecting_item_of_item = 0; i_connecting_item_of_item < item_to_connecting_item_list.size();
+               ++i_connecting_item_of_item) {
+            const ConnectingItemId connecting_item_id_of_item = item_to_connecting_item_list[i_connecting_item_of_item];
+            if (symmetry_item_list[connecting_item_id_of_item][i]) {
+              auto item_of_connecting_item_list = connecting_item_to_item_matrix[connecting_item_id_of_item];
+              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_set.insert(item_id_of_connecting_item);
+              }
+            }
+          }
+          by_boundary_symmetry_item[i] = symmetry_item_set;
+
+          std::vector<ItemId> item_vector;
+          for (auto&& set_item_id : symmetry_item_set) {
+            item_vector.push_back(set_item_id);
+          }
+          std::sort(item_vector.begin(), item_vector.end(),
+                    [&item_number](const ItemId& item0_id, const ItemId& item1_id) {
+                      return item_number[item0_id] < item_number[item1_id];
+                    });
+
+          for (auto&& vector_item_id : item_vector) {
+            symmetry_column_indices_vector[i].push_back(vector_item_id);
+          }
+        }
+      }
+      row_map[item_id + 1] = row_map[item_id] + item_set.size();
+
+      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] + by_boundary_symmetry_item[i].size();
+      }
+    }
+    ConnectivityMatrix primal_stencil{row_map, convert_to_array(column_indices_vector)};
+
+    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
+{
+  static_assert(source_item_type != target_item_type);
+  throw NotImplementedError("different source target");
+}
+
+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&, const StencilDescriptor&, const BoundaryDescriptorList&) const
+{
+  throw NotImplementedError("node to cell stencil");
+}
diff --git a/src/mesh/StencilBuilder.hpp b/src/mesh/StencilBuilder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f7ead00b618db999dd5386258ac058b664c65a7
--- /dev/null
+++ b/src/mesh/StencilBuilder.hpp
@@ -0,0 +1,99 @@
+#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 <typename ConnectivityType>
+  Array<const uint32_t> _getRowMap(const ConnectivityType& connectivity) const;
+
+  template <typename ConnectivityType>
+  Array<const uint32_t> _getColumnIndices(const ConnectivityType& connectivity,
+                                          const Array<const uint32_t>& row_map) const;
+
+  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..842f8858f9d1a571e28d3f0397f70e97682da0af
--- /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/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 bf529a9749bcf9907f6899da37e84b5c86d7b244..e26e157ec3c145d4d53397859a4ff6f4de1e0c3f 100644
--- a/src/scheme/CMakeLists.txt
+++ b/src/scheme/CMakeLists.txt
@@ -3,7 +3,6 @@
 add_library(
   PugsScheme
   AcousticSolver.cpp
-  HyperelasticSolver.cpp
   DiscreteFunctionIntegrator.cpp
   DiscreteFunctionInterpoler.cpp
   DiscreteFunctionUtils.cpp
@@ -32,6 +31,8 @@ add_library(
   EulerKineticSolverAcousticLagrangeFV.cpp
   EulerKineticSolverAcoustic2LagrangeFVOrder2.cpp
   FluxingAdvectionSolver.cpp
+  HyperelasticSolver.cpp
+  PolynomialReconstruction.cpp
 )
 
 target_link_libraries(
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..0d683c1783c9aad299a81dcc4d7ef3817470d54a
--- /dev/null
+++ b/src/scheme/DiscreteFunctionDPkVariant.hpp
@@ -0,0 +1,108 @@
+#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<2, const double>,
+                               DiscreteFunctionDPkVector<3, const double>>;
+
+ 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>,
+                  "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..2fefc078b192304ab0a80b63782986867e13c3d8
--- /dev/null
+++ b/src/scheme/DiscreteFunctionDPkVector.hpp
@@ -0,0 +1,211 @@
+#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_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/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 a2ebb2b256a3957169cf52457fd87a3d3236e8f3..d57cb9add43530ff8bbbaee3ff9e4786a61bb57c 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>
@@ -201,6 +202,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();
   }
 
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..2af32e157475b0fe99b9f2cc9cc6c445e05681b3
--- /dev/null
+++ b/src/scheme/PolynomialReconstruction.cpp
@@ -0,0 +1,1348 @@
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <algebra/Givens.hpp>
+#include <algebra/ShrinkMatrixView.hpp>
+#include <algebra/ShrinkVectorView.hpp>
+#include <algebra/SmallMatrix.hpp>
+#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
+#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <analysis/QuadratureFormula.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <geometry/CubeTransformation.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <geometry/PrismTransformation.hpp>
+#include <geometry/PyramidTransformation.hpp>
+#include <geometry/SquareTransformation.hpp>
+#include <geometry/TetrahedronTransformation.hpp>
+#include <geometry/TriangleTransformation.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>
+
+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_coordinates(const TinyVector<Dimension>& origin,
+                       const TinyVector<Dimension>& normal,
+                       const TinyVector<Dimension>& u)
+{
+  return u - 2 * dot(u - origin, normal) * normal;
+}
+
+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<2, double>,
+                               DiscreteFunctionDPkVector<3, double>>;
+
+ 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>,
+                  "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;
+};
+
+template <typename ConformTransformationT>
+void
+_computeEjkMean(const QuadratureFormula<1>& quadrature,
+                const ConformTransformationT& T,
+                const TinyVector<1>& Xj,
+                const double Vi,
+                const size_t degree,
+                const size_t dimension,
+                SmallArray<double>& inv_Vj_wq_detJ_ek,
+                SmallArray<double>& mean_of_ejk)
+{
+  using Rd = TinyVector<1>;
+  mean_of_ejk.fill(0);
+
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq   = quadrature.weight(i_q);
+    const Rd& xi_q    = quadrature.point(i_q);
+    const double detT = T.jacobianDeterminant();
+
+    const Rd X_Xj = T(xi_q) - Xj;
+
+    const double x_xj = X_Xj[0];
+
+    {
+      size_t k               = 0;
+      inv_Vj_wq_detJ_ek[k++] = wq * detT / Vi;
+      for (; k <= degree; ++k) {
+        inv_Vj_wq_detJ_ek[k] = x_xj * inv_Vj_wq_detJ_ek[k - 1];
+      }
+    }
+
+    for (size_t k = 1; k < dimension; ++k) {
+      mean_of_ejk[k - 1] += inv_Vj_wq_detJ_ek[k];
+    }
+  }
+}
+
+template <typename ConformTransformationT>
+void
+_computeEjkMean(const QuadratureFormula<2>& quadrature,
+                const ConformTransformationT& T,
+                const TinyVector<2>& Xj,
+                const double Vi,
+                const size_t degree,
+                const size_t dimension,
+                SmallArray<double>& inv_Vj_wq_detJ_ek,
+                SmallArray<double>& mean_of_ejk)
+{
+  using Rd = TinyVector<2>;
+
+  mean_of_ejk.fill(0);
+
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq   = quadrature.weight(i_q);
+    const Rd& xi_q    = quadrature.point(i_q);
+    const double detT = [&] {
+      if constexpr (std::is_same_v<TriangleTransformation<2>, std::decay_t<decltype(T)>>) {
+        return T.jacobianDeterminant();
+      } else {
+        return T.jacobianDeterminant(xi_q);
+      }
+    }();
+
+    const Rd X_Xj = T(xi_q) - Xj;
+
+    const double x_xj = X_Xj[0];
+    const double y_yj = X_Xj[1];
+
+    {
+      size_t k               = 0;
+      inv_Vj_wq_detJ_ek[k++] = wq * detT / Vi;
+      for (; k <= degree; ++k) {
+        inv_Vj_wq_detJ_ek[k] = x_xj * inv_Vj_wq_detJ_ek[k - 1];
+      }
+
+      for (size_t i_y = 1; i_y <= degree; ++i_y) {
+        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
+        for (size_t l = 0; l <= degree - i_y; ++l) {
+          inv_Vj_wq_detJ_ek[k++] = y_yj * inv_Vj_wq_detJ_ek[begin_i_y_1 + l];
+        }
+      }
+    }
+
+    for (size_t k = 1; k < dimension; ++k) {
+      mean_of_ejk[k - 1] += inv_Vj_wq_detJ_ek[k];
+    }
+  }
+}
+
+template <typename ConformTransformationT>
+void
+_computeEjkMean(const QuadratureFormula<3>& quadrature,
+                const ConformTransformationT& T,
+                const TinyVector<3>& Xj,
+                const double Vi,
+                const size_t degree,
+                const size_t dimension,
+                SmallArray<double>& inv_Vj_wq_detJ_ek,
+                SmallArray<double>& mean_of_ejk)
+{
+  using Rd = TinyVector<3>;
+  mean_of_ejk.fill(0);
+
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq   = quadrature.weight(i_q);
+    const Rd& xi_q    = quadrature.point(i_q);
+    const double detT = [&] {
+      if constexpr (std::is_same_v<TetrahedronTransformation, std::decay_t<decltype(T)>>) {
+        return T.jacobianDeterminant();
+      } else {
+        return T.jacobianDeterminant(xi_q);
+      }
+    }();
+
+    const Rd X_Xj = T(xi_q) - Xj;
+
+    const double x_xj = X_Xj[0];
+    const double y_yj = X_Xj[1];
+    const double z_zj = X_Xj[2];
+
+    {
+      size_t k               = 0;
+      inv_Vj_wq_detJ_ek[k++] = wq * detT / Vi;
+      for (; k <= degree; ++k) {
+        inv_Vj_wq_detJ_ek[k] = x_xj * inv_Vj_wq_detJ_ek[k - 1];
+      }
+
+      for (size_t i_y = 1; i_y <= degree; ++i_y) {
+        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
+        for (size_t l = 0; l <= degree - i_y; ++l) {
+          inv_Vj_wq_detJ_ek[k++] = y_yj * inv_Vj_wq_detJ_ek[begin_i_y_1 + l];
+        }
+      }
+
+      for (size_t i_z = 1; i_z <= degree; ++i_z) {
+        const size_t begin_i_z_1 = ((i_z - 1) * (3 * (degree + 2) * (degree + 3) + (i_z - (3 * degree + 8)) * i_z)) / 6;
+        for (size_t i_y = 0; i_y <= degree - i_z; ++i_y) {
+          const size_t begin_i_y_1 = (i_y * (2 * degree - i_y + 3)) / 2 + begin_i_z_1;
+          for (size_t l = 0; l <= degree - i_y - i_z; ++l) {
+            inv_Vj_wq_detJ_ek[k++] = z_zj * inv_Vj_wq_detJ_ek[begin_i_y_1 + l];
+          }
+        }
+      }
+    }
+
+    for (size_t k = 1; k < dimension; ++k) {
+      mean_of_ejk[k - 1] += inv_Vj_wq_detJ_ek[k];
+    }
+  }
+}
+
+template <typename NodeListT, size_t Dimension>
+void _computeEjkMean(const TinyVector<Dimension>& Xj,
+                     const NodeValue<const TinyVector<Dimension>>& xr,
+                     const NodeListT& node_list,
+                     const CellType& cell_type,
+                     const double Vi,
+                     const size_t degree,
+                     const size_t basis_dimension,
+                     SmallArray<double>& inv_Vj_wq_detJ_ek,
+                     SmallArray<double>& mean_of_ejk);
+
+template <typename NodeListT>
+void
+_computeEjkMean(const TinyVector<1>& Xj,
+                const NodeValue<const TinyVector<1>>& xr,
+                const NodeListT& node_list,
+                const CellType& cell_type,
+                const double Vi,
+                const size_t degree,
+                const size_t basis_dimension,
+                SmallArray<double>& inv_Vj_wq_detJ_ek,
+                SmallArray<double>& mean_of_ejk)
+{
+  if (cell_type == CellType::Line) {
+    const LineTransformation<1> T{xr[node_list[0]], xr[node_list[1]]};
+
+    const auto& quadrature = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{degree});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else {
+    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+  }
+}
+
+template <typename NodeListT>
+void
+_computeEjkMeanInSymmetricCell(const TinyVector<1>& origin,
+                               const TinyVector<1>& normal,
+                               const TinyVector<1>& Xj,
+                               const NodeValue<const TinyVector<1>>& xr,
+                               const NodeListT& node_list,
+                               const CellType& cell_type,
+                               const double Vi,
+                               const size_t degree,
+                               const size_t basis_dimension,
+                               SmallArray<double>& inv_Vj_wq_detJ_ek,
+                               SmallArray<double>& mean_of_ejk)
+{
+  if (cell_type == CellType::Line) {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+
+    const LineTransformation<1> T{x0, x1};
+
+    const auto& quadrature = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{degree});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else {
+    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+  }
+}
+
+template <typename ConformTransformationT>
+void _computeEjkBoundaryMean(const QuadratureFormula<1>& quadrature,
+                             const ConformTransformationT& T,
+                             const TinyVector<2>& Xj,
+                             const double Vi,
+                             const size_t degree,
+                             const size_t dimension,
+                             SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
+                             SmallArray<double>& mean_of_ejk);
+
+template <>
+void
+_computeEjkBoundaryMean(const QuadratureFormula<1>& quadrature,
+                        const LineTransformation<2>& T,
+                        const TinyVector<2>& Xj,
+                        const double Vi,
+                        const size_t degree,
+                        const size_t dimension,
+                        SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
+                        SmallArray<double>& mean_of_ejk)
+{
+  using Rd = TinyVector<2>;
+
+  const double velocity_perp_e1 = T.velocity()[1];
+
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq          = quadrature.weight(i_q);
+    const TinyVector<1> xi_q = quadrature.point(i_q);
+
+    const Rd X_Xj = T(xi_q) - Xj;
+
+    const double x_xj = X_Xj[0];
+    const double y_yj = X_Xj[1];
+
+    {
+      size_t k                                 = 0;
+      inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = x_xj * wq * velocity_perp_e1 / Vi;
+      for (; k <= degree; ++k) {
+        inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k] =
+          x_xj * inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k - 1] * ((1. * k) / (k + 1));
+      }
+
+      for (size_t i_y = 1; i_y <= degree; ++i_y) {
+        const size_t begin_i_y_1 = ((i_y - 1) * (2 * degree - i_y + 4)) / 2;
+        for (size_t l = 0; l <= degree - i_y; ++l) {
+          inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k++] = y_yj * inv_Vj_alpha_p_1_wq_X_prime_orth_ek[begin_i_y_1 + l];
+        }
+      }
+    }
+
+    for (size_t k = 1; k < dimension; ++k) {
+      mean_of_ejk[k - 1] += inv_Vj_alpha_p_1_wq_X_prime_orth_ek[k];
+    }
+  }
+}
+
+template <MeshConcept MeshType>
+void
+_computeEjkMeanByBoundary(const MeshType& mesh,
+                          const TinyVector<2>& Xj,
+                          const CellId& cell_id,
+                          const auto& cell_to_face_matrix,
+                          const auto& face_to_node_matrix,
+                          const auto& cell_face_is_reversed,
+                          const CellValue<const double>& Vi,
+                          const size_t degree,
+                          const size_t basis_dimension,
+                          SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
+                          SmallArray<double>& mean_of_ejk)
+{
+  const auto& quadrature = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(degree + 1));
+
+  auto xr = mesh.xr();
+  mean_of_ejk.fill(0);
+  auto cell_face_list = cell_to_face_matrix[cell_id];
+  for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+    bool is_reversed = cell_face_is_reversed[cell_id][i_face];
+
+    const FaceId face_id = cell_face_list[i_face];
+    auto face_node_list  = face_to_node_matrix[face_id];
+    if (is_reversed) {
+      const LineTransformation<2> T{xr[face_node_list[1]], xr[face_node_list[0]]};
+      _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
+                              inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
+    } else {
+      const LineTransformation<2> T{xr[face_node_list[0]], xr[face_node_list[1]]};
+      _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
+                              inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
+    }
+  }
+}
+
+void
+_computeEjkMeanByBoundaryInSymmetricCell(const TinyVector<2>& origin,
+                                         const TinyVector<2>& normal,
+                                         const TinyVector<2>& Xj,
+                                         const CellId& cell_id,
+                                         const NodeValue<const TinyVector<2>>& xr,
+                                         const auto& cell_to_face_matrix,
+                                         const auto& face_to_node_matrix,
+                                         const auto& cell_face_is_reversed,
+                                         const CellValue<const double>& Vi,
+                                         const size_t degree,
+                                         const size_t basis_dimension,
+                                         SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
+                                         SmallArray<double>& mean_of_ejk)
+{
+  const auto& quadrature = QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor(degree + 1));
+
+  mean_of_ejk.fill(0);
+  auto cell_face_list = cell_to_face_matrix[cell_id];
+  for (size_t i_face = 0; i_face < cell_face_list.size(); ++i_face) {
+    bool is_reversed = cell_face_is_reversed[cell_id][i_face];
+
+    const FaceId face_id = cell_face_list[i_face];
+    auto face_node_list  = face_to_node_matrix[face_id];
+
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[face_node_list[1]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[face_node_list[0]]);
+
+    if (is_reversed) {
+      const LineTransformation<2> T{x1, x0};
+      _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
+                              inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
+    } else {
+      const LineTransformation<2> T{x0, x1};
+      _computeEjkBoundaryMean(quadrature, T, Xj, Vi[cell_id], degree, basis_dimension,
+                              inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_of_ejk);
+    }
+  }
+}
+
+template <typename NodeListT>
+void
+_computeEjkMean(const TinyVector<2>& Xj,
+                const NodeValue<const TinyVector<2>>& xr,
+                const NodeListT& node_list,
+                const CellType& cell_type,
+                const double Vi,
+                const size_t degree,
+                const size_t basis_dimension,
+                SmallArray<double>& inv_Vj_wq_detJ_ek,
+                SmallArray<double>& mean_of_ejk)
+{
+  switch (cell_type) {
+  case CellType::Triangle: {
+    const TriangleTransformation<2> T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]]};
+    const auto& quadrature = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{degree});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+    break;
+  }
+  case CellType::Quadrangle: {
+    const SquareTransformation<2> T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]]};
+    const auto& quadrature =
+      QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+    break;
+  }
+  default: {
+    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+  }
+  }
+}
+
+template <typename NodeListT>
+void
+_computeEjkMeanInSymmetricCell(const TinyVector<2>& origin,
+                               const TinyVector<2>& normal,
+                               const TinyVector<2>& Xj,
+                               const NodeValue<const TinyVector<2>>& xr,
+                               const NodeListT& node_list,
+                               const CellType& cell_type,
+                               const double Vi,
+                               const size_t degree,
+                               const size_t basis_dimension,
+                               SmallArray<double>& inv_Vj_wq_detJ_ek,
+                               SmallArray<double>& mean_of_ejk)
+{
+  switch (cell_type) {
+  case CellType::Triangle: {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+
+    const TriangleTransformation<2> T{x0, x1, x2};
+    const auto& quadrature = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{degree});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+    break;
+  }
+  case CellType::Quadrangle: {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
+    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+
+    const SquareTransformation<2> T{x0, x1, x2, x3};
+    const auto& quadrature =
+      QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+    break;
+  }
+  default: {
+    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+  }
+  }
+}
+
+template <typename NodeListT>
+void
+_computeEjkMean(const TinyVector<3>& Xj,
+                const NodeValue<const TinyVector<3>>& xr,
+                const NodeListT& node_list,
+                const CellType& cell_type,
+                const double Vi,
+                const size_t degree,
+                const size_t basis_dimension,
+                SmallArray<double>& inv_Vj_wq_detJ_ek,
+                SmallArray<double>& mean_of_ejk)
+{
+  if (cell_type == CellType::Tetrahedron) {
+    const TetrahedronTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]]};
+
+    const auto& quadrature = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{degree});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else if (cell_type == CellType::Prism) {
+    const PrismTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]],   //
+                                xr[node_list[3]], xr[node_list[4]], xr[node_list[5]]};
+
+    const auto& quadrature = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else if (cell_type == CellType::Pyramid) {
+    const PyramidTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]],
+                                  xr[node_list[4]]};
+
+    const auto& quadrature = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else if (cell_type == CellType::Hexahedron) {
+    const CubeTransformation T{xr[node_list[0]], xr[node_list[1]], xr[node_list[2]], xr[node_list[3]],
+                               xr[node_list[4]], xr[node_list[5]], xr[node_list[6]], xr[node_list[7]]};
+
+    const auto& quadrature =
+      QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else {
+    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+  }
+}
+
+template <typename NodeListT>
+void
+_computeEjkMeanInSymmetricCell(const TinyVector<3>& origin,
+                               const TinyVector<3>& normal,
+                               const TinyVector<3>& Xj,
+                               const NodeValue<const TinyVector<3>>& xr,
+                               const NodeListT& node_list,
+                               const CellType& cell_type,
+                               const double Vi,
+                               const size_t degree,
+                               const size_t basis_dimension,
+                               SmallArray<double>& inv_Vj_wq_detJ_ek,
+                               SmallArray<double>& mean_of_ejk)
+{
+  if (cell_type == CellType::Tetrahedron) {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
+    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
+
+    const TetrahedronTransformation T{x0, x1, x2, x3};
+
+    const auto& quadrature = QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{degree});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else if (cell_type == CellType::Prism) {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
+    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[4]]);
+    const auto x4 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
+    const auto x5 = symmetrize_coordinates(origin, normal, xr[node_list[5]]);
+
+    const PrismTransformation T{x0, x1, x2,   //
+                                x3, x4, x5};
+
+    const auto& quadrature = QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else if (cell_type == CellType::Pyramid) {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
+    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+    const auto x4 = symmetrize_coordinates(origin, normal, xr[node_list[4]]);
+    const PyramidTransformation T{x0, x1, x2, x3, x4};
+
+    const auto& quadrature = QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else if (cell_type == CellType::Hexahedron) {
+    const auto x0 = symmetrize_coordinates(origin, normal, xr[node_list[3]]);
+    const auto x1 = symmetrize_coordinates(origin, normal, xr[node_list[2]]);
+    const auto x2 = symmetrize_coordinates(origin, normal, xr[node_list[1]]);
+    const auto x3 = symmetrize_coordinates(origin, normal, xr[node_list[0]]);
+    const auto x4 = symmetrize_coordinates(origin, normal, xr[node_list[7]]);
+    const auto x5 = symmetrize_coordinates(origin, normal, xr[node_list[6]]);
+    const auto x6 = symmetrize_coordinates(origin, normal, xr[node_list[5]]);
+    const auto x7 = symmetrize_coordinates(origin, normal, xr[node_list[4]]);
+
+    const CubeTransformation T{x0, x1, x2, x3, x4, x5, x6, x7};
+
+    const auto& quadrature =
+      QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{degree + 1});
+
+    _computeEjkMean(quadrature, T, Xj, Vi, degree, basis_dimension, inv_Vj_wq_detJ_ek, mean_of_ejk);
+
+  } else {
+    throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+  }
+}
+
+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_same_v<data_type, double>) {
+            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>) {
+          return discrete_function.size();
+        } 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>
+[[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
+{
+  const MeshType& mesh = *p_mesh;
+
+  using Rd = TinyVector<MeshType::Dimension>;
+
+  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 cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+  auto cell_to_face_matrix = mesh.connectivity().cellToFaceMatrix();
+
+  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());
+
+  SmallArray<SmallArray<double>> inv_Vj_wq_detJ_ek_pool(Kokkos::DefaultExecutionSpace::concurrency());
+  SmallArray<SmallArray<double>> mean_j_of_ejk_pool(Kokkos::DefaultExecutionSpace::concurrency());
+  SmallArray<SmallArray<double>> mean_i_of_ejk_pool(Kokkos::DefaultExecutionSpace::concurrency());
+
+  SmallArray<SmallArray<double>> inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool(Kokkos::DefaultExecutionSpace::concurrency());
+  SmallArray<SmallArray<double>> mean_l_of_ejk_pool(Kokkos::DefaultExecutionSpace::concurrency());
+
+  for (size_t i = 0; i < A_pool.size(); ++i) {
+    A_pool[i] = SmallMatrix<double>(max_stencil_size, basis_dimension - 1);
+    B_pool[i] = SmallMatrix<double>(max_stencil_size, number_of_columns);
+    G_pool[i] = SmallVector<double>(basis_dimension - 1);
+    X_pool[i] = SmallMatrix<double>(basis_dimension - 1, number_of_columns);
+
+    inv_Vj_wq_detJ_ek_pool[i] = SmallArray<double>(basis_dimension);
+    mean_j_of_ejk_pool[i]     = SmallArray<double>(basis_dimension - 1);
+    mean_i_of_ejk_pool[i]     = SmallArray<double>(basis_dimension - 1);
+
+    inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool[i] = SmallArray<double>(basis_dimension);
+  }
+
+  parallel_for(
+    mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_j_id) {
+      if (cell_is_owned[cell_j_id]) {
+        const int32_t t = tokens.acquire();
+
+        auto stencil_cell_list = stencil_array[cell_j_id];
+
+        ShrinkMatrixView B(B_pool[t], full_stencil_size(cell_j_id));
+
+        size_t column_begin = 0;
+        for (size_t i_discrete_function_variant = 0;
+             i_discrete_function_variant < discrete_function_variant_list.size(); ++i_discrete_function_variant) {
+          const auto& discrete_function_variant = discrete_function_variant_list[i_discrete_function_variant];
+
+          std::visit(
+            [&](auto&& discrete_function) {
+              using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
+              if constexpr (is_discrete_function_P0_v<DiscreteFunctionT>) {
+                using DataType     = std::decay_t<typename DiscreteFunctionT::data_type>;
+                const DataType& qj = discrete_function[cell_j_id];
+                size_t index       = 0;
+                for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                  const CellId cell_i_id = stencil_cell_list[i];
+                  const DataType& qi_qj  = discrete_function[cell_i_id] - qj;
+                  if constexpr (std::is_arithmetic_v<DataType>) {
+                    B(index, column_begin) = qi_qj;
+                  } else if constexpr (is_tiny_vector_v<DataType>) {
+                    for (size_t kB = column_begin, k = 0; k < DataType::Dimension; ++k, ++kB) {
+                      B(index, kB) = qi_qj[k];
+                    }
+                  } else if constexpr (is_tiny_matrix_v<DataType>) {
+                    for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                      for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                        B(index, column_begin + k * DataType::NumberOfColumns + l) = qi_qj(k, l);
+                      }
+                    }
+                  }
+                }
+
+                for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                     ++i_symmetry) {
+                  auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                  auto ghost_cell_list = ghost_stencil[cell_j_id];
+                  for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                    const CellId cell_i_id = ghost_cell_list[i];
+                    if constexpr (std::is_arithmetic_v<DataType>) {
+                      const DataType& qi_qj  = discrete_function[cell_i_id] - qj;
+                      B(index, column_begin) = qi_qj;
+                    } else if constexpr (is_tiny_vector_v<DataType>) {
+                      if constexpr (DataType::Dimension == MeshType::Dimension) {
+                        const Rd& normal = symmetry_normal_list[i_symmetry];
+
+                        const DataType& qi    = discrete_function[cell_i_id];
+                        const DataType& qi_qj = symmetrize_vector(normal, qi) - qj;
+                        for (size_t kB = column_begin, k = 0; k < DataType::Dimension; ++k, ++kB) {
+                          B(index, kB) = qi_qj[k];
+                        }
+                      } else {
+                        const DataType& qi_qj = discrete_function[cell_i_id] - qj;
+                        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>) {
+                      if constexpr ((DataType::NumberOfColumns == DataType::NumberOfRows) and
+                                    (DataType::NumberOfColumns == MeshType::Dimension)) {
+                        throw NotImplementedError("TinyMatrix symmetries for reconstruction");
+                      }
+                      const DataType& qi_qj = discrete_function[cell_i_id] - qj;
+                      for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                        for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                          B(index, column_begin + k * DataType::NumberOfColumns + l) = qi_qj(k, l);
+                        }
+                      }
+                    }
+                  }
+                }
+
+                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];
+                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;
+                    }
+                  }
+                }
+                column_begin += qj_vector.size();
+              } else {
+                // LCOV_EXCL_START
+                throw UnexpectedError("invalid discrete function type");
+                // LCOV_EXCL_STOP
+              }
+            },
+            discrete_function_variant->discreteFunction());
+        }
+
+        ShrinkMatrixView A(A_pool[t], full_stencil_size(cell_j_id));
+
+        if ((m_descriptor.degree() == 1) and
+            (m_descriptor.integrationMethodType() == IntegrationMethodType::cell_center)) {
+          const Rd& Xj = xj[cell_j_id];
+          size_t index = 0;
+          for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+            const CellId cell_i_id = stencil_cell_list[i];
+            const Rd Xi_Xj         = xj[cell_i_id] - Xj;
+            for (size_t l = 0; l < basis_dimension - 1; ++l) {
+              A(index, l) = Xi_Xj[l];
+            }
+          }
+          for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+               ++i_symmetry) {
+            auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+            auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+            const Rd& origin = symmetry_origin_list[i_symmetry];
+            const Rd& normal = symmetry_normal_list[i_symmetry];
+
+            for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+              const CellId cell_i_id = ghost_cell_list[i];
+              const Rd Xi_Xj         = symmetrize_coordinates(origin, normal, xj[cell_i_id]) - Xj;
+              for (size_t l = 0; l < basis_dimension - 1; ++l) {
+                A(index, l) = Xi_Xj[l];
+              }
+            }
+          }
+
+        } else if ((m_descriptor.integrationMethodType() == IntegrationMethodType::element) or
+                   (m_descriptor.integrationMethodType() == IntegrationMethodType::boundary)) {
+          if ((m_descriptor.integrationMethodType() == IntegrationMethodType::boundary) and
+              (MeshType::Dimension == 2)) {
+            if constexpr (MeshType::Dimension == 2) {
+              SmallArray<double>& inv_Vj_alpha_p_1_wq_X_prime_orth_ek = inv_Vj_alpha_p_1_wq_X_prime_orth_ek_pool[t];
+              SmallArray<double>& mean_j_of_ejk                       = mean_j_of_ejk_pool[t];
+              SmallArray<double>& mean_i_of_ejk                       = mean_i_of_ejk_pool[t];
+
+              auto face_to_node_matrix   = p_mesh->connectivity().faceToNodeMatrix();
+              auto cell_face_is_reversed = p_mesh->connectivity().cellFaceIsReversed();
+              const Rd& Xj               = xj[cell_j_id];
+
+              _computeEjkMeanByBoundary(*p_mesh, Xj, cell_j_id, cell_to_face_matrix, face_to_node_matrix,
+                                        cell_face_is_reversed, Vj, m_descriptor.degree(), basis_dimension,
+                                        inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_j_of_ejk);
+
+              size_t index = 0;
+              for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                const CellId cell_i_id = stencil_cell_list[i];
+
+                _computeEjkMeanByBoundary(*p_mesh, Xj, cell_i_id, cell_to_face_matrix, face_to_node_matrix,
+                                          cell_face_is_reversed, Vj, m_descriptor.degree(), basis_dimension,
+                                          inv_Vj_alpha_p_1_wq_X_prime_orth_ek, mean_i_of_ejk);
+
+                for (size_t l = 0; l < basis_dimension - 1; ++l) {
+                  A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
+                }
+              }
+              for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                   ++i_symmetry) {
+                auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+                const Rd& origin = symmetry_origin_list[i_symmetry];
+                const Rd& normal = symmetry_normal_list[i_symmetry];
+
+                for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                  const CellId cell_i_id = ghost_cell_list[i];
+
+                  _computeEjkMeanByBoundaryInSymmetricCell(origin, normal,   //
+                                                           Xj, cell_i_id, xr, cell_to_face_matrix, face_to_node_matrix,
+                                                           cell_face_is_reversed, Vj, m_descriptor.degree(),
+                                                           basis_dimension, inv_Vj_alpha_p_1_wq_X_prime_orth_ek,
+                                                           mean_i_of_ejk);
+
+                  for (size_t l = 0; l < basis_dimension - 1; ++l) {
+                    A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
+                  }
+                }
+              }
+            } else {
+              throw UnexpectedError("invalid mesh dimension");
+            }
+          } else {
+            SmallArray<double>& inv_Vj_wq_detJ_ek = inv_Vj_wq_detJ_ek_pool[t];
+            SmallArray<double>& mean_j_of_ejk     = mean_j_of_ejk_pool[t];
+            SmallArray<double>& mean_i_of_ejk     = mean_i_of_ejk_pool[t];
+
+            const Rd& Xj = xj[cell_j_id];
+
+            _computeEjkMean(Xj, xr, cell_to_node_matrix[cell_j_id], cell_type[cell_j_id], Vj[cell_j_id],
+                            m_descriptor.degree(), basis_dimension, inv_Vj_wq_detJ_ek, mean_j_of_ejk);
+
+            size_t index = 0;
+            for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+              const CellId cell_i_id = stencil_cell_list[i];
+
+              _computeEjkMean(Xj, xr, cell_to_node_matrix[cell_i_id], cell_type[cell_i_id], Vj[cell_i_id],
+                              m_descriptor.degree(), basis_dimension, inv_Vj_wq_detJ_ek, mean_i_of_ejk);
+
+              for (size_t l = 0; l < basis_dimension - 1; ++l) {
+                A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
+              }
+            }
+            for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                 ++i_symmetry) {
+              auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+              auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+              const Rd& origin = symmetry_origin_list[i_symmetry];
+              const Rd& normal = symmetry_normal_list[i_symmetry];
+
+              for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                const CellId cell_i_id = ghost_cell_list[i];
+
+                _computeEjkMeanInSymmetricCell(origin, normal,   //
+                                               Xj, xr, cell_to_node_matrix[cell_i_id], cell_type[cell_i_id],
+                                               Vj[cell_i_id], m_descriptor.degree(), basis_dimension, inv_Vj_wq_detJ_ek,
+                                               mean_i_of_ejk);
+
+                for (size_t l = 0; l < basis_dimension - 1; ++l) {
+                  A(index, l) = mean_i_of_ejk[l] - mean_j_of_ejk[l];
+                }
+              }
+            }
+          }
+        } else {
+          throw UnexpectedError("invalid integration strategy");
+        }
+
+        if (m_descriptor.rowWeighting()) {
+          // Add row weighting (give more importance to cells that are
+          // closer to j)
+          const Rd& Xj = xj[cell_j_id];
+
+          size_t index = 0;
+          for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+            const CellId cell_i_id = stencil_cell_list[i];
+            const double weight    = 1. / l2Norm(xj[cell_i_id] - Xj);
+            for (size_t l = 0; l < basis_dimension - 1; ++l) {
+              A(index, l) *= weight;
+            }
+            for (size_t l = 0; l < number_of_columns; ++l) {
+              B(index, l) *= weight;
+            }
+          }
+          for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+               ++i_symmetry) {
+            auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+            auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+            const Rd& origin = symmetry_origin_list[i_symmetry];
+            const Rd& normal = symmetry_normal_list[i_symmetry];
+
+            for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+              const CellId cell_i_id = ghost_cell_list[i];
+              const double weight    = 1. / l2Norm(symmetrize_coordinates(origin, normal, xj[cell_i_id]) - Xj);
+              for (size_t l = 0; l < basis_dimension - 1; ++l) {
+                A(index, l) *= weight;
+              }
+              for (size_t l = 0; l < number_of_columns; ++l) {
+                B(index, l) *= weight;
+              }
+            }
+          }
+        }
+
+        const SmallMatrix<double>& X = X_pool[t];
+
+        if (m_descriptor.preconditioning()) {
+          // Add column  weighting preconditioning (increase the presition)
+          SmallVector<double>& G = G_pool[t];
+
+          for (size_t l = 0; l < A.numberOfColumns(); ++l) {
+            double g = 0;
+            for (size_t i = 0; i < A.numberOfRows(); ++i) {
+              const double Ail = A(i, l);
+
+              g += Ail * Ail;
+            }
+            G[l] = std::sqrt(g);
+          }
+
+          for (size_t l = 0; l < A.numberOfColumns(); ++l) {
+            const double Gl = G[l];
+            for (size_t i = 0; i < A.numberOfRows(); ++i) {
+              A(i, l) *= Gl;
+            }
+          }
+
+          Givens::solveCollectionInPlace(A, X, B);
+
+          for (size_t l = 0; l < X.numberOfRows(); ++l) {
+            const double Gl = G[l];
+            for (size_t i = 0; i < X.numberOfColumns(); ++i) {
+              X(l, i) *= Gl;
+            }
+          }
+        } else {
+          Givens::solveCollectionInPlace(A, X, B);
+        }
+
+        column_begin = 0;
+        for (size_t i_dpk_variant = 0; i_dpk_variant < mutable_discrete_function_dpk_variant_list.size();
+             ++i_dpk_variant) {
+          const auto& dpk_variant = mutable_discrete_function_dpk_variant_list[i_dpk_variant];
+
+          const auto& discrete_function_variant = discrete_function_variant_list[i_dpk_variant];
+
+          std::visit(
+            [&](auto&& dpk_function, auto&& p0_function) {
+              using DPkFunctionT = std::decay_t<decltype(dpk_function)>;
+              using P0FunctionT  = std::decay_t<decltype(p0_function)>;
+              using DataType     = std::remove_const_t<std::decay_t<typename DPkFunctionT::data_type>>;
+              using P0DataType   = std::remove_const_t<std::decay_t<typename P0FunctionT::data_type>>;
+
+              if constexpr (std::is_same_v<DataType, P0DataType>) {
+                if constexpr (is_discrete_function_P0_v<P0FunctionT>) {
+                  if constexpr (is_discrete_function_dpk_scalar_v<DPkFunctionT>) {
+                    auto dpk_j = dpk_function.coefficients(cell_j_id);
+                    dpk_j[0]   = p0_function[cell_j_id];
+
+                    if constexpr (std::is_arithmetic_v<DataType>) {
+                      if (m_descriptor.degree() > 1) {
+                        auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
+                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                          dpk_j[0] -= X(i, column_begin) * mean_j_of_ejk[i];
+                        }
+                      }
+
+                      for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                        auto& dpk_j_ip1 = dpk_j[i + 1];
+                        dpk_j_ip1       = X(i, column_begin);
+                      }
+                      ++column_begin;
+                    } else if constexpr (is_tiny_vector_v<DataType>) {
+                      if (m_descriptor.degree() > 1) {
+                        auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
+                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                          auto& dpk_j_0 = dpk_j[0];
+                          for (size_t k = 0; k < DataType::Dimension; ++k) {
+                            dpk_j_0[k] -= X(i, column_begin + k) * mean_j_of_ejk[i];
+                          }
+                        }
+                      }
+
+                      for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                        auto& dpk_j_ip1 = dpk_j[i + 1];
+                        for (size_t k = 0; k < DataType::Dimension; ++k) {
+                          dpk_j_ip1[k] = X(i, column_begin + k);
+                        }
+                      }
+                      column_begin += DataType::Dimension;
+                    } else if constexpr (is_tiny_matrix_v<DataType>) {
+                      if (m_descriptor.degree() > 1) {
+                        auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
+                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                          auto& dpk_j_0 = dpk_j[0];
+                          for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                            for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                              dpk_j_0(k, l) -=
+                                X(i, column_begin + k * DataType::NumberOfColumns + l) * mean_j_of_ejk[i];
+                            }
+                          }
+                        }
+                      }
+
+                      for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                        auto& dpk_j_ip1 = dpk_j[i + 1];
+                        for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                          for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                            dpk_j_ip1(k, l) = X(i, column_begin + k * DataType::NumberOfColumns + l);
+                          }
+                        }
+                      }
+                      column_begin += DataType::Dimension;
+                    } else {
+                      // LCOV_EXCL_START
+                      throw UnexpectedError("unexpected data type");
+                      // LCOV_EXCL_STOP
+                    }
+                  } else {
+                    // LCOV_EXCL_START
+                    throw UnexpectedError("unexpected discrete dpk function type");
+                    // LCOV_EXCL_STOP
+                  }
+                } else if constexpr (is_discrete_function_P0_vector_v<P0FunctionT>) {
+                  if constexpr (is_discrete_function_dpk_vector_v<DPkFunctionT>) {
+                    auto dpk_j        = dpk_function.coefficients(cell_j_id);
+                    auto cell_vector  = p0_function[cell_j_id];
+                    const size_t size = basis_dimension;
+
+                    for (size_t l = 0; l < cell_vector.size(); ++l) {
+                      const size_t component_begin = l * size;
+                      dpk_j[component_begin]       = cell_vector[l];
+                      if constexpr (std::is_arithmetic_v<DataType>) {
+                        if (m_descriptor.degree() > 1) {
+                          auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
+                          for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                            dpk_j[component_begin] -= X(i, column_begin) * mean_j_of_ejk[i];
+                          }
+                        }
+
+                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                          auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
+                          dpk_j_ip1       = X(i, column_begin);
+                        }
+                        ++column_begin;
+                      } else {
+                        // 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());
+        }
+
+        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;
+}
+
+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..b2b855271d1ed4140ebb51d1b21314602caa0dc6
--- /dev/null
+++ b/src/scheme/PolynomialReconstruction.hpp
@@ -0,0 +1,63 @@
+#ifndef POLYNOMIAL_RECONSTRUCTION_HPP
+#define POLYNOMIAL_RECONSTRUCTION_HPP
+
+#include <mesh/MeshTraits.hpp>
+#include <scheme/PolynomialReconstructionDescriptor.hpp>
+
+class DiscreteFunctionDPkVariant;
+class DiscreteFunctionVariant;
+
+class PolynomialReconstruction
+{
+ private:
+  class MutableDiscreteFunctionDPkVariant;
+
+  const PolynomialReconstructionDescriptor m_descriptor;
+
+  size_t _getNumberOfColumns(
+    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 <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/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 f720252fdd7c04b1ddd8e234d890bf5b1956c899..8b7e0a339fd91c3569901ba0d179b0713b6ea391 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;
@@ -32,6 +40,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
   static GlobalVariableManager&
   instance()
diff --git a/src/utils/PugsTraits.hpp b/src/utils/PugsTraits.hpp
index 2e1d409578c9295a1ad032466842c2f336ba272f..cb8f154e3b344ae34bb2b769b5a0419e37d44e4a 100644
--- a/src/utils/PugsTraits.hpp
+++ b/src/utils/PugsTraits.hpp
@@ -26,6 +26,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>
@@ -106,6 +112,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>
@@ -114,6 +126,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>>;
+
 // Trais is ItemValue
 
 template <typename T>
@@ -151,6 +169,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 f89f543f92bc3a175e4f055b3286e4a45d985396..e19e6a29a694d4f9971683a99050c0e41f9ea190 100644
--- a/src/utils/PugsUtils.cpp
+++ b/src/utils/PugsUtils.cpp
@@ -129,6 +129,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]");
@@ -165,6 +169,7 @@ initialize(int& argc, char* argv[])
       CommunicatorManager::setSplitColor(mpi_split_color);
     }
 
+    GlobalVariableManager::instance().setNumberOfGhostLayers(nb_ghost_layers);
     ExecutionStatManager::getInstance().setPrint(print_exec_stat);
     BacktraceManager::setShow(show_backtrace);
     ConsoleManager::setShowPreamble(show_preamble);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e7665f8944202ff0c139ca1ccce1403026891f50..feac4de4a40c420233e87f4d636ccee751e8ac78 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -96,6 +96,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
@@ -127,6 +128,7 @@ add_executable (unit_tests
   test_PugsUtils.cpp
   test_PyramidGaussQuadrature.cpp
   test_PyramidTransformation.cpp
+  test_QuadraticPolynomialReconstruction.cpp
   test_QuadratureType.cpp
   test_RefId.cpp
   test_RefItemList.cpp
@@ -159,6 +161,9 @@ add_executable (unit_tests
 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
@@ -202,7 +207,10 @@ add_executable (mpi_unit_tests
   test_OFStream.cpp
   test_ParallelChecker_read.cpp
   test_Partitioner.cpp
+  test_PolynomialReconstruction.cpp
+  test_PolynomialReconstructionDescriptor.cpp
   test_RandomEngine.cpp
+  test_StencilBuilder.cpp
   test_SubItemArrayPerItemVariant.cpp
   test_SubItemValuePerItem.cpp
   test_SubItemValuePerItemVariant.cpp
diff --git a/tests/mpi_test_main.cpp b/tests/mpi_test_main.cpp
index ef3fdfcc00569ea3042de6724171fbd24e2ed680..7947364942fff1b8213a63b1972f910f12dfe63d 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>
@@ -99,7 +100,10 @@ main(int argc, char* argv[])
       MeshDataManager::create();
       DualConnectivityManager::create();
       DualMeshManager::create();
+      StencilManager::create();
+
       GlobalVariableManager::create();
+      GlobalVariableManager::instance().setNumberOfGhostLayers(1);
 
       MeshDataBaseForTests::create();
 
@@ -111,6 +115,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..abe07cc21e21a1b34ae3f20dfe13ff7f52ac83b9
--- /dev/null
+++ b/tests/test_ConnectivityDispatcher.cpp
@@ -0,0 +1,214 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <mesh/CartesianMeshBuilder.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/GmshReader.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <utils/Messenger.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <filesystem>
+
+// clazy:excludeall=non-pod-global-static
+
+class NbGhostLayersTester
+{
+ private:
+  const size_t m_original_number_of_ghost_layers;
+
+ public:
+  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;
+  }
+
+  ~NbGhostLayersTester()
+  {
+    GlobalVariableManager::instance().m_number_of_ghost_layers = m_original_number_of_ghost_layers;
+  }
+};
+
+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);
+      }
+    }
+  }
+
+  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);
+      }
+
+      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);
+      }
+
+      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);
+      }
+
+      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);
+      }
+
+      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);
+      }
+
+      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);
+      }
+    }
+
+    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_PolynomialReconstruction.cpp b/tests/test_PolynomialReconstruction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cdfb532f2cd6a30327f09c5f0b1b8e54e6917a29
--- /dev/null
+++ b/tests/test_PolynomialReconstruction.cpp
@@ -0,0 +1,983 @@
+#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 <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("PolynomialReconstruction", "[scheme]")
+{
+  SECTION("degree 1")
+  {
+    std::vector<PolynomialReconstructionDescriptor> descriptor_list = {
+      PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, 1},
+      PolynomialReconstructionDescriptor{IntegrationMethodType::element, 1},
+    };
+
+    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_affine = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
+                auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<double> fh{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1})) / 0.2;
+
+                    max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+
+          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_affine = [](const R1& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0],   //
+                            +1.4 - 0.6 * x[0],   //
+                            -0.2 + 3.1 * x[0]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<R3> uh{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope =
+                      (1 / 0.2) * (dpk_uh[cell_id](R1{0.1} + xj[cell_id]) - dpk_uh[cell_id](xj[cell_id] - R1{0.1}));
+
+                    max_slope_error = std::max(max_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+
+          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_affine = [](const R1& x) -> R3x3 {
+                  return R3x3{
+                    +2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0],   //
+                    +2.4 - 2.3 * x[0], -0.2 + 3.1 * x[0], -3.2 - 3.6 * x[0],
+                    -4.1 + 3.1 * x[0], +0.8 + 2.9 * x[0], -1.6 + 2.3 * x[0],
+                  };
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<R3x3> Ah{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R3x3_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<1, const R3x3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R3x3_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3x3 reconstructed_slope =
+                      (1 / 0.2) * (dpk_Ah[cell_id](R1{0.1} + xj[cell_id]) - dpk_Ah[cell_id](xj[cell_id] - R1{0.1}));
+
+                    R3x3 slops = R3x3{+1.7, +2.1, -0.6,   //
+                                      -2.3, +3.1, -3.6,   //
+                                      +3.1, +2.9, +2.3};
+
+                    max_slope_error = std::max(max_slope_error,   //
+                                               frobeniusNorm(reconstructed_slope - slops));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+              }
+            }
+          }
+
+          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 vector_affine = [](const R1& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0Vector<double> Vh{p_mesh, 3};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      Vh[cell_id][i] = vector[i];
+                    }
+                  });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  const TinyVector<3> slope{+1.7, +2.1, -0.6};
+
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R1{0.1} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R1{0.1}));
+
+                      max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+
+          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_affine = [](const R1& x) { return 2.3 + 1.7 * x[0]; };
+
+                auto R3_affine = [](const R1& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0],   //
+                            +1.4 - 0.6 * x[0],   //
+                            -0.2 + 3.1 * x[0]};
+                };
+
+                auto R3x3_affine = [](const R1& x) -> R3x3 {
+                  return R3x3{
+                    +2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0],   //
+                    +2.4 - 2.3 * x[0], -0.2 + 3.1 * x[0], -3.2 - 3.6 * x[0],
+                    -4.1 + 3.1 * x[0], +0.8 + 2.9 * x[0], -1.6 + 2.3 * x[0],
+                  };
+                };
+
+                auto vector_affine = [](const R1& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
+                };
+
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<double> fh{p_mesh};
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+
+                DiscreteFunctionP0<R3> uh{p_mesh};
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+
+                DiscreteFunctionP0<R3x3> Ah{p_mesh};
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R3x3_affine(xj[cell_id]); });
+
+                DiscreteFunctionP0Vector<double> Vh{p_mesh, 3};
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      Vh[cell_id][i] = vector[i];
+                    }
+                  });
+
+                auto reconstructions =
+                  PolynomialReconstruction{descriptor}.build(std::make_shared<DiscreteFunctionVariant>(fh), uh,
+                                                             std::make_shared<DiscreteFunctionP0<R3x3>>(Ah),
+                                                             DiscreteFunctionVariant(Vh));
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1})) / 0.2;
+
+                    max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                auto dpk_uh = reconstructions[1]->get<DiscreteFunctionDPk<1, const R3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope =
+                      (1 / 0.2) * (dpk_uh[cell_id](R1{0.1} + xj[cell_id]) - dpk_uh[cell_id](xj[cell_id] - R1{0.1}));
+
+                    max_slope_error = std::max(max_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                auto dpk_Ah = reconstructions[2]->get<DiscreteFunctionDPk<1, const R3x3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R3x3_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3x3 reconstructed_slope =
+                      (1 / 0.2) * (dpk_Ah[cell_id](R1{0.1} + xj[cell_id]) - dpk_Ah[cell_id](xj[cell_id] - R1{0.1}));
+
+                    R3x3 slops = R3x3{+1.7, +2.1, -0.6,   //
+                                      -2.3, +3.1, -3.6,   //
+                                      +3.1, +2.9, +2.3};
+
+                    max_slope_error = std::max(max_slope_error,   //
+                                               frobeniusNorm(reconstructed_slope - slops));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                auto dpk_Vh = reconstructions[3]->get<DiscreteFunctionDPkVector<1, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  const TinyVector<3> slope{+1.7, +2.1, -0.6};
+
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R1{0.1} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R1{0.1}));
+
+                      max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+        }
+
+        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_affine = [](const R2& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1]; };
+                auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<double> fh{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<2, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R2{0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R2{0.1, 0})) / 0.2;
+
+                    max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R2{0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R2{0, 0.1})) / 0.2;
+
+                    max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_affine = [](const R2& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1],   //
+                            +1.4 - 0.6 * x[0] + 1.3 * x[1],   //
+                            -0.2 + 3.1 * x[0] - 1.1 * x[1]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<R3> uh{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<2, const R3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R2{0.1, 0} + xj[cell_id]) -
+                                                                dpk_uh[cell_id](xj[cell_id] - R2{0.1, 0}));
+
+                    max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R2{0, 0.1} + xj[cell_id]) -
+                                                                dpk_uh[cell_id](xj[cell_id] - R2{0, 0.1}));
+
+                    max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3, -1.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+              }
+            }
+          }
+
+          SECTION("R^2x2 data")
+          {
+            using R2x2 = TinyMatrix<2, 2>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto R2x2_affine = [](const R2& x) -> R2x2 {
+                  return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1], -1.7 + 2.1 * x[0] - 2.2 * x[1],   //
+                              +1.4 - 0.6 * x[0] - 2.1 * x[1], +2.4 - 2.3 * x[0] + 1.3 * x[1]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<R2x2> Ah{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<2, const R2x2>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R2x2_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R2{0.1, 0} + xj[cell_id]) -
+                                                                  dpk_Ah[cell_id](xj[cell_id] - R2{0.1, 0}));
+
+                    max_x_slope_error =
+                      std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, +2.1,   //
+                                                                                           -0.6, -2.3}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R2{0, 0.1} + xj[cell_id]) -
+                                                                  dpk_Ah[cell_id](xj[cell_id] - R2{0, 0.1}));
+
+                    max_y_slope_error =
+                      std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
+                                                                                           -2.1, +1.3}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+              }
+            }
+          }
+
+          SECTION("vector data")
+          {
+            using R4 = TinyVector<4>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all2DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<2>>();
+                auto& mesh  = *p_mesh;
+
+                auto vector_affine = [](const R2& x) -> R4 {
+                  return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1], -1.7 + 2.1 * x[0] - 2.2 * x[1],   //
+                            +1.4 - 0.6 * x[0] - 2.1 * x[1], +2.4 - 2.3 * x[0] + 1.3 * x[1]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      Vh[cell_id][i] = vector[i];
+                    }
+                  });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<2, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  const R4 slope{+1.7, +2.1, -0.6, -2.3};
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R2{0.1, 0} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R2{0.1, 0}));
+
+                      max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  const R4 slope{+1.2, -2.2, -2.1, +1.3};
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R2{0, 0.1} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R2{0, 0.1}));
+
+                      max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+              }
+            }
+          }
+        }
+
+        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_affine = [](const R3& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]; };
+                auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<double> fh{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+                auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0.1, 0, 0})) /
+                      0.2;
+
+                    max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0.1, 0})) /
+                      0.2;
+
+                    max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_z_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0, 0.1})) /
+                      0.2;
+
+                    max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - 2.1));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+              }
+            }
+          }
+
+          SECTION("R^3 data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                auto R3_affine = [](const R3& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1] + 1.8 * x[2],   //
+                            +1.4 - 0.6 * x[0] + 1.3 * x[1] - 3.7 * x[2],   //
+                            -0.2 + 3.1 * x[0] - 1.1 * x[1] + 1.9 * x[2]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<R3> uh{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+                auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
+                                                                dpk_uh[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
+
+                    max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7, -0.6, 3.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
+                                                                dpk_uh[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
+
+                    max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3, -1.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  double max_z_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
+                                                                dpk_uh[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
+
+                    max_z_slope_error = std::max(max_z_slope_error, l2Norm(reconstructed_slope - R3{1.8, -3.7, 1.9}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+              }
+            }
+          }
+
+          SECTION("R^2x2 data")
+          {
+            using R2x2 = TinyMatrix<2, 2>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                auto R2x2_affine = [](const R3& x) -> R2x2 {
+                  return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2],
+                              //
+                              +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 * x[2]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0<R2x2> Ah{p_mesh};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]); });
+
+                descriptor.setRowWeighting(false);
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+                auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) - R2x2_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
+                                                                  dpk_Ah[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
+
+                    max_x_slope_error =
+                      std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, 2.1,   //
+                                                                                           -2.3, +3.1}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
+                                                                  dpk_Ah[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
+
+                    max_y_slope_error =
+                      std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
+                                                                                           1.3, +0.8}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  double max_z_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
+                                                                  dpk_Ah[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
+
+                    max_z_slope_error =
+                      std::max(max_z_slope_error, frobeniusNorm(reconstructed_slope - R2x2{-1.3, -2.4,   //
+                                                                                           +1.4, -1.8}));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+              }
+            }
+          }
+
+          SECTION("vector data")
+          {
+            using R4 = TinyVector<4>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+                auto& mesh  = *p_mesh;
+
+                auto vector_affine = [](const R3& x) -> R4 {
+                  return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2],
+                            //
+                            +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 * x[2]};
+                };
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      Vh[cell_id][i] = vector[i];
+                    }
+                  });
+
+                descriptor.setPreconditioning(false);
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    auto vector = vector_affine(xj[cell_id]);
+                    for (size_t i = 0; i < vector.dimension(); ++i) {
+                      max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) - vector[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_x_slope_error = 0;
+                  const R4 slope{+1.7, 2.1, -2.3, +3.1};
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0.1, 0, 0} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R3{0.1, 0, 0}));
+
+                      max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+
+                {
+                  double max_y_slope_error = 0;
+                  const R4 slope{+1.2, -2.2, 1.3, +0.8};
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0.1, 0} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0.1, 0}));
+
+                      max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
+                }
+
+                {
+                  double max_z_slope_error = 0;
+                  const R4 slope{-1.3, -2.4, +1.4, -1.8};
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    for (size_t i = 0; i < slope.dimension(); ++i) {
+                      const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0, 0.1} + xj[cell_id]) -
+                                                                      dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0, 0.1}));
+
+                      max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - slope[i]));
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-13));
+                }
+              }
+            }
+          }
+        }
+
+        SECTION("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");
+        }
+      }
+    }
+  }
+}
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_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.cpp b/tests/test_StencilBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b99ba218aa1e3eb5d8ec884e974cfad2f4a157db
--- /dev/null
+++ b/tests/test_StencilBuilder.cpp
@@ -0,0 +1,349 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ConnectivityUtils.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/ItemValueUtils.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFlatNodeBoundary.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/StencilManager.hpp>
+#include <utils/Messenger.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("StencilBuilder", "[mesh]")
+{
+  SECTION("inner stencil")
+  {
+    auto is_valid = [](const auto& connectivity, const auto& stencil_array) {
+      auto cell_to_node_matrix = connectivity.cellToNodeMatrix();
+      auto node_to_cell_matrix = connectivity.nodeToCellMatrix();
+
+      auto cell_is_owned = connectivity.cellIsOwned();
+      auto cell_number   = connectivity.cellNumber();
+
+      for (CellId cell_id = 0; cell_id < connectivity.numberOfCells(); ++cell_id) {
+        if (cell_is_owned[cell_id]) {
+          std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+            [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+          auto cell_nodes = cell_to_node_matrix[cell_id];
+          for (size_t i_node = 0; i_node < cell_nodes.size(); ++i_node) {
+            const NodeId node_id = cell_nodes[i_node];
+            auto node_cells      = node_to_cell_matrix[node_id];
+            for (size_t i_node_cell = 0; i_node_cell < node_cells.size(); ++i_node_cell) {
+              const CellId node_cell_id = node_cells[i_node_cell];
+              if (node_cell_id != cell_id) {
+                cell_set.insert(node_cell_id);
+              }
+            }
+          }
+
+          auto cell_stencil = stencil_array[cell_id];
+
+          auto i_set_cell = cell_set.begin();
+          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("1D")
+    {
+      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});
+
+        REQUIRE(is_valid(connectivity, stencil_array));
+      }
+
+      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});
+
+        REQUIRE(is_valid(connectivity, stencil_array));
+      }
+    }
+
+    SECTION("2D")
+    {
+      SECTION("cartesian")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+        const Connectivity<2>& connectivity = mesh.connectivity();
+        REQUIRE(
+          is_valid(connectivity,
+                   StencilManager::instance()
+                     .getCellToCellStencilArray(connectivity,
+                                                StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes})));
+      }
+
+      SECTION("hybrid")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+        const Connectivity<2>& connectivity = mesh.connectivity();
+        REQUIRE(
+          is_valid(connectivity,
+                   StencilManager::instance()
+                     .getCellToCellStencilArray(connectivity,
+                                                StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes})));
+      }
+    }
+
+    SECTION("3D")
+    {
+      SECTION("carteian")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+        const Connectivity<3>& connectivity = mesh.connectivity();
+        REQUIRE(
+          is_valid(connectivity,
+                   StencilManager::instance()
+                     .getCellToCellStencilArray(connectivity,
+                                                StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes})));
+      }
+
+      SECTION("hybrid")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+        const Connectivity<3>& connectivity = mesh.connectivity();
+        REQUIRE(
+          is_valid(connectivity,
+                   StencilManager::instance()
+                     .getCellToCellStencilArray(connectivity,
+                                                StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes})));
+      }
+    }
+  }
+
+  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 symmetry_stencils_are_valid = [](const auto& stencil_array, const auto& mesh) {
+      bool is_valid = true;
+
+      auto node_to_cell_matrix = mesh.connectivity().nodeToCellMatrix();
+      auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+      auto cell_is_owned       = mesh.connectivity().cellIsOwned();
+      auto cell_number         = mesh.connectivity().cellNumber();
+
+      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_node_list = getMeshFlatNodeBoundary(mesh, boundary_descriptor);
+
+        CellValue<bool> boundary_cell{mesh.connectivity()};
+        boundary_cell.fill(false);
+        auto node_list = boundary_node_list.nodeList();
+        for (size_t i_node = 0; i_node < node_list.size(); ++i_node) {
+          const NodeId node_id = node_list[i_node];
+          auto node_cell_list  = node_to_cell_matrix[node_id];
+          for (size_t i_cell = 0; i_cell < node_cell_list.size(); ++i_cell) {
+            const CellId cell_id   = node_cell_list[i_cell];
+            boundary_cell[cell_id] = true;
+          }
+        }
+
+        std::set<NodeId> symmetry_node;
+        for (size_t i_node = 0; i_node < node_list.size(); ++i_node) {
+          const NodeId node_id = node_list[i_node];
+          symmetry_node.insert(node_id);
+        }
+
+        for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          if ((not boundary_cell[cell_id]) or (not cell_is_owned[cell_id])) {
+            is_valid &= (boundary_stencil[cell_id].size() == 0);
+          } else {
+            auto cell_node_list = cell_to_node_matrix[cell_id];
+            std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+              [&](CellId cell0_id, CellId cell1_id) { return cell_number[cell0_id] < cell_number[cell1_id]; });
+            for (size_t i_node = 0; i_node < cell_node_list.size(); ++i_node) {
+              const NodeId node_id = cell_node_list[i_node];
+              if (symmetry_node.contains(node_id)) {
+                auto node_cell_list = node_to_cell_matrix[node_id];
+                for (size_t i_node_cell = 0; i_node_cell < node_cell_list.size(); ++i_node_cell) {
+                  const CellId node_cell_id = node_cell_list[i_node_cell];
+                  cell_set.insert(node_cell_id);
+                }
+              }
+            }
+
+            if (cell_set.size() == boundary_stencil[cell_id].size()) {
+              size_t i = 0;
+              for (auto&& id : cell_set) {
+                is_valid &= (id == boundary_stencil[cell_id][i++]);
+              }
+            } else {
+              is_valid = false;
+            }
+          }
+        }
+      }
+
+      return is_valid;
+    };
+
+    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(symmetry_stencils_are_valid(stencil_array, mesh));
+      }
+
+      SECTION("hybrid")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+        const Connectivity<1>& connectivity = mesh.connectivity();
+        auto stencil =
+          StencilManager::instance()
+            .getCellToCellStencilArray(connectivity, StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes},
+                                       list);
+
+        REQUIRE(stencil.symmetryBoundaryStencilArrayList().size() == 2);
+        REQUIRE(check_ghost_cells_have_empty_stencils(stencil, connectivity));
+        REQUIRE(symmetry_stencils_are_valid(stencil, mesh));
+      }
+    }
+
+    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(symmetry_stencils_are_valid(stencil_array, mesh));
+      }
+
+      SECTION("hybrid")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+        const Connectivity<2>& connectivity = mesh.connectivity();
+        auto stencil =
+          StencilManager::instance()
+            .getCellToCellStencilArray(connectivity, StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes},
+                                       list);
+
+        REQUIRE(stencil.symmetryBoundaryStencilArrayList().size() == 4);
+        REQUIRE(check_ghost_cells_have_empty_stencils(stencil, connectivity));
+        REQUIRE(symmetry_stencils_are_valid(stencil, mesh));
+      }
+    }
+
+    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 =
+          StencilManager::instance()
+            .getCellToCellStencilArray(connectivity, StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes},
+                                       list);
+
+        REQUIRE(stencil.symmetryBoundaryStencilArrayList().size() == 6);
+        REQUIRE(check_ghost_cells_have_empty_stencils(stencil, connectivity));
+        REQUIRE(symmetry_stencils_are_valid(stencil, mesh));
+      }
+
+      SECTION("hybrid")
+      {
+        const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+        const Connectivity<3>& connectivity = mesh.connectivity();
+        auto stencil =
+          StencilManager::instance()
+            .getCellToCellStencilArray(connectivity, StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes},
+                                       list);
+
+        REQUIRE(stencil.symmetryBoundaryStencilArrayList().size() == 6);
+        REQUIRE(check_ghost_cells_have_empty_stencils(stencil, connectivity));
+        REQUIRE(symmetry_stencils_are_valid(stencil, mesh));
+      }
+    }
+  }
+}
diff --git a/tests/test_TinyMatrix.cpp b/tests/test_TinyMatrix.cpp
index 80159a021b54ebb162d339be2bbd5195aec815a0..7bade539c5cd153efb9fde5cab2f5a2323dd26ca 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
@@ -209,6 +210,38 @@ TEST_CASE("TinyMatrix", "[algebra]")
     REQUIRE(trace(TinyMatrix<4>(1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 2, 0, 0, 2, 2)) == 1 + 0 + 1 + 2);
   }
 
+  SECTION("checking for dot product calculation")
+  {
+    {
+      TinyMatrix<1, 1, int> M(6);
+      TinyMatrix<1, 1, int> N(7);
+      REQUIRE(dot(M, N) == trace(M * transpose(N)));
+    }
+
+    {
+      TinyMatrix<2, 3, int> M(6, 3, -2,   //
+                              5, -1, 4);
+      TinyMatrix<2, 3, int> N(7, 8, -6,   //
+                              -3, 4, 2);
+      REQUIRE(dot(M, N) == trace(M * transpose(N)));
+      REQUIRE(dot(M, N) == trace(N * transpose(M)));
+    }
+  }
+
+  SECTION("checking for Frobenius norm calculation")
+  {
+    {
+      TinyMatrix<1, 1, int> M(-6);
+      REQUIRE(frobeniusNorm(M) == 6);
+    }
+
+    {
+      TinyMatrix<2, 3, int> M(6, 3, -2,   //
+                              5, -1, 4);
+      REQUIRE(frobeniusNorm(M) == std::sqrt(dot(M, M)));
+    }
+  }
+
   SECTION("checking for inverse calculations")
   {
     {
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index d8641d318f243eb624915882a92fc15d57a71346..504541ee0951895bd73899eb8aa0b5d89f7d46f6 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>
@@ -58,7 +59,10 @@ main(int argc, char* argv[])
       MeshDataManager::create();
       DualConnectivityManager::create();
       DualMeshManager::create();
+      StencilManager::create();
+
       GlobalVariableManager::create();
+      GlobalVariableManager::instance().setNumberOfGhostLayers(1);
 
       MeshDataBaseForTests::create();
 
@@ -71,6 +75,7 @@ main(int argc, char* argv[])
       MeshDataBaseForTests::destroy();
 
       GlobalVariableManager::destroy();
+      StencilManager::destroy();
       DualMeshManager::destroy();
       DualConnectivityManager::destroy();
       MeshDataManager::destroy();