diff --git a/CMakeLists.txt b/CMakeLists.txt
index d3306db95476b2e91773271f548b3937a417b421..bb8832a50565bde6f37ab589737a15e08f116030 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -737,6 +737,7 @@ target_link_libraries(
   PugsMesh
   PugsAlgebra
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsUtils
   PugsOutput
   PugsLanguageUtils
@@ -774,6 +775,7 @@ target_link_libraries(
   PugsLanguageModules
   PugsLanguageUtils
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsDev
   PugsAnalysis
   PugsAlgebra
@@ -816,6 +818,7 @@ install(TARGETS
   PugsMesh
   PugsOutput
   PugsScheme
+  PugsSchemeReconstructionUtils
   kokkos
   Catch2
 
diff --git a/src/scheme/CMakeLists.txt b/src/scheme/CMakeLists.txt
index c299cd24f1ac2faae22b6b93d86a31bab00bb8bf..5a555c426c7f1bda6d1859bd15d49a410a7dabf2 100644
--- a/src/scheme/CMakeLists.txt
+++ b/src/scheme/CMakeLists.txt
@@ -1,5 +1,7 @@
 # ------------------- Source files --------------------
 
+add_subdirectory(reconstruction_utils)
+
 add_library(
   PugsScheme
   AcousticSolver.cpp
@@ -20,3 +22,9 @@ target_link_libraries(
   PugsScheme
   ${HIGHFIVE_TARGET}
 )
+
+# Additional dependencies
+add_dependencies(
+  PugsScheme
+  PugsSchemeReconstructionUtils
+)
diff --git a/src/scheme/reconstruction_utils/CMakeLists.txt b/src/scheme/reconstruction_utils/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..66aaaa241b28ab025cec63f8bd8d9c1719b0f6f6
--- /dev/null
+++ b/src/scheme/reconstruction_utils/CMakeLists.txt
@@ -0,0 +1,15 @@
+# ------------------- Source files --------------------
+
+add_library(PugsSchemeReconstructionUtils
+  ElementIntegralReconstructionMatrixBuilder.cpp
+)
+
+add_dependencies(
+  PugsUtils
+  PugsMesh
+)
+
+target_link_libraries(
+  PugsSchemeReconstructionUtils
+  ${HIGHFIVE_TARGET}
+)
diff --git a/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..faededf698a291fbfb90addca1ac6fdcf15320a5
--- /dev/null
+++ b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.cpp
@@ -0,0 +1,505 @@
+#include <scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp>
+
+#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
+#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <geometry/CubeTransformation.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <geometry/PrismTransformation.hpp>
+#include <geometry/PyramidTransformation.hpp>
+#include <geometry/SquareTransformation.hpp>
+#include <geometry/SymmetryUtils.hpp>
+#include <geometry/TetrahedronTransformation.hpp>
+#include <geometry/TriangleTransformation.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <scheme/DiscreteFunctionDPk.hpp>
+
+template <MeshConcept MeshTypeT>
+template <typename ConformTransformationT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMean(
+  const QuadratureFormula<MeshType::Dimension>& quadrature,
+  const ConformTransformationT& T,
+  const Rd& Xj,
+  const double Vi,
+  SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT)
+{
+  mean_of_ejk.fill(0);
+
+  for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
+    const double wq = quadrature.weight(i_q);
+    const Rd& xi_q  = quadrature.point(i_q);
+
+    const Rd X_Xj = T(xi_q) - Xj;
+
+    if constexpr (MeshType::Dimension == 1) {
+      const double detT = T.jacobianDeterminant();
+
+      const double x_xj = X_Xj[0];
+
+      {
+        m_wq_detJ_ek[0] = wq * detT;
+        for (size_t k = 1; k <= m_polynomial_degree; ++k) {
+          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
+        }
+      }
+
+    } else if constexpr (MeshType::Dimension == 2) {
+      const double detT = [&] {
+        if constexpr (std::is_same_v<TriangleTransformation<2>, std::decay_t<decltype(T)>>) {
+          return T.jacobianDeterminant();
+        } else {
+          return T.jacobianDeterminant(xi_q);
+        }
+      }();
+
+      const double x_xj = X_Xj[0];
+      const double y_yj = X_Xj[1];
+
+      {
+        size_t k          = 0;
+        m_wq_detJ_ek[k++] = wq * detT;
+        for (; k <= m_polynomial_degree; ++k) {
+          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
+        }
+
+        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
+          const size_t begin_i_y_1 = m_y_row_index[i_y - 1];
+          for (size_t l = 0; l <= m_polynomial_degree - i_y; ++l, ++k) {
+            m_wq_detJ_ek[k] = y_yj * m_wq_detJ_ek[begin_i_y_1 + l];
+          }
+        }
+      }
+
+    } else if constexpr (MeshType::Dimension == 3) {
+      static_assert(MeshType::Dimension == 3);
+
+      const double detT = [&] {
+        if constexpr (std::is_same_v<TetrahedronTransformation, std::decay_t<decltype(T)>>) {
+          return T.jacobianDeterminant();
+        } else {
+          return T.jacobianDeterminant(xi_q);
+        }
+      }();
+
+      const double x_xj = X_Xj[0];
+      const double y_yj = X_Xj[1];
+      const double z_zj = X_Xj[2];
+
+      {
+        size_t k          = 0;
+        m_wq_detJ_ek[k++] = wq * detT;
+        for (; k <= m_polynomial_degree; ++k) {
+          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
+        }
+
+        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
+          const size_t begin_i_y_1 = m_yz_row_index[i_y - 1];
+          const size_t nb_monoms   = m_yz_row_size[i_y];
+          for (size_t l = 0; l < nb_monoms; ++l, ++k) {
+            m_wq_detJ_ek[k] = y_yj * m_wq_detJ_ek[begin_i_y_1 + l];
+          }
+        }
+
+        for (size_t i_z = 1; i_z <= m_polynomial_degree; ++i_z) {
+          const size_t nb_y      = m_yz_row_size[m_z_triangle_index[i_z]];
+          const size_t index_z   = m_z_triangle_index[i_z];
+          const size_t index_z_1 = m_z_triangle_index[i_z - 1];
+          for (size_t i_y = 0; i_y < nb_y; ++i_y) {
+            const size_t begin_i_yz_1 = m_yz_row_index[index_z_1 + i_y];
+            const size_t nb_monoms    = m_yz_row_size[index_z + i_y];
+            for (size_t l = 0; l < nb_monoms; ++l, ++k) {
+              m_wq_detJ_ek[k] = z_zj * m_wq_detJ_ek[begin_i_yz_1 + l];
+            }
+          }
+        }
+      }
+    }
+
+    for (size_t k = 1; k < m_basis_dimension; ++k) {
+      mean_of_ejk[k - 1] += m_wq_detJ_ek[k];
+    }
+  }
+
+  const double inv_Vi = 1. / Vi;
+  for (size_t k = 0; k < mean_of_ejk.size(); ++k) {
+    mean_of_ejk[k] *= inv_Vi;
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMean(
+  const Rd& Xj,
+  const CellId& cell_i_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  const CellType cell_type = m_cell_type[cell_i_id];
+  const auto node_list     = m_cell_to_node_matrix[cell_i_id];
+  const double Vi          = m_Vj[cell_i_id];
+
+  if constexpr (MeshType::Dimension == 1) {
+    if (m_cell_type[cell_i_id] == CellType::Line) {
+      const LineTransformation<1> T{m_xr[node_list[0]], m_xr[node_list[1]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+    } else {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+  } else if constexpr (MeshType::Dimension == 2) {
+    switch (cell_type) {
+    case CellType::Triangle: {
+      const TriangleTransformation<2> T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]]};
+      const auto& quadrature =
+        QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Quadrangle: {
+      const SquareTransformation<2> T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]]};
+      const auto& quadrature =
+        QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  } else {
+    static_assert(MeshType::Dimension == 3);
+
+    switch (cell_type) {
+    case CellType::Tetrahedron: {
+      const TetrahedronTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+      break;
+    }
+    case CellType::Prism: {
+      const PrismTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]],   //
+                                  m_xr[node_list[3]], m_xr[node_list[4]], m_xr[node_list[5]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+      break;
+    }
+    case CellType::Pyramid: {
+      const PyramidTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]],
+                                    m_xr[node_list[4]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Hexahedron: {
+      const CubeTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]],
+                                 m_xr[node_list[4]], m_xr[node_list[5]], m_xr[node_list[6]], m_xr[node_list[7]]};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::_computeEjkMeanInSymmetricCell(
+  const Rd& origin,
+  const Rd& normal,
+  const Rd& Xj,
+  const CellId& cell_i_id,
+  SmallArray<double>& mean_of_ejk)
+{
+  if constexpr (MeshType::Dimension == 1) {
+    auto node_list           = m_cell_to_node_matrix[cell_i_id];
+    const CellType cell_type = m_cell_type[cell_i_id];
+    const double Vi          = m_Vj[cell_i_id];
+
+    if (cell_type == CellType::Line) {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+
+      const LineTransformation<1> T{x0, x1};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+
+    } else {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+  } else if constexpr (MeshType::Dimension == 2) {
+    auto node_list           = m_cell_to_node_matrix[cell_i_id];
+    const CellType cell_type = m_cell_type[cell_i_id];
+    const double Vi          = m_Vj[cell_i_id];
+
+    switch (cell_type) {
+    case CellType::Triangle: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+
+      const TriangleTransformation<2> T{x0, x1, x2};
+      const auto& quadrature =
+        QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Quadrangle: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+
+      const SquareTransformation<2> T{x0, x1, x2, x3};
+      const auto& quadrature =
+        QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  } else {
+    static_assert(MeshType::Dimension == 3);
+    auto node_list           = m_cell_to_node_matrix[cell_i_id];
+    const CellType cell_type = m_cell_type[cell_i_id];
+    const double Vi          = m_Vj[cell_i_id];
+    switch (cell_type) {
+    case CellType::Tetrahedron: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+
+      const TetrahedronTransformation T{x0, x1, x2, x3};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{m_polynomial_degree});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Prism: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
+      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x5 = symmetrize_coordinates(origin, normal, m_xr[node_list[5]]);
+
+      const PrismTransformation T{x0, x1, x2,   //
+                                  x3, x4, x5};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Pyramid: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
+      const PyramidTransformation T{x0, x1, x2, x3, x4};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    case CellType::Hexahedron: {
+      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
+      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
+      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
+      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
+      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[7]]);
+      const auto x5 = symmetrize_coordinates(origin, normal, m_xr[node_list[6]]);
+      const auto x6 = symmetrize_coordinates(origin, normal, m_xr[node_list[5]]);
+      const auto x7 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
+
+      const CubeTransformation T{x0, x1, x2, x3, x4, x5, x6, x7};
+
+      const auto& quadrature =
+        QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
+
+      this->_computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
+      break;
+    }
+    default: {
+      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
+    }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+void
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<MeshTypeT>::build(
+  const CellId cell_j_id,
+  ShrinkMatrixView<SmallMatrix<double>>& A)
+{
+  const auto& stencil_cell_list = m_stencil_array[cell_j_id];
+
+  const Rd& Xj = m_xj[cell_j_id];
+
+  this->_computeEjkMean(Xj, cell_j_id, m_mean_j_of_ejk);
+
+  size_t index = 0;
+  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+    const CellId cell_i_id = stencil_cell_list[i];
+
+    this->_computeEjkMean(Xj, cell_i_id, m_mean_i_of_ejk);
+
+    for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+      A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
+    }
+  }
+
+  for (size_t i_symmetry = 0; i_symmetry < m_stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry) {
+    auto& ghost_stencil  = m_stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+    auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+    const Rd& origin = m_symmetry_origin_list[i_symmetry];
+    const Rd& normal = m_symmetry_normal_list[i_symmetry];
+
+    for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+      const CellId cell_i_id = ghost_cell_list[i];
+
+      this->_computeEjkMeanInSymmetricCell(origin, normal, Xj, cell_i_id, m_mean_i_of_ejk);
+
+      for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
+        A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
+      }
+    }
+  }
+}
+
+template <MeshConcept MeshTypeT>
+PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  MeshTypeT>::ElementIntegralReconstructionMatrixBuilder(const MeshType& mesh,
+                                                         const size_t polynomial_degree,
+                                                         const SmallArray<const Rd>& symmetry_origin_list,
+                                                         const SmallArray<const Rd>& symmetry_normal_list,
+                                                         const CellToCellStencilArray& stencil_array)
+  : m_basis_dimension{DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(
+      polynomial_degree)},
+    m_polynomial_degree{polynomial_degree},
+
+    m_wq_detJ_ek{m_basis_dimension},
+    m_mean_j_of_ejk{m_basis_dimension - 1},
+    m_mean_i_of_ejk{m_basis_dimension - 1},
+
+    m_cell_to_node_matrix{mesh.connectivity().cellToNodeMatrix()},
+    m_stencil_array{stencil_array},
+    m_symmetry_origin_list{symmetry_origin_list},
+    m_symmetry_normal_list{symmetry_normal_list},
+    m_cell_type{mesh.connectivity().cellType()},
+    m_Vj{MeshDataManager::instance().getMeshData(mesh).Vj()},
+    m_xj{MeshDataManager::instance().getMeshData(mesh).xj()},
+    m_xr{mesh.xr()}
+{
+  if constexpr (MeshType::Dimension == 2) {
+    SmallArray<size_t> y_row_index(m_polynomial_degree + 1);
+
+    size_t i_y = 0;
+
+    y_row_index[i_y++] = 0;
+    for (ssize_t n = m_polynomial_degree + 1; n > 1; --n, ++i_y) {
+      y_row_index[i_y] = y_row_index[i_y - 1] + n;
+    }
+
+    m_y_row_index = y_row_index;
+
+  } else if constexpr (MeshType::Dimension == 3) {
+    SmallArray<size_t> yz_row_index((m_polynomial_degree + 2) * (m_polynomial_degree + 1) / 2 + 1);
+    SmallArray<size_t> z_triangle_index(m_polynomial_degree + 1);
+
+    {
+      size_t i_z  = 0;
+      size_t i_yz = 0;
+
+      yz_row_index[i_yz++] = 0;
+      for (ssize_t n = m_polynomial_degree + 1; n >= 1; --n) {
+        z_triangle_index[i_z++] = i_yz - 1;
+        for (ssize_t i = n; i >= 1; --i) {
+          yz_row_index[i_yz] = yz_row_index[i_yz - 1] + i;
+          ++i_yz;
+        }
+      }
+    }
+
+    SmallArray<size_t> yz_row_size{yz_row_index.size() - 1};
+    for (size_t i = 0; i < yz_row_size.size(); ++i) {
+      yz_row_size[i] = yz_row_index[i + 1] - yz_row_index[i];
+    }
+
+    m_yz_row_index     = yz_row_index;
+    m_z_triangle_index = z_triangle_index;
+    m_yz_row_size      = yz_row_size;
+  }
+}
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<Mesh<1>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<Mesh<2>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template void PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<Mesh<3>>::build(
+  const CellId,
+  ShrinkMatrixView<SmallMatrix<double>>&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  Mesh<1>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                       const size_t,
+                                                       const SmallArray<const Rd>&,
+                                                       const SmallArray<const Rd>&,
+                                                       const CellToCellStencilArray&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  Mesh<2>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                       const size_t,
+                                                       const SmallArray<const Rd>&,
+                                                       const SmallArray<const Rd>&,
+                                                       const CellToCellStencilArray&);
+
+template PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder<
+  Mesh<3>>::ElementIntegralReconstructionMatrixBuilder(const MeshType&,
+                                                       const size_t,
+                                                       const SmallArray<const Rd>&,
+                                                       const SmallArray<const Rd>&,
+                                                       const CellToCellStencilArray&);
diff --git a/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
index fd877ccdbfa2d7de8eb32150c6b4ad310d17c4bc..1f4573438260e321dbd1461d2566f854236b25f4 100644
--- a/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
+++ b/src/scheme/reconstruction_utils/ElementIntegralReconstructionMatrixBuilder.hpp
@@ -2,22 +2,12 @@
 #define ELEMENT_INTEGRAL_RECONSTRUCTION_MATRIX_BUILDER_HPP
 
 #include <algebra/ShrinkMatrixView.hpp>
-#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
-#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <algebra/SmallMatrix.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/Connectivity.hpp>
-#include <mesh/Mesh.hpp>
-#include <mesh/MeshDataManager.hpp>
+#include <mesh/CellType.hpp>
+#include <mesh/ItemValue.hpp>
 #include <mesh/StencilArray.hpp>
-#include <scheme/DiscreteFunctionDPk.hpp>
 #include <scheme/PolynomialReconstruction.hpp>
 #include <utils/SmallArray.hpp>
 
@@ -59,410 +49,21 @@ class PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder
   SmallArray<const size_t> m_yz_row_size;
 
   template <typename ConformTransformationT>
-  void
-  _computeEjkMean(const QuadratureFormula<1>& quadrature,
-                  const ConformTransformationT& T,
-                  const TinyVector<1>& Xj,
-                  const double Vi,
-                  SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT)
-  {
-    mean_of_ejk.fill(0);
-
-    for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-      const double wq   = quadrature.weight(i_q);
-      const Rd& xi_q    = quadrature.point(i_q);
-      const double detT = T.jacobianDeterminant();
-
-      const Rd X_Xj = T(xi_q) - Xj;
-
-      const double x_xj = X_Xj[0];
-
-      {
-        m_wq_detJ_ek[0] = wq * detT;
-        for (size_t k = 1; k <= m_polynomial_degree; ++k) {
-          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
-        }
-      }
-
-      for (size_t k = 1; k < m_basis_dimension; ++k) {
-        mean_of_ejk[k - 1] += m_wq_detJ_ek[k];
-      }
-    }
-
-    const double inv_Vi = 1. / Vi;
-    for (size_t k = 0; k < mean_of_ejk.size(); ++k) {
-      mean_of_ejk[k] *= inv_Vi;
-    }
-  }
-
-  template <typename ConformTransformationT>
-  void
-  _computeEjkMean(const QuadratureFormula<2>& quadrature,
-                  const ConformTransformationT& T,
-                  const TinyVector<2>& Xj,
-                  const double Vi,
-                  SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT)
-  {
-    mean_of_ejk.fill(0);
-
-    for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-      const double wq   = quadrature.weight(i_q);
-      const Rd& xi_q    = quadrature.point(i_q);
-      const 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;
-        m_wq_detJ_ek[k++] = wq * detT;
-        for (; k <= m_polynomial_degree; ++k) {
-          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
-        }
-
-        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
-          const size_t begin_i_y_1 = m_y_row_index[i_y - 1];
-          for (size_t l = 0; l <= m_polynomial_degree - i_y; ++l, ++k) {
-            m_wq_detJ_ek[k] = y_yj * m_wq_detJ_ek[begin_i_y_1 + l];
-          }
-        }
-      }
-
-      for (size_t k = 1; k < m_basis_dimension; ++k) {
-        mean_of_ejk[k - 1] += m_wq_detJ_ek[k];
-      }
-    }
-
-    const double inv_Vi = 1. / Vi;
-    for (size_t k = 0; k < mean_of_ejk.size(); ++k) {
-      mean_of_ejk[k] *= inv_Vi;
-    }
-  }
-
-  template <typename ConformTransformationT>
-  void
-  _computeEjkMean(const QuadratureFormula<3>& quadrature,
-                  const ConformTransformationT& T,
-                  const TinyVector<3>& Xj,
-                  const double Vi,
-                  SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT)
-  {
-    mean_of_ejk.fill(0);
-
-    for (size_t i_q = 0; i_q < quadrature.numberOfPoints(); ++i_q) {
-      const double wq   = quadrature.weight(i_q);
-      const Rd& xi_q    = quadrature.point(i_q);
-      const 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;
-        m_wq_detJ_ek[k++] = wq * detT;
-        for (; k <= m_polynomial_degree; ++k) {
-          m_wq_detJ_ek[k] = x_xj * m_wq_detJ_ek[k - 1];
-        }
-
-        for (size_t i_y = 1; i_y <= m_polynomial_degree; ++i_y) {
-          const size_t begin_i_y_1 = m_yz_row_index[i_y - 1];
-          const size_t nb_monoms   = m_yz_row_size[i_y];
-          for (size_t l = 0; l < nb_monoms; ++l, ++k) {
-            m_wq_detJ_ek[k] = y_yj * m_wq_detJ_ek[begin_i_y_1 + l];
-          }
-        }
-
-        for (size_t i_z = 1; i_z <= m_polynomial_degree; ++i_z) {
-          const size_t nb_y      = m_yz_row_size[m_z_triangle_index[i_z]];
-          const size_t index_z   = m_z_triangle_index[i_z];
-          const size_t index_z_1 = m_z_triangle_index[i_z - 1];
-          for (size_t i_y = 0; i_y < nb_y; ++i_y) {
-            const size_t begin_i_yz_1 = m_yz_row_index[index_z_1 + i_y];
-            const size_t nb_monoms    = m_yz_row_size[index_z + i_y];
-            for (size_t l = 0; l < nb_monoms; ++l, ++k) {
-              m_wq_detJ_ek[k] = z_zj * m_wq_detJ_ek[begin_i_yz_1 + l];
-            }
-          }
-        }
-      }
-
-      for (size_t k = 1; k < m_basis_dimension; ++k) {
-        mean_of_ejk[k - 1] += m_wq_detJ_ek[k];
-      }
-    }
-
-    const double inv_Vi = 1. / Vi;
-    for (size_t k = 0; k < mean_of_ejk.size(); ++k) {
-      mean_of_ejk[k] *= inv_Vi;
-    }
-  }
-
-  void
-  _computeEjkMean(const TinyVector<1>& Xj, const CellId& cell_i_id, SmallArray<double>& mean_of_ejk)
-  {
-    const CellType cell_type = m_cell_type[cell_i_id];
-    const auto node_list     = m_cell_to_node_matrix[cell_i_id];
-    const double Vi          = m_Vj[cell_i_id];
-
-    if (m_cell_type[cell_i_id] == CellType::Line) {
-      const LineTransformation<1> T{m_xr[node_list[0]], m_xr[node_list[1]]};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-    } else {
-      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-    }
-  }
-
-  void
-  _computeEjkMean(const TinyVector<2>& Xj, const CellId& cell_i_id, SmallArray<double>& mean_of_ejk)
-  {
-    const CellType cell_type = m_cell_type[cell_i_id];
-    const auto node_list     = m_cell_to_node_matrix[cell_i_id];
-    const double Vi          = m_Vj[cell_i_id];
-
-    switch (cell_type) {
-    case CellType::Triangle: {
-      const TriangleTransformation<2> T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]]};
-      const auto& quadrature =
-        QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{m_polynomial_degree});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-      break;
-    }
-    case CellType::Quadrangle: {
-      const SquareTransformation<2> T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]]};
-      const auto& quadrature =
-        QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-      break;
-    }
-    default: {
-      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-    }
-    }
-  }
-
-  void
-  _computeEjkMean(const TinyVector<3>& Xj, const CellId& cell_i_id, SmallArray<double>& mean_of_ejk)
-  {
-    const CellType cell_type = m_cell_type[cell_i_id];
-    const auto node_list     = m_cell_to_node_matrix[cell_i_id];
-    const double Vi          = m_Vj[cell_i_id];
-
-    switch (cell_type) {
-    case CellType::Tetrahedron: {
-      const TetrahedronTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]]};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{m_polynomial_degree});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-      break;
-    }
-    case CellType::Prism: {
-      const PrismTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]],   //
-                                  m_xr[node_list[3]], m_xr[node_list[4]], m_xr[node_list[5]]};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-      break;
-    }
-    case CellType::Pyramid: {
-      const PyramidTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]],
-                                    m_xr[node_list[4]]};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-      break;
-    }
-    case CellType::Hexahedron: {
-      const CubeTransformation T{m_xr[node_list[0]], m_xr[node_list[1]], m_xr[node_list[2]], m_xr[node_list[3]],
-                                 m_xr[node_list[4]], m_xr[node_list[5]], m_xr[node_list[6]], m_xr[node_list[7]]};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-      break;
-    }
-    default: {
-      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-    }
-    }
-  }
-
-  void
-  _computeEjkMeanInSymmetricCell(const TinyVector<1>& origin,
-                                 const TinyVector<1>& normal,
-                                 const TinyVector<1>& Xj,
-                                 const CellId& cell_i_id,
-                                 SmallArray<double>& mean_of_ejk)
-  {
-    auto node_list           = m_cell_to_node_matrix[cell_i_id];
-    const CellType cell_type = m_cell_type[cell_i_id];
-    const double Vi          = m_Vj[cell_i_id];
-
-    if (cell_type == CellType::Line) {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-
-      const LineTransformation<1> T{x0, x1};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getLineFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-    } else {
-      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-    }
-  }
-
-  void
-  _computeEjkMeanInSymmetricCell(const TinyVector<2>& origin,
-                                 const TinyVector<2>& normal,
-                                 const TinyVector<2>& Xj,
-                                 const CellId& cell_i_id,
-                                 SmallArray<double>& mean_of_ejk)
-  {
-    auto node_list           = m_cell_to_node_matrix[cell_i_id];
-    const CellType cell_type = m_cell_type[cell_i_id];
-    const double Vi          = m_Vj[cell_i_id];
-
-    switch (cell_type) {
-    case CellType::Triangle: {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-
-      const TriangleTransformation<2> T{x0, x1, x2};
-      const auto& quadrature =
-        QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor{m_polynomial_degree});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-      break;
-    }
-    case CellType::Quadrangle: {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
-      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-
-      const SquareTransformation<2> T{x0, x1, x2, x3};
-      const auto& quadrature =
-        QuadratureManager::instance().getSquareFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-      break;
-    }
-    default: {
-      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-    }
-    }
-  }
-
-  void
-  _computeEjkMeanInSymmetricCell(const TinyVector<3>& origin,
-                                 const TinyVector<3>& normal,
-                                 const TinyVector<3>& Xj,
-                                 const CellId cell_i_id,
-                                 SmallArray<double>& mean_of_ejk)
-  {
-    auto node_list           = m_cell_to_node_matrix[cell_i_id];
-    const CellType cell_type = m_cell_type[cell_i_id];
-    const double Vi          = m_Vj[cell_i_id];
-
-    if (cell_type == CellType::Tetrahedron) {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
-      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
-
-      const TetrahedronTransformation T{x0, x1, x2, x3};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getTetrahedronFormula(GaussQuadratureDescriptor{m_polynomial_degree});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-    } else if (cell_type == CellType::Prism) {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
-      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
-      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
-      const auto x5 = symmetrize_coordinates(origin, normal, m_xr[node_list[5]]);
-
-      const PrismTransformation T{x0, x1, x2,   //
-                                  x3, x4, x5};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getPrismFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-    } else if (cell_type == CellType::Pyramid) {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
-      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
-      const PyramidTransformation T{x0, x1, x2, x3, x4};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getPyramidFormula(GaussQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-    } else if (cell_type == CellType::Hexahedron) {
-      const auto x0 = symmetrize_coordinates(origin, normal, m_xr[node_list[3]]);
-      const auto x1 = symmetrize_coordinates(origin, normal, m_xr[node_list[2]]);
-      const auto x2 = symmetrize_coordinates(origin, normal, m_xr[node_list[1]]);
-      const auto x3 = symmetrize_coordinates(origin, normal, m_xr[node_list[0]]);
-      const auto x4 = symmetrize_coordinates(origin, normal, m_xr[node_list[7]]);
-      const auto x5 = symmetrize_coordinates(origin, normal, m_xr[node_list[6]]);
-      const auto x6 = symmetrize_coordinates(origin, normal, m_xr[node_list[5]]);
-      const auto x7 = symmetrize_coordinates(origin, normal, m_xr[node_list[4]]);
-
-      const CubeTransformation T{x0, x1, x2, x3, x4, x5, x6, x7};
-
-      const auto& quadrature =
-        QuadratureManager::instance().getCubeFormula(GaussLegendreQuadratureDescriptor{m_polynomial_degree + 1});
-
-      _computeEjkMean(quadrature, T, Xj, Vi, mean_of_ejk);
-
-    } else {
-      throw NotImplementedError("unexpected cell type: " + std::string{name(cell_type)});
-    }
-  }
+  void _computeEjkMean(const QuadratureFormula<MeshType::Dimension>& quadrature,
+                       const ConformTransformationT& T,
+                       const Rd& Xj,
+                       const double Vi,
+                       SmallArray<double>& mean_of_ejk) noexcept(NO_ASSERT);
+
+  void _computeEjkMean(const TinyVector<MeshType::Dimension>& Xj,
+                       const CellId& cell_i_id,
+                       SmallArray<double>& mean_of_ejk);
+
+  void _computeEjkMeanInSymmetricCell(const Rd& origin,
+                                      const Rd& normal,
+                                      const Rd& Xj,
+                                      const CellId& cell_i_id,
+                                      SmallArray<double>& mean_of_ejk);
 
  public:
   PUGS_INLINE
@@ -472,108 +73,13 @@ class PolynomialReconstruction::ElementIntegralReconstructionMatrixBuilder
     return m_mean_j_of_ejk;
   }
 
-  template <typename MatrixType>
-  void
-  build(const CellId cell_j_id, ShrinkMatrixView<MatrixType>& A)
-  {
-    const auto& stencil_cell_list = m_stencil_array[cell_j_id];
-
-    const Rd& Xj = m_xj[cell_j_id];
-
-    _computeEjkMean(Xj, cell_j_id, m_mean_j_of_ejk);
-
-    size_t index = 0;
-    for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
-      const CellId cell_i_id = stencil_cell_list[i];
-
-      _computeEjkMean(Xj, cell_i_id, m_mean_i_of_ejk);
-
-      for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
-        A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
-      }
-    }
-
-    for (size_t i_symmetry = 0; i_symmetry < m_stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry) {
-      auto& ghost_stencil  = m_stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
-      auto ghost_cell_list = ghost_stencil[cell_j_id];
-
-      const Rd& origin = m_symmetry_origin_list[i_symmetry];
-      const Rd& normal = m_symmetry_normal_list[i_symmetry];
-
-      for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
-        const CellId cell_i_id = ghost_cell_list[i];
-
-        _computeEjkMeanInSymmetricCell(origin, normal, Xj, cell_i_id, m_mean_i_of_ejk);
-
-        for (size_t l = 0; l < m_basis_dimension - 1; ++l) {
-          A(index, l) = m_mean_i_of_ejk[l] - m_mean_j_of_ejk[l];
-        }
-      }
-    }
-  }
+  void build(const CellId cell_j_id, ShrinkMatrixView<SmallMatrix<double>>& A);
 
   ElementIntegralReconstructionMatrixBuilder(const MeshType& mesh,
                                              const size_t polynomial_degree,
                                              const SmallArray<const Rd>& symmetry_origin_list,
                                              const SmallArray<const Rd>& symmetry_normal_list,
-                                             const CellToCellStencilArray& stencil_array)
-    : m_basis_dimension{DiscreteFunctionDPk<MeshType::Dimension, double>::BasisViewType::dimensionFromDegree(
-        polynomial_degree)},
-      m_polynomial_degree{polynomial_degree},
-
-      m_wq_detJ_ek{m_basis_dimension},
-      m_mean_j_of_ejk{m_basis_dimension - 1},
-      m_mean_i_of_ejk{m_basis_dimension - 1},
-
-      m_cell_to_node_matrix{mesh.connectivity().cellToNodeMatrix()},
-      m_stencil_array{stencil_array},
-      m_symmetry_origin_list{symmetry_origin_list},
-      m_symmetry_normal_list{symmetry_normal_list},
-      m_cell_type{mesh.connectivity().cellType()},
-      m_Vj{MeshDataManager::instance().getMeshData(mesh).Vj()},
-      m_xj{MeshDataManager::instance().getMeshData(mesh).xj()},
-      m_xr{mesh.xr()}
-  {
-    if constexpr (MeshType::Dimension == 2) {
-      SmallArray<size_t> y_row_index(m_polynomial_degree + 1);
-
-      size_t i_y = 0;
-
-      y_row_index[i_y++] = 0;
-      for (ssize_t n = m_polynomial_degree + 1; n > 1; --n, ++i_y) {
-        y_row_index[i_y] = y_row_index[i_y - 1] + n;
-      }
-
-      m_y_row_index = y_row_index;
-
-    } else if constexpr (MeshType::Dimension == 3) {
-      SmallArray<size_t> yz_row_index((m_polynomial_degree + 2) * (m_polynomial_degree + 1) / 2 + 1);
-      SmallArray<size_t> z_triangle_index(m_polynomial_degree + 1);
-
-      {
-        size_t i_z  = 0;
-        size_t i_yz = 0;
-
-        yz_row_index[i_yz++] = 0;
-        for (ssize_t n = m_polynomial_degree + 1; n >= 1; --n) {
-          z_triangle_index[i_z++] = i_yz - 1;
-          for (ssize_t i = n; i >= 1; --i) {
-            yz_row_index[i_yz] = yz_row_index[i_yz - 1] + i;
-            ++i_yz;
-          }
-        }
-      }
-
-      SmallArray<size_t> yz_row_size{yz_row_index.size() - 1};
-      for (size_t i = 0; i < yz_row_size.size(); ++i) {
-        yz_row_size[i] = yz_row_index[i + 1] - yz_row_index[i];
-      }
-
-      m_yz_row_index     = yz_row_index;
-      m_z_triangle_index = z_triangle_index;
-      m_yz_row_size      = yz_row_size;
-    }
-  }
+                                             const CellToCellStencilArray& stencil_array);
 
   ~ElementIntegralReconstructionMatrixBuilder() = default;
 };
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index fd8915b83320770bc36e8a83ad002db44b71bfd5..8c05b9ed0ae319b803f053dda1bda8246c9339be 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -317,6 +317,7 @@ target_link_libraries (unit_tests
   PugsAlgebra
   PugsAnalysis
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsOutput
   PugsUtils
   PugsCheckpointing
@@ -347,6 +348,7 @@ target_link_libraries (mpi_unit_tests
   PugsUtils
   PugsLanguageUtils
   PugsScheme
+  PugsSchemeReconstructionUtils
   PugsOutput
   PugsUtils
   PugsCheckpointing