diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8047520391cb75d3f46acad5c87a720414f3c040..7c0f69fcf6395a96f88e74e9143e8e85730ba66e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,12 +22,20 @@ if("${PUGS_SHORT_VERSION}" STREQUAL "")
   message(FATAL_ERROR "Unable to compute short version from PUGS_VERSION=${PUGS_VERSION}")
 endif()
 
-
-set(CMAKE_CONFIGURATION_TYPES "Release;Debug;Coverage" CACHE STRING INTERNAL FORCE )
-
 # set project version as PUGS_SHORT_VERSION
 project (Pugs VERSION ${PUGS_SHORT_VERSION})
 
+# -----------------------------------------------------
+# dynamic libraries
+
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+link_libraries("-rdynamic")
+set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
+
+#------------------------------------------------------
+
+set(CMAKE_CONFIGURATION_TYPES "Release;Debug;Coverage" CACHE STRING INTERNAL FORCE )
+
 #------------------------------------------------------
 
 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -133,9 +141,42 @@ endif()
 #------------------------------------------------------
 # Search for ParMETIS
 
+find_package(ParMETIS)
+if(PARMETIS_LIBRARIES)
+  add_library(PkgConfig::ParMETIS STATIC IMPORTED)
+  set_property(TARGET PkgConfig::ParMETIS PROPERTY
+    IMPORTED_LOCATION "${PARMETIS_LIBRARIES}")
+
+  set(PUGS_HAS_PARMETIS TRUE)
+  set(PARMETIS_TARGET PkgConfig::ParMETIS)
+else()
+  unset(PUGS_HAS_PARMETIS)
+endif()
+
+#------------------------------------------------------
+# Search for PTScotch
+
+find_package(PTScotch)
+
+set(PUGS_HAS_PTSCOTCH ${PTScotch_FOUND})
+
+if (PTScotch_FOUND)
+  add_library(PkgConfig::PTScotch STATIC IMPORTED)
+  set_property(TARGET PkgConfig::PTScotch PROPERTY
+    IMPORTED_LOCATION "${PTSCOTCH_LIBRARY}")
+  set_property(TARGET PkgConfig::PTScotch PROPERTY
+    INTERFACE_INCLUDE_DIRECTORIES "${PTSCOTCH_INCLUDE_DIR}")
+
+  set(PTSCOTCH_TARGET PkgConfig::PTScotch)
+  include_directories(SYSTEM "${PTSCOTCH_INCLUDE_DIR}")
+else()
+  set(PTSCOTCH_LIBRARY "")
+endif()
+
+#------------------------------------------------------
+
 if(${MPI_FOUND})
-  find_package(ParMETIS)
-  if (NOT PARMETIS_LIBRARIES)
+  if ((NOT PARMETIS_LIBRARIES) AND (NOT PTSCOTCH_LIBRARIES))
     if(PUGS_ENABLE_MPI MATCHES "^AUTO$")
       message(STATUS "MPI support deactivated: ParMETIS cannot be found!")
       unset(MPI_FOUND)
@@ -160,7 +201,18 @@ set(PUGS_ENABLE_PETSC AUTO CACHE STRING
 if (PUGS_ENABLE_PETSC MATCHES "^(AUTO|ON)$")
   if (MPI_FOUND)
     # PETSc support is deactivated if MPI is not found
-    pkg_check_modules(PETSC PETSc)
+    pkg_check_modules(PETSC IMPORTED_TARGET GLOBAL PETSc)
+
+    if (${PETSC_FOUND})
+      set_property(TARGET PkgConfig::PETSC PROPERTY
+	IMPORTED_LOCATION "${PETSC_LIBRARIES}"
+      )
+      set_property(TARGET PkgConfig::PETSC PROPERTY
+	INTERFACE_INCLUDE_DIRECTORIES "${PETSC_INCLUDE_DIRS}"
+      )
+
+      set(PETSC_TARGET PkgConfig::PETSC)
+    endif()
   else()
     message(STATUS "PETSc support is deactivated since pugs will not be build with MPI support")
     set(PETSC_FOUND FALSE)
@@ -188,7 +240,18 @@ set(PUGS_ENABLE_SLEPC AUTO CACHE STRING
 if (PUGS_ENABLE_SLEPC MATCHES "^(AUTO|ON)$")
   if (PETSC_FOUND)
     # SLEPc support is deactivated if PETSc is not found
-    pkg_check_modules(SLEPC SLEPc)
+    pkg_check_modules(SLEPC IMPORTED_TARGET GLOBAL SLEPc)
+
+    if (${SLEPC_FOUND})
+      set_property(TARGET PkgConfig::SLEPC PROPERTY
+	IMPORTED_LOCATION "${SLEPC_LIBRARIES}"
+      )
+      set_property(TARGET PkgConfig::SLEPC PROPERTY
+	INTERFACE_INCLUDE_DIRECTORIES "${SLEPC_INCLUDE_DIRS}"
+      )
+
+      set(SLEPC_TARGET PkgConfig::SLEPC)
+    endif()
   else()
     message(STATUS "SLEPc support is deactivated since pugs will not be build with PETSc support")
     set(SLEPC_FOUND FALSE)
@@ -207,6 +270,26 @@ else()
   endif()
 endif()
 
+# -----------------------------------------------------
+# Check for Eigen3
+# defaults use Eigen3
+set(PUGS_ENABLE_EIGEN3 AUTO CACHE STRING
+  "Choose one of: AUTO ON OFF")
+
+if (PUGS_ENABLE_EIGEN3 MATCHES "^(AUTO|ON)$")
+  find_package (Eigen3 NO_MODULE)
+
+  if (TARGET Eigen3::Eigen)
+    set(EIGEN3_TARGET Eigen3::Eigen)
+    set(EIGEN3_FOUND TRUE)
+  else()
+    set(EIGEN3_FOUND FALSE)
+  endif (TARGET Eigen3::Eigen)
+  set(PUGS_HAS_EIGEN3 ${EIGEN3_FOUND})
+else()
+  unset(PUGS_HAS_EIGEN3)
+endif()
+
 # -----------------------------------------------------
 
 if (${MPI_FOUND})
@@ -225,22 +308,37 @@ set(PUGS_ENABLE_HDF5 AUTO CACHE STRING
   "Choose one of: AUTO ON OFF")
 
 if (PUGS_ENABLE_HDF5 MATCHES "^(AUTO|ON)$")
-  # May be risky. (make to show pugs build options)
-  set(HDF5_PREFER_PARALLEL TRUE)
-  find_package(HDF5)
-  if (HDF5_FOUND)
-    # HighFive
-    set(HIGHFIVE_USE_BOOST  OFF)   # no Boost
-    set(HIGHFIVE_BUILD_DOCS OFF)   # no doc
-    set(HIGHFIVE_UNIT_TESTS OFF)   # no unit tests
-    set(HIGHFIVE_UNIT_TESTS OFF)   # no unit tests
-    set(HIGHFIVE_EXAMPLES OFF)     # no examples
-    add_subdirectory(${PUGS_SOURCE_DIR}/packages/HighFive/)
-    set(HIGHFIVE_TARGET HighFive)
+  if (MPI_FOUND)
+    # May be risky. (make to show pugs build options)
+    set(HDF5_PREFER_PARALLEL TRUE)
+    find_package(HDF5)
+    if (HDF5_FOUND)
+      # HighFive
+      set(HIGHFIVE_USE_BOOST  OFF)   # no Boost
+      set(HIGHFIVE_BUILD_DOCS OFF)   # no doc
+      set(HIGHFIVE_UNIT_TESTS OFF)   # no unit tests
+      set(HIGHFIVE_UNIT_TESTS OFF)   # no unit tests
+      set(HIGHFIVE_EXAMPLES OFF)     # no examples
+      add_subdirectory(${PUGS_SOURCE_DIR}/packages/HighFive/)
+      set(HIGHFIVE_TARGET HighFive::HighFive)
+
+      add_library(PkgConfig::HDF5 STATIC IMPORTED)
+      set_property(TARGET PkgConfig::HDF5 PROPERTY
+	IMPORTED_LOCATION "${HDF5_LIBRARIES}")
+
+      set_property(TARGET PkgConfig::HDF5 PROPERTY
+	INTERFACE_INCLUDE_DIRECTORIES "${HDF5_INCLUDE_DIRS}")
+
+      set(HDF5_TARGET PkgConfig::HDF5)
+    endif()
+  else()
+    message(STATUS "HDF5 support is deactivated since pugs will not be build with MPI support")
+    set(HDF5_FOUND FALSE)
   endif()
   set(PUGS_HAS_HDF5 ${HDF5_FOUND})
 else()
   unset(HIGHFIVE_TARGET)
+  unset(HDF5_TARGET)
   unset(PUGS_HAS_HDF5)
 endif()
 
@@ -251,7 +349,14 @@ find_package(Slurm)
 
 set(PUGS_HAS_SLURM ${SLURM_FOUND})
 
-if (${SLURM_FOUND})
+if (SLURM_FOUND)
+  add_library(PkgConfig::Slurm STATIC IMPORTED)
+  set_property(TARGET PkgConfig::Slurm PROPERTY
+    IMPORTED_LOCATION "${SLURM_LIBRARY}")
+  set_property(TARGET PkgConfig::Slurm PROPERTY
+    INTERFACE_INCLUDE_DIRECTORIES "${SLURM_INCLUDE_DIR}")
+
+  set(SLURM_TARGET PkgConfig::Slurm)
   include_directories(SYSTEM "${SLURM_INCLUDE_DIR}")
 else()
   set(SLURM_LIBRARY "")
@@ -364,6 +469,10 @@ endif()
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${PUGS_CXX_FLAGS}")
 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${PUGS_CXX_FLAGS}")
 
+
+message(STATUS "CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}")
+
+
 # Add debug mode for Standard C++ library (not for AppleClang since it is broken)
 if (NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang")
   set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG -D_LIBCPP_DEBUG=1")
@@ -372,12 +481,14 @@ endif()
 #------------------------------------------------------
 
 # Rang (colors? Useless thus necessary!)
+add_subdirectory(${PUGS_SOURCE_DIR}/packages/rang/)
 include_directories(SYSTEM "${PUGS_SOURCE_DIR}/packages/rang/include")
 
 # CLI11
 include_directories(SYSTEM "${PUGS_SOURCE_DIR}/packages/CLI11/include")
 
 # PEGTL
+add_subdirectory(${PUGS_SOURCE_DIR}/packages/PEGTL/)
 include_directories(SYSTEM "${PUGS_SOURCE_DIR}/packages/PEGTL/include/tao")
 
 # Pugs src
@@ -400,7 +511,7 @@ if(${PUGS_HAS_MPI})
   endif()
   set(MPIEXEC_NUMPROC 3)
   set(MPIEXEC_PATH_FLAG --path)
-  set(MPI_LAUNCHER ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${MPIEXEC_NUMPROC} ${MPIEXEC_PATH_FLAG} ${PUGS_BINARY_DIR} ${MPIEXEC_OPTION_FLAGS})
+  set(MPI_LAUNCHER ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${MPIEXEC_NUMPROC} ${MPIEXEC_OPTION_FLAGS})
 endif()
 
 add_custom_target(all_unit_tests
@@ -427,11 +538,11 @@ endif()
 
 add_custom_target(run_mpi_unit_tests
   COMMAND ${MPI_LAUNCHER} "${PUGS_BINARY_DIR}/mpi_unit_tests"
+  WORKING_DIRECTORY ${PUGS_BINARY_DIR}
   DEPENDS run_unit_tests
   COMMENT ${RUN_MPI_UNIT_TESTS_COMMENT}
   )
 
-
 add_custom_target(run_unit_tests
   COMMAND "${PUGS_BINARY_DIR}/unit_tests"
   DEPENDS all_unit_tests
@@ -594,10 +705,6 @@ if("${CMAKE_BUILD_TYPE}" STREQUAL "Coverage")
   endif()
 endif()
 
-# -----------------------------------------------------
-
-link_libraries("-rdynamic")
-
 # ------------------- Source files --------------------
 # Pugs binary
 add_executable(
@@ -625,17 +732,20 @@ target_link_libraries(
   PugsLanguageUtils
   PugsCheckpointing
   Kokkos::kokkos
-  ${PETSC_LIBRARIES}
-  ${SLEPC_LIBRARIES}
-  ${PARMETIS_LIBRARIES}
+  ${PETSC_TARGET}
+  ${SLEPC_TARGET}
+  ${PARMETIS_TARGET}
   ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
   ${KOKKOS_CXX_FLAGS}
   ${OPENMP_LINK_FLAGS}
   ${PUGS_STD_LINK_FLAGS}
   ${HIGHFIVE_TARGET}
-  ${SLURM_LIBRARY}
+  ${SLURM_TARGET}
   stdc++fs
-  )
+)
+
+target_include_directories(pugs PUBLIC ${PETSC_INCLUDE_DIRS})
+target_include_directories(pugs PUBLIC ${HDF5_INCLUDE_DIRS})
 
 # Checkpoint management tool
 add_executable(
@@ -660,19 +770,18 @@ target_link_libraries(
   PugsMesh
   PugsOutput
   Kokkos::kokkos
-  ${PETSC_LIBRARIES}
-  ${SLEPC_LIBRARIES}
-  ${PARMETIS_LIBRARIES}
+  ${PETSC_TARGET}
+  ${SLEPC_TARGET}
+  ${PARMETIS_TARGET}
   ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
   ${KOKKOS_CXX_FLAGS}
   ${OPENMP_LINK_FLAGS}
   ${PUGS_STD_LINK_FLAGS}
   ${HIGHFIVE_TARGET}
-  ${SLURM_LIBRARY}
+  ${SLURM_TARGET}
   stdc++fs
   )
 
-
 # -------------------- Documentation --------------------
 
 include(PugsDoc)
@@ -685,7 +794,6 @@ include(PugsDoxygen)
 install(TARGETS
   pugs
   pugs_checkpoint
-  PugsMesh
   PugsAlgebra
   PugsAnalysis
   PugsCheckpointing
@@ -694,18 +802,33 @@ install(TARGETS
   PugsLanguage
   PugsLanguageAST
   PugsLanguageModules
-  PugsLanguageAlgorithms
+  PugsLanguageUtils
   PugsMesh
-  PugsAlgebra
-  PugsScheme
-  PugsUtils
   PugsOutput
-  PugsLanguageUtils
+  PugsScheme
   kokkos
+  Catch2
+
+  EXPORT "${PROJECT_NAME}Targets"
 
   RUNTIME DESTINATION bin
   LIBRARY DESTINATION lib
-  ARCHIVE DESTINATION lib)
+  ARCHIVE DESTINATION lib
+)
+
+include(CMakePackageConfigHelpers)
+
+write_basic_package_version_file(
+  PugsConfigVersion.cmake
+  VERSION ${PACKAGE_VERSION}
+  COMPATIBILITY AnyNewerVersion
+)
+
+install(EXPORT PugsTargets
+  FILE PugsTargets.cmake
+  NAMESPACE Pugs::
+  DESTINATION lib/cmake/pugs
+)
 
 # ------------------- Build options -------------------
 message("")
@@ -751,6 +874,16 @@ else()
   endif()
 endif()
 
+if (EIGEN3_FOUND)
+  message(" Eigen3: ${Eigen3_VERSION}")
+else()
+  if (PUGS_ENABLE_EIGEN3 MATCHES "^(AUTO|ON)$")
+    message(" Eigen3: not found!")
+  else()
+      message(" Eigen3: explicitly deactivated!")
+  endif()
+endif()
+
 if (HDF5_FOUND)
   message(" HDF5: ${HDF5_VERSION} parallel: ${HDF5_IS_PARALLEL}")
 else()
@@ -840,3 +973,24 @@ endif()
 
 message("================================")
 message("")
+
+configure_file(
+  ${PUGS_SOURCE_DIR}/cmake/PugsCompileFlags.cmake.in
+  ${PUGS_BINARY_DIR}/cmake/PugsCompileFlags.cmake
+  @ONLY
+)
+
+install(
+  FILES ${PUGS_BINARY_DIR}/cmake/PugsCompileFlags.cmake
+  DESTINATION lib/cmake/pugs
+)
+
+# Ugly patch to install user headers for Catch2 (for plugins)
+install(
+  DIRECTORY
+  "${PUGS_BINARY_DIR}/packages/Catch2/generated-includes/catch2" # Also install the generated header
+  DESTINATION
+  "include"
+  FILES_MATCHING
+  PATTERN "*.hpp"
+  )
diff --git a/README.md b/README.md
index 36e460a851a53d40b4ab442460594091fa22b20f..473b27b07ed129357a206fa59c4af7d74471de52 100644
--- a/README.md
+++ b/README.md
@@ -11,10 +11,10 @@ that are assembled together by a user friendly language.
 ## Requirements
 
 For the most basic build, `pugs` only requires
-- a C++-17 compiler.
+- a C++-20 compiler.
   - g++-10 or higher versions
-  - clang-10 or higher versions
-- `CMake` (at least 3.16)
+  - clang-11 or higher versions
+- `CMake` (at least 3.19)
 
 > **Warning**:<br>
 > Building `pugs` in its source directory is **forbidden**. Trying to
@@ -82,6 +82,15 @@ To install `SLEPc` on Debian-like systems
 apt install slepc-dev
 ```
 
+#### `Eigen3`
+
+`Eigen3` is linear system solver and an eigenvalue problem solver.
+
+To install `Eigen3` on Debian-like systems
+```shell
+apt install libeigen3-dev
+```
+
 ## Documentation
 
 ### User documentation
@@ -234,6 +243,7 @@ the way `pugs` is built.
 | Description     | Variable            | Values                       |
 |:----------------|:--------------------|:----------------------------:|
 | MPI support     | `PUGS_ENABLE_MPI`   | `AUTO`(default), `ON`, `OFF` |
+| `Eigen3` support| `PUGS_ENABLE_EIGEN3`| `AUTO`(default), `ON`, `OFF` |
 | `PETSc` support | `PUGS_ENABLE_PETSC` | `AUTO`(default), `ON`, `OFF` |
 | `SLEPc` support | `PUGS_ENABLE_SLEPC` | `AUTO`(default), `ON`, `OFF` |
 
diff --git a/cmake/FindPTScotch.cmake b/cmake/FindPTScotch.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..da6286e3e8ea110460a068f8e40839b15080319c
--- /dev/null
+++ b/cmake/FindPTScotch.cmake
@@ -0,0 +1,29 @@
+# Looking for PTScotch
+
+find_package(PkgConfig)
+pkg_check_modules(PC_PTSCOTCH QUIET PTSCOTCH)
+
+find_path(PTSCOTCH_INCLUDE_DIR
+  NAMES ptscotch.h
+  PATH_SUFFIXES  "include" "include/scotch"
+  HINTS "$ENV{PTSCOTCH_INCDIR}")
+
+if(EXISTS "${PTSCOTCH_INCLUDE_DIR}/ptscotch.h")
+  message(STATUS "Found ptscotch.h in ${PTSCOTCH_INCLUDE_DIR}")
+  find_library(LIB_PTSCOTCH ptscotch $ENV{PTSCOTCH_LIBDIR})
+  if("${LIB_PTSCOTCH}" STREQUAL "LIB_PTSCOTCH-NOTFOUND")
+    message(WARNING "** Could not find ptscotch library.\n** Is PTSCOTCH_LIBDIR correctly set (Actual: \"$ENV{PTSCOTCH_LIBDIR}\")?")
+  endif()
+  set(PTSCOTCH_LIBRARIES ${LIB_PTSCOTCH})
+  message(STATUS "Found ptscotch/scotch libraries ${PTSCOTCH_LIBRARIES}")
+else()
+  message(WARNING "** Could not find ptscotch.h.\n** Is PTSCOTCH_INCDIR correctly set (Actual: \"$ENV{PTSCOTCH_INCDIR}\")?")
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(PTScotch
+  FOUND_VAR
+    PTSCOTCH_FOUND
+  REQUIRED_VARS
+    PTSCOTCH_LIBRARIES
+    PTSCOTCH_INCLUDE_DIR)
diff --git a/cmake/FindParMETIS.cmake b/cmake/FindParMETIS.cmake
index d9b91d33d92617f3282d4eac2184549b62f62418..5fb1bb961c53897ab8f9cd33a2bf6bedfa41a783 100644
--- a/cmake/FindParMETIS.cmake
+++ b/cmake/FindParMETIS.cmake
@@ -1,6 +1,5 @@
 # Looking for ParMETIS
 
-
 find_path(PARMETIS_INCLUDE_DIR parmetis.h
   PATH_SUFFIX include parmetis $ENV{PARMETIS_INCDIR})
 
@@ -17,7 +16,8 @@ if(EXISTS "${PARMETIS_INCLUDE_DIR}/parmetis.h")
   find_path(METIS_INCLUDE_DIR metis.h $ENV{METIS_INCDIR})
   if(EXISTS "${METIS_INCLUDE_DIR}/metis.h")
     message(STATUS "Found metis.h in ${METIS_INCLUDE_DIR}")
-    set(PARMETIS_LIBRARIES ${LIB_PARMETIS} ${LIB_METIS})
+    set(PARMETIS_LIBRARIES ${LIB_PARMETIS})
+    set(METIS_LIBRARIES  ${LIB_METIS})
     message(STATUS "Found parmetis/metis libraries ${PARMETIS_LIBRARIES}")
     else()
       message(WARNING "** Could not find metis.h.\n** Is METIS_INCDIR correctly set (Actual: \"$ENV{METIS_INCDIR}\")?")
@@ -27,3 +27,4 @@ else()
 endif()
 
 mark_as_advanced(PARMETIS_INCLUDE_DIR PARMETIS_LIBRARIES)
+mark_as_advanced(METIS_INCLUDE_DIR METIS_LIBRARIES)
diff --git a/cmake/PugsCompileFlags.cmake.in b/cmake/PugsCompileFlags.cmake.in
new file mode 100644
index 0000000000000000000000000000000000000000..83ecd3e6036a82c3e8696d2b75de2f35d724051c
--- /dev/null
+++ b/cmake/PugsCompileFlags.cmake.in
@@ -0,0 +1,10 @@
+@PACKAGE_INIT@
+
+set(PUGS_CMAKE_CXX_FLAGS "@CMAKE_CXX_FLAGS@")
+set(PUGS_CMAKE_CXX_STANDARD "@CMAKE_CXX_STANDARD@")
+
+set(PUGS_CMAKE_BUILD_TYPE "@CMAKE_BUILD_TYPE@")
+set(PUGS_CMAKE_CXX_COMPILER "@CMAKE_CXX_COMPILER@")
+set(PUGS_CMAKE_C_COMPILER "@CMAKE_C_COMPILER@")
+
+set(PUGS_HAS_MPI "@PUGS_HAS_MPI@")
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index eebccac12c3c1c7b80b6b1e13ff81d0e802a8def..30d920bb0c111049ae655f67e3951424134dbb01 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -3,6 +3,21 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR})
 
 #------------------------------------------------------
 
+install(
+  DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/"
+  DESTINATION "include"
+  FILES_MATCHING
+  PATTERN "*.hpp"
+)
+
+install(
+  DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/"
+  DESTINATION "include"
+  FILES_MATCHING
+  PATTERN "*.hpp"
+  PATTERN "CMakeFiles" EXCLUDE
+)
+
 # Pugs utils
 add_subdirectory(utils)
 
diff --git a/src/algebra/CMakeLists.txt b/src/algebra/CMakeLists.txt
index 74f40290a2f9250cc2296ec62358b44c366ffc85..78c041c5cee2e297258d299152d5cc3a4880f599 100644
--- a/src/algebra/CMakeLists.txt
+++ b/src/algebra/CMakeLists.txt
@@ -3,13 +3,17 @@
 add_library(
   PugsAlgebra
   EigenvalueSolver.cpp
+  EigenvalueSolverOptions.cpp
   LinearSolver.cpp
   LinearSolverOptions.cpp
   PETScUtils.cpp)
 
 target_link_libraries(
   PugsAlgebra
-  ${PETSC_LIBRARIES}
-  ${SLEPC_LIBRARIES}
+  ${PETSC_TARGET}
+  ${SLEPC_TARGET}
   ${HIGHFIVE_TARGET}
 )
+
+target_include_directories(PugsAlgebra PUBLIC ${PETSC_INCLUDE_DIRS})
+target_include_directories(PugsAlgebra PUBLIC ${SLEPC_INCLUDE_DIRS})
diff --git a/src/algebra/DenseMatrixWrapper.hpp b/src/algebra/DenseMatrixWrapper.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..dee57f27eff3d4c906cb256754d86ca6eeafbdfa
--- /dev/null
+++ b/src/algebra/DenseMatrixWrapper.hpp
@@ -0,0 +1,59 @@
+#ifndef DENSE_MATRIX_WRAPPER_HPP
+#define DENSE_MATRIX_WRAPPER_HPP
+
+#include <algebra/SmallMatrix.hpp>
+#include <algebra/TinyMatrix.hpp>
+
+template <typename DataType>
+class DenseMatrixWrapper
+{
+ private:
+  const size_t m_number_of_rows;
+  const size_t m_number_of_columns;
+  const DataType* const m_matrix_ptr;
+
+ public:
+  PUGS_INLINE
+  bool
+  isSquare() const
+  {
+    return (m_number_of_rows == m_number_of_columns);
+  }
+
+  PUGS_INLINE
+  size_t
+  numberOfRows() const
+  {
+    return m_number_of_rows;
+  }
+
+  PUGS_INLINE
+  size_t
+  numberOfColumns() const
+  {
+    return m_number_of_columns;
+  }
+
+  PUGS_INLINE
+  const DataType* const&
+  ptr() const
+  {
+    return m_matrix_ptr;
+  }
+
+  DenseMatrixWrapper(const SmallMatrix<DataType>& A)
+    : m_number_of_rows{A.numberOfRows()},
+      m_number_of_columns{A.numberOfColumns()},
+      m_matrix_ptr{(m_number_of_columns * m_number_of_rows > 0) ? (&A(0, 0)) : nullptr}
+  {}
+
+  template <size_t M, size_t N>
+  DenseMatrixWrapper(const TinyMatrix<M, N, DataType>& A)
+    : m_number_of_rows{M}, m_number_of_columns{N}, m_matrix_ptr{(M * N > 0) ? (&A(0, 0)) : nullptr}
+  {}
+
+  DenseMatrixWrapper(const DenseMatrixWrapper&) = delete;
+  DenseMatrixWrapper(DenseMatrixWrapper&&)      = delete;
+};
+
+#endif   // DENSE_MATRIX_WRAPPER_HPP
diff --git a/src/algebra/Eigen3Utils.hpp b/src/algebra/Eigen3Utils.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d9d61e02746c77e22002fb1c1ecfb93db6c1a9bb
--- /dev/null
+++ b/src/algebra/Eigen3Utils.hpp
@@ -0,0 +1,145 @@
+#ifndef EIGEN3_UTILS_HPP
+#define EIGEN3_UTILS_HPP
+
+#include <utils/pugs_config.hpp>
+
+#ifdef PUGS_HAS_EIGEN3
+
+#include <algebra/CRSMatrix.hpp>
+#include <algebra/DenseMatrixWrapper.hpp>
+
+#include <eigen3/Eigen/Eigen>
+
+class Eigen3DenseMatrixEmbedder
+{
+ public:
+  using Eigen3MatrixType    = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
+  using Eigen3MapMatrixType = Eigen::Map<const Eigen3MatrixType>;
+
+ private:
+  Eigen3MapMatrixType m_eigen_matrix;
+
+  Eigen3DenseMatrixEmbedder(const size_t nb_rows, const size_t nb_columns, const double* A)
+    : m_eigen_matrix{A, int(nb_rows), int(nb_columns)}
+  {}
+
+ public:
+  PUGS_INLINE
+  size_t
+  isSquare() const
+  {
+    return (m_eigen_matrix.rows() == m_eigen_matrix.cols());
+  }
+
+  PUGS_INLINE
+  size_t
+  numberOfRows() const
+  {
+    return m_eigen_matrix.rows();
+  }
+
+  PUGS_INLINE
+  size_t
+  numberOfColumns() const
+  {
+    return m_eigen_matrix.cols();
+  }
+
+  PUGS_INLINE
+  Eigen3MapMatrixType&
+  matrix()
+  {
+    return m_eigen_matrix;
+  }
+
+  PUGS_INLINE
+  const Eigen3MapMatrixType&
+  matrix() const
+  {
+    return m_eigen_matrix;
+  }
+
+  Eigen3DenseMatrixEmbedder(const DenseMatrixWrapper<double>& A)
+    : Eigen3DenseMatrixEmbedder{A.numberOfRows(), A.numberOfColumns(), A.ptr()}
+  {}
+
+  ~Eigen3DenseMatrixEmbedder() = default;
+};
+
+class Eigen3SparseMatrixEmbedder
+{
+ public:
+  using Eigen3MatrixType    = Eigen::SparseMatrix<double, Eigen::RowMajor>;
+  using Eigen3MapMatrixType = Eigen::Map<const Eigen3MatrixType>;
+
+ private:
+  Eigen3MapMatrixType m_eigen_matrix;
+
+ public:
+  PUGS_INLINE
+  size_t
+  numberOfRows() const
+  {
+    return m_eigen_matrix.rows();
+  }
+
+  PUGS_INLINE
+  size_t
+  numberOfColumns() const
+  {
+    return m_eigen_matrix.cols();
+  }
+
+  PUGS_INLINE
+  size_t
+  isSquare() const
+  {
+    return (m_eigen_matrix.rows() == m_eigen_matrix.cols());
+  }
+
+  PUGS_INLINE
+  Eigen3MapMatrixType&
+  matrix()
+  {
+    return m_eigen_matrix;
+  }
+
+  PUGS_INLINE
+  const Eigen3MapMatrixType&
+  matrix() const
+  {
+    return m_eigen_matrix;
+  }
+
+  Eigen3SparseMatrixEmbedder(const CRSMatrix<double>& A)
+    : m_eigen_matrix{A.numberOfRows(),
+                     A.numberOfColumns(),
+                     static_cast<int>(A.values().size()),                                  //
+                     (A.rowMap().size() > 0) ? &(A.rowMap()[0]) : nullptr,                 //
+                     (A.columnIndices().size() > 0) ? &(A.columnIndices()[0]) : nullptr,   //
+                     (A.values().size() > 0) ? &(A.values()[0]) : nullptr}
+  {}
+  ~Eigen3SparseMatrixEmbedder() = default;
+};
+
+#else   // PUGS_HAS_EIGEN3
+
+class Eigen3DenseMatrixEmbedder
+{
+ public:
+  Eigen3DenseMatrixEmbedder(const DenseMatrixWrapper<double>&) {}
+
+  ~Eigen3DenseMatrixEmbedder() = default;
+};
+
+class Eigen3SparseMatrixEmbedder
+{
+ public:
+  Eigen3SparseMatrixEmbedder(const CRSMatrix<double>&) {}
+
+  ~Eigen3SparseMatrixEmbedder() = default;
+};
+
+#endif   // PUGS_HAS_EIGEN3
+
+#endif   // EIGEN3_UTILS_HPP
diff --git a/src/algebra/EigenvalueSolver.cpp b/src/algebra/EigenvalueSolver.cpp
index c71be58ddcb1c37498a172f30a1eb39b89f43aab..b17e650af775530d3e61f4561b7676dccf1efdd4 100644
--- a/src/algebra/EigenvalueSolver.cpp
+++ b/src/algebra/EigenvalueSolver.cpp
@@ -2,10 +2,17 @@
 #include <utils/pugs_config.hpp>
 
 #ifdef PUGS_HAS_SLEPC
+#include <algebra/PETScUtils.hpp>
 #include <slepc.h>
+#endif   // PUGS_HAS_SLEPC
+
+#ifdef PUGS_HAS_EIGEN3
+#include <algebra/Eigen3Utils.hpp>
+#endif   // PUGS_HAS_EIGEN3
 
 struct EigenvalueSolver::Internals
 {
+#ifdef PUGS_HAS_SLEPC
   static PetscReal
   computeSmallestRealEigenvalueOfSymmetricMatrix(EPS& eps)
   {
@@ -65,93 +72,357 @@ struct EigenvalueSolver::Internals
     computeAllEigenvaluesOfSymmetricMatrixInInterval(eps, left_bound - 0.01 * std::abs(left_bound),
                                                      right_bound + 0.01 * std::abs(right_bound));
   }
+
+  static void
+  slepscComputeForSymmetricMatrix(const PETScAijMatrixEmbedder& A, SmallArray<double>& eigenvalues)
+  {
+    EPS eps;
+
+    EPSCreate(PETSC_COMM_SELF, &eps);
+    EPSSetOperators(eps, A, nullptr);
+    EPSSetProblemType(eps, EPS_HEP);
+
+    Internals::computeAllEigenvaluesOfSymmetricMatrix(eps);
+
+    PetscInt nb_eigenvalues;
+    EPSGetDimensions(eps, &nb_eigenvalues, nullptr, nullptr);
+
+    eigenvalues = SmallArray<double>(nb_eigenvalues);
+    for (PetscInt i = 0; i < nb_eigenvalues; ++i) {
+      EPSGetEigenpair(eps, i, &(eigenvalues[i]), nullptr, nullptr, nullptr);
+    }
+
+    EPSDestroy(&eps);
+  }
+
+  static void
+  slepscComputeForSymmetricMatrix(const PETScAijMatrixEmbedder& A,
+                                  SmallArray<double>& eigenvalues,
+                                  std::vector<SmallVector<double>>& eigenvector_list)
+  {
+    EPS eps;
+
+    EPSCreate(PETSC_COMM_SELF, &eps);
+    EPSSetOperators(eps, A, nullptr);
+    EPSSetProblemType(eps, EPS_HEP);
+
+    Internals::computeAllEigenvaluesOfSymmetricMatrix(eps);
+
+    PetscInt nb_eigenvalues;
+    EPSGetDimensions(eps, &nb_eigenvalues, nullptr, nullptr);
+
+    eigenvalues = SmallArray<double>(nb_eigenvalues);
+    eigenvector_list.reserve(nb_eigenvalues);
+    for (PetscInt i = 0; i < nb_eigenvalues; ++i) {
+      Vec Vr;
+      SmallVector<double> eigenvector{A.numberOfRows()};
+      VecCreateSeqWithArray(PETSC_COMM_SELF, 1, A.numberOfRows(), &(eigenvector[0]), &Vr);
+      EPSGetEigenpair(eps, i, &(eigenvalues[i]), nullptr, Vr, nullptr);
+      VecDestroy(&Vr);
+      eigenvector_list.push_back(eigenvector);
+    }
+
+    EPSDestroy(&eps);
+  }
+
+  static void
+  slepscComputeForSymmetricMatrix(const PETScAijMatrixEmbedder& A,
+                                  SmallArray<double>& eigenvalues,
+                                  SmallMatrix<double>& P)
+  {
+    EPS eps;
+
+    EPSCreate(PETSC_COMM_SELF, &eps);
+    EPSSetOperators(eps, A, nullptr);
+    EPSSetProblemType(eps, EPS_HEP);
+
+    Internals::computeAllEigenvaluesOfSymmetricMatrix(eps);
+
+    PetscInt nb_eigenvalues;
+    EPSGetDimensions(eps, &nb_eigenvalues, nullptr, nullptr);
+
+    eigenvalues = SmallArray<double>(nb_eigenvalues);
+    P           = SmallMatrix<double>(nb_eigenvalues, nb_eigenvalues);
+
+    Array<double> eigenvector(nb_eigenvalues);
+    for (PetscInt i = 0; i < nb_eigenvalues; ++i) {
+      Vec Vr;
+      VecCreateSeqWithArray(PETSC_COMM_SELF, 1, A.numberOfRows(), &(eigenvector[0]), &Vr);
+      EPSGetEigenpair(eps, i, &(eigenvalues[i]), nullptr, Vr, nullptr);
+      VecDestroy(&Vr);
+      for (size_t j = 0; j < eigenvector.size(); ++j) {
+        P(j, i) = eigenvector[j];
+      }
+    }
+
+    EPSDestroy(&eps);
+  }
+#endif   // PUGS_HAS_SLEPC
+
+#ifdef PUGS_HAS_EIGEN3
+
+  template <typename Eigen3MatrixEmbedderType>
+  static void
+  eigen3ComputeForSymmetricMatrix(const Eigen3MatrixEmbedderType& A, SmallArray<double>& eigenvalues)
+  {
+    using Eigen3MatrixType = typename Eigen3MatrixEmbedderType::Eigen3MatrixType;
+    Eigen::SelfAdjointEigenSolver<Eigen3MatrixType> eigen_solver;
+
+    eigen_solver.compute(A.matrix(), Eigen::EigenvaluesOnly);
+
+    eigenvalues = SmallArray<double>(A.numberOfRows());
+    for (size_t i = 0; i < eigenvalues.size(); ++i) {
+      eigenvalues[i] = eigen_solver.eigenvalues()[i];
+    }
+  }
+
+  template <typename Eigen3MatrixEmbedderType>
+  static void
+  eigen3ComputeForSymmetricMatrix(const Eigen3MatrixEmbedderType& A,
+                                  SmallArray<double>& eigenvalues,
+                                  std::vector<SmallVector<double>>& eigenvectors)
+  {
+    using Eigen3MatrixType = typename Eigen3MatrixEmbedderType::Eigen3MatrixType;
+    Eigen::SelfAdjointEigenSolver<Eigen3MatrixType> eigen_solver;
+
+    eigen_solver.compute(A.matrix(), Eigen::ComputeEigenvectors);
+
+    eigenvalues = SmallArray<double>(A.numberOfRows());
+    for (size_t i = 0; i < eigenvalues.size(); ++i) {
+      eigenvalues[i] = eigen_solver.eigenvalues()[i];
+    }
+
+    eigenvectors.resize(eigenvalues.size());
+    for (size_t i = 0; i < eigenvalues.size(); ++i) {
+      eigenvectors[i] = SmallVector<double>{eigenvalues.size()};
+      for (size_t j = 0; j < eigenvalues.size(); ++j) {
+        eigenvectors[i][j] = eigen_solver.eigenvectors().coeff(j, i);
+      }
+    }
+  }
+
+  template <typename Eigen3MatrixEmbedderType>
+  static void
+  eigen3ComputeForSymmetricMatrix(const Eigen3MatrixEmbedderType& A,
+                                  SmallArray<double>& eigenvalues,
+                                  SmallMatrix<double>& P)
+  {
+    using Eigen3MatrixType = typename Eigen3MatrixEmbedderType::Eigen3MatrixType;
+    Eigen::SelfAdjointEigenSolver<Eigen3MatrixType> eigen_solver;
+
+    eigen_solver.compute(A.matrix(), Eigen::ComputeEigenvectors);
+
+    eigenvalues = SmallArray<double>(A.numberOfRows());
+    for (size_t i = 0; i < eigenvalues.size(); ++i) {
+      eigenvalues[i] = eigen_solver.eigenvalues()[i];
+    }
+
+    P = SmallMatrix<double>(eigenvalues.size(), eigenvalues.size());
+    for (size_t i = 0; i < eigenvalues.size(); ++i) {
+      for (size_t j = 0; j < eigenvalues.size(); ++j) {
+        P(i, j) = eigen_solver.eigenvectors().coeff(i, j);
+      }
+    }
+  }
+#endif   // PUGS_HAS_EIGEN3
 };
 
+#ifdef PUGS_HAS_SLEPC
 void
-EigenvalueSolver::computeForSymmetricMatrix(const PETScAijMatrixEmbedder& A, SmallArray<double>& eigenvalues)
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const CRSMatrix<double>& A, SmallArray<double>& eigenvalues)
 {
-  EPS eps;
-
-  EPSCreate(PETSC_COMM_SELF, &eps);
-  EPSSetOperators(eps, A, nullptr);
-  EPSSetProblemType(eps, EPS_HEP);
+  Internals::slepscComputeForSymmetricMatrix(A, eigenvalues);
+}
 
-  Internals::computeAllEigenvaluesOfSymmetricMatrix(eps);
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A, SmallArray<double>& eigenvalues)
+{
+  Internals::slepscComputeForSymmetricMatrix(A, eigenvalues);
+}
 
-  PetscInt nb_eigenvalues;
-  EPSGetDimensions(eps, &nb_eigenvalues, nullptr, nullptr);
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   std::vector<SmallVector<double>>& eigenvectors)
+{
+  Internals::slepscComputeForSymmetricMatrix(A, eigenvalues, eigenvectors);
+}
 
-  eigenvalues = SmallArray<double>(nb_eigenvalues);
-  for (PetscInt i = 0; i < nb_eigenvalues; ++i) {
-    EPSGetEigenpair(eps, i, &(eigenvalues[i]), nullptr, nullptr, nullptr);
-  }
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   std::vector<SmallVector<double>>& eigenvectors)
+{
+  Internals::slepscComputeForSymmetricMatrix(A, eigenvalues, eigenvectors);
+}
 
-  EPSDestroy(&eps);
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   SmallMatrix<double>& P)
+{
+  Internals::slepscComputeForSymmetricMatrix(A, eigenvalues, P);
 }
 
 void
-EigenvalueSolver::computeForSymmetricMatrix(const PETScAijMatrixEmbedder& A,
-                                            SmallArray<double>& eigenvalues,
-                                            std::vector<SmallVector<double>>& eigenvector_list)
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   SmallMatrix<double>& P)
 {
-  EPS eps;
+  Internals::slepscComputeForSymmetricMatrix(A, eigenvalues, P);
+}
 
-  EPSCreate(PETSC_COMM_SELF, &eps);
-  EPSSetOperators(eps, A, nullptr);
-  EPSSetProblemType(eps, EPS_HEP);
+#else   // PUGS_HAS_SLEPC
 
-  Internals::computeAllEigenvaluesOfSymmetricMatrix(eps);
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const CRSMatrix<double>&, SmallArray<double>&)
+{}
 
-  PetscInt nb_eigenvalues;
-  EPSGetDimensions(eps, &nb_eigenvalues, nullptr, nullptr);
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>&, SmallArray<double>&)
+{}
 
-  eigenvalues = SmallArray<double>(nb_eigenvalues);
-  eigenvector_list.reserve(nb_eigenvalues);
-  for (PetscInt i = 0; i < nb_eigenvalues; ++i) {
-    Vec Vr;
-    SmallVector<double> eigenvector{A.numberOfRows()};
-    VecCreateSeqWithArray(PETSC_COMM_SELF, 1, A.numberOfRows(), &(eigenvector[0]), &Vr);
-    EPSGetEigenpair(eps, i, &(eigenvalues[i]), nullptr, Vr, nullptr);
-    VecDestroy(&Vr);
-    eigenvector_list.push_back(eigenvector);
-  }
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const CRSMatrix<double>&,
+                                                   SmallArray<double>&,
+                                                   std::vector<SmallVector<double>>&)
+{}
+
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>&,
+                                                   SmallArray<double>&,
+                                                   std::vector<SmallVector<double>>&)
+{}
+
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const CRSMatrix<double>&, SmallArray<double>&, SmallMatrix<double>&)
+{}
+
+void
+EigenvalueSolver::_slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>&,
+                                                   SmallArray<double>&,
+                                                   SmallMatrix<double>&)
+{}
+
+#endif   // PUGS_HAS_SLEPC
+
+#ifdef PUGS_HAS_EIGEN3
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>& A, SmallArray<double>& eigenvalues)
+{
+  Eigen3SparseMatrixEmbedder embedded_A{A};
+  Internals::eigen3ComputeForSymmetricMatrix(embedded_A, eigenvalues);
+}
+
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A, SmallArray<double>& eigenvalues)
+{
+  Internals::eigen3ComputeForSymmetricMatrix(Eigen3DenseMatrixEmbedder{A}, eigenvalues);
+}
+
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   std::vector<SmallVector<double>>& eigenvectors)
+{
+  Internals::eigen3ComputeForSymmetricMatrix(Eigen3SparseMatrixEmbedder{A}, eigenvalues, eigenvectors);
+}
+
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   std::vector<SmallVector<double>>& eigenvectors)
+{
+  Internals::eigen3ComputeForSymmetricMatrix(Eigen3DenseMatrixEmbedder{A}, eigenvalues, eigenvectors);
+}
 
-  EPSDestroy(&eps);
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   SmallMatrix<double>& P)
+{
+  Internals::eigen3ComputeForSymmetricMatrix(Eigen3SparseMatrixEmbedder{A}, eigenvalues, P);
 }
 
 void
-EigenvalueSolver::computeForSymmetricMatrix(const PETScAijMatrixEmbedder& A,
-                                            SmallArray<double>& eigenvalues,
-                                            SmallMatrix<double>& P)
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                                   SmallArray<double>& eigenvalues,
+                                                   SmallMatrix<double>& P)
 {
-  EPS eps;
+  Internals::eigen3ComputeForSymmetricMatrix(Eigen3DenseMatrixEmbedder{A}, eigenvalues, P);
+}
 
-  EPSCreate(PETSC_COMM_SELF, &eps);
-  EPSSetOperators(eps, A, nullptr);
-  EPSSetProblemType(eps, EPS_HEP);
+#else   // PUGS_HAS_EIGEN3
 
-  Internals::computeAllEigenvaluesOfSymmetricMatrix(eps);
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>&, SmallArray<double>&)
+{}
 
-  PetscInt nb_eigenvalues;
-  EPSGetDimensions(eps, &nb_eigenvalues, nullptr, nullptr);
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>&, SmallArray<double>&)
+{}
 
-  eigenvalues = SmallArray<double>(nb_eigenvalues);
-  P           = SmallMatrix<double>(nb_eigenvalues, nb_eigenvalues);
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>&,
+                                                   SmallArray<double>&,
+                                                   std::vector<SmallVector<double>>&)
+{}
 
-  Array<double> eigenvector(nb_eigenvalues);
-  for (PetscInt i = 0; i < nb_eigenvalues; ++i) {
-    Vec Vr;
-    VecCreateSeqWithArray(PETSC_COMM_SELF, 1, A.numberOfRows(), &(eigenvector[0]), &Vr);
-    EPSGetEigenpair(eps, i, &(eigenvalues[i]), nullptr, Vr, nullptr);
-    VecDestroy(&Vr);
-    for (size_t j = 0; j < eigenvector.size(); ++j) {
-      P(j, i) = eigenvector[j];
-    }
-  }
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>&,
+                                                   SmallArray<double>&,
+                                                   std::vector<SmallVector<double>>&)
+{}
+
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>&, SmallArray<double>&, SmallMatrix<double>&)
+{}
 
-  EPSDestroy(&eps);
+void
+EigenvalueSolver::_eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>&,
+                                                   SmallArray<double>&,
+                                                   SmallMatrix<double>&)
+{}
+
+#endif   // PUGS_HAS_EIGEN3
+
+bool
+EigenvalueSolver::hasLibrary(const ESLibrary library)
+{
+  switch (library) {
+  case ESLibrary::eigen3: {
+#ifdef PUGS_HAS_EIGEN3
+    return true;
+#else
+    return false;
+#endif
+  }
+  case ESLibrary::slepsc: {
+#ifdef PUGS_HAS_SLEPC
+    return true;
+#else
+    return false;
+#endif
+  }
+    // LCOV_EXCL_START
+  default: {
+    throw UnexpectedError("Eigenvalue  library (" + ::name(library) + ") was not set!");
+  }
+    // LCOV_EXCL_STOP
+  }
 }
 
-#endif   // PUGS_HAS_SLEPC
+void
+EigenvalueSolver::checkHasLibrary(const ESLibrary library)
+{
+  if (not hasLibrary(library)) {
+    // LCOV_EXCL_START
+    throw NormalError(::name(library) + " is not linked to pugs. Cannot use it!");
+    // LCOV_EXCL_STOP
+  }
+}
 
-EigenvalueSolver::EigenvalueSolver() {}
+EigenvalueSolver::EigenvalueSolver(const EigenvalueSolverOptions& options) : m_options{options}
+{
+  checkHasLibrary(options.library());
+}
diff --git a/src/algebra/EigenvalueSolver.hpp b/src/algebra/EigenvalueSolver.hpp
index a9b462509514ace4b736424c65f7c9abe4572f26..6567adcdc12caabae69baede83e74166696baaf0 100644
--- a/src/algebra/EigenvalueSolver.hpp
+++ b/src/algebra/EigenvalueSolver.hpp
@@ -1,10 +1,13 @@
 #ifndef EIGENVALUE_SOLVER_HPP
 #define EIGENVALUE_SOLVER_HPP
 
-#include <algebra/PETScUtils.hpp>
+#include <algebra/EigenvalueSolverOptions.hpp>
 
+#include <algebra/CRSMatrix.hpp>
+#include <algebra/DenseMatrixWrapper.hpp>
 #include <algebra/SmallMatrix.hpp>
 #include <algebra/SmallVector.hpp>
+#include <algebra/TinyMatrix.hpp>
 #include <utils/Exceptions.hpp>
 #include <utils/SmallArray.hpp>
 
@@ -13,57 +16,118 @@ class EigenvalueSolver
  private:
   struct Internals;
 
-#ifdef PUGS_HAS_SLEPC
-  void computeForSymmetricMatrix(const PETScAijMatrixEmbedder& A, SmallArray<double>& eigenvalues);
+  const EigenvalueSolverOptions m_options;
 
-  void computeForSymmetricMatrix(const PETScAijMatrixEmbedder& A,
-                                 SmallArray<double>& eigenvalues,
-                                 std::vector<SmallVector<double>>& eigenvectors);
+  void _slepscComputeForSymmetricMatrix(const CRSMatrix<double>& A, SmallArray<double>& eigenvalues);
 
-  void computeForSymmetricMatrix(const PETScAijMatrixEmbedder& A,
-                                 SmallArray<double>& eigenvalues,
-                                 SmallMatrix<double>& P);
-#endif   // PUGS_HAS_SLEPC
+  void _slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A, SmallArray<double>& eigenvalues);
+
+  void _slepscComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        std::vector<SmallVector<double>>& eigenvectors);
+
+  void _slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        std::vector<SmallVector<double>>& eigenvectors);
+
+  void _slepscComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        SmallMatrix<double>& P);
+
+  void _slepscComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        SmallMatrix<double>& P);
+
+  void _eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>& A, SmallArray<double>& eigenvalues);
+
+  void _eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A, SmallArray<double>& eigenvalues);
+
+  void _eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        std::vector<SmallVector<double>>& eigenvectors);
+
+  void _eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        std::vector<SmallVector<double>>& eigenvectors);
+
+  void _eigen3ComputeForSymmetricMatrix(const CRSMatrix<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        SmallMatrix<double>& P);
+
+  void _eigen3ComputeForSymmetricMatrix(const DenseMatrixWrapper<double>& A,
+                                        SmallArray<double>& eigenvalues,
+                                        SmallMatrix<double>& P);
 
  public:
+  static bool hasLibrary(ESLibrary library);
+  static void checkHasLibrary(const ESLibrary library);
+
   template <typename MatrixType>
   void
-  computeForSymmetricMatrix([[maybe_unused]] const MatrixType& A, [[maybe_unused]] SmallArray<double>& eigenvalues)
+  computeForSymmetricMatrix(const MatrixType& A, SmallArray<double>& eigenvalues)
   {
-#ifdef PUGS_HAS_SLEPC
-    this->computeForSymmetricMatrix(PETScAijMatrixEmbedder{A}, eigenvalues);
-#else    // PUGS_HAS_SLEPC
-    throw NotImplementedError("SLEPc is required to solve eigenvalue problems");
-#endif   // PUGS_HAS_SLEPC
+    Assert(A.isSquare());
+
+    switch (m_options.library()) {
+    case ESLibrary::eigen3: {
+      this->_eigen3ComputeForSymmetricMatrix(A, eigenvalues);
+      break;
+    }
+    case ESLibrary::slepsc: {
+      this->_slepscComputeForSymmetricMatrix(A, eigenvalues);
+      break;
+    }
+    default: {
+      throw UnexpectedError(::name(m_options.library()) + " cannot compute for symmetric matrices");
+    }
+    }
   }
 
   template <typename MatrixType>
   void
-  computeForSymmetricMatrix([[maybe_unused]] const MatrixType& A,
-                            [[maybe_unused]] SmallArray<double>& eigenvalues,
-                            [[maybe_unused]] std::vector<SmallVector<double>>& eigenvectors)
+  computeForSymmetricMatrix(const MatrixType& A,
+                            SmallArray<double>& eigenvalues,
+                            std::vector<SmallVector<double>>& eigenvectors)
   {
-#ifdef PUGS_HAS_SLEPC
-    this->computeForSymmetricMatrix(PETScAijMatrixEmbedder{A}, eigenvalues, eigenvectors);
-#else    // PUGS_HAS_SLEPC
-    throw NotImplementedError("SLEPc is required to solve eigenvalue problems");
-#endif   // PUGS_HAS_SLEPC
+    Assert(A.isSquare());
+
+    switch (m_options.library()) {
+    case ESLibrary::eigen3: {
+      this->_eigen3ComputeForSymmetricMatrix(A, eigenvalues, eigenvectors);
+      break;
+    }
+    case ESLibrary::slepsc: {
+      this->_slepscComputeForSymmetricMatrix(A, eigenvalues, eigenvectors);
+      break;
+    }
+    default: {
+      throw UnexpectedError(::name(m_options.library()) + " cannot compute for symmetric matrices");
+    }
+    }
   }
 
   template <typename MatrixType>
   void
-  computeForSymmetricMatrix([[maybe_unused]] const MatrixType& A,
-                            [[maybe_unused]] SmallArray<double>& eigenvalues,
-                            [[maybe_unused]] SmallMatrix<double>& P)
+  computeForSymmetricMatrix(const MatrixType& A, SmallArray<double>& eigenvalues, SmallMatrix<double>& P)
   {
-#ifdef PUGS_HAS_SLEPC
-    this->computeForSymmetricMatrix(PETScAijMatrixEmbedder{A}, eigenvalues, P);
-#else    // PUGS_HAS_SLEPC
-    throw NotImplementedError("SLEPc is required to solve eigenvalue problems");
-#endif   // PUGS_HAS_SLEPC
+    Assert(A.isSquare());
+
+    switch (m_options.library()) {
+    case ESLibrary::eigen3: {
+      this->_eigen3ComputeForSymmetricMatrix(A, eigenvalues, P);
+      break;
+    }
+    case ESLibrary::slepsc: {
+      this->_slepscComputeForSymmetricMatrix(A, eigenvalues, P);
+      break;
+    }
+    default: {
+      throw UnexpectedError(::name(m_options.library()) + " cannot compute for symmetric matrices");
+    }
+    }
   }
 
-  EigenvalueSolver();
+  EigenvalueSolver(const EigenvalueSolverOptions& options = EigenvalueSolverOptions::default_options);
   ~EigenvalueSolver() = default;
 };
 
diff --git a/src/algebra/EigenvalueSolverOptions.cpp b/src/algebra/EigenvalueSolverOptions.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a4ef9549789da993760d72a5d7f289a76d9f2d6
--- /dev/null
+++ b/src/algebra/EigenvalueSolverOptions.cpp
@@ -0,0 +1,10 @@
+#include <algebra/EigenvalueSolverOptions.hpp>
+
+#include <rang.hpp>
+
+std::ostream&
+operator<<(std::ostream& os, const EigenvalueSolverOptions& options)
+{
+  os << "  library: " << rang::style::bold << name(options.library()) << rang::style::reset << '\n';
+  return os;
+}
diff --git a/src/algebra/EigenvalueSolverOptions.hpp b/src/algebra/EigenvalueSolverOptions.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1c047ef3cb63df23cfb7f8725edf68ffa8ad26a6
--- /dev/null
+++ b/src/algebra/EigenvalueSolverOptions.hpp
@@ -0,0 +1,90 @@
+#ifndef EIGENVALUE_SOLVER_OPTIONS_HPP
+#define EIGENVALUE_SOLVER_OPTIONS_HPP
+
+#include <utils/Exceptions.hpp>
+
+#include <iostream>
+
+enum class ESLibrary : int8_t
+{
+  ES__begin = 0,
+  //
+  eigen3 = ES__begin,
+  slepsc,
+  //
+  ES__end
+};
+
+inline std::string
+name(const ESLibrary library)
+{
+  switch (library) {
+  case ESLibrary::eigen3: {
+    return "Eigen3";
+  }
+  case ESLibrary::slepsc: {
+    return "SLEPSc";
+  }
+  case ESLibrary::ES__end: {
+  }
+  }
+  throw UnexpectedError("Eigenvalue solver library name is not defined!");
+}
+
+template <typename ESEnumType>
+inline ESEnumType
+getESEnumFromName(const std::string& enum_name)
+{
+  using BaseT = std::underlying_type_t<ESEnumType>;
+  for (BaseT enum_value = static_cast<BaseT>(ESEnumType::ES__begin);
+       enum_value < static_cast<BaseT>(ESEnumType::ES__end); ++enum_value) {
+    if (name(ESEnumType{enum_value}) == enum_name) {
+      return ESEnumType{enum_value};
+    }
+  }
+  throw NormalError(std::string{"could not find '"} + enum_name + "' associate type!");
+}
+
+template <typename ESEnumType>
+inline void
+printESEnumListNames(std::ostream& os)
+{
+  using BaseT = std::underlying_type_t<ESEnumType>;
+  for (BaseT enum_value = static_cast<BaseT>(ESEnumType::ES__begin);
+       enum_value < static_cast<BaseT>(ESEnumType::ES__end); ++enum_value) {
+    os << "  - " << name(ESEnumType{enum_value}) << '\n';
+  }
+}
+
+class EigenvalueSolverOptions
+{
+ private:
+  ESLibrary m_library = ESLibrary::slepsc;
+
+ public:
+  static EigenvalueSolverOptions default_options;
+
+  friend std::ostream& operator<<(std::ostream& os, const EigenvalueSolverOptions& options);
+
+  ESLibrary&
+  library()
+  {
+    return m_library;
+  }
+
+  ESLibrary
+  library() const
+  {
+    return m_library;
+  }
+
+  EigenvalueSolverOptions(const EigenvalueSolverOptions&) = default;
+  EigenvalueSolverOptions(EigenvalueSolverOptions&&)      = default;
+
+  EigenvalueSolverOptions()  = default;
+  ~EigenvalueSolverOptions() = default;
+};
+
+inline EigenvalueSolverOptions EigenvalueSolverOptions::default_options;
+
+#endif   // EIGENVALUE_SOLVER_OPTIONS_HPP
diff --git a/src/algebra/LinearSolver.cpp b/src/algebra/LinearSolver.cpp
index 99bff927c744c9ba216e575dcc914a2bdba1d4da..52bf8fc3969d2001685c5548f31c30ce7af20dd0 100644
--- a/src/algebra/LinearSolver.cpp
+++ b/src/algebra/LinearSolver.cpp
@@ -3,8 +3,13 @@
 
 #include <algebra/BiCGStab.hpp>
 #include <algebra/CG.hpp>
+#include <algebra/Eigen3Utils.hpp>
 #include <algebra/PETScUtils.hpp>
 
+#ifdef PUGS_HAS_EIGEN3
+#include <eigen3/unsupported/Eigen/IterativeSolvers>
+#endif   // PUGS_HAS_EIGEN3
+
 #ifdef PUGS_HAS_PETSC
 #include <petsc.h>
 #endif   // PUGS_HAS_PETSC
@@ -18,6 +23,13 @@ struct LinearSolver::Internals
     case LSLibrary::builtin: {
       return true;
     }
+    case LSLibrary::eigen3: {
+#ifdef PUGS_HAS_EIGEN3
+      return true;
+#else
+      return false;
+#endif
+    }
     case LSLibrary::petsc: {
 #ifdef PUGS_HAS_PETSC
       return true;
@@ -57,6 +69,25 @@ struct LinearSolver::Internals
     }
   }
 
+  static void
+  checkEigen3Method(const LSMethod method)
+  {
+    switch (method) {
+    case LSMethod::cg:
+    case LSMethod::bicgstab:
+    case LSMethod::gmres:
+    case LSMethod::lu:
+    case LSMethod::cholesky: {
+      break;
+    }
+      // LCOV_EXCL_START
+    default: {
+      throw NormalError(name(method) + " is not an Eigen3 linear solver!");
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
+
   static void
   checkPETScMethod(const LSMethod method)
   {
@@ -90,6 +121,24 @@ struct LinearSolver::Internals
     }
   }
 
+  static void
+  checkEigen3Precond(const LSPrecond precond)
+  {
+    switch (precond) {
+    case LSPrecond::none:
+    case LSPrecond::diagonal:
+    case LSPrecond::incomplete_cholesky:
+    case LSPrecond::incomplete_LU: {
+      break;
+    }
+      // LCOV_EXCL_START
+    default: {
+      throw NormalError(name(precond) + " is not an Eigen3 preconditioner!");
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
+
   static void
   checkPETScPrecond(const LSPrecond precond)
   {
@@ -118,6 +167,11 @@ struct LinearSolver::Internals
       checkBuiltinPrecond(options.precond());
       break;
     }
+    case LSLibrary::eigen3: {
+      checkEigen3Method(options.method());
+      checkEigen3Precond(options.precond());
+      break;
+    }
     case LSLibrary::petsc: {
       checkPETScMethod(options.method());
       checkPETScPrecond(options.precond());
@@ -133,10 +187,10 @@ struct LinearSolver::Internals
 
   template <typename MatrixType, typename SolutionVectorType, typename RHSVectorType>
   static void
-  builtinSolveLocalSystem(const MatrixType& A,
-                          SolutionVectorType& x,
-                          const RHSVectorType& b,
-                          const LinearSolverOptions& options)
+  _builtinSolveLocalSystem(const MatrixType& A,
+                           SolutionVectorType& x,
+                           const RHSVectorType& b,
+                           const LinearSolverOptions& options)
   {
     if (options.precond() != LSPrecond::none) {
       // LCOV_EXCL_START
@@ -160,6 +214,134 @@ struct LinearSolver::Internals
     }
   }
 
+#ifdef PUGS_HAS_EIGEN3
+  template <typename PreconditionerType, typename Eigen3MatrixEmbedderType>
+  static void
+  _preconditionedEigen3SolveLocalSystem(const Eigen3MatrixEmbedderType& eigen3_A,
+                                        VectorWrapper<double>& x,
+                                        const VectorWrapper<const double>& b,
+                                        const LinearSolverOptions& options)
+  {
+    Eigen::Map<Eigen::VectorX<double>> eigen3_x{x.ptr(), static_cast<int>(x.size())};
+    Eigen::Map<const Eigen::VectorX<double>> eigen3_b{b.ptr(), static_cast<int>(b.size())};
+
+    using Eigen3MatrixType = typename Eigen3MatrixEmbedderType::Eigen3MatrixType;
+
+    switch (options.method()) {
+    case LSMethod::bicgstab: {
+      Eigen::BiCGSTAB<Eigen3MatrixType, PreconditionerType> solver;
+      solver.compute(eigen3_A.matrix());
+      solver.setMaxIterations(options.maximumIteration());
+      solver.setTolerance(options.epsilon());
+      eigen3_x = solver.solve(eigen3_b);
+      break;
+    }
+    case LSMethod::cg: {
+      Eigen::ConjugateGradient<Eigen3MatrixType, Eigen::Lower | Eigen::Upper, PreconditionerType> solver;
+      solver.compute(eigen3_A.matrix());
+      solver.setMaxIterations(options.maximumIteration());
+      solver.setTolerance(options.epsilon());
+      eigen3_x = solver.solve(eigen3_b);
+      break;
+    }
+    case LSMethod::gmres: {
+      Eigen::GMRES<Eigen3MatrixType, PreconditionerType> solver;
+      solver.compute(eigen3_A.matrix());
+      solver.setMaxIterations(options.maximumIteration());
+      solver.setTolerance(options.epsilon());
+      eigen3_x = solver.solve(eigen3_b);
+      break;
+    }
+    case LSMethod::lu: {
+      if constexpr (std::is_same_v<Eigen3MatrixEmbedderType, Eigen3DenseMatrixEmbedder>) {
+        Eigen::FullPivLU<Eigen3MatrixType> solver;
+        solver.compute(eigen3_A.matrix());
+        eigen3_x = solver.solve(eigen3_b);
+      } else {
+        Eigen::SparseLU<Eigen3MatrixType> solver;
+        solver.compute(eigen3_A.matrix());
+        eigen3_x = solver.solve(eigen3_b);
+      }
+      break;
+    }
+    case LSMethod::cholesky: {
+      if constexpr (std::is_same_v<Eigen3MatrixEmbedderType, Eigen3DenseMatrixEmbedder>) {
+        Eigen::LLT<Eigen3MatrixType> solver;
+        solver.compute(eigen3_A.matrix());
+        eigen3_x = solver.solve(eigen3_b);
+      } else {
+        Eigen::SimplicialLLT<Eigen3MatrixType> solver;
+        solver.compute(eigen3_A.matrix());
+        eigen3_x = solver.solve(eigen3_b);
+      }
+      break;
+    }
+      // LCOV_EXCL_START
+    default: {
+      throw UnexpectedError("unexpected method: " + name(options.method()));
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
+
+  template <typename Eigen3MatrixEmbedderType>
+  static void
+  _eigen3SolveLocalSystem(const Eigen3MatrixEmbedderType& eigen3_A,
+                          VectorWrapper<double>& x,
+                          const VectorWrapper<const double>& b,
+                          const LinearSolverOptions& options)
+  {
+    switch (options.precond()) {
+    case LSPrecond::none: {
+      _preconditionedEigen3SolveLocalSystem<Eigen::IdentityPreconditioner, Eigen3MatrixEmbedderType>(eigen3_A, x, b,
+                                                                                                     options);
+      break;
+    }
+    case LSPrecond::diagonal: {
+      _preconditionedEigen3SolveLocalSystem<Eigen::DiagonalPreconditioner<double>, Eigen3MatrixEmbedderType>(eigen3_A,
+                                                                                                             x, b,
+                                                                                                             options);
+      break;
+    }
+    case LSPrecond::incomplete_cholesky: {
+      if constexpr (std::is_same_v<Eigen3SparseMatrixEmbedder, Eigen3MatrixEmbedderType>) {
+        _preconditionedEigen3SolveLocalSystem<Eigen::IncompleteCholesky<double>, Eigen3MatrixEmbedderType>(eigen3_A, x,
+                                                                                                           b, options);
+      } else {
+        throw NormalError("incomplete cholesky is not available for dense matrices in Eigen3");
+      }
+      break;
+    }
+    case LSPrecond::incomplete_LU: {
+      if constexpr (std::is_same_v<Eigen3SparseMatrixEmbedder, Eigen3MatrixEmbedderType>) {
+        _preconditionedEigen3SolveLocalSystem<Eigen::IncompleteLUT<double>, Eigen3MatrixEmbedderType>(eigen3_A, x, b,
+                                                                                                      options);
+      } else {
+        throw NormalError("incomplete LU is not available for dense matrices in Eigen3");
+      }
+      break;
+    }
+      // LCOV_EXCL_START
+    default: {
+      throw UnexpectedError("unexpected preconditioner: " + name(options.precond()));
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
+#else   // PUGS_HAS_EIGEN3
+
+  // LCOV_EXCL_START
+  template <typename MatrixType, typename SolutionVectorType, typename RHSVectorType>
+  static void
+  _eigen3SolveLocalSystem(const MatrixType&, SolutionVectorType&, const RHSVectorType&, const LinearSolverOptions&)
+  {
+    checkHasLibrary(LSLibrary::eigen3);
+    throw UnexpectedError("unexpected situation should not reach this point!");
+  }
+  // LCOV_EXCL_STOP
+
+#endif   // PUGS_HAS_EIGEN3
+
 #ifdef PUGS_HAS_PETSC
   static int
   petscMonitor(KSP, int i, double residu, void*)
@@ -168,19 +350,17 @@ struct LinearSolver::Internals
     return 0;
   }
 
-  template <typename MatrixType, typename SolutionVectorType, typename RHSVectorType>
+  template <typename MatrixType>
   static void
-  petscSolveLocalSystem(const MatrixType& A,
-                        SolutionVectorType& x,
-                        const RHSVectorType& b,
-                        const LinearSolverOptions& options)
+  _petscSolveLocalSystem(const MatrixType& A,
+                         VectorWrapper<double>& x,
+                         const VectorWrapper<const double>& b,
+                         const LinearSolverOptions& options)
   {
-    Assert((x.size() == b.size()) and (x.size() - A.numberOfColumns() == 0) and A.isSquare());
-
     Vec petscB;
-    VecCreateMPIWithArray(PETSC_COMM_SELF, 1, b.size(), b.size(), &b[0], &petscB);
+    VecCreateMPIWithArray(PETSC_COMM_SELF, 1, b.size(), b.size(), b.ptr(), &petscB);
     Vec petscX;
-    VecCreateMPIWithArray(PETSC_COMM_SELF, 1, x.size(), x.size(), &x[0], &petscX);
+    VecCreateMPIWithArray(PETSC_COMM_SELF, 1, x.size(), x.size(), x.ptr(), &petscX);
 
     PETScAijMatrixEmbedder petscA(A);
 
@@ -280,7 +460,7 @@ struct LinearSolver::Internals
   // LCOV_EXCL_START
   template <typename MatrixType, typename SolutionVectorType, typename RHSVectorType>
   static void
-  petscSolveLocalSystem(const MatrixType&, SolutionVectorType&, const RHSVectorType&, const LinearSolverOptions&)
+  _petscSolveLocalSystem(const MatrixType&, SolutionVectorType&, const RHSVectorType&, const LinearSolverOptions&)
   {
     checkHasLibrary(LSLibrary::petsc);
     throw UnexpectedError("unexpected situation should not reach this point!");
@@ -288,33 +468,6 @@ struct LinearSolver::Internals
   // LCOV_EXCL_STOP
 
 #endif   // PUGS_HAS_PETSC
-
-  template <typename MatrixType, typename SolutionVectorType, typename RHSVectorType>
-  static void
-  solveLocalSystem(const MatrixType& A,
-                   SolutionVectorType& x,
-                   const RHSVectorType& b,
-                   const LinearSolverOptions& options)
-  {
-    switch (options.library()) {
-    case LSLibrary::builtin: {
-      builtinSolveLocalSystem(A, x, b, options);
-      break;
-    }
-      // LCOV_EXCL_START
-    case LSLibrary::petsc: {
-      // not covered since if PETSc is not linked this point is
-      // unreachable: LinearSolver throws an exception at construction
-      // in this case.
-      petscSolveLocalSystem(A, x, b, options);
-      break;
-    }
-    default: {
-      throw UnexpectedError(::name(options.library()) + " cannot solve local systems");
-    }
-      // LCOV_EXCL_STOP
-    }
-  }
 };
 
 bool
@@ -330,15 +483,52 @@ LinearSolver::checkOptions(const LinearSolverOptions& options) const
 }
 
 void
-LinearSolver::solveLocalSystem(const CRSMatrix<double, int>& A, Vector<double>& x, const Vector<const double>& b)
+LinearSolver::_builtinSolveLocalSystem(const CRSMatrix<double, int>& A,
+                                       Vector<double>& x,
+                                       const Vector<const double>& b)
+{
+  Internals::_builtinSolveLocalSystem(A, x, b, m_options);
+}
+
+void
+LinearSolver::_builtinSolveLocalSystem(const SmallMatrix<double>& A,
+                                       SmallVector<double>& x,
+                                       const SmallVector<const double>& b)
+{
+  Internals::_builtinSolveLocalSystem(A, x, b, m_options);
+}
+
+void
+LinearSolver::_eigen3SolveLocalSystem(const Eigen3SparseMatrixEmbedder& A,
+                                      VectorWrapper<double> x,
+                                      const VectorWrapper<const double>& b)
+{
+  Internals::_eigen3SolveLocalSystem(A, x, b, m_options);
+}
+
+void
+LinearSolver::_eigen3SolveLocalSystem(const DenseMatrixWrapper<double>& A,
+                                      VectorWrapper<double> x,
+                                      const VectorWrapper<const double>& b)
+{
+  Eigen3DenseMatrixEmbedder A_wrapper{A};
+  Internals::_eigen3SolveLocalSystem(A_wrapper, x, b, m_options);
+}
+
+void
+LinearSolver::_petscSolveLocalSystem(const CRSMatrix<double, int>& A,
+                                     VectorWrapper<double> x,
+                                     const VectorWrapper<const double>& b)
 {
-  Internals::solveLocalSystem(A, x, b, m_options);
+  Internals::_petscSolveLocalSystem(A, x, b, m_options);
 }
 
 void
-LinearSolver::solveLocalSystem(const SmallMatrix<double>& A, SmallVector<double>& x, const SmallVector<const double>& b)
+LinearSolver::_petscSolveLocalSystem(const DenseMatrixWrapper<double>& A,
+                                     VectorWrapper<double> x,
+                                     const VectorWrapper<const double>& b)
 {
-  Internals::solveLocalSystem(A, x, b, m_options);
+  Internals::_petscSolveLocalSystem(A, x, b, m_options);
 }
 
 LinearSolver::LinearSolver(const LinearSolverOptions& options) : m_options{options}
diff --git a/src/algebra/LinearSolver.hpp b/src/algebra/LinearSolver.hpp
index e90402c8ddc78c9426f97300ab6b10730b4c503b..2d03a9cf5e7cced7f99b6707c042395f48054199 100644
--- a/src/algebra/LinearSolver.hpp
+++ b/src/algebra/LinearSolver.hpp
@@ -2,11 +2,14 @@
 #define LINEAR_SOLVER_HPP
 
 #include <algebra/CRSMatrix.hpp>
+#include <algebra/DenseMatrixWrapper.hpp>
+#include <algebra/Eigen3Utils.hpp>
 #include <algebra/LinearSolverOptions.hpp>
 #include <algebra/SmallMatrix.hpp>
 #include <algebra/TinyMatrix.hpp>
 #include <algebra/TinyVector.hpp>
 #include <algebra/Vector.hpp>
+#include <algebra/VectorWrapper.hpp>
 #include <utils/Exceptions.hpp>
 
 class LinearSolver
@@ -16,29 +19,139 @@ class LinearSolver
 
   const LinearSolverOptions m_options;
 
+  void _builtinSolveLocalSystem(const CRSMatrix<double, int>& A, Vector<double>& x, const Vector<const double>& b);
+
+  void _builtinSolveLocalSystem(const SmallMatrix<double>& A,
+                                SmallVector<double>& x,
+                                const SmallVector<const double>& b);
+
+  void _eigen3SolveLocalSystem(const Eigen3SparseMatrixEmbedder& A,
+                               VectorWrapper<double> x,
+                               const VectorWrapper<const double>& b);
+
+  void _eigen3SolveLocalSystem(const DenseMatrixWrapper<double>& A,
+                               VectorWrapper<double> x,
+                               const VectorWrapper<const double>& b);
+
+  void _petscSolveLocalSystem(const CRSMatrix<double, int>& A,
+                              VectorWrapper<double> x,
+                              const VectorWrapper<const double>& b);
+
+  void _petscSolveLocalSystem(const DenseMatrixWrapper<double>& A,
+                              VectorWrapper<double> x,
+                              const VectorWrapper<const double>& b);
+
  public:
   bool hasLibrary(LSLibrary library) const;
   void checkOptions(const LinearSolverOptions& options) const;
 
+  void
+  solveLocalSystem(const CRSMatrix<double, int>& A, Vector<double>& x, const Vector<const double>& b)
+  {
+    Assert((x.size() == b.size()) and (x.size() - A.numberOfColumns() == 0) and A.isSquare());
+    switch (m_options.library()) {
+    case LSLibrary::builtin: {
+      _builtinSolveLocalSystem(A, x, b);
+      break;
+    }
+      // LCOV_EXCL_START
+    case LSLibrary::eigen3: {
+      // not covered since if Eigen3 is not linked this point is
+      // unreachable: LinearSolver throws an exception at construction
+      // in this case.
+      _eigen3SolveLocalSystem(A, x, b);
+      break;
+    }
+    case LSLibrary::petsc: {
+      // not covered since if PETSc is not linked this point is
+      // unreachable: LinearSolver throws an exception at construction
+      // in this case.
+      _petscSolveLocalSystem(A, x, b);
+      break;
+    }
+    default: {
+      throw UnexpectedError(::name(m_options.library()) + " cannot solve local systems");
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
+
+  void
+  solveLocalSystem(const SmallMatrix<double>& A, SmallVector<double>& x, const SmallVector<const double>& b)
+  {
+    Assert((x.size() == b.size()) and (x.size() - A.numberOfColumns() == 0) and A.isSquare());
+    switch (m_options.library()) {
+    case LSLibrary::builtin: {
+      _builtinSolveLocalSystem(A, x, b);
+      break;
+    }
+      // LCOV_EXCL_START
+    case LSLibrary::eigen3: {
+      // not covered since if Eigen3 is not linked this point is
+      // unreachable: LinearSolver throws an exception at construction
+      // in this case.
+      _eigen3SolveLocalSystem(A, x, b);
+      break;
+    }
+    case LSLibrary::petsc: {
+      // not covered since if PETSc is not linked this point is
+      // unreachable: LinearSolver throws an exception at construction
+      // in this case.
+      _petscSolveLocalSystem(A, x, b);
+      break;
+    }
+    default: {
+      throw UnexpectedError(::name(m_options.library()) + " cannot solve local systems");
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
+
   template <size_t N>
   void
   solveLocalSystem(const TinyMatrix<N>& A, TinyVector<N>& x, const TinyVector<N>& b)
   {
-    SmallMatrix A_dense{A};
+    Assert((x.size() == b.size()) and (x.size() - A.numberOfColumns() == 0) and A.isSquare());
+    switch (m_options.library()) {
+    case LSLibrary::builtin: {
+      // Probably expensive but builtin is not the preferred way to
+      // solve linear systems
+      SmallMatrix A_dense{A};
+
+      SmallVector x_vector{x};
+      SmallVector<const double> b_vector{b};
 
-    SmallVector x_vector{x};
-    SmallVector b_vector{b};
+      this->solveLocalSystem(A_dense, x_vector, b_vector);
 
-    this->solveLocalSystem(A_dense, x_vector, b_vector);
+      for (size_t i = 0; i < N; ++i) {
+        x[i] = x_vector[i];
+      }
 
-    for (size_t i = 0; i < N; ++i) {
-      x[i] = x_vector[i];
+      _builtinSolveLocalSystem(A, x, b);
+      break;
+    }
+      // LCOV_EXCL_START
+    case LSLibrary::eigen3: {
+      // not covered since if Eigen3 is not linked this point is
+      // unreachable: LinearSolver throws an exception at construction
+      // in this case.
+      _eigen3SolveLocalSystem(A, x, b);
+      break;
+    }
+    case LSLibrary::petsc: {
+      // not covered since if PETSc is not linked this point is
+      // unreachable: LinearSolver throws an exception at construction
+      // in this case.
+      _petscSolveLocalSystem(A, x, b);
+      break;
+    }
+    default: {
+      throw UnexpectedError(::name(m_options.library()) + " cannot solve local systems");
+    }
+      // LCOV_EXCL_STOP
     }
   }
 
-  void solveLocalSystem(const CRSMatrix<double, int>& A, Vector<double>& x, const Vector<const double>& b);
-  void solveLocalSystem(const SmallMatrix<double>& A, SmallVector<double>& x, const SmallVector<const double>& b);
-
   LinearSolver();
   LinearSolver(const LinearSolverOptions& options);
 };
diff --git a/src/algebra/LinearSolverOptions.hpp b/src/algebra/LinearSolverOptions.hpp
index 940ce1ed63a2dbbf046022aee838c3f9438c1906..97a1748780f19d28e98f299fd7679d7ab15466f0 100644
--- a/src/algebra/LinearSolverOptions.hpp
+++ b/src/algebra/LinearSolverOptions.hpp
@@ -10,6 +10,7 @@ enum class LSLibrary : int8_t
   LS__begin = 0,
   //
   builtin = LS__begin,
+  eigen3,
   petsc,
   //
   LS__end
@@ -49,6 +50,9 @@ name(const LSLibrary library)
   case LSLibrary::builtin: {
     return "builtin";
   }
+  case LSLibrary::eigen3: {
+    return "Eigen3";
+  }
   case LSLibrary::petsc: {
     return "PETSc";
   }
diff --git a/src/algebra/PETScUtils.hpp b/src/algebra/PETScUtils.hpp
index c4db9ee5910ef8e58333dbfb6dc20c8a1987bb46..c6d2e8be88c6098e2f187cda9177c120facc6f23 100644
--- a/src/algebra/PETScUtils.hpp
+++ b/src/algebra/PETScUtils.hpp
@@ -6,6 +6,7 @@
 #ifdef PUGS_HAS_PETSC
 
 #include <algebra/CRSMatrix.hpp>
+#include <algebra/DenseMatrixWrapper.hpp>
 #include <algebra/SmallMatrix.hpp>
 #include <algebra/TinyMatrix.hpp>
 
@@ -52,12 +53,8 @@ class PETScAijMatrixEmbedder
     return m_petscMat;
   }
 
-  template <size_t N>
-  PETScAijMatrixEmbedder(const TinyMatrix<N>& A) : PETScAijMatrixEmbedder{N, N, &A(0, 0)}
-  {}
-
-  PETScAijMatrixEmbedder(const SmallMatrix<double>& A)
-    : PETScAijMatrixEmbedder{A.numberOfRows(), A.numberOfColumns(), &A(0, 0)}
+  PETScAijMatrixEmbedder(const DenseMatrixWrapper<double>& A)
+    : PETScAijMatrixEmbedder{A.numberOfRows(), A.numberOfColumns(), A.ptr()}
   {}
 
   PETScAijMatrixEmbedder(const CRSMatrix<double, int>& A);
diff --git a/src/algebra/ShrinkVectorView.hpp b/src/algebra/ShrinkVectorView.hpp
index cdf786c5a30970b747b0f540642d5637059198b8..83aed7cb2295ee4d20a044f7ba1039f34e7b5ffa 100644
--- a/src/algebra/ShrinkVectorView.hpp
+++ b/src/algebra/ShrinkVectorView.hpp
@@ -23,10 +23,10 @@ class ShrinkVectorView
   friend std::ostream&
   operator<<(std::ostream& os, const ShrinkVectorView& x)
   {
-    if (x.size() > 0) {
+    if (x.dimension() > 0) {
       os << 0 << ':' << NaNHelper(x[0]);
     }
-    for (size_t i = 1; i < x.size(); ++i) {
+    for (size_t i = 1; i < x.dimension(); ++i) {
       os << ' ' << i << ':' << NaNHelper(x[i]);
     }
     return os;
diff --git a/src/algebra/VectorWrapper.hpp b/src/algebra/VectorWrapper.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0bdeefbfd3f10407358613e7f1d2487bba22b26d
--- /dev/null
+++ b/src/algebra/VectorWrapper.hpp
@@ -0,0 +1,47 @@
+#ifndef VECTOR_WRAPPER_HPP
+#define VECTOR_WRAPPER_HPP
+
+#include <algebra/SmallVector.hpp>
+#include <algebra/TinyVector.hpp>
+#include <algebra/Vector.hpp>
+
+template <typename DataType>
+class VectorWrapper
+{
+ private:
+  const size_t m_size;
+  DataType* const m_vector_ptr;
+
+ public:
+  PUGS_INLINE
+  size_t
+  size() const
+  {
+    return m_size;
+  }
+
+  PUGS_INLINE
+  DataType* const&
+  ptr() const
+  {
+    return m_vector_ptr;
+  }
+
+  VectorWrapper(const Vector<DataType>& x) : m_size{x.size()}, m_vector_ptr{(m_size > 0) ? (&x[0]) : nullptr} {}
+  VectorWrapper(Vector<DataType>& x) : m_size{x.size()}, m_vector_ptr{(m_size > 0) ? (&x[0]) : nullptr} {}
+
+  VectorWrapper(const SmallVector<DataType>& x) : m_size{x.size()}, m_vector_ptr{(m_size > 0) ? (&x[0]) : nullptr} {}
+
+  template <size_t N>
+  VectorWrapper(const TinyVector<N, DataType>& x) : m_size{N}, m_vector_ptr{(N > 0) ? (&x[0]) : nullptr}
+  {}
+
+  template <size_t N>
+  VectorWrapper(TinyVector<N, DataType>& x) : m_size{N}, m_vector_ptr{(N > 0) ? (&x[0]) : nullptr}
+  {}
+
+  VectorWrapper(const VectorWrapper&) = delete;
+  VectorWrapper(VectorWrapper&&)      = delete;
+};
+
+#endif   // VECTOR_WRAPPER_HPP
diff --git a/src/language/ast/ASTModulesImporter.cpp b/src/language/ast/ASTModulesImporter.cpp
index 4cae0a921c139b3ae68a3decb0ce4f83876c4701..e1dc0043e689b506776540bb57e4851dc10883ae 100644
--- a/src/language/ast/ASTModulesImporter.cpp
+++ b/src/language/ast/ASTModulesImporter.cpp
@@ -26,10 +26,10 @@ ASTModulesImporter::_importModule(ASTNode& import_node)
     std::cout << " * importing '" << rang::fgB::green << module_name << rang::style::reset << "' module\n";
   }
 
-  m_module_repository.populateSymbolTable(module_name_node, m_symbol_table);
-  m_module_repository.registerOperators(module_name);
+  ModuleRepository::getInstance().populateSymbolTable(module_name_node, m_symbol_table);
+  ModuleRepository::getInstance().registerOperators(module_name);
   if (CheckpointResumeRepository::isCreated()) {
-    m_module_repository.registerCheckpointResume(module_name);
+    ModuleRepository::getInstance().registerCheckpointResume(module_name);
   }
 }
 
@@ -49,7 +49,7 @@ ASTModulesImporter::ASTModulesImporter(ASTNode& root_node) : m_symbol_table{*roo
 {
   Assert(root_node.is_root());
   OperatorRepository::instance().reset();
-  m_module_repository.populateMandatoryData(root_node, m_symbol_table);
+  ModuleRepository::getInstance().populateMandatoryData(root_node, m_symbol_table);
 
   this->_importAllModules(root_node);
 
diff --git a/src/language/ast/ASTModulesImporter.hpp b/src/language/ast/ASTModulesImporter.hpp
index 566155c4a19ecd00f0b049a29030c0036f212d8a..15ba8a7dc6cecbbb39bfb11c4d202d39200061a4 100644
--- a/src/language/ast/ASTModulesImporter.hpp
+++ b/src/language/ast/ASTModulesImporter.hpp
@@ -14,8 +14,6 @@ class ASTModulesImporter
   std::set<std::string> m_imported_modules;
   SymbolTable& m_symbol_table;
 
-  ModuleRepository m_module_repository;
-
   void _importModule(ASTNode& import_node);
   void _importAllModules(ASTNode& node);
 
@@ -23,7 +21,7 @@ class ASTModulesImporter
   const ModuleRepository&
   moduleRepository() const
   {
-    return m_module_repository;
+    return ModuleRepository::getInstance();
   }
 
   ASTModulesImporter(ASTNode& root_node);
diff --git a/src/language/modules/BinaryOperatorRegisterForVh.cpp b/src/language/modules/BinaryOperatorRegisterForVh.cpp
index 3eb83537323d91bbf6266cbefebf4d004d74c2be..d1c20cb78718568a3535e30f0b5c9ce255be1db6 100644
--- a/src/language/modules/BinaryOperatorRegisterForVh.cpp
+++ b/src/language/modules/BinaryOperatorRegisterForVh.cpp
@@ -1,6 +1,6 @@
 #include <language/modules/BinaryOperatorRegisterForVh.hpp>
 
-#include <language/modules/SchemeModule.hpp>
+#include <language/modules/SchemeModuleTypes.hpp>
 #include <language/utils/BinaryOperatorProcessorBuilder.hpp>
 #include <language/utils/DataHandler.hpp>
 #include <language/utils/DataVariant.hpp>
diff --git a/src/language/modules/CoreModule.cpp b/src/language/modules/CoreModule.cpp
index d7b331a1dc606a83cf77dfe405b184b9ff143308..5b5753eb8b3952d6d9c08c34ccc843f4bece2dc9 100644
--- a/src/language/modules/CoreModule.cpp
+++ b/src/language/modules/CoreModule.cpp
@@ -32,6 +32,7 @@
 #include <language/utils/UnaryOperatorRegisterForRnxn.hpp>
 #include <language/utils/UnaryOperatorRegisterForZ.hpp>
 #include <utils/Messenger.hpp>
+#include <utils/PartitionerOptions.hpp>
 #include <utils/PugsUtils.hpp>
 #include <utils/RandomEngine.hpp>
 #include <utils/Stop.hpp>
@@ -42,7 +43,7 @@
 #include <utils/checkpointing/ResumingManager.hpp>
 #include <utils/checkpointing/WriteOStream.hpp>
 
-#include <random>
+#include <language/modules/ModuleRepository.hpp>
 
 CoreModule::CoreModule() : BuiltinModule(true)
 {
@@ -94,6 +95,27 @@ CoreModule::CoreModule() : BuiltinModule(true)
 
                                                  ));
 
+  this->_addBuiltinFunction("setPartitionerLibrary", std::function(
+
+                                                       [](const std::string& library_name) -> void {
+                                                         PartitionerOptions::default_options.library() =
+                                                           getPartitionerEnumFromName<PartitionerLibrary>(library_name);
+                                                       }
+
+                                                       ));
+
+  this->_addBuiltinFunction("getPartitionerOptions", std::function(
+
+                                                       []() -> std::string {
+                                                         std::ostringstream os;
+                                                         os << rang::fgB::yellow << "Partitioner options"
+                                                            << rang::style::reset << '\n';
+                                                         os << PartitionerOptions::default_options;
+                                                         return os.str();
+                                                       }
+
+                                                       ));
+
   this->_addTypeDescriptor(ast_node_data_type_from<std::shared_ptr<const OStream>>);
 
   this->_addBuiltinFunction("ofstream", std::function(
@@ -219,3 +241,5 @@ CoreModule::registerCheckpointResume() const
 
 #endif   // PUGS_HAS_HDF5
 }
+
+ModuleRepository::Subscribe<CoreModule> core_module;
diff --git a/src/language/modules/DevUtilsModule.cpp b/src/language/modules/DevUtilsModule.cpp
index 361b4357eda0ebbfe4350cc6c64bbe72cfaa0829..4595ea6c9d9a45431b303c00b4de6defdda38b7b 100644
--- a/src/language/modules/DevUtilsModule.cpp
+++ b/src/language/modules/DevUtilsModule.cpp
@@ -1,5 +1,7 @@
 #include <language/modules/DevUtilsModule.hpp>
 
+#include <language/modules/ModuleRepository.hpp>
+
 #include <dev/ParallelChecker.hpp>
 #include <language/modules/MeshModuleTypes.hpp>
 #include <language/modules/SchemeModuleTypes.hpp>
@@ -126,3 +128,5 @@ DevUtilsModule::registerOperators() const
 void
 DevUtilsModule::registerCheckpointResume() const
 {}
+
+ModuleRepository::Subscribe<DevUtilsModule> dev_utils_module;
diff --git a/src/language/modules/LinearSolverModule.cpp b/src/language/modules/LinearSolverModule.cpp
index aef6470d2455909f5ad2d307aa4e5cb859b8bcbb..1ff8be36d3d0649caaae56e250477fda2ee7690d 100644
--- a/src/language/modules/LinearSolverModule.cpp
+++ b/src/language/modules/LinearSolverModule.cpp
@@ -1,6 +1,9 @@
 #include <language/modules/LinearSolverModule.hpp>
 
-#include <algebra/LinearSolver.hpp>
+#include <language/modules/ModuleRepository.hpp>
+
+#include <algebra/EigenvalueSolverOptions.hpp>
+#include <algebra/LinearSolverOptions.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 #include <language/utils/TypeDescriptor.hpp>
 
@@ -88,6 +91,43 @@ LinearSolverModule::LinearSolverModule()
                                                   return os.str();
                                                 }
 
+                                                ));
+
+  this->_addBuiltinFunction("setESLibrary", std::function(
+
+                                              [](const std::string& library_name) -> void {
+                                                EigenvalueSolverOptions::default_options.library() =
+                                                  getESEnumFromName<ESLibrary>(library_name);
+                                              }
+
+                                              ));
+
+  this->_addBuiltinFunction("getESOptions", std::function(
+
+                                              []() -> std::string {
+                                                std::ostringstream os;
+                                                os << rang::fgB::yellow << "Eigenvalue solver options"
+                                                   << rang::style::reset << '\n';
+                                                os << EigenvalueSolverOptions::default_options;
+                                                return os.str();
+                                              }
+
+                                              ));
+
+  this->_addBuiltinFunction("getESAvailable", std::function(
+
+                                                []() -> std::string {
+                                                  std::ostringstream os;
+
+                                                  os << rang::fgB::yellow << "Available eigenvalue solver options"
+                                                     << rang::style::reset << '\n';
+                                                  os << rang::fgB::blue << " libraries" << rang::style::reset << '\n';
+
+                                                  printESEnumListNames<ESLibrary>(os);
+
+                                                  return os.str();
+                                                }
+
                                                 ));
 }
 
@@ -98,3 +138,5 @@ LinearSolverModule::registerOperators() const
 void
 LinearSolverModule::registerCheckpointResume() const
 {}
+
+ModuleRepository::Subscribe<LinearSolverModule> linear_solver_module;
diff --git a/src/language/modules/MathFunctionRegisterForVh.cpp b/src/language/modules/MathFunctionRegisterForVh.cpp
index b31fe47d0eba87ff2e7f0823a530bf170e410d6c..d9a30e86996a9cd6b99af63d64a8ab33264c7fe3 100644
--- a/src/language/modules/MathFunctionRegisterForVh.cpp
+++ b/src/language/modules/MathFunctionRegisterForVh.cpp
@@ -1,6 +1,7 @@
 #include <language/modules/MathFunctionRegisterForVh.hpp>
 
 #include <language/modules/SchemeModule.hpp>
+#include <language/modules/SchemeModuleTypes.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 #include <language/utils/EmbeddedDiscreteFunctionMathFunctions.hpp>
 #include <scheme/DiscreteFunctionVariant.hpp>
diff --git a/src/language/modules/MathModule.cpp b/src/language/modules/MathModule.cpp
index 19a35a1529b1b65310c3c51d69fe4c5d02dc4d8a..1caea452428125e7a1c16aa88cb8278a5685fb35 100644
--- a/src/language/modules/MathModule.cpp
+++ b/src/language/modules/MathModule.cpp
@@ -1,5 +1,6 @@
 #include <language/modules/MathModule.hpp>
 
+#include <language/modules/ModuleRepository.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 
 #include <cmath>
@@ -136,3 +137,5 @@ MathModule::registerOperators() const
 void
 MathModule::registerCheckpointResume() const
 {}
+
+ModuleRepository::Subscribe<MathModule> math_module;
diff --git a/src/language/modules/MeshModule.cpp b/src/language/modules/MeshModule.cpp
index c76260ce58c75c130e4638054fc394458fedbf63..9b698b9e095ff6c227c58a2e6fe6bae5f7f72ad4 100644
--- a/src/language/modules/MeshModule.cpp
+++ b/src/language/modules/MeshModule.cpp
@@ -1,5 +1,7 @@
 #include <language/modules/MeshModule.hpp>
 
+#include <language/modules/ModuleRepository.hpp>
+
 #include <algebra/TinyVector.hpp>
 #include <language/node_processor/ExecutionPolicy.hpp>
 #include <language/utils/BinaryOperatorProcessorBuilder.hpp>
@@ -160,27 +162,28 @@ MeshModule::MeshModule()
 
                                           ));
 
-  this
-    ->_addBuiltinFunction("interpolate",
-                          std::function(
+  this->_addBuiltinFunction("interpolate",
+                            std::function(
 
-                            [](std::shared_ptr<const MeshVariant> mesh_v, std::shared_ptr<const ItemType> item_type,
-                               const FunctionSymbolId& function_id) -> std::shared_ptr<const ItemValueVariant> {
-                              return ItemValueVariantFunctionInterpoler{mesh_v, *item_type, function_id}.interpolate();
-                            }
+                              [](std::shared_ptr<const MeshVariant> mesh_v, std::shared_ptr<const ItemType> item_type,
+                                 const FunctionSymbolId& function_id) -> std::shared_ptr<const ItemValueVariant> {
+                                return ItemValueVariantFunctionInterpoler{mesh_v, *item_type, function_id}
+                                  .interpolate();
+                              }
 
-                            ));
+                              ));
 
-  this->_addBuiltinFunction(
-    "interpolate_array",
-    std::function(
+  this->_addBuiltinFunction("interpolate_array",
+                            std::function(
 
-      [](std::shared_ptr<const MeshVariant> mesh_v, std::shared_ptr<const ItemType> item_type,
-         const std::vector<FunctionSymbolId>& function_id_list) -> std::shared_ptr<const ItemArrayVariant> {
-        return ItemArrayVariantFunctionInterpoler{mesh_v, *item_type, function_id_list}.interpolate();
-      }
+                              [](std::shared_ptr<const MeshVariant> mesh_v, std::shared_ptr<const ItemType> item_type,
+                                 const std::vector<FunctionSymbolId>& function_id_list)
+                                -> std::shared_ptr<const ItemArrayVariant> {
+                                return ItemArrayVariantFunctionInterpoler{mesh_v, *item_type, function_id_list}
+                                  .interpolate();
+                              }
 
-      ));
+                              ));
 
   this->_addBuiltinFunction("transform",
                             std::function(
@@ -305,6 +308,46 @@ MeshModule::MeshModule()
                                               return DualMeshManager::instance().getMedianDualMesh(mesh_v);
                                             }
 
+                                            ));
+
+  this->_addBuiltinFunction("cell_owner", std::function(
+
+                                            [](const std::shared_ptr<const MeshVariant>& mesh_v)
+                                              -> std::shared_ptr<const ItemValueVariant> {
+                                              return std::visit(
+                                                [&](auto&& mesh) {
+                                                  const auto& connectivity = mesh->connectivity();
+                                                  auto cell_owner          = connectivity.cellOwner();
+                                                  CellValue<long int> cell_owner_long{connectivity};
+                                                  parallel_for(
+                                                    connectivity.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                                                      cell_owner_long[cell_id] = cell_owner[cell_id];
+                                                    });
+                                                  return std::make_shared<const ItemValueVariant>(cell_owner_long);
+                                                },
+                                                mesh_v->variant());
+                                            }
+
+                                            ));
+
+  this->_addBuiltinFunction("node_owner", std::function(
+
+                                            [](const std::shared_ptr<const MeshVariant>& mesh_v)
+                                              -> std::shared_ptr<const ItemValueVariant> {
+                                              return std::visit(
+                                                [&](auto&& mesh) {
+                                                  const auto& connectivity = mesh->connectivity();
+                                                  auto node_owner          = connectivity.nodeOwner();
+                                                  NodeValue<long int> node_owner_long{connectivity};
+                                                  parallel_for(
+                                                    connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) {
+                                                      node_owner_long[node_id] = node_owner[node_id];
+                                                    });
+                                                  return std::make_shared<const ItemValueVariant>(node_owner_long);
+                                                },
+                                                mesh_v->variant());
+                                            }
+
                                             ));
 }
 
@@ -442,3 +485,5 @@ MeshModule::registerCheckpointResume() const
 
 #endif   // PUGS_HAS_HDF5
 }
+
+ModuleRepository::Subscribe<MeshModule> mesh_module;
diff --git a/src/language/modules/ModuleRepository.cpp b/src/language/modules/ModuleRepository.cpp
index 59689fb6fee512e45cd76c7c9d3cd5c11d543ee8..c9c487b67b5fdbf63d2b307baf1497519666a5d8 100644
--- a/src/language/modules/ModuleRepository.cpp
+++ b/src/language/modules/ModuleRepository.cpp
@@ -1,14 +1,6 @@
 #include <language/modules/ModuleRepository.hpp>
 
 #include <language/ast/ASTNode.hpp>
-#include <language/modules/CoreModule.hpp>
-#include <language/modules/DevUtilsModule.hpp>
-#include <language/modules/LinearSolverModule.hpp>
-#include <language/modules/MathModule.hpp>
-#include <language/modules/MeshModule.hpp>
-#include <language/modules/SchemeModule.hpp>
-#include <language/modules/SocketModule.hpp>
-#include <language/modules/WriterModule.hpp>
 #include <language/utils/BasicAffectationRegistrerFor.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 #include <language/utils/CheckpointResumeRepository.hpp>
@@ -19,9 +11,29 @@
 
 #include <utils/PugsAssert.hpp>
 
-#include <algorithm>
 #include <set>
 
+ModuleRepository* ModuleRepository::m_instance = nullptr;
+
+ModuleRepository&
+ModuleRepository::getInstance()
+{
+  if (m_instance == nullptr) {
+    m_instance = new ModuleRepository;
+  }
+
+  return *m_instance;
+}
+
+void
+ModuleRepository::destroy()
+{
+  if (m_instance != nullptr) {
+    delete m_instance;
+    m_instance = nullptr;
+  }
+}
+
 void
 ModuleRepository::_subscribe(std::unique_ptr<IModule> m)
 {
@@ -52,18 +64,6 @@ ModuleRepository::_subscribe(std::unique_ptr<IModule> m)
   Assert(success, "module has already been subscribed");
 }
 
-ModuleRepository::ModuleRepository()
-{
-  this->_subscribe(std::make_unique<CoreModule>());
-  this->_subscribe(std::make_unique<LinearSolverModule>());
-  this->_subscribe(std::make_unique<MathModule>());
-  this->_subscribe(std::make_unique<MeshModule>());
-  this->_subscribe(std::make_unique<SchemeModule>());
-  this->_subscribe(std::make_unique<SocketModule>());
-  this->_subscribe(std::make_unique<DevUtilsModule>());
-  this->_subscribe(std::make_unique<WriterModule>());
-}
-
 template <typename NameEmbedderMapT, typename EmbedderTableT>
 void
 ModuleRepository::_populateEmbedderTableT(const ASTNode& module_node,
diff --git a/src/language/modules/ModuleRepository.hpp b/src/language/modules/ModuleRepository.hpp
index 407d7b55bd9b7c8454159a315b9c2f7a256eecb6..35bf6f7aa6a7704f2804125c488ac0bf115ddf39 100644
--- a/src/language/modules/ModuleRepository.hpp
+++ b/src/language/modules/ModuleRepository.hpp
@@ -31,7 +31,23 @@ class ModuleRepository
                             const IModule::NameValueMap& name_value_map,
                             SymbolTable& symbol_table);
 
+  static ModuleRepository* m_instance;
+
  public:
+  static ModuleRepository& getInstance();
+
+  static void destroy();
+
+  template <typename ModuleT>
+  struct Subscribe
+  {
+    Subscribe()
+    {
+      static_assert(std::is_base_of_v<IModule, ModuleT>, "module must inherit IModule interface");
+      ModuleRepository::getInstance()._subscribe(std::make_unique<ModuleT>());
+    }
+  };
+
   void populateSymbolTable(const ASTNode& module_name_node, SymbolTable& symbol_table);
   void populateMandatoryData(const ASTNode& root_node, SymbolTable& symbol_table);
   void registerOperators(const std::string& module_name);
@@ -46,7 +62,10 @@ class ModuleRepository
   ModuleRepository(const ModuleRepository&) = delete;
   ModuleRepository(ModuleRepository&&)      = delete;
 
-  ModuleRepository();
+ private:
+  ModuleRepository() = default;
+
+ public:
   ~ModuleRepository() = default;
 };
 
diff --git a/src/language/modules/SchemeModule.cpp b/src/language/modules/SchemeModule.cpp
index b0dc16e29b899377a46b6add90aeab475bcc1504..6ffb816b48739d57412bb21db2bbd703bdc4bcf4 100644
--- a/src/language/modules/SchemeModule.cpp
+++ b/src/language/modules/SchemeModule.cpp
@@ -1,4 +1,7 @@
 #include <language/modules/SchemeModule.hpp>
+#include <language/modules/SchemeModuleTypes.hpp>
+
+#include <language/modules/ModuleRepository.hpp>
 
 #include <analysis/GaussLegendreQuadratureDescriptor.hpp>
 #include <analysis/GaussLobattoQuadratureDescriptor.hpp>
@@ -1170,3 +1173,5 @@ SchemeModule::registerCheckpointResume() const
 
 #endif   // PUGS_HAS_HDF5
 }
+
+ModuleRepository::Subscribe<SchemeModule> scheme_module;
diff --git a/src/language/modules/SchemeModule.hpp b/src/language/modules/SchemeModule.hpp
index c61b3084406dbd8ae9889c79b4172195daaa6fe8..cf169175c4b46f7364799e16bdb4b84633d1f61c 100644
--- a/src/language/modules/SchemeModule.hpp
+++ b/src/language/modules/SchemeModule.hpp
@@ -2,7 +2,6 @@
 #define SCHEME_MODULE_HPP
 
 #include <language/modules/BuiltinModule.hpp>
-#include <language/modules/SchemeModuleTypes.hpp>
 
 class SchemeModule : public BuiltinModule
 {
diff --git a/src/language/modules/SocketModule.cpp b/src/language/modules/SocketModule.cpp
index 79321d088f960e0bb7450c82820cf6568e79d174..9c2ecdbe1b40730d0181d7f4ee6f3bf396f70a34 100644
--- a/src/language/modules/SocketModule.cpp
+++ b/src/language/modules/SocketModule.cpp
@@ -1,5 +1,7 @@
 #include <language/modules/SocketModule.hpp>
 
+#include <language/modules/ModuleRepository.hpp>
+
 #include <language/utils/BinaryOperatorProcessorBuilder.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 #include <language/utils/CheckpointResumeRepository.hpp>
@@ -271,3 +273,5 @@ SocketModule::registerCheckpointResume() const
                            throw NotImplementedError("checkpoint/resume with sockets");
                          }));
 }
+
+ModuleRepository::Subscribe<SocketModule> socket_module;
diff --git a/src/language/modules/UnaryOperatorRegisterForVh.cpp b/src/language/modules/UnaryOperatorRegisterForVh.cpp
index e2e9c2db9b72a9a687806d24dee836cfd37a22e8..3d52ab54687eabcd6162536d88325b3285d45e1f 100644
--- a/src/language/modules/UnaryOperatorRegisterForVh.cpp
+++ b/src/language/modules/UnaryOperatorRegisterForVh.cpp
@@ -1,6 +1,6 @@
 #include <language/modules/UnaryOperatorRegisterForVh.hpp>
 
-#include <language/modules/SchemeModule.hpp>
+#include <language/modules/SchemeModuleTypes.hpp>
 #include <language/utils/DataHandler.hpp>
 #include <language/utils/DataVariant.hpp>
 #include <language/utils/EmbeddedDiscreteFunctionOperators.hpp>
diff --git a/src/language/modules/WriterModule.cpp b/src/language/modules/WriterModule.cpp
index 6d84506501de277f1970c13a7c24cd587ed79f20..e524a885639ac9a2a2da6330422565447bcb347d 100644
--- a/src/language/modules/WriterModule.cpp
+++ b/src/language/modules/WriterModule.cpp
@@ -1,7 +1,9 @@
 #include <language/modules/WriterModule.hpp>
 
-#include <language/modules/MeshModule.hpp>
-#include <language/modules/SchemeModule.hpp>
+#include <language/modules/ModuleRepository.hpp>
+
+#include <language/modules/MeshModuleTypes.hpp>
+#include <language/modules/SchemeModuleTypes.hpp>
 #include <language/utils/BuiltinFunctionEmbedder.hpp>
 #include <language/utils/CheckpointResumeRepository.hpp>
 #include <language/utils/TypeDescriptor.hpp>
@@ -242,3 +244,5 @@ WriterModule::registerCheckpointResume() const
 
 #endif   // PUGS_HAS_HDF5
 }
+
+ModuleRepository::Subscribe<WriterModule> writer_module{};
diff --git a/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp b/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp
index 5d000813aeff1e670a2f547a69827a679cbfa699..892037219c389e6ff7ebd28e8b5afd586845c14f 100644
--- a/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp
+++ b/src/language/utils/EmbeddedDiscreteFunctionMathFunctions.cpp
@@ -234,10 +234,14 @@ dot(const std::shared_ptr<const DiscreteFunctionVariant>& f_v,
             throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
           }
         } else if constexpr (is_discrete_function_P0_vector_v<TypeOfF>) {
-          if (f.size() == g.size()) {
-            return std::make_shared<DiscreteFunctionVariant>(dot(f, g));
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            if (f.size() == g.size()) {
+              return std::make_shared<DiscreteFunctionVariant>(dot(f, g));
+            } else {
+              throw NormalError("operands have different dimension");
+            }
           } else {
-            throw NormalError("operands have different dimension");
+            throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
           }
         } else {
           throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
@@ -691,8 +695,6 @@ sum_of_Vh_components(const std::shared_ptr<const DiscreteFunctionVariant>& f)
     [&](auto&& discrete_function) -> std::shared_ptr<const DiscreteFunctionVariant> {
       using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
       if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-        using DataType = std::decay_t<typename DiscreteFunctionT::data_type>;
-        static_assert(std::is_same_v<DataType, double>);
         return std::make_shared<DiscreteFunctionVariant>(sumOfComponents(discrete_function));
       } else {
         throw NormalError(EmbeddedDiscreteFunctionUtils::invalidOperandType(f));
diff --git a/src/main.cpp b/src/main.cpp
index c1eeef6a633d5074fef20545a2348a77e5a6888a..cbaf6277e5ee0c1c7d689403e8d78cae54c3ed5f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,6 +1,7 @@
 #include <analysis/QuadratureManager.hpp>
 #include <dev/ParallelChecker.hpp>
 #include <language/PugsParser.hpp>
+#include <language/modules/ModuleRepository.hpp>
 #include <mesh/DualConnectivityManager.hpp>
 #include <mesh/DualMeshManager.hpp>
 #include <mesh/MeshDataManager.hpp>
@@ -12,6 +13,8 @@
 #include <utils/RandomEngine.hpp>
 #include <utils/checkpointing/ResumingManager.hpp>
 
+#include <utils/PluginsLoader.hpp>
+
 int
 main(int argc, char* argv[])
 {
@@ -23,6 +26,8 @@ main(int argc, char* argv[])
 
   std::string filename = initialize(argc, argv);
 
+  PluginsLoader plugins_loader;
+
   SynchronizerManager::create();
   RandomEngine::create();
   QuadratureManager::create();
@@ -34,6 +39,8 @@ main(int argc, char* argv[])
   parser(filename);
   ExecutionStatManager::printInfo();
 
+  ModuleRepository::destroy();
+
   StencilManager::destroy();
   DualMeshManager::destroy();
   DualConnectivityManager::destroy();
diff --git a/src/mesh/CMakeLists.txt b/src/mesh/CMakeLists.txt
index b4d70359a5a4a7f8a4d2e352ab702ecc26761dbc..781b7e583595eab942d2f924de705cf332841fad 100644
--- a/src/mesh/CMakeLists.txt
+++ b/src/mesh/CMakeLists.txt
@@ -50,5 +50,6 @@ add_library(
 
 target_link_libraries(
   PugsMesh
+  ${PARMETIS_TARGET}
   ${HIGHFIVE_TARGET}
 )
diff --git a/src/mesh/Connectivity.hpp b/src/mesh/Connectivity.hpp
index c04bdbdf9d215ec9324d5545dcf2a079d4590c7f..111411baa8f527e544aa98105939593a99d0bd09 100644
--- a/src/mesh/Connectivity.hpp
+++ b/src/mesh/Connectivity.hpp
@@ -19,7 +19,6 @@
 #include <utils/PugsTraits.hpp>
 #include <utils/PugsUtils.hpp>
 
-#include <algorithm>
 #include <iostream>
 #include <set>
 #include <vector>
diff --git a/src/mesh/StencilArray.hpp b/src/mesh/StencilArray.hpp
index 9571c346bae6e4e345a16b09188dce15ab73b456..ffe8638bf0d8fdb472780150603c7fab21ca1eb9 100644
--- a/src/mesh/StencilArray.hpp
+++ b/src/mesh/StencilArray.hpp
@@ -12,6 +12,9 @@ class StencilArray
  public:
   using ItemToItemMatrixT = ItemToItemMatrix<source_item_type, target_item_type>;
 
+  using SourceItemId = ItemIdT<source_item_type>;
+  using TargetItemId = ItemIdT<target_item_type>;
+
   class BoundaryDescriptorStencilArray
   {
    private:
@@ -71,9 +74,9 @@ class StencilArray
 
   PUGS_INLINE
   auto
-  operator[](CellId cell_id) const
+  operator[](SourceItemId source_item_id) const
   {
-    return m_stencil_array[cell_id];
+    return m_stencil_array[source_item_id];
   }
 
   StencilArray(const ConnectivityMatrix& connectivity_matrix,
diff --git a/src/mesh/StencilBuilder.cpp b/src/mesh/StencilBuilder.cpp
index 31db762a1002e3c633afeaabd4f06d6a98dff634..20fc3bf61dd6a59ef82ff7bdb25dc5e32c7eef24 100644
--- a/src/mesh/StencilBuilder.cpp
+++ b/src/mesh/StencilBuilder.cpp
@@ -5,16 +5,23 @@
 #include <utils/GlobalVariableManager.hpp>
 #include <utils/Messenger.hpp>
 
-#include <set>
-
 template <ItemType item_type>
 class StencilBuilder::Layer
 {
   using ItemId = ItemIdT<item_type>;
+  ItemValue<const int, item_type> m_number_of;
+
   std::vector<ItemId> m_item_id_vector;
   std::vector<int> m_item_number_vector;
 
  public:
+  void
+  clear()
+  {
+    m_item_id_vector.clear();
+    m_item_number_vector.clear();
+  }
+
   size_t
   size() const
   {
@@ -22,9 +29,17 @@ class StencilBuilder::Layer
     return m_item_id_vector.size();
   }
 
+  const std::vector<ItemId>&
+  itemIdList() const
+  {
+    return m_item_id_vector;
+  }
+
   bool
-  hasItemNumber(const int item_number) const
+  hasItemNumber(const ItemId item_id) const
   {
+    const int item_number = m_number_of[item_id];
+
     ssize_t begin = 0;
     ssize_t end   = m_item_number_vector.size();
 
@@ -37,26 +52,22 @@ class StencilBuilder::Layer
         if (begin == mid) {
           break;
         }
-        begin = mid - 1;
+        begin = mid;
       } else {
         if (end == mid) {
           break;
         }
-        end = mid + 1;
+        end = mid;
       }
     }
     return false;
   }
 
-  const std::vector<ItemId>&
-  itemIdList() const
-  {
-    return m_item_id_vector;
-  }
-
   void
-  add(const ItemId item_id, const int item_number)
+  add(const ItemId item_id)
   {
+    const int item_number = m_number_of[item_id];
+
     ssize_t begin = 0;
     ssize_t end   = m_item_number_vector.size();
 
@@ -97,7 +108,14 @@ class StencilBuilder::Layer
     }
   }
 
-  Layer() = default;
+  Layer& operator=(const Layer&) = default;
+  Layer& operator=(Layer&&)      = default;
+
+  template <size_t Dimension>
+  Layer(const Connectivity<Dimension>& connectivity) : m_number_of{connectivity.template number<item_type>()}
+  {}
+
+  //  Layer() = default;
 
   Layer(const Layer&) = default;
   Layer(Layer&&)      = default;
@@ -117,27 +135,25 @@ StencilBuilder::_buildSymmetryConnectingItemList(const ConnectivityType& connect
                                                                       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>();
+  if constexpr (ItemTypeId<ConnectivityType::Dimension>::itemTId(connecting_item_type) ==
+                ItemTypeId<ConnectivityType::Dimension>::itemTId(ItemType::face)) {
     size_t i_symmetry_boundary = 0;
     for (auto p_boundary_descriptor : symmetry_boundary_descriptor_list) {
       const IBoundaryDescriptor& boundary_descriptor = *p_boundary_descriptor;
 
       bool found = false;
-      for (size_t i_ref_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];
+      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;
-            }
+            symmetry_connecting_item_list[connecting_item_id][i_symmetry_boundary] = true;
           }
           break;
         }
@@ -149,26 +165,27 @@ StencilBuilder::_buildSymmetryConnectingItemList(const ConnectivityType& connect
         throw NormalError(error_msg.str());
       }
     }
-
-    return symmetry_connecting_item_list;
   } else {
+    auto face_to_connecting_item_matrix =
+      connectivity.template getItemToItemMatrix<ItemType::face, connecting_item_type>();
     size_t i_symmetry_boundary = 0;
     for (auto p_boundary_descriptor : symmetry_boundary_descriptor_list) {
       const IBoundaryDescriptor& boundary_descriptor = *p_boundary_descriptor;
 
       bool found = false;
-      for (size_t i_ref_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];
+      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;
+              symmetry_connecting_item_list[connecting_item_id][i_symmetry_boundary] = true;
+            }
           }
           break;
         }
@@ -180,357 +197,572 @@ StencilBuilder::_buildSymmetryConnectingItemList(const ConnectivityType& connect
         throw NormalError(error_msg.str());
       }
     }
-
-    return symmetry_connecting_item_list;
   }
+
+  return symmetry_connecting_item_list;
 }
 
-template <typename ConnectivityType>
-Array<const uint32_t>
-StencilBuilder::_getRowMap(const ConnectivityType& connectivity) const
+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
 {
-  auto cell_to_node_matrix = connectivity.cellToNodeMatrix();
-  auto node_to_cell_matrix = connectivity.nodeToCellMatrix();
+  if (number_of_layers == 0) {
+    throw NormalError("number of layers must be greater than 0 to build stencils");
+  }
+
+  auto item_to_connecting_item_matrix = connectivity.template getItemToItemMatrix<item_type, connecting_item_type>();
+  auto connecting_item_to_item_matrix = connectivity.template getItemToItemMatrix<connecting_item_type, item_type>();
+
+  auto item_is_owned          = connectivity.template isOwned<item_type>();
+  auto item_number            = connectivity.template number<item_type>();
+  auto connecting_item_number = connectivity.template number<connecting_item_type>();
+
+  using ItemId           = ItemIdT<item_type>;
+  using ConnectingItemId = ItemIdT<connecting_item_type>;
 
-  auto cell_is_owned = connectivity.cellIsOwned();
+  ItemArray<bool, connecting_item_type> symmetry_item_list =
+    this->_buildSymmetryConnectingItemList<connecting_item_type>(connectivity, symmetry_boundary_descriptor_list);
 
-  Array<uint32_t> row_map{connectivity.numberOfCells() + 1};
+  Array<uint32_t> row_map{connectivity.template numberOf<item_type>() + 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::vector<Array<uint32_t>> symmetry_row_map_list(symmetry_boundary_descriptor_list.size());
+  for (auto&& symmetry_row_map : symmetry_row_map_list) {
+    symmetry_row_map    = Array<uint32_t>{connectivity.template numberOf<item_type>() + 1};
+    symmetry_row_map[0] = 0;
+  }
+
+  std::vector<ItemId> column_indices_vector;
+  std::vector<std::vector<uint32_t>> symmetry_column_indices_vector(symmetry_boundary_descriptor_list.size());
+
+  std::vector<Layer<item_type>> item_layer_list;
+  std::vector<Layer<connecting_item_type>> connecting_layer_list;
+
+  std::vector<Layer<item_type>> symmetry_item_layer_list;
+  std::vector<Layer<connecting_item_type>> symmetry_connecting_layer_list;
+
+  for (size_t i = 0; i < number_of_layers; ++i) {
+    item_layer_list.emplace_back(Layer<item_type>{connectivity});
+    connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+
+    if (symmetry_boundary_descriptor_list.size() > 0) {
+      symmetry_item_layer_list.emplace_back(Layer<item_type>{connectivity});
+      symmetry_connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+    }
+  }
+
+  for (ItemId item_id = 0; item_id < connectivity.template numberOf<item_type>(); ++item_id) {
+    if (item_is_owned[item_id]) {
+      for (auto&& item_layer : item_layer_list) {
+        item_layer.clear();
+      }
+      for (auto&& connecting_layer : connecting_layer_list) {
+        connecting_layer.clear();
+      }
+
+      // First layer is a special case
+      {
+        auto& item_layer       = item_layer_list[0];
+        auto& connecting_layer = connecting_layer_list[0];
+
+        for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_matrix[item_id].size();
+             ++i_connecting_item) {
+          const ConnectingItemId connecting_item_id = item_to_connecting_item_matrix[item_id][i_connecting_item];
+
+          connecting_layer.add(connecting_item_id);
+        }
+
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          for (size_t i_item = 0; i_item < connecting_item_to_item_matrix[connecting_item_id].size(); ++i_item) {
+            const ItemId layer_item_id = connecting_item_to_item_matrix[connecting_item_id][i_item];
+            if (layer_item_id != item_id) {
+              item_layer.add(layer_item_id);
+            }
           }
         }
       }
-      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;
-}
+      for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+        auto& connecting_layer = connecting_layer_list[i_layer];
 
-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();
+        auto has_connecting_item = [&i_layer, &connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+              return true;
+            }
+          }
 
-  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;
+          return false;
+        };
+
+        for (auto&& previous_layer_item_id_list : item_layer_list[i_layer - 1].itemIdList()) {
+          const auto item_to_connecting_item_list = item_to_connecting_item_matrix[previous_layer_item_id_list];
+          for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_list.size();
+               ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = item_to_connecting_item_list[i_connecting_item];
+
+            if (not has_connecting_item(connecting_item_id)) {
+              connecting_layer.add(connecting_item_id);
+            }
+          }
+        }
+
+        auto& item_layer = item_layer_list[i_layer];
+
+        auto has_layer_item = [&i_layer, &item_layer_list](ItemId layer_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (item_layer_list[i].hasItemNumber(layer_item_id)) {
+              return true;
+            }
+          }
+
+          return false;
+        };
+
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          const auto& connecting_item_to_item_list = connecting_item_to_item_matrix[connecting_item_id];
+          for (size_t i_item = 0; i_item < connecting_item_to_item_list.size(); ++i_item) {
+            const ItemId layer_item_id = connecting_item_to_item_list[i_item];
+            if ((layer_item_id != item_id) and (not has_layer_item(layer_item_id))) {
+              item_layer.add(layer_item_id);
+            }
+          }
+        }
+      }
+
+      for (size_t i_symmetry = 0; i_symmetry < symmetry_boundary_descriptor_list.size(); ++i_symmetry) {
+        for (auto&& symmetry_item_layer : symmetry_item_layer_list) {
+          symmetry_item_layer.clear();
+        }
+        for (auto&& symmetry_connecting_layer : symmetry_connecting_layer_list) {
+          symmetry_connecting_layer.clear();
+        }
+
+        // First layer is a special case
+        for (auto&& connecting_item_id : connecting_layer_list[0].itemIdList()) {
+          if (symmetry_item_list[connecting_item_id][i_symmetry]) {
+            symmetry_connecting_layer_list[0].add(connecting_item_id);
+          }
+        }
+
+        for (auto&& connecting_item_id : symmetry_connecting_layer_list[0].itemIdList()) {
+          auto item_of_connecting_item_list = connecting_item_to_item_matrix[connecting_item_id];
+          for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < item_of_connecting_item_list.size();
+               ++i_item_of_connecting_item) {
+            const ItemId item_id_of_connecting_item = item_of_connecting_item_list[i_item_of_connecting_item];
+            symmetry_item_layer_list[0].add(item_id_of_connecting_item);
+          }
+        }
+
+        for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+          auto has_connecting_item = [&i_layer,
+                                      &symmetry_connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+                return true;
               }
             }
-            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;
-                }
+
+            return false;
+          };
+
+          for (auto&& symmetry_connecting_item_id : connecting_layer_list[i_layer].itemIdList()) {
+            if (symmetry_item_list[symmetry_connecting_item_id][i_symmetry]) {
+              if (not has_connecting_item(symmetry_connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(symmetry_connecting_item_id);
               }
+            }
+          }
 
-              for (size_t i_destination = max_index[cell_id]; i_destination > i_index; --i_destination) {
-                const size_t i_source = i_destination - 1;
+          for (auto&& previous_layer_item_id_list : symmetry_item_layer_list[i_layer - 1].itemIdList()) {
+            const auto item_to_connecting_item_list = item_to_connecting_item_matrix[previous_layer_item_id_list];
+            for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = item_to_connecting_item_list[i_connecting_item];
 
-                column_indices[i_destination] = column_indices[i_source];
+              if (not has_connecting_item(connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(connecting_item_id);
+              }
+            }
+          }
+
+          auto has_symmetry_layer_item = [&i_layer, &symmetry_item_layer_list](ItemId layer_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_item_layer_list[i].hasItemNumber(layer_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& connecting_item_id : symmetry_connecting_layer_list[i_layer].itemIdList()) {
+            auto item_of_connecting_item_list = connecting_item_to_item_matrix[connecting_item_id];
+            for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < item_of_connecting_item_list.size();
+                 ++i_item_of_connecting_item) {
+              const ItemId item_id_of_connecting_item = item_of_connecting_item_list[i_item_of_connecting_item];
+              if (not has_symmetry_layer_item(item_id_of_connecting_item)) {
+                symmetry_item_layer_list[i_layer].add(item_id_of_connecting_item);
               }
-              ++max_index[cell_id];
-              column_indices[i_index] = node_cell_id;
             }
           }
         }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          for (auto symmetry_layer_item_id : symmetry_item_layer_list[i_layer].itemIdList()) {
+            symmetry_column_indices_vector[i_symmetry].push_back(symmetry_layer_item_id);
+          }
+        }
+
+        {
+          size_t stencil_size = 0;
+          for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+            auto& item_layer = symmetry_item_layer_list[i_layer];
+            stencil_size += item_layer.size();
+          }
+
+          symmetry_row_map_list[i_symmetry][item_id + 1] = symmetry_row_map_list[i_symmetry][item_id] + stencil_size;
+        }
+      }
+
+      {
+        size_t stencil_size = 0;
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          auto& item_layer = item_layer_list[i_layer];
+          for (auto stencil_item_id : item_layer.itemIdList()) {
+            column_indices_vector.push_back(stencil_item_id);
+          }
+          stencil_size += item_layer.size();
+        }
+        row_map[item_id + 1] = row_map[item_id] + stencil_size;
+      }
+
+    } else {
+      row_map[item_id + 1] = row_map[item_id];
+      for (size_t i = 0; i < symmetry_row_map_list.size(); ++i) {
+        symmetry_row_map_list[i][item_id + 1] = symmetry_row_map_list[i][item_id];
       }
     }
   }
 
-  return column_indices;
+  Array<uint32_t> column_indices{column_indices_vector.size()};
+  parallel_for(
+    column_indices_vector.size(), PUGS_LAMBDA(size_t i) { column_indices[i] = column_indices_vector[i]; });
+
+  ConnectivityMatrix primal_stencil{row_map, column_indices};
+
+  typename StencilArray<item_type, item_type>::BoundaryDescriptorStencilArrayList symmetry_boundary_stencil_list;
+  {
+    size_t i = 0;
+    for (auto&& p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+      symmetry_boundary_stencil_list.emplace_back(
+        typename StencilArray<item_type, item_type>::
+          BoundaryDescriptorStencilArray{p_boundary_descriptor,
+                                         ConnectivityMatrix{symmetry_row_map_list[i],
+                                                            convert_to_array(symmetry_column_indices_vector[i])}});
+      ++i;
+    }
+  }
+
+  return {{primal_stencil}, {symmetry_boundary_stencil_list}};
 }
 
-template <ItemType 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
+template <ItemType source_item_type,
+          ItemType connecting_item_type,
+          ItemType target_item_type,
+          typename ConnectivityType>
+StencilArray<source_item_type, target_item_type>
+StencilBuilder::_build_for_different_source_and_target(
+  const ConnectivityType& connectivity,
+  const size_t& number_of_layers,
+  const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
 {
+  constexpr size_t Dimension = ConnectivityType::Dimension;
+
   if (number_of_layers == 0) {
     throw NormalError("number of layers must be greater than 0 to build stencils");
   }
-  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_to_target_item_matrix =
+    connectivity.template getItemToItemMatrix<connecting_item_type, target_item_type>();
+  auto target_item_to_connecting_item_matrix =
+    connectivity.template getItemToItemMatrix<target_item_type, connecting_item_type>();
+
+  auto source_item_to_connecting_item_matrix =
+    [](const auto& given_connectivity) -> ItemToItemMatrix<source_item_type, connecting_item_type> {
+    if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                  ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+      return {ConnectivityMatrix{}};
+    } else {
+      return given_connectivity.template getItemToItemMatrix<source_item_type, connecting_item_type>();
+    }
+  }(connectivity);
+
+  auto source_item_is_owned   = connectivity.template isOwned<source_item_type>();
+  auto target_item_number     = connectivity.template number<target_item_type>();
   auto connecting_item_number = connectivity.template number<connecting_item_type>();
 
-  using ItemId           = ItemIdT<item_type>;
+  using SourceItemId     = ItemIdT<source_item_type>;
+  using TargetItemId     = ItemIdT<target_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;
+  ItemArray<bool, connecting_item_type> symmetry_item_list =
+    this->_buildSymmetryConnectingItemList<connecting_item_type>(connectivity, symmetry_boundary_descriptor_list);
+
+  Array<uint32_t> row_map{connectivity.template numberOf<source_item_type>() + 1};
+  row_map[0] = 0;
+  std::vector<Array<uint32_t>> symmetry_row_map_list(symmetry_boundary_descriptor_list.size());
+  for (auto&& symmetry_row_map : symmetry_row_map_list) {
+    symmetry_row_map    = Array<uint32_t>{connectivity.template numberOf<source_item_type>() + 1};
+    symmetry_row_map[0] = 0;
+  }
+
+  std::vector<TargetItemId> column_indices_vector;
+  std::vector<std::vector<uint32_t>> symmetry_column_indices_vector(symmetry_boundary_descriptor_list.size());
 
-      std::vector<ItemId> column_indices_vector;
+  std::vector<Layer<target_item_type>> target_item_layer_list;
+  std::vector<Layer<connecting_item_type>> connecting_layer_list;
 
-      for (ItemId item_id = 0; item_id < connectivity.template numberOf<item_type>(); ++item_id) {
-        // First layer is a special case
+  std::vector<Layer<target_item_type>> symmetry_target_item_layer_list;
+  std::vector<Layer<connecting_item_type>> symmetry_connecting_layer_list;
 
-        Layer<item_type> item_layer;
-        Layer<connecting_item_type> connecting_layer;
+  for (size_t i = 0; i < number_of_layers; ++i) {
+    target_item_layer_list.emplace_back(Layer<target_item_type>{connectivity});
+    connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
 
-        if (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]);
-          }
+    if (symmetry_boundary_descriptor_list.size() > 0) {
+      symmetry_target_item_layer_list.emplace_back(Layer<target_item_type>{connectivity});
+      symmetry_connecting_layer_list.emplace_back(Layer<connecting_item_type>{connectivity});
+    }
+  }
 
-          for (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 (SourceItemId source_item_id = 0; source_item_id < connectivity.template numberOf<source_item_type>();
+       ++source_item_id) {
+    if (source_item_is_owned[source_item_id]) {
+      for (auto&& target_item_layer : target_item_layer_list) {
+        target_item_layer.clear();
+      }
+      for (auto&& connecting_layer : connecting_layer_list) {
+        connecting_layer.clear();
+      }
+
+      // First layer is a special case
+      {
+        auto& target_item_layer = target_item_layer_list[0];
+        auto& connecting_layer  = connecting_layer_list[0];
+
+        if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                      ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+          connecting_layer.add(ConnectingItemId{static_cast<typename ConnectingItemId::base_type>(source_item_id)});
+        } else {
+          for (size_t i_connecting_item = 0;
+               i_connecting_item < source_item_to_connecting_item_matrix[source_item_id].size(); ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id =
+              source_item_to_connecting_item_matrix[source_item_id][i_connecting_item];
+
+            connecting_layer.add(connecting_item_id);
           }
         }
 
-        for (auto layer_item_id : item_layer.itemIdList()) {
-          column_indices_vector.push_back(layer_item_id);
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          for (size_t i_item = 0; i_item < connecting_item_to_target_item_matrix[connecting_item_id].size(); ++i_item) {
+            const TargetItemId layer_item_id = connecting_item_to_target_item_matrix[connecting_item_id][i_item];
+            target_item_layer.add(layer_item_id);
+          }
         }
-        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];
-      }
+      for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+        auto& connecting_layer = connecting_layer_list[i_layer];
 
-      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);
-                  }
-                }
-              }
+        auto has_connecting_item = [&i_layer, &connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+              return true;
             }
           }
 
-          for (auto stencil_item_id : item_set) {
-            column_indices_vector.push_back(stencil_item_id);
+          return false;
+        };
+
+        for (auto&& previous_layer_item_id_list : target_item_layer_list[i_layer - 1].itemIdList()) {
+          const auto target_item_to_connecting_item_list =
+            target_item_to_connecting_item_matrix[previous_layer_item_id_list];
+          for (size_t i_connecting_item = 0; i_connecting_item < target_item_to_connecting_item_list.size();
+               ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = target_item_to_connecting_item_list[i_connecting_item];
+
+            if (not has_connecting_item(connecting_item_id)) {
+              connecting_layer.add(connecting_item_id);
+            }
           }
-          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");
-      }
+        auto& target_item_layer = target_item_layer_list[i_layer];
 
-      Array<uint32_t> column_indices(row_map[row_map.size() - 1]);
-      column_indices.fill(std::numeric_limits<uint32_t>::max());
+        auto has_layer_item = [&i_layer, &target_item_layer_list](TargetItemId layer_item_id) -> bool {
+          for (ssize_t i = i_layer - 1; i >= 0; --i) {
+            if (target_item_layer_list[i].hasItemNumber(layer_item_id)) {
+              return true;
+            }
+          }
 
-      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 false;
+        };
 
-      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;
-    }
+        for (auto connecting_item_id : connecting_layer.itemIdList()) {
+          const auto& connecting_item_to_target_item_list = connecting_item_to_target_item_matrix[connecting_item_id];
+          for (size_t i_item = 0; i_item < connecting_item_to_target_item_list.size(); ++i_item) {
+            const TargetItemId layer_item_id = connecting_item_to_target_item_list[i_item];
+            if (not has_layer_item(layer_item_id)) {
+              target_item_layer.add(layer_item_id);
+            }
+          }
+        }
+      }
 
-    std::vector<uint32_t> column_indices_vector;
-    std::vector<std::vector<uint32_t>> symmetry_column_indices_vector(symmetry_boundary_descriptor_list.size());
+      for (size_t i_symmetry = 0; i_symmetry < symmetry_boundary_descriptor_list.size(); ++i_symmetry) {
+        for (auto&& symmetry_target_item_layer : symmetry_target_item_layer_list) {
+          symmetry_target_item_layer.clear();
+        }
+        for (auto&& symmetry_connecting_layer : symmetry_connecting_layer_list) {
+          symmetry_connecting_layer.clear();
+        }
 
-    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());
+        // First layer is a special case
+        for (auto&& connecting_item_id : connecting_layer_list[0].itemIdList()) {
+          if (symmetry_item_list[connecting_item_id][i_symmetry]) {
+            symmetry_connecting_layer_list[0].add(connecting_item_id);
+          }
+        }
 
-      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();
+        for (auto&& connecting_item_id : symmetry_connecting_layer_list[0].itemIdList()) {
+          auto item_of_connecting_item_list = connecting_item_to_target_item_matrix[connecting_item_id];
+          for (size_t i_item_of_connecting_item = 0; i_item_of_connecting_item < item_of_connecting_item_list.size();
                ++i_item_of_connecting_item) {
-            const 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);
-            }
+            const TargetItemId item_id_of_connecting_item = item_of_connecting_item_list[i_item_of_connecting_item];
+            symmetry_target_item_layer_list[0].add(item_id_of_connecting_item);
           }
         }
 
-        {
-          std::vector<ItemId> item_vector;
-          for (auto&& set_item_id : item_set) {
-            item_vector.push_back(set_item_id);
+        for (size_t i_layer = 1; i_layer < number_of_layers; ++i_layer) {
+          auto has_connecting_item = [&i_layer,
+                                      &symmetry_connecting_layer_list](ConnectingItemId connecting_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_connecting_layer_list[i].hasItemNumber(connecting_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& symmetry_connecting_item_id : connecting_layer_list[i_layer].itemIdList()) {
+            if (symmetry_item_list[symmetry_connecting_item_id][i_symmetry]) {
+              if (not has_connecting_item(symmetry_connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(symmetry_connecting_item_id);
+              }
+            }
           }
-          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 (auto&& previous_layer_target_item_id_list : symmetry_target_item_layer_list[i_layer - 1].itemIdList()) {
+            const auto item_to_connecting_item_list =
+              target_item_to_connecting_item_matrix[previous_layer_target_item_id_list];
+            for (size_t i_connecting_item = 0; i_connecting_item < item_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = item_to_connecting_item_list[i_connecting_item];
+
+              if (not has_connecting_item(connecting_item_id)) {
+                symmetry_connecting_layer_list[i_layer].add(connecting_item_id);
+              }
+            }
           }
-        }
 
-        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);
+          auto has_symmetry_layer_item = [&i_layer,
+                                          &symmetry_target_item_layer_list](TargetItemId layer_target_item_id) -> bool {
+            for (ssize_t i = i_layer - 1; i >= 0; --i) {
+              if (symmetry_target_item_layer_list[i].hasItemNumber(layer_target_item_id)) {
+                return true;
+              }
+            }
+
+            return false;
+          };
+
+          for (auto&& connecting_item_id : symmetry_connecting_layer_list[i_layer].itemIdList()) {
+            auto item_of_connecting_item_list = connecting_item_to_target_item_matrix[connecting_item_id];
+            for (size_t i_target_item_of_connecting_item = 0;
+                 i_target_item_of_connecting_item < item_of_connecting_item_list.size();
+                 ++i_target_item_of_connecting_item) {
+              const TargetItemId target_item_id_of_connecting_item =
+                item_of_connecting_item_list[i_target_item_of_connecting_item];
+              if (not has_symmetry_layer_item(target_item_id_of_connecting_item)) {
+                symmetry_target_item_layer_list[i_layer].add(target_item_id_of_connecting_item);
               }
             }
           }
-          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);
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          for (auto symmetry_layer_target_item_id : symmetry_target_item_layer_list[i_layer].itemIdList()) {
+            symmetry_column_indices_vector[i_symmetry].push_back(symmetry_layer_target_item_id);
           }
-          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);
+        {
+          size_t stencil_size = 0;
+          for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+            auto& target_item_layer = symmetry_target_item_layer_list[i_layer];
+            stencil_size += target_item_layer.size();
           }
+
+          symmetry_row_map_list[i_symmetry][source_item_id + 1] =
+            symmetry_row_map_list[i_symmetry][source_item_id] + stencil_size;
         }
       }
-      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();
+      {
+        size_t stencil_size = 0;
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          auto& item_layer = target_item_layer_list[i_layer];
+          for (auto stencil_item_id : item_layer.itemIdList()) {
+            column_indices_vector.push_back(stencil_item_id);
+          }
+          stencil_size += item_layer.size();
+        }
+        row_map[source_item_id + 1] = row_map[source_item_id] + stencil_size;
       }
-    }
-    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;
+
+    } else {
+      row_map[source_item_id + 1] = row_map[source_item_id];
+      for (size_t i = 0; i < symmetry_row_map_list.size(); ++i) {
+        symmetry_row_map_list[i][source_item_id + 1] = symmetry_row_map_list[i][source_item_id];
       }
     }
+  }
+
+  Array<uint32_t> column_indices{column_indices_vector.size()};
+  parallel_for(
+    column_indices_vector.size(), PUGS_LAMBDA(size_t i) { column_indices[i] = column_indices_vector[i]; });
+
+  ConnectivityMatrix primal_stencil{row_map, column_indices};
 
-    return {{primal_stencil}, {symmetry_boundary_stencil_list}};
+  typename StencilArray<source_item_type, target_item_type>::BoundaryDescriptorStencilArrayList
+    symmetry_boundary_stencil_list;
+  {
+    size_t i = 0;
+    for (auto&& p_boundary_descriptor : symmetry_boundary_descriptor_list) {
+      symmetry_boundary_stencil_list.emplace_back(
+        typename StencilArray<source_item_type, target_item_type>::
+          BoundaryDescriptorStencilArray{p_boundary_descriptor,
+                                         ConnectivityMatrix{symmetry_row_map_list[i],
+                                                            convert_to_array(symmetry_column_indices_vector[i])}});
+      ++i;
+    }
   }
-}
 
-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");
+  return {{primal_stencil}, {symmetry_boundary_stencil_list}};
 }
 
 template <ItemType source_item_type,
@@ -631,7 +863,35 @@ StencilBuilder::buildC2C(const IConnectivity& connectivity,
 }
 
 NodeToCellStencilArray
-StencilBuilder::buildN2C(const IConnectivity&, const StencilDescriptor&, const BoundaryDescriptorList&) const
+StencilBuilder::buildN2C(const IConnectivity& connectivity,
+                         const StencilDescriptor& stencil_descriptor,
+                         const BoundaryDescriptorList& symmetry_boundary_descriptor_list) const
 {
-  throw NotImplementedError("node to cell stencil");
+  if ((parallel::size() > 1) and
+      (stencil_descriptor.numberOfLayers() > GlobalVariableManager::instance().getNumberOfGhostLayers())) {
+    std::ostringstream error_msg;
+    error_msg << "Stencil builder requires" << rang::fgB::yellow << stencil_descriptor.numberOfLayers()
+              << rang::fg::reset << " layers while parallel number of ghost layer is "
+              << GlobalVariableManager::instance().getNumberOfGhostLayers() << ".\n";
+    error_msg << "Increase the number of ghost layers (using the '--number-of-ghost-layers' option).";
+    throw NormalError(error_msg.str());
+  }
+
+  switch (connectivity.dimension()) {
+  case 1: {
+    return this->_build<ItemType::node, ItemType::cell>(dynamic_cast<const Connectivity<1>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  case 2: {
+    return this->_build<ItemType::node, ItemType::cell>(dynamic_cast<const Connectivity<2>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  case 3: {
+    return this->_build<ItemType::node, ItemType::cell>(dynamic_cast<const Connectivity<3>&>(connectivity),
+                                                        stencil_descriptor, symmetry_boundary_descriptor_list);
+  }
+  default: {
+    throw UnexpectedError("invalid connectivity dimension");
+  }
+  }
 }
diff --git a/src/mesh/StencilBuilder.hpp b/src/mesh/StencilBuilder.hpp
index 4f7ead00b618db999dd5386258ac058b664c65a7..2ed1d3a6c45fdac651ce62ba05690532c506cf26 100644
--- a/src/mesh/StencilBuilder.hpp
+++ b/src/mesh/StencilBuilder.hpp
@@ -19,13 +19,6 @@ class StencilBuilder
   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;
@@ -74,11 +67,12 @@ class StencilBuilder
                                   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
+  _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);
diff --git a/src/mesh/StencilManager.cpp b/src/mesh/StencilManager.cpp
index 842f8858f9d1a571e28d3f0397f70e97682da0af..04a8e32a1d50d872d95896e8e7a58e76e32e2189 100644
--- a/src/mesh/StencilManager.cpp
+++ b/src/mesh/StencilManager.cpp
@@ -49,8 +49,8 @@ StencilManager::_getStencilArray(
         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));
+        StencilBuilder{}.template _build<source_item_type, target_item_type>(connectivity, stencil_descriptor,
+                                                                             symmetry_boundary_descriptor_list));
   }
 
   return *stored_source_to_target_stencil_map.at(
diff --git a/src/output/OutputNamedItemValueSet.hpp b/src/output/OutputNamedItemValueSet.hpp
index aa2c5e2ed67140d47c7d4ddf1f57caecc2f28360..8c43330420c91417e0c339228b4bfb41f9dc908b 100644
--- a/src/output/OutputNamedItemValueSet.hpp
+++ b/src/output/OutputNamedItemValueSet.hpp
@@ -38,7 +38,7 @@ class NamedItemData
   }
 
   NamedItemData& operator=(const NamedItemData&) = default;
-  NamedItemData& operator=(NamedItemData&&) = default;
+  NamedItemData& operator=(NamedItemData&&)      = default;
 
   NamedItemData(const std::string& name, const ItemDataT<DataType, item_type, ConnectivityPtr>& item_data)
     : m_name(name), m_item_data(item_data)
@@ -113,24 +113,48 @@ class OutputNamedItemDataSet
                                        NodeArray<const long int>,
                                        NodeArray<const unsigned long int>,
                                        NodeArray<const double>,
+                                       NodeArray<const TinyVector<1, double>>,
+                                       NodeArray<const TinyVector<2, double>>,
+                                       NodeArray<const TinyVector<3, double>>,
+                                       NodeArray<const TinyMatrix<1, 1, double>>,
+                                       NodeArray<const TinyMatrix<2, 2, double>>,
+                                       NodeArray<const TinyMatrix<3, 3, double>>,
 
                                        EdgeArray<const bool>,
                                        EdgeArray<const int>,
                                        EdgeArray<const long int>,
                                        EdgeArray<const unsigned long int>,
                                        EdgeArray<const double>,
+                                       EdgeArray<const TinyVector<1, double>>,
+                                       EdgeArray<const TinyVector<2, double>>,
+                                       EdgeArray<const TinyVector<3, double>>,
+                                       EdgeArray<const TinyMatrix<1, 1, double>>,
+                                       EdgeArray<const TinyMatrix<2, 2, double>>,
+                                       EdgeArray<const TinyMatrix<3, 3, double>>,
 
                                        FaceArray<const bool>,
                                        FaceArray<const int>,
                                        FaceArray<const long int>,
                                        FaceArray<const unsigned long int>,
                                        FaceArray<const double>,
+                                       FaceArray<const TinyVector<1, double>>,
+                                       FaceArray<const TinyVector<2, double>>,
+                                       FaceArray<const TinyVector<3, double>>,
+                                       FaceArray<const TinyMatrix<1, 1, double>>,
+                                       FaceArray<const TinyMatrix<2, 2, double>>,
+                                       FaceArray<const TinyMatrix<3, 3, double>>,
 
                                        CellArray<const bool>,
                                        CellArray<const int>,
                                        CellArray<const long int>,
                                        CellArray<const unsigned long int>,
-                                       CellArray<const double>>;
+                                       CellArray<const double>,
+                                       CellArray<const TinyVector<1, double>>,
+                                       CellArray<const TinyVector<2, double>>,
+                                       CellArray<const TinyVector<3, double>>,
+                                       CellArray<const TinyMatrix<1, 1, double>>,
+                                       CellArray<const TinyMatrix<2, 2, double>>,
+                                       CellArray<const TinyMatrix<3, 3, double>>>;
 
  private:
   // We do not use a map, we want variables to be written in the
diff --git a/src/output/VTKWriter.cpp b/src/output/VTKWriter.cpp
index 64d35434462e5980a11b69ea2257ce779b3ec581..2aedce3b1fe264bf3a11a94592077f4631e666aa 100644
--- a/src/output/VTKWriter.cpp
+++ b/src/output/VTKWriter.cpp
@@ -396,10 +396,17 @@ VTKWriter::_write(const MeshType& mesh,
          << "\">\n";
     fout << "<CellData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](
-                   auto&&
-                     item_value) { return this->_write_cell_data(fout, var_name, item_value, serialize_data_list); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_cell_data(fout, var_name, item_value, serialize_data_list);
+          }
+        },
+        item_value_variant);
     }
     if (parallel::size() > 1) {
       CellValue<uint8_t> vtk_ghost_type{mesh.connectivity()};
@@ -413,10 +420,17 @@ VTKWriter::_write(const MeshType& mesh,
     fout << "</CellData>\n";
     fout << "<PointData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](
-                   auto&&
-                     item_value) { return this->_write_node_data(fout, var_name, item_value, serialize_data_list); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_node_data(fout, var_name, item_value, serialize_data_list);
+          }
+        },
+        item_value_variant);
     }
     fout << "</PointData>\n";
     fout << "<Points>\n";
@@ -644,15 +658,33 @@ VTKWriter::_write(const MeshType& mesh,
 
     fout << "<PPointData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](auto&& item_value) { return this->_write_node_pvtu(fout, var_name, item_value); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_node_pvtu(fout, var_name, item_value);
+          }
+        },
+        item_value_variant);
     }
     fout << "</PPointData>\n";
 
     fout << "<PCellData>\n";
     for (const auto& [name, item_value_variant] : output_named_item_data_set) {
-      std::visit([&, var_name = name](auto&& item_value) { return this->_write_cell_pvtu(fout, var_name, item_value); },
-                 item_value_variant);
+      std::visit(
+        [&, var_name = name](auto&& item_value) {
+          using IVType   = std::decay_t<decltype(item_value)>;
+          using DataType = typename IVType::data_type;
+          if constexpr (is_item_array_v<IVType> and not std::is_arithmetic_v<DataType>) {
+            throw NotImplementedError("DiscreteFunctionP0Vector of non arithmetic type");
+          } else {
+            return this->_write_cell_pvtu(fout, var_name, item_value);
+          }
+        },
+        item_value_variant);
     }
     if (parallel::size() > 1) {
       fout << "<PDataArray type=\"UInt8\" Name=\"vtkGhostType\" NumberOfComponents=\"1\"/>\n";
diff --git a/src/scheme/DirichletVectorBoundaryConditionDescriptor.hpp b/src/scheme/DirichletVectorBoundaryConditionDescriptor.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c1f1dcc3fadeced1dbed1f560a6e72c86eebf0ed
--- /dev/null
+++ b/src/scheme/DirichletVectorBoundaryConditionDescriptor.hpp
@@ -0,0 +1,58 @@
+#ifndef DIRICHLET_VECTOR_BOUNDARY_CONDITION_DESCRIPTOR_HPP
+#define DIRICHLET_VECTOR_BOUNDARY_CONDITION_DESCRIPTOR_HPP
+
+#include <language/utils/FunctionSymbolId.hpp>
+#include <mesh/IBoundaryDescriptor.hpp>
+#include <scheme/BoundaryConditionDescriptorBase.hpp>
+
+#include <memory>
+#include <vector>
+
+class DirichletVectorBoundaryConditionDescriptor : public BoundaryConditionDescriptorBase
+{
+ private:
+  std::ostream&
+  _write(std::ostream& os) const final
+  {
+    os << "dirichlet_vector(" << m_name << ',' << *m_boundary_descriptor << ")";
+    return os;
+  }
+
+  const std::string m_name;
+
+  const std::vector<FunctionSymbolId> m_rhs_symbol_id_list;
+
+ public:
+  const std::string&
+  name() const
+  {
+    return m_name;
+  }
+
+  const std::vector<FunctionSymbolId>&
+  rhsSymbolIdList() const
+  {
+    return m_rhs_symbol_id_list;
+  }
+
+  Type
+  type() const final
+  {
+    return Type::dirichlet_vector;
+  }
+
+  DirichletVectorBoundaryConditionDescriptor(const std::string_view name,
+                                             std::shared_ptr<const IBoundaryDescriptor> boundary_descriptor,
+                                             const std::vector<FunctionSymbolId>& rhs_symbol_id_list)
+    : BoundaryConditionDescriptorBase{boundary_descriptor}, m_name{name}, m_rhs_symbol_id_list{rhs_symbol_id_list}
+  {
+    ;
+  }
+
+  DirichletVectorBoundaryConditionDescriptor(const DirichletVectorBoundaryConditionDescriptor&) = delete;
+  DirichletVectorBoundaryConditionDescriptor(DirichletVectorBoundaryConditionDescriptor&&)      = delete;
+
+  ~DirichletVectorBoundaryConditionDescriptor() = default;
+};
+
+#endif   // DIRICHLET_VECTOR_BOUNDARY_CONDITION_DESCRIPTOR_HPP
diff --git a/src/scheme/DiscreteFunctionDPkVariant.hpp b/src/scheme/DiscreteFunctionDPkVariant.hpp
index 0d683c1783c9aad299a81dcc4d7ef3817470d54a..2942f141f38452faed9e95a5f95ca675bc7d8c29 100644
--- a/src/scheme/DiscreteFunctionDPkVariant.hpp
+++ b/src/scheme/DiscreteFunctionDPkVariant.hpp
@@ -35,8 +35,28 @@ class DiscreteFunctionDPkVariant
                                DiscreteFunctionDPk<3, const TinyMatrix<3>>,
 
                                DiscreteFunctionDPkVector<1, const double>,
+                               DiscreteFunctionDPkVector<1, const TinyVector<1>>,
+                               DiscreteFunctionDPkVector<1, const TinyVector<2>>,
+                               DiscreteFunctionDPkVector<1, const TinyVector<3>>,
+                               DiscreteFunctionDPkVector<1, const TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<1, const TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<1, const TinyMatrix<3>>,
+
                                DiscreteFunctionDPkVector<2, const double>,
-                               DiscreteFunctionDPkVector<3, const double>>;
+                               DiscreteFunctionDPkVector<2, const TinyVector<1>>,
+                               DiscreteFunctionDPkVector<2, const TinyVector<2>>,
+                               DiscreteFunctionDPkVector<2, const TinyVector<3>>,
+                               DiscreteFunctionDPkVector<2, const TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<2, const TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<2, const TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<3, const double>,
+                               DiscreteFunctionDPkVector<3, const TinyVector<1>>,
+                               DiscreteFunctionDPkVector<3, const TinyVector<2>>,
+                               DiscreteFunctionDPkVector<3, const TinyVector<3>>,
+                               DiscreteFunctionDPkVector<3, const TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<3, const TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<3, const TinyMatrix<3>>>;
 
  private:
   Variant m_discrete_function_dpk;
@@ -91,7 +111,13 @@ class DiscreteFunctionDPkVariant
   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>,
+    static_assert(std::is_same_v<std::remove_const_t<DataType>, double> or                       //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<1, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<2, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<3, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<3, 3, double>>,
                   "DiscreteFunctionDPkVector with this DataType is not allowed in variant");
   }
 
diff --git a/src/scheme/DiscreteFunctionDPkVector.hpp b/src/scheme/DiscreteFunctionDPkVector.hpp
index 2fefc078b192304ab0a80b63782986867e13c3d8..e8ac729f4b3e002f8c1c3b2c1cdcbd5f4aef3f77 100644
--- a/src/scheme/DiscreteFunctionDPkVector.hpp
+++ b/src/scheme/DiscreteFunctionDPkVector.hpp
@@ -139,6 +139,16 @@ class DiscreteFunctionDPkVector
     return m_by_cell_coefficients[cell_id];
   }
 
+  PUGS_INLINE auto
+  componentCoefficients(const CellId cell_id, size_t i_component) const noexcept(NO_ASSERT)
+  {
+    Assert(m_mesh_v.use_count() > 0, "DiscreteFunctionDPkVector is not built");
+    Assert(i_component < m_number_of_components, "incorrect component number");
+
+    return ComponentCoefficientView{&m_by_cell_coefficients[cell_id][i_component * m_nb_coefficients_per_component],
+                                    m_nb_coefficients_per_component};
+  }
+
   PUGS_FORCEINLINE BasisView
   operator()(const CellId cell_id, size_t i_component) const noexcept(NO_ASSERT)
   {
diff --git a/src/scheme/DiscreteFunctionP0Vector.hpp b/src/scheme/DiscreteFunctionP0Vector.hpp
index da3904b2ad9853a05a521920c3a3a9434aac14a9..bc08cb2a7f27286b9bafdd8bb15d0c761b892d1b 100644
--- a/src/scheme/DiscreteFunctionP0Vector.hpp
+++ b/src/scheme/DiscreteFunctionP0Vector.hpp
@@ -19,8 +19,6 @@ class DiscreteFunctionP0Vector
   friend class DiscreteFunctionP0Vector<std::add_const_t<DataType>>;
   friend class DiscreteFunctionP0Vector<std::remove_const_t<DataType>>;
 
-  static_assert(std::is_arithmetic_v<DataType>, "DiscreteFunctionP0Vector are only defined for arithmetic data type");
-
  private:
   std::shared_ptr<const MeshVariant> m_mesh;
   CellArray<DataType> m_cell_arrays;
@@ -188,16 +186,23 @@ class DiscreteFunctionP0Vector
     return product;
   }
 
-  PUGS_INLINE friend DiscreteFunctionP0<double>
+  PUGS_INLINE friend DiscreteFunctionP0<std::remove_const_t<DataType>>
   sumOfComponents(const DiscreteFunctionP0Vector& f)
   {
-    DiscreteFunctionP0<double> result{f.m_mesh};
+    DiscreteFunctionP0<std::remove_const_t<DataType>> result{f.m_mesh};
 
     parallel_for(
       f.m_mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
         const auto& f_cell_id = f[cell_id];
 
-        double sum = 0;
+        std::remove_const_t<DataType> sum = [] {
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            return 0;
+          } else {
+            return zero;
+          }
+        }();
+
         for (size_t i = 0; i < f.size(); ++i) {
           sum += f_cell_id[i];
         }
@@ -213,6 +218,7 @@ class DiscreteFunctionP0Vector
   {
     Assert(f.meshVariant()->id() == g.meshVariant()->id(), "functions are nor defined on the same mesh");
     Assert(f.size() == g.size());
+    static_assert(std::is_arithmetic_v<std::decay_t<DataType>>);
     DiscreteFunctionP0<double> result{f.m_mesh};
     parallel_for(
       f.m_mesh->numberOfCells(), PUGS_LAMBDA(CellId cell_id) {
diff --git a/src/scheme/DiscreteFunctionVariant.hpp b/src/scheme/DiscreteFunctionVariant.hpp
index 901b7989084249ca0e437f9eef24e0371dd723a8..616184298a212c10fe4865b451d0a7879a18b2ea 100644
--- a/src/scheme/DiscreteFunctionVariant.hpp
+++ b/src/scheme/DiscreteFunctionVariant.hpp
@@ -19,7 +19,13 @@ class DiscreteFunctionVariant
                                DiscreteFunctionP0<const TinyMatrix<2>>,
                                DiscreteFunctionP0<const TinyMatrix<3>>,
 
-                               DiscreteFunctionP0Vector<const double>>;
+                               DiscreteFunctionP0Vector<const double>,
+                               DiscreteFunctionP0Vector<const TinyVector<1>>,
+                               DiscreteFunctionP0Vector<const TinyVector<2>>,
+                               DiscreteFunctionP0Vector<const TinyVector<3>>,
+                               DiscreteFunctionP0Vector<const TinyMatrix<1>>,
+                               DiscreteFunctionP0Vector<const TinyMatrix<2>>,
+                               DiscreteFunctionP0Vector<const TinyMatrix<3>>>;
 
   Variant m_discrete_function;
 
@@ -70,7 +76,13 @@ class DiscreteFunctionVariant
   DiscreteFunctionVariant(const DiscreteFunctionP0Vector<DataType>& discrete_function)
     : m_discrete_function{DiscreteFunctionP0Vector<const DataType>{discrete_function}}
   {
-    static_assert(std::is_same_v<std::remove_const_t<DataType>, double>,
+    static_assert(std::is_same_v<std::remove_const_t<DataType>, double> or                       //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<1, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<2, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyVector<3, double>> or      //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<std::remove_const_t<DataType>, TinyMatrix<3, 3, double>>,
                   "DiscreteFunctionP0Vector with this DataType is not allowed in variant");
   }
 
diff --git a/src/scheme/FluxingAdvectionSolver.cpp b/src/scheme/FluxingAdvectionSolver.cpp
index d57eae2c45d93968df66fac6ffb3503d3aa74815..6f85610e0f1f87d2bf77ba516ddc9211f0b93fe6 100644
--- a/src/scheme/FluxingAdvectionSolver.cpp
+++ b/src/scheme/FluxingAdvectionSolver.cpp
@@ -86,10 +86,15 @@ class FluxingAdvectionSolver
     m_remapped_list.emplace_back(copy(old_q.cellValues()));
   }
 
+  template <typename DataType>
   void
-  _storeValues(const DiscreteFunctionP0Vector<const double>& old_q)
+  _storeValues(const DiscreteFunctionP0Vector<const DataType>& old_q)
   {
-    m_remapped_list.emplace_back(copy(old_q.cellArrays()));
+    if constexpr (std::is_arithmetic_v<DataType>) {
+      m_remapped_list.emplace_back(copy(old_q.cellArrays()));
+    } else {
+      throw NormalError("remapping DiscreteFunctionP0Vector of non arithmetic data type is not supported");
+    }
   }
 
   template <typename DataType>
@@ -741,8 +746,12 @@ FluxingAdvectionSolver<MeshType>::remap(
           new_variables.push_back(std::make_shared<DiscreteFunctionVariant>(
             DiscreteFunctionT(m_new_mesh, std::get<CellValue<DataType>>(m_remapped_list[i]))));
         } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-          new_variables.push_back(std::make_shared<DiscreteFunctionVariant>(
-            DiscreteFunctionT(m_new_mesh, std::get<CellArray<DataType>>(m_remapped_list[i]))));
+          if constexpr (std::is_arithmetic_v<DataType>) {
+            new_variables.push_back(std::make_shared<DiscreteFunctionVariant>(
+              DiscreteFunctionT(m_new_mesh, std::get<CellArray<DataType>>(m_remapped_list[i]))));
+          } else {
+            throw NormalError("remapping DiscreteFunctionP0Vector of non arithmetic data type is not supported");
+          }
         } else {
           throw UnexpectedError("invalid discrete function type");
         }
diff --git a/src/scheme/IBoundaryConditionDescriptor.hpp b/src/scheme/IBoundaryConditionDescriptor.hpp
index fc9ff381b97758e79642647a36680364093c193b..63f698e16eb611f9cb03bc82e6d737372924d36d 100644
--- a/src/scheme/IBoundaryConditionDescriptor.hpp
+++ b/src/scheme/IBoundaryConditionDescriptor.hpp
@@ -13,6 +13,7 @@ class IBoundaryConditionDescriptor
   {
     axis,
     dirichlet,
+    dirichlet_vector,
     external,
     fourier,
     fixed,
diff --git a/src/scheme/PolynomialReconstruction.cpp b/src/scheme/PolynomialReconstruction.cpp
index 2af32e157475b0fe99b9f2cc9cc6c445e05681b3..1a2b1e072734154516b01001cb9831bfaff8ea73 100644
--- a/src/scheme/PolynomialReconstruction.cpp
+++ b/src/scheme/PolynomialReconstruction.cpp
@@ -32,6 +32,14 @@ symmetrize_vector(const TinyVector<Dimension>& normal, const TinyVector<Dimensio
   return u - 2 * dot(u, normal) * normal;
 }
 
+template <size_t Dimension>
+PUGS_INLINE auto
+symmetrize_matrix(const TinyVector<Dimension>& normal, const TinyMatrix<Dimension>& A)
+{
+  const TinyMatrix S = TinyMatrix<Dimension>{identity} - 2 * tensorProduct(normal, normal);
+  return S * A * S;
+}
+
 template <size_t Dimension>
 PUGS_INLINE auto
 symmetrize_coordinates(const TinyVector<Dimension>& origin,
@@ -69,8 +77,28 @@ class PolynomialReconstruction::MutableDiscreteFunctionDPkVariant
                                DiscreteFunctionDPk<3, TinyMatrix<3>>,
 
                                DiscreteFunctionDPkVector<1, double>,
+                               DiscreteFunctionDPkVector<1, TinyVector<1>>,
+                               DiscreteFunctionDPkVector<1, TinyVector<2>>,
+                               DiscreteFunctionDPkVector<1, TinyVector<3>>,
+                               DiscreteFunctionDPkVector<1, TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<1, TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<1, TinyMatrix<3>>,
+
                                DiscreteFunctionDPkVector<2, double>,
-                               DiscreteFunctionDPkVector<3, double>>;
+                               DiscreteFunctionDPkVector<2, TinyVector<1>>,
+                               DiscreteFunctionDPkVector<2, TinyVector<2>>,
+                               DiscreteFunctionDPkVector<2, TinyVector<3>>,
+                               DiscreteFunctionDPkVector<2, TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<2, TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<2, TinyMatrix<3>>,
+
+                               DiscreteFunctionDPkVector<3, double>,
+                               DiscreteFunctionDPkVector<3, TinyVector<1>>,
+                               DiscreteFunctionDPkVector<3, TinyVector<2>>,
+                               DiscreteFunctionDPkVector<3, TinyVector<3>>,
+                               DiscreteFunctionDPkVector<3, TinyMatrix<1>>,
+                               DiscreteFunctionDPkVector<3, TinyMatrix<2>>,
+                               DiscreteFunctionDPkVector<3, TinyMatrix<3>>>;
 
  private:
   Variant m_mutable_discrete_function_dpk;
@@ -123,7 +151,13 @@ class PolynomialReconstruction::MutableDiscreteFunctionDPkVariant
   MutableDiscreteFunctionDPkVariant(const DiscreteFunctionDPkVector<Dimension, DataType>& discrete_function_dpk)
     : m_mutable_discrete_function_dpk{discrete_function_dpk}
   {
-    static_assert(std::is_same_v<DataType, double>,
+    static_assert(std::is_same_v<DataType, double> or                       //
+                    std::is_same_v<DataType, TinyVector<1, double>> or      //
+                    std::is_same_v<DataType, TinyVector<2, double>> or      //
+                    std::is_same_v<DataType, TinyVector<3, double>> or      //
+                    std::is_same_v<DataType, TinyMatrix<1, 1, double>> or   //
+                    std::is_same_v<DataType, TinyMatrix<2, 2, double>> or   //
+                    std::is_same_v<DataType, TinyMatrix<3, 3, double>>,
                   "DiscreteFunctionDPkVector with this DataType is not allowed in variant");
   }
 
@@ -698,7 +732,7 @@ PolynomialReconstruction::_getNumberOfColumns(
         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>) {
+          if constexpr (std::is_arithmetic_v<data_type>) {
             return 1;
           } else if constexpr (is_tiny_vector_v<data_type> or is_tiny_matrix_v<data_type>) {
             return data_type::Dimension;
@@ -708,7 +742,16 @@ PolynomialReconstruction::_getNumberOfColumns(
             // LCOV_EXCL_STOP
           }
         } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-          return discrete_function.size();
+          using data_type = std::decay_t<typename DiscreteFunctionT::data_type>;
+          if constexpr (std::is_arithmetic_v<data_type>) {
+            return discrete_function.size();
+          } else if constexpr (is_tiny_vector_v<data_type> or is_tiny_matrix_v<data_type>) {
+            return discrete_function.size() * data_type::Dimension;
+          } else {
+            // LCOV_EXCL_START
+            throw UnexpectedError("unexpected data type " + demangle<data_type>());
+            // LCOV_EXCL_STOP
+          }
         } else {
           // LCOV_EXCL_START
           throw UnexpectedError("unexpected discrete function type");
@@ -755,6 +798,44 @@ PolynomialReconstruction::_createMutableDiscreteFunctionDPKVariantList(
   return mutable_discrete_function_dpk_variant_list;
 }
 
+template <MeshConcept MeshType>
+void
+PolynomialReconstruction::_checkDataAndSymmetriesCompatibility(
+  const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const
+{
+  for (auto&& discrete_function_variant : discrete_function_variant_list) {
+    std::visit(
+      [&](auto&& discrete_function) {
+        using DiscreteFunctionT = std::decay_t<decltype(discrete_function)>;
+        if constexpr (is_discrete_function_P0_v<DiscreteFunctionT> or
+                      is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
+          using DataType = std::decay_t<typename DiscreteFunctionT::data_type>;
+          if constexpr (is_tiny_vector_v<DataType>) {
+            if constexpr (DataType::Dimension != MeshType::Dimension) {
+              std::stringstream error_msg;
+              error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                        << " using a mesh of dimension " << MeshType::Dimension;
+              throw NormalError(error_msg.str());
+            }
+          } else if constexpr (is_tiny_matrix_v<DataType>) {
+            if constexpr ((DataType::NumberOfRows != MeshType::Dimension) or
+                          (DataType::NumberOfColumns != MeshType::Dimension)) {
+              std::stringstream error_msg;
+              error_msg << "cannot symmetrize matrices of dimensions " << DataType::NumberOfRows << 'x'
+                        << DataType::NumberOfColumns << " using a mesh of dimension " << MeshType::Dimension;
+              throw NormalError(error_msg.str());
+            }
+          }
+        } else {
+          // LCOV_EXCL_START
+          throw UnexpectedError("invalid discrete function type");
+          // LCOV_EXCL_STOP
+        }
+      },
+      discrete_function_variant->discreteFunction());
+  }
+}
+
 template <MeshConcept MeshType>
 [[nodiscard]] std::vector<std::shared_ptr<const DiscreteFunctionDPkVariant>>
 PolynomialReconstruction::_build(
@@ -765,6 +846,10 @@ PolynomialReconstruction::_build(
 
   using Rd = TinyVector<MeshType::Dimension>;
 
+  if (m_descriptor.symmetryBoundaryDescriptorList().size() > 0) {
+    this->_checkDataAndSymmetriesCompatibility<MeshType>(discrete_function_variant_list);
+  }
+
   const size_t number_of_columns = this->_getNumberOfColumns(discrete_function_variant_list);
 
   const size_t basis_dimension =
@@ -862,7 +947,7 @@ PolynomialReconstruction::_build(
   }
 
   parallel_for(
-    mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_j_id) {
+    mesh.numberOfCells(), PUGS_CLASS_LAMBDA(const CellId cell_j_id) {
       if (cell_is_owned[cell_j_id]) {
         const int32_t t = tokens.acquire();
 
@@ -919,21 +1004,32 @@ PolynomialReconstruction::_build(
                           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];
-                        }
+                        // LCOV_EXCL_START
+                        std::stringstream error_msg;
+                        error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                                  << " using a mesh of dimension " << MeshType::Dimension;
+                        throw UnexpectedError(error_msg.str());
+                        // LCOV_EXCL_STOP
                       }
                     } else if constexpr (is_tiny_matrix_v<DataType>) {
                       if constexpr ((DataType::NumberOfColumns == DataType::NumberOfRows) and
                                     (DataType::NumberOfColumns == MeshType::Dimension)) {
-                        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);
+                        const Rd& normal = symmetry_normal_list[i_symmetry];
+
+                        const DataType& qi    = discrete_function[cell_i_id];
+                        const DataType& qi_qj = symmetrize_matrix(normal, qi) - qj;
+                        for (size_t k = 0; k < DataType::NumberOfRows; ++k) {
+                          for (size_t l = 0; l < DataType::NumberOfColumns; ++l) {
+                            B(index, column_begin + k * DataType::NumberOfColumns + l) = qi_qj(k, l);
+                          }
                         }
+                      } else {
+                        // LCOV_EXCL_START
+                        std::stringstream error_msg;
+                        error_msg << "cannot symmetrize matrices of dimensions " << DataType::NumberOfRows << 'x'
+                                  << DataType::NumberOfColumns << " using a mesh of dimension " << MeshType::Dimension;
+                        throw UnexpectedError(error_msg.str());
+                        // LCOV_EXCL_STOP
                       }
                     }
                   }
@@ -945,32 +1041,95 @@ PolynomialReconstruction::_build(
                   column_begin += DataType::Dimension;
                 }
               } else if constexpr (is_discrete_function_P0_vector_v<DiscreteFunctionT>) {
-                using DataType       = std::decay_t<typename DiscreteFunctionT::data_type>;
+                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];
+                if constexpr (std::is_arithmetic_v<DataType>) {
+                  size_t index = 0;
+                  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                    const CellId cell_i_id = stencil_cell_list[i];
                     for (size_t l = 0; l < qj_vector.size(); ++l) {
                       const DataType& qj         = qj_vector[l];
                       const DataType& qi_qj      = discrete_function[cell_i_id][l] - qj;
                       B(index, column_begin + l) = qi_qj;
                     }
                   }
+
+                  for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                       ++i_symmetry) {
+                    auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                    auto ghost_cell_list = ghost_stencil[cell_j_id];
+                    for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                      const CellId cell_i_id = ghost_cell_list[i];
+                      for (size_t l = 0; l < qj_vector.size(); ++l) {
+                        const DataType& qj         = qj_vector[l];
+                        const DataType& qi_qj      = discrete_function[cell_i_id][l] - qj;
+                        B(index, column_begin + l) = qi_qj;
+                      }
+                    }
+                  }
+                } else if constexpr (is_tiny_vector_v<DataType>) {
+                  size_t index = 0;
+                  for (size_t i = 0; i < stencil_cell_list.size(); ++i, ++index) {
+                    const CellId cell_i_id = stencil_cell_list[i];
+                    for (size_t l = 0; l < qj_vector.size(); ++l) {
+                      const DataType& qj    = qj_vector[l];
+                      const DataType& qi_qj = discrete_function[cell_i_id][l] - qj;
+                      for (size_t kB = column_begin + l * DataType::Dimension, k = 0; k < DataType::Dimension;
+                           ++k, ++kB) {
+                        B(index, kB) = qi_qj[k];
+                      }
+                    }
+                  }
+
+                  for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                       ++i_symmetry) {
+                    if constexpr (DataType::Dimension == MeshType::Dimension) {
+                      auto& ghost_stencil = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                      auto ghost_cell_list = ghost_stencil[cell_j_id];
+
+                      const Rd& normal = symmetry_normal_list[i_symmetry];
+
+                      for (size_t i = 0; i < ghost_cell_list.size(); ++i, ++index) {
+                        const CellId cell_i_id = ghost_cell_list[i];
+
+                        for (size_t l = 0; l < qj_vector.size(); ++l) {
+                          const DataType& qj    = qj_vector[l];
+                          const DataType& qi    = discrete_function[cell_i_id][l];
+                          const DataType& qi_qj = symmetrize_vector(normal, qi) - qj;
+                          for (size_t kB = column_begin + l * DataType::Dimension, k = 0; k < DataType::Dimension;
+                               ++k, ++kB) {
+                            B(index, kB) = qi_qj[k];
+                          }
+                        }
+                      }
+                    } else {
+                      // LCOV_EXCL_START
+                      std::stringstream error_msg;
+                      error_msg << "cannot symmetrize vectors of dimension " << DataType::Dimension
+                                << " using a mesh of dimension " << MeshType::Dimension;
+                      throw UnexpectedError(error_msg.str());
+                      // LCOV_EXCL_STOP
+                    }
+                  }
+                } else if constexpr (is_tiny_matrix_v<DataType>) {
+                  for (size_t i_symmetry = 0; i_symmetry < stencil_array.symmetryBoundaryStencilArrayList().size();
+                       ++i_symmetry) {
+                    auto& ghost_stencil  = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry].stencilArray();
+                    auto ghost_cell_list = ghost_stencil[cell_j_id];
+                    if (ghost_cell_list.size() > 0) {
+                      throw NotImplementedError("TinyMatrix symmetries for reconstruction of DiscreteFunctionP0Vector");
+                    }
+                  }
+                }
+
+                if constexpr (std::is_arithmetic_v<DataType>) {
+                  column_begin += qj_vector.size();
+                } else if constexpr (is_tiny_vector_v<DataType> or is_tiny_matrix_v<DataType>) {
+                  column_begin += qj_vector.size() * DataType::Dimension;
                 }
-                column_begin += qj_vector.size();
+
               } else {
                 // LCOV_EXCL_START
                 throw UnexpectedError("invalid discrete function type");
@@ -1289,6 +1448,24 @@ PolynomialReconstruction::_build(
                           dpk_j_ip1       = X(i, column_begin);
                         }
                         ++column_begin;
+                      } else if constexpr (is_tiny_vector_v<DataType>) {
+                        if (m_descriptor.degree() > 1) {
+                          auto& mean_j_of_ejk = mean_j_of_ejk_pool[t];
+                          for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                            auto& dpk_j_0 = dpk_j[0];
+                            for (size_t k = 0; k < DataType::Dimension; ++k) {
+                              dpk_j_0[k] -= X(i, column_begin + k) * mean_j_of_ejk[i];
+                            }
+                          }
+                        }
+
+                        for (size_t i = 0; i < basis_dimension - 1; ++i) {
+                          auto& dpk_j_ip1 = dpk_j[component_begin + i + 1];
+                          for (size_t k = 0; k < DataType::Dimension; ++k) {
+                            dpk_j_ip1[k] = X(i, column_begin + k);
+                          }
+                        }
+                        column_begin += DataType::Dimension;
                       } else {
                         // LCOV_EXCL_START
                         throw UnexpectedError("unexpected data type");
diff --git a/src/scheme/PolynomialReconstruction.hpp b/src/scheme/PolynomialReconstruction.hpp
index b2b855271d1ed4140ebb51d1b21314602caa0dc6..bf6013639f732fd52f9c145ba4982701c0e4bcfc 100644
--- a/src/scheme/PolynomialReconstruction.hpp
+++ b/src/scheme/PolynomialReconstruction.hpp
@@ -17,6 +17,10 @@ class PolynomialReconstruction
   size_t _getNumberOfColumns(
     const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
 
+  template <MeshConcept MeshType>
+  void _checkDataAndSymmetriesCompatibility(
+    const std::vector<std::shared_ptr<const DiscreteFunctionVariant>>& discrete_function_variant_list) const;
+
   template <MeshConcept MeshType>
   std::vector<MutableDiscreteFunctionDPkVariant> _createMutableDiscreteFunctionDPKVariantList(
     const std::shared_ptr<const MeshType>& p_mesh,
diff --git a/src/utils/BuildInfo.cpp b/src/utils/BuildInfo.cpp
index 81d7567750eaef8513b580daf11eac6d3f916642..2413bdbaeb5712e3b7eaf96fd9d09df5e8f0a6e0 100644
--- a/src/utils/BuildInfo.cpp
+++ b/src/utils/BuildInfo.cpp
@@ -17,6 +17,10 @@
 #include <slepc.h>
 #endif   // PUGS_HAS_PETSC
 
+#ifdef PUGS_HAS_EIGEN3
+#include <eigen3/Eigen/Eigen>
+#endif   // PUGS_HAS_EIGEN3
+
 #ifdef PUGS_HAS_HDF5
 #include <highfive/highfive.hpp>
 #endif   // PUGS_HAS_HDF5
@@ -82,6 +86,16 @@ BuildInfo::slepcLibrary()
 #endif   // PUGS_HAS_SLEPC
 }
 
+std::string
+BuildInfo::eigen3Library()
+{
+#ifdef PUGS_HAS_EIGEN3
+  return stringify(EIGEN_WORLD_VERSION) + "." + stringify(EIGEN_MAJOR_VERSION) + "." + stringify(EIGEN_MINOR_VERSION);
+#else    // PUGS_HAS_EIGEN3
+  return "none";
+#endif   // PUGS_HAS_EIGEN3
+}
+
 std::string
 BuildInfo::hdf5Library()
 {
diff --git a/src/utils/BuildInfo.hpp b/src/utils/BuildInfo.hpp
index df4a32d93a284db7fc8d3abbea9ac2e43ea5d36e..86f7c04f035827a936a00c9b1f42e304e33e7b26 100644
--- a/src/utils/BuildInfo.hpp
+++ b/src/utils/BuildInfo.hpp
@@ -11,6 +11,7 @@ struct BuildInfo
   static std::string mpiLibrary();
   static std::string petscLibrary();
   static std::string slepcLibrary();
+  static std::string eigen3Library();
   static std::string hdf5Library();
   static std::string slurmLibrary();
 };
diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt
index 13b0db80862c2b28e80dc18ff22951b08157db28..bc66a97faf800dc0a10be3165f8d4f2c94059010 100644
--- a/src/utils/CMakeLists.txt
+++ b/src/utils/CMakeLists.txt
@@ -14,8 +14,12 @@ add_library(
   FPEManager.cpp
   GlobalVariableManager.cpp
   Messenger.cpp
+  ParMETISPartitioner.cpp
   Partitioner.cpp
+  PartitionerOptions.cpp
   PETScWrapper.cpp
+  PluginsLoader.cpp
+  PTScotchPartitioner.cpp
   PugsUtils.cpp
   RandomEngine.cpp
   ReproducibleSumManager.cpp
@@ -32,11 +36,15 @@ endif()
 
 target_link_libraries(
   PugsUtils
-  ${PETSC_LIBRARIES}
-  ${SLEPC_LIBRARIES}
+  ${PETSC_TARGET}
+  ${SLEPC_TARGET}
+  ${SLURM_TARGET}
   ${HIGHFIVE_TARGET}
 )
 
+target_include_directories(PugsUtils PUBLIC ${PETSC_INCLUDE_DIRS})
+target_include_directories(PugsUtils PUBLIC ${SLURM_INCLUDE_DIRS})
+
 # --------------- get git revision info ---------------
 
 # Generates revision header file
@@ -58,12 +66,11 @@ set_source_files_properties(
 add_custom_command(TARGET PugsGitRevison
   COMMAND ${CMAKE_COMMAND} -E remove -f
   ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision
-  DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision.hpp
+  POST_BUILD
   COMMENT ""
   )
 
 add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision.hpp
-  PRE_BUILD
   COMMAND ${CMAKE_COMMAND} -E copy_if_different
   ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision
   ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision.hpp
@@ -73,7 +80,6 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision.hpp
   )
 
 add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pugs_git_revision
-  PRE_BUILD
   COMMAND ${CMAKE_COMMAND} -DPUGS_VERSION=${PUGS_VERSION} -DPUGS_SOURCE_DIR=${PUGS_SOURCE_DIR} -P ${PUGS_SOURCE_DIR}/cmake/GetPugsGitRevision.cmake
   COMMENT "Check pugs git status"
   VERBATIM
diff --git a/src/utils/ConsoleManager.hpp b/src/utils/ConsoleManager.hpp
index a2afe46fbc0ad16e263d1f5a9b3f8a84d5202d94..693219e30285d1b0acae26d4b43d310882069784 100644
--- a/src/utils/ConsoleManager.hpp
+++ b/src/utils/ConsoleManager.hpp
@@ -1,7 +1,7 @@
 #ifndef CONSOLE_MANAGER_HPP
 #define CONSOLE_MANAGER_HPP
 
-#include <string>
+#include <ostream>
 
 class ConsoleManager
 {
diff --git a/src/utils/PTScotchPartitioner.cpp b/src/utils/PTScotchPartitioner.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..112ddfecdf161d8b924b6f9b962d16e75c22c507
--- /dev/null
+++ b/src/utils/PTScotchPartitioner.cpp
@@ -0,0 +1,112 @@
+#include <utils/PTScotchPartitioner.hpp>
+
+#include <utils/pugs_config.hpp>
+
+#ifdef PUGS_HAS_PTSCOTCH
+
+#include <utils/Exceptions.hpp>
+#include <utils/Messenger.hpp>
+
+#include <ptscotch.h>
+
+#include <iostream>
+#include <vector>
+
+Array<int>
+PTScotchPartitioner::partition(const CRSGraph& graph)
+{
+  std::cout << "Partitioning graph into " << rang::style::bold << parallel::size() << rang::style::reset
+            << " parts using " << rang::fgB::green << "PTScotch" << rang::fg::reset << '\n';
+
+  MPI_Group world_group;
+  MPI_Comm_group(parallel::Messenger::getInstance().comm(), &world_group);
+
+  MPI_Group mesh_group;
+  std::vector<int> group_ranks = [&]() {
+    Array<int> graph_node_owners = parallel::allGather(static_cast<int>(graph.numberOfNodes()));
+    std::vector<int> grp_ranks;
+    grp_ranks.reserve(graph_node_owners.size());
+    for (size_t i = 0; i < graph_node_owners.size(); ++i) {
+      if (graph_node_owners[i] > 0) {
+        grp_ranks.push_back(i);
+      }
+    }
+    return grp_ranks;
+  }();
+
+  MPI_Group_incl(world_group, group_ranks.size(), &(group_ranks[0]), &mesh_group);
+
+  MPI_Comm partitioning_comm;
+  MPI_Comm_create_group(parallel::Messenger::getInstance().comm(), mesh_group, 1, &partitioning_comm);
+
+  Array<int> partition;
+  if (graph.numberOfNodes() > 0) {
+    SCOTCH_Strat scotch_strategy;
+    SCOTCH_Dgraph scotch_grah;
+
+    SCOTCH_stratInit(&scotch_strategy);   // use default strategy
+    SCOTCH_dgraphInit(&scotch_grah, partitioning_comm);
+
+    const Array<const int>& entries   = graph.entries();
+    const Array<const int>& neighbors = graph.neighbors();
+
+    static_assert(std::is_same_v<int, SCOTCH_Num>);
+
+    SCOTCH_Num* entries_ptr   = const_cast<int*>(&(entries[0]));
+    SCOTCH_Num* neighbors_ptr = const_cast<int*>(&(neighbors[0]));
+
+    if (SCOTCH_dgraphBuild(&scotch_grah,
+                           0,                          // 0 for C-like arrays
+                           graph.numberOfNodes(),      //
+                           graph.numberOfNodes(),      //
+                           entries_ptr,                //
+                           nullptr,                    //
+                           nullptr,                    //
+                           nullptr,                    // optional local node label array
+                           graph.neighbors().size(),   //
+                           graph.neighbors().size(),   //
+                           neighbors_ptr,              //
+                           nullptr,                    //
+                           nullptr) != 0) {
+      //  LCOV_EXCL_START
+      throw UnexpectedError("PTScotch invalid graph");
+      //   LCOV_EXCL_STOP
+    }
+
+    partition = Array<int>(graph.numberOfNodes());
+
+    SCOTCH_Num* partition_ptr = static_cast<SCOTCH_Num*>(&(partition[0]));
+
+    if (SCOTCH_dgraphPart(&scotch_grah,       //
+                          parallel::size(),   //
+                          &scotch_strategy,   //
+                          partition_ptr) != 0) {
+      // LCOV_EXCL_START
+      throw UnexpectedError("PTScotch Error");
+      // LCOV_EXCL_STOP
+    }
+
+    SCOTCH_dgraphExit(&scotch_grah);
+    SCOTCH_stratExit(&scotch_strategy);
+  }
+
+  if (partitioning_comm != MPI_COMM_NULL){
+    MPI_Comm_free(&partitioning_comm);
+  }
+  MPI_Group_free(&mesh_group);
+  MPI_Group_free(&world_group);
+
+  return partition;
+}
+
+#else   // PUGS_HAS_PTSCOTCH
+
+Array<int>
+PTScotchPartitioner::partition(const CRSGraph& graph)
+{
+  Array<int> partition{graph.entries().size() - 1};
+  partition.fill(0);
+  return partition;
+}
+
+#endif   // PUGS_HAS_PTSCOTCH
diff --git a/src/utils/PTScotchPartitioner.hpp b/src/utils/PTScotchPartitioner.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..cefb0a1df2e1c39966d6c8ed0eaffe655a836df0
--- /dev/null
+++ b/src/utils/PTScotchPartitioner.hpp
@@ -0,0 +1,19 @@
+#ifndef PT_SCOTCH_PARTITIONER_HPP
+#define PT_SCOTCH_PARTITIONER_HPP
+
+#include <utils/Array.hpp>
+#include <utils/CRSGraph.hpp>
+
+class PTScotchPartitioner
+{
+ private:
+  friend class Partitioner;
+
+  // Forbids direct calls
+  static Array<int> partition(const CRSGraph& graph);
+
+ public:
+  PTScotchPartitioner() = delete;
+};
+
+#endif   // PT_SCOTCH_PARTITIONER_HPP
diff --git a/src/utils/ParMETISPartitioner.cpp b/src/utils/ParMETISPartitioner.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f1b17c3d070a71e0290c8c6434b0e13d2e442bad
--- /dev/null
+++ b/src/utils/ParMETISPartitioner.cpp
@@ -0,0 +1,94 @@
+#include <utils/ParMETISPartitioner.hpp>
+
+#include <utils/pugs_config.hpp>
+
+#ifdef PUGS_HAS_PARMETIS
+
+#include <utils/Exceptions.hpp>
+#include <utils/Messenger.hpp>
+
+#include <parmetis.h>
+
+#include <iostream>
+#include <vector>
+
+Array<int>
+ParMETISPartitioner::partition(const CRSGraph& graph)
+{
+  std::cout << "Partitioning graph into " << rang::style::bold << parallel::size() << rang::style::reset
+            << " parts using " << rang::fgB::green << "ParMETIS" << rang::fg::reset << '\n';
+
+  MPI_Group world_group;
+  MPI_Comm_group(parallel::Messenger::getInstance().comm(), &world_group);
+
+  MPI_Group mesh_group;
+  std::vector<int> group_ranks = [&]() {
+    Array<int> graph_node_owners = parallel::allGather(static_cast<int>(graph.numberOfNodes()));
+    std::vector<int> grp_ranks;
+    grp_ranks.reserve(graph_node_owners.size());
+    for (size_t i = 0; i < graph_node_owners.size(); ++i) {
+      if (graph_node_owners[i] > 0) {
+        grp_ranks.push_back(i);
+      }
+    }
+    return grp_ranks;
+  }();
+
+  MPI_Group_incl(world_group, group_ranks.size(), &(group_ranks[0]), &mesh_group);
+
+  MPI_Comm partitioning_comm;
+  MPI_Comm_create_group(parallel::Messenger::getInstance().comm(), mesh_group, 1, &partitioning_comm);
+
+  Array<int> partition;
+  if (graph.numberOfNodes() > 0) {
+    int wgtflag = 0;
+    int numflag = 0;
+    int ncon    = 1;
+    int npart   = parallel::size();
+    std::vector<float> tpwgts(npart, 1. / npart);
+
+    std::vector<float> ubvec{1.05};
+    std::vector<int> options{1, 0, 0};
+    int edgecut = 0;
+
+    int local_number_of_nodes = graph.numberOfNodes();
+
+    partition = Array<int>(local_number_of_nodes);
+    std::vector<int> vtxdist{0, local_number_of_nodes};
+
+    const Array<const int>& entries   = graph.entries();
+    const Array<const int>& neighbors = graph.neighbors();
+
+    int* entries_ptr   = const_cast<int*>(&(entries[0]));
+    int* neighbors_ptr = const_cast<int*>(&(neighbors[0]));
+
+    int result =
+      ParMETIS_V3_PartKway(&(vtxdist[0]), entries_ptr, neighbors_ptr, NULL, NULL, &wgtflag, &numflag, &ncon, &npart,
+                           &(tpwgts[0]), &(ubvec[0]), &(options[0]), &edgecut, &(partition[0]), &partitioning_comm);
+    // LCOV_EXCL_START
+    if (result == METIS_ERROR) {
+      throw UnexpectedError("Metis Error");
+    }
+    // LCOV_EXCL_STOP
+  }
+
+  if (partitioning_comm != MPI_COMM_NULL){
+    MPI_Comm_free(&partitioning_comm);
+  }
+  MPI_Group_free(&mesh_group);
+  MPI_Group_free(&world_group);
+
+  return partition;
+}
+
+#else   // PUGS_HAS_PARMETIS
+
+Array<int>
+ParMETISPartitioner::partition(const CRSGraph& graph)
+{
+  Array<int> partition{graph.entries().size() - 1};
+  partition.fill(0);
+  return partition;
+}
+
+#endif   // PUGS_HAS_PARMETIS
diff --git a/src/utils/ParMETISPartitioner.hpp b/src/utils/ParMETISPartitioner.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..53c1d2dc50e96c85a2c57d0abfb59735b471cc6f
--- /dev/null
+++ b/src/utils/ParMETISPartitioner.hpp
@@ -0,0 +1,19 @@
+#ifndef PARMETIS_PARTITIONER_HPP
+#define PARMETIS_PARTITIONER_HPP
+
+#include <utils/Array.hpp>
+#include <utils/CRSGraph.hpp>
+
+class ParMETISPartitioner
+{
+ private:
+  friend class Partitioner;
+
+  // Forbids direct calls
+  static Array<int> partition(const CRSGraph& graph);
+
+ public:
+  ParMETISPartitioner() = delete;
+};
+
+#endif   // PARMETIS_PARTITIONER_HPP
diff --git a/src/utils/Partitioner.cpp b/src/utils/Partitioner.cpp
index 85e13c22d80f449c211f4c37ee20f3107556d6ba..4f53d322f5e52c130122ccc3966270e7220d34d7 100644
--- a/src/utils/Partitioner.cpp
+++ b/src/utils/Partitioner.cpp
@@ -1,94 +1,22 @@
 #include <utils/Partitioner.hpp>
 
-#include <utils/Messenger.hpp>
-#include <utils/pugs_config.hpp>
+#include <utils/PTScotchPartitioner.hpp>
+#include <utils/ParMETISPartitioner.hpp>
 
-#ifdef PUGS_HAS_MPI
-
-#define IDXTYPEWIDTH 64
-#define REALTYPEWIDTH 64
-#include <parmetis.h>
-
-#include <iostream>
-#include <vector>
-
-#include <utils/Exceptions.hpp>
+Partitioner::Partitioner(const PartitionerOptions& options) : m_partitioner_options{options} {}
 
 Array<int>
 Partitioner::partition(const CRSGraph& graph)
 {
-  std::cout << "Partitioning graph into " << rang::style::bold << parallel::size() << rang::style::reset << " parts\n";
-
-  int wgtflag = 0;
-  int numflag = 0;
-  int ncon    = 1;
-  int npart   = parallel::size();
-  std::vector<float> tpwgts(npart, 1. / npart);
-
-  std::vector<float> ubvec{1.05};
-  std::vector<int> options{1, 0, 0};
-  int edgecut = 0;
-  Array<int> part(0);
-
-  MPI_Group world_group;
-
-  MPI_Comm_group(parallel::Messenger::getInstance().comm(), &world_group);
-
-  MPI_Group mesh_group;
-  std::vector<int> group_ranks = [&]() {
-    Array<int> graph_node_owners = parallel::allGather(static_cast<int>(graph.numberOfNodes()));
-    std::vector<int> grp_ranks;
-    grp_ranks.reserve(graph_node_owners.size());
-    for (size_t i = 0; i < graph_node_owners.size(); ++i) {
-      if (graph_node_owners[i] > 0) {
-        grp_ranks.push_back(i);
-      }
-    }
-    return grp_ranks;
-  }();
-
-  MPI_Group_incl(world_group, group_ranks.size(), &(group_ranks[0]), &mesh_group);
-
-  MPI_Comm parmetis_comm;
-  MPI_Comm_create_group(parallel::Messenger::getInstance().comm(), mesh_group, 1, &parmetis_comm);
-
-  int local_number_of_nodes = graph.numberOfNodes();
-
-  if (graph.numberOfNodes() > 0) {
-    part = Array<int>(local_number_of_nodes);
-    std::vector<int> vtxdist{0, local_number_of_nodes};
-
-    const Array<const int>& entries   = graph.entries();
-    const Array<const int>& neighbors = graph.neighbors();
-
-    int* entries_ptr   = const_cast<int*>(&(entries[0]));
-    int* neighbors_ptr = const_cast<int*>(&(neighbors[0]));
-
-    int result =
-      ParMETIS_V3_PartKway(&(vtxdist[0]), entries_ptr, neighbors_ptr, NULL, NULL, &wgtflag, &numflag, &ncon, &npart,
-                           &(tpwgts[0]), &(ubvec[0]), &(options[0]), &edgecut, &(part[0]), &parmetis_comm);
-    // LCOV_EXCL_START
-    if (result == METIS_ERROR) {
-      throw UnexpectedError("Metis Error");
-    }
-    // LCOV_EXCL_STOP
-
-    MPI_Comm_free(&parmetis_comm);
+  switch (m_partitioner_options.library()) {
+  case PartitionerLibrary::parmetis: {
+    return ParMETISPartitioner::partition(graph);
+  }
+  case PartitionerLibrary::ptscotch: {
+    return PTScotchPartitioner::partition(graph);
+  }
+  default: {
+    throw UnexpectedError("invalid partition library");
+  }
   }
-
-  MPI_Group_free(&mesh_group);
-
-  return part;
-}
-
-#else   // PUGS_HAS_MPI
-
-Array<int>
-Partitioner::partition(const CRSGraph& graph)
-{
-  Array<int> partition{graph.entries().size() - 1};
-  partition.fill(0);
-  return partition;
 }
-
-#endif   // PUGS_HAS_MPI
diff --git a/src/utils/Partitioner.hpp b/src/utils/Partitioner.hpp
index 2c720bfd87e7853e12cd0736a46c5eda246f085f..a923d2301635d43f346159aeff6de4331e7dceef 100644
--- a/src/utils/Partitioner.hpp
+++ b/src/utils/Partitioner.hpp
@@ -2,11 +2,15 @@
 #define PARTITIONER_HPP
 
 #include <utils/CRSGraph.hpp>
+#include <utils/PartitionerOptions.hpp>
 
 class Partitioner
 {
+ private:
+  PartitionerOptions m_partitioner_options;
+
  public:
-  Partitioner()                   = default;
+  Partitioner(const PartitionerOptions& options = PartitionerOptions::default_options);
   Partitioner(const Partitioner&) = default;
   ~Partitioner()                  = default;
 
diff --git a/src/utils/PartitionerOptions.cpp b/src/utils/PartitionerOptions.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..207d649803be16ac79795084517eb90feb97f283
--- /dev/null
+++ b/src/utils/PartitionerOptions.cpp
@@ -0,0 +1,10 @@
+#include <utils/PartitionerOptions.hpp>
+
+#include <rang.hpp>
+
+std::ostream&
+operator<<(std::ostream& os, const PartitionerOptions& options)
+{
+  os << "  library: " << rang::style::bold << name(options.library()) << rang::style::reset << '\n';
+  return os;
+}
diff --git a/src/utils/PartitionerOptions.hpp b/src/utils/PartitionerOptions.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e4157f7659cac94ef8b4c7cd02a54d289156d4a8
--- /dev/null
+++ b/src/utils/PartitionerOptions.hpp
@@ -0,0 +1,86 @@
+#ifndef PARTITIONER_OPTIONS_HPP
+#define PARTITIONER_OPTIONS_HPP
+
+#include <utils/Exceptions.hpp>
+#include <utils/pugs_config.hpp>
+
+#include <iostream>
+
+enum class PartitionerLibrary : int8_t
+{
+  PT__begin = 0,
+  //
+  parmetis = PT__begin,
+  ptscotch,
+  //
+  PT__end
+};
+
+inline std::string
+name(const PartitionerLibrary library)
+{
+  switch (library) {
+  case PartitionerLibrary::parmetis: {
+    return "ParMETIS";
+  }
+  case PartitionerLibrary::ptscotch: {
+    return "PTScotch";
+  }
+  case PartitionerLibrary::PT__end: {
+  }
+  }
+  throw UnexpectedError("Linear system library name is not defined!");
+}
+
+template <typename PartitionerEnumType>
+inline PartitionerEnumType
+getPartitionerEnumFromName(const std::string& enum_name)
+{
+  using BaseT = std::underlying_type_t<PartitionerEnumType>;
+  for (BaseT enum_value = static_cast<BaseT>(PartitionerEnumType::PT__begin);
+       enum_value < static_cast<BaseT>(PartitionerEnumType::PT__end); ++enum_value) {
+    if (name(PartitionerEnumType{enum_value}) == enum_name) {
+      return PartitionerEnumType{enum_value};
+    }
+  }
+  throw NormalError(std::string{"could not find '"} + enum_name + "' associate type!");
+}
+
+class PartitionerOptions
+{
+ private:
+  PartitionerLibrary m_library = []() {
+#if !defined(PUGS_HAS_PARMETIS) && defined(PUGS_HAS_PTSCOTCH)
+    return PartitionerLibrary::ptscotch;
+#else   // sets parmetis as default if no alternative is available
+    return PartitionerLibrary::parmetis;
+#endif
+  }();
+
+ public:
+  static PartitionerOptions default_options;
+
+  friend std::ostream& operator<<(std::ostream& os, const PartitionerOptions& options);
+
+  PartitionerLibrary&
+  library()
+  {
+    return m_library;
+  }
+
+  PartitionerLibrary
+  library() const
+  {
+    return m_library;
+  }
+
+  PartitionerOptions(const PartitionerOptions&) = default;
+  PartitionerOptions(PartitionerOptions&&)      = default;
+
+  PartitionerOptions()  = default;
+  ~PartitionerOptions() = default;
+};
+
+inline PartitionerOptions PartitionerOptions::default_options;
+
+#endif   // PARTITIONER_OPTIONS_HPP
diff --git a/src/utils/PluginsLoader.cpp b/src/utils/PluginsLoader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..35e24605e8a01b4d350aadb18bcacda313e9004c
--- /dev/null
+++ b/src/utils/PluginsLoader.cpp
@@ -0,0 +1,104 @@
+#include <utils/PluginsLoader.hpp>
+
+#include <utils/ConsoleManager.hpp>
+
+#include <dlfcn.h>
+
+#include <rang.hpp>
+
+#include <filesystem>
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+#include <vector>
+
+std::vector<std::string>
+split(const std::string& full_string)
+{
+  std::vector<std::string> split_string;
+
+  std::stringstream is(full_string);
+
+  std::string segment;
+  while (std::getline(is, segment, ';')) {
+    if (segment.size() > 0) {
+      split_string.push_back(segment);
+    }
+  }
+  return split_string;
+}
+
+void
+PluginsLoader::_open(const std::string& plugin)
+{
+  auto handle = dlopen(plugin.c_str(), RTLD_NOW);
+  if (handle != nullptr) {
+    m_dl_handler_stack.push(handle);
+    if (ConsoleManager::showPreamble()) {
+      std::cout << " * \"" << rang::fgB::green << plugin << rang::fg::reset << "\"\n";
+    }
+  } else {
+    std::cerr << "   " << rang::fgB::red << "cannot load " << rang::fg::reset << '\"' << rang::fgB::yellow << plugin
+              << rang::fg::reset << "\"\n";
+  }
+}
+
+PluginsLoader::PluginsLoader()
+{
+  std::vector<std::string> plugin_vector;
+
+  {
+    char* env = getenv("PUGS_PLUGIN");
+    if (env != nullptr) {
+      std::string plugins = env;
+      plugin_vector       = split(plugins);
+    }
+  }
+
+  {
+    char* env = getenv("PUGS_PLUGIN_DIR");
+    if (env != nullptr) {
+      std::string paths                    = env;
+      std::vector<std::string> path_vector = split(paths);
+      for (auto&& path : path_vector) {
+        if (access(path.c_str(), R_OK) == -1) {
+          std::cerr << ' ' << rang::fgB::red << 'X' << rang::fg::reset << " cannot access plugin dir \""
+                    << rang::fgB::yellow << path << rang::fg::reset << "\"\n";
+        } else {
+          for (auto&& entry :
+               std::filesystem::directory_iterator(path,
+                                                   (std::filesystem::directory_options::follow_directory_symlink |
+                                                    std::filesystem::directory_options::skip_permission_denied))) {
+            if (entry.path().extension() == ".so") {
+              plugin_vector.push_back(entry.path().string());
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // keep unique entries
+  std::sort(plugin_vector.begin(), plugin_vector.end());
+  plugin_vector.resize(std::distance(plugin_vector.begin(), std::unique(plugin_vector.begin(), plugin_vector.end())));
+
+  if (plugin_vector.size() > 0) {
+    if (ConsoleManager::showPreamble()) {
+      std::cout << rang::style::bold << "Loading plugins" << rang::style::reset << '\n';
+    }
+    for (auto&& plugin : plugin_vector) {
+      this->_open(plugin);
+    }
+    if (ConsoleManager::showPreamble()) {
+      std::cout << "-------------------------------------------------------\n";
+    }
+  }
+}
+
+PluginsLoader::~PluginsLoader()
+{
+  while (not m_dl_handler_stack.empty()) {
+    dlclose(m_dl_handler_stack.top());
+    m_dl_handler_stack.pop();
+  }
+}
diff --git a/src/utils/PluginsLoader.hpp b/src/utils/PluginsLoader.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..76be25c6934c5f336360e938d9a40d75f075e6d3
--- /dev/null
+++ b/src/utils/PluginsLoader.hpp
@@ -0,0 +1,23 @@
+#ifndef PLUGINS_LOADER_HPP
+#define PLUGINS_LOADER_HPP
+
+#include <stack>
+#include <string>
+
+class PluginsLoader
+{
+ private:
+  std::stack<void*> m_dl_handler_stack;
+
+  void _open(const std::string& filename);
+
+ public:
+  PluginsLoader();
+
+  PluginsLoader(const PluginsLoader&) = delete;
+  PluginsLoader(PluginsLoader&&)      = delete;
+
+  ~PluginsLoader();
+};
+
+#endif   // PLUGINS_LOADER_HPP
diff --git a/src/utils/PugsTraits.hpp b/src/utils/PugsTraits.hpp
index cb8f154e3b344ae34bb2b769b5a0419e37d44e4a..e0008a7a4a0ecf30998e8a739bbf91511d73e3d4 100644
--- a/src/utils/PugsTraits.hpp
+++ b/src/utils/PugsTraits.hpp
@@ -15,6 +15,12 @@ class TinyVector;
 template <size_t M, size_t N, typename T>
 class TinyMatrix;
 
+template <typename DataType>
+class SmallMatrix;
+
+template <typename DataType, typename IndexType>
+class CRSMatrix;
+
 template <typename DataType, ItemType item_type, typename ConnectivityPtr>
 class ItemValue;
 template <typename DataType, ItemType item_type, typename ConnectivityPtr>
@@ -132,6 +138,34 @@ inline constexpr bool is_tiny_matrix_v<const T> = is_tiny_matrix_v<std::remove_c
 template <typename T>
 inline constexpr bool is_tiny_matrix_v<T&> = is_tiny_matrix_v<std::remove_cvref_t<T>>;
 
+// Traits is_small_matrix
+
+template <typename T>
+inline constexpr bool is_small_matrix_v = false;
+
+template <typename DataType>
+inline constexpr bool is_small_matrix_v<SmallMatrix<DataType>> = true;
+
+template <typename T>
+inline constexpr bool is_small_matrix_v<const T> = is_small_matrix_v<std::remove_cvref_t<T>>;
+
+template <typename T>
+inline constexpr bool is_small_matrix_v<T&> = is_small_matrix_v<std::remove_cvref_t<T>>;
+
+// Traits is_crs_matrix
+
+template <typename T>
+inline constexpr bool is_crs_matrix_v = false;
+
+template <typename DataType, typename IndexType>
+inline constexpr bool is_crs_matrix_v<CRSMatrix<DataType, IndexType>> = true;
+
+template <typename T>
+inline constexpr bool is_crs_matrix_v<const T> = is_crs_matrix_v<std::remove_cvref_t<T>>;
+
+template <typename T>
+inline constexpr bool is_crs_matrix_v<T&> = is_crs_matrix_v<std::remove_cvref_t<T>>;
+
 // Trais is ItemValue
 
 template <typename T>
diff --git a/src/utils/PugsUtils.cpp b/src/utils/PugsUtils.cpp
index d33396b20af483afa968917f5fde80d1e9465ca4..9890894dcc3291d0f3136f27a88127ef22aecb9b 100644
--- a/src/utils/PugsUtils.cpp
+++ b/src/utils/PugsUtils.cpp
@@ -64,6 +64,7 @@ pugsBuildInfo()
   os << "MPI:      " << rang::style::bold << BuildInfo::mpiLibrary() << rang::style::reset << '\n';
   os << "PETSc:    " << rang::style::bold << BuildInfo::petscLibrary() << rang::style::reset << '\n';
   os << "SLEPc:    " << rang::style::bold << BuildInfo::slepcLibrary() << rang::style::reset << '\n';
+  os << "Eigen3:   " << rang::style::bold << BuildInfo::eigen3Library() << rang::style::reset << '\n';
   os << "HDF5:     " << rang::style::bold << BuildInfo::hdf5Library() << rang::style::reset << '\n';
   os << "SLURM:    " << rang::style::bold << BuildInfo::slurmLibrary() << rang::style::reset << '\n';
   os << "-------------------------------------------------------";
diff --git a/src/utils/Stop.cpp b/src/utils/Stop.cpp
index 593094cc311a8d1d72ed687d9a90d3af65f1c881..fb2755c7d4a52fa04b0cdce0096793a990994e49 100644
--- a/src/utils/Stop.cpp
+++ b/src/utils/Stop.cpp
@@ -9,6 +9,41 @@
 
 #ifdef PUGS_HAS_SLURM
 #include <slurm/slurm.h>
+
+// LCOV_EXCL_START
+class SlurmWrapper
+{
+ private:
+  int m_slurm_job_id = -1;
+
+ public:
+  bool
+  mustStop() const
+  {
+    if (m_slurm_job_id == -1) {
+      return false;
+
+    } else {
+      return slurm_get_rem_time(m_slurm_job_id) < 150;
+    }
+  }
+
+  SlurmWrapper()
+  {
+    char* env = getenv("SLURM_JOB_ID");
+    if (env != nullptr) {
+      slurm_init(nullptr);
+      m_slurm_job_id = std::atoi(env);
+    }
+  }
+
+  ~SlurmWrapper()
+  {
+    slurm_fini();
+  }
+};
+// LCOV_EXCL_STOP
+
 #endif   // PUGS_HAS_SLURM
 
 bool
@@ -31,16 +66,9 @@ stop()
 
 #ifdef PUGS_HAS_SLURM
     // LCOV_EXCL_START
-    char* env = getenv("SLURM_JOB_ID");
-    if (env != nullptr) {
-      slurm_init(nullptr);
-      int slurm_job_id = std::atoi(env);
-
-      if (slurm_get_rem_time(slurm_job_id) < 150) {
-        stop = true;
-      }
-
-      slurm_fini();
+    static SlurmWrapper slurm_wrapper;
+    if (slurm_wrapper.mustStop()) {
+      stop = true;
     }
     // LCOV_EXCL_STOP
 #endif   // PUGS_HAS_SLURM
diff --git a/src/utils/checkpointing/CMakeLists.txt b/src/utils/checkpointing/CMakeLists.txt
index b051975e8538df40a2b9b8a8d7f0486874cda938..06d34a349ab63a6e0de206908e50a82d418a857a 100644
--- a/src/utils/checkpointing/CMakeLists.txt
+++ b/src/utils/checkpointing/CMakeLists.txt
@@ -66,4 +66,8 @@ add_dependencies(PugsCheckpointing
 target_link_libraries(
   PugsCheckpointing
   ${HIGHFIVE_TARGET}
+#  ${HDF5_TARGET}
 )
+
+#target_include_directories(PugsCheckpointing PUBLIC ${HDF5_INCLUDE_DIRS})
+#target_include_directories(PugsAlgebra PUBLIC ${SLEPC_INCLUDE_DIRS})
diff --git a/src/utils/checkpointing/Checkpoint.cpp b/src/utils/checkpointing/Checkpoint.cpp
index 5976cb2fb5b3c5024bc56a1b77d384abd698e97c..ebef0dddd218887a7c48187909757eeb719a14a6 100644
--- a/src/utils/checkpointing/Checkpoint.cpp
+++ b/src/utils/checkpointing/Checkpoint.cpp
@@ -4,7 +4,6 @@
 
 #ifdef PUGS_HAS_HDF5
 
-#include <algebra/LinearSolverOptions.hpp>
 #include <dev/ParallelChecker.hpp>
 #include <language/ast/ASTExecutionStack.hpp>
 #include <language/utils/ASTCheckpointsInfo.hpp>
@@ -21,12 +20,13 @@
 #include <utils/HighFivePugsUtils.hpp>
 #include <utils/RandomEngine.hpp>
 #include <utils/checkpointing/DualMeshTypeHFType.hpp>
+#include <utils/checkpointing/EigenvalueSolverOptionsHFType.hpp>
 #include <utils/checkpointing/LinearSolverOptionsHFType.hpp>
 #include <utils/checkpointing/ParallelCheckerHFType.hpp>
+#include <utils/checkpointing/PartitionerOptionsHFType.hpp>
 #include <utils/checkpointing/ResumingManager.hpp>
 
 #include <iostream>
-#include <map>
 
 void
 checkpoint()
@@ -122,6 +122,22 @@ checkpoint()
       linear_solver_options_default_group.createAttribute("method", default_options.method());
       linear_solver_options_default_group.createAttribute("precond", default_options.precond());
     }
+    {
+      HighFive::Group eigenvalue_solver_options_default_group =
+        checkpoint.createGroup("singleton/eigenvalue_solver_options_default");
+
+      const EigenvalueSolverOptions& default_options = EigenvalueSolverOptions::default_options;
+
+      eigenvalue_solver_options_default_group.createAttribute("library", default_options.library());
+    }
+    {
+      HighFive::Group partitioner_options_default_group =
+        checkpoint.createGroup("singleton/partitioner_options_default");
+
+      const PartitionerOptions& default_options = PartitionerOptions::default_options;
+
+      partitioner_options_default_group.createAttribute("library", default_options.library());
+    }
     {
       const auto& primal_to_dual_connectivity_info_map =
         DualConnectivityManager::instance().primalToDualConnectivityInfoMap();
diff --git a/src/utils/checkpointing/EigenvalueSolverOptionsHFType.hpp b/src/utils/checkpointing/EigenvalueSolverOptionsHFType.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..32995c1f5475b04534ac42f344d9e5e9ccef1fa2
--- /dev/null
+++ b/src/utils/checkpointing/EigenvalueSolverOptionsHFType.hpp
@@ -0,0 +1,15 @@
+#ifndef EIGENVALUE_SOLVER_OPTIONS_HF_TYPE_HPP
+#define EIGENVALUE_SOLVER_OPTIONS_HF_TYPE_HPP
+
+#include <algebra/EigenvalueSolverOptions.hpp>
+#include <utils/HighFivePugsUtils.hpp>
+#include <utils/PugsMacros.hpp>
+
+HighFive::EnumType<ESLibrary> PUGS_INLINE
+create_enum_ESOptions_library_type()
+{
+  return {{"eigen3", ESLibrary::eigen3}, {"slepsc", ESLibrary::slepsc}};
+}
+HIGHFIVE_REGISTER_TYPE(ESLibrary, create_enum_ESOptions_library_type)
+
+#endif   // EIGENVALUE_SOLVER_OPTIONS_HF_TYPE_HPP
diff --git a/src/utils/checkpointing/IBoundaryConditionDescriptorHFType.hpp b/src/utils/checkpointing/IBoundaryConditionDescriptorHFType.hpp
index 210b521164075f275829865397ea1771a460c207..4a6e5c92cded1a614d43a17ace96f211a3f48e2d 100644
--- a/src/utils/checkpointing/IBoundaryConditionDescriptorHFType.hpp
+++ b/src/utils/checkpointing/IBoundaryConditionDescriptorHFType.hpp
@@ -10,6 +10,7 @@ create_enum_i_boundary_condition_descriptor_type()
 {
   return {{"axis", IBoundaryConditionDescriptor::Type::axis},
           {"dirichlet", IBoundaryConditionDescriptor::Type::dirichlet},
+          {"dirichlet_vector", IBoundaryConditionDescriptor::Type::dirichlet_vector},
           {"external", IBoundaryConditionDescriptor::Type::external},
           {"fixed", IBoundaryConditionDescriptor::Type::fixed},
           {"fourier", IBoundaryConditionDescriptor::Type::fourier},
diff --git a/src/utils/checkpointing/LinearSolverOptionsHFType.hpp b/src/utils/checkpointing/LinearSolverOptionsHFType.hpp
index 94f88fc60088c297479af36975d533d1c94bdd71..1c94d74a948a27f9bc8fc54915ef819171379152 100644
--- a/src/utils/checkpointing/LinearSolverOptionsHFType.hpp
+++ b/src/utils/checkpointing/LinearSolverOptionsHFType.hpp
@@ -8,7 +8,7 @@
 HighFive::EnumType<LSLibrary> PUGS_INLINE
 create_enum_LSOptions_library_type()
 {
-  return {{"builtin", LSLibrary::builtin}, {"petsc", LSLibrary::petsc}};
+  return {{"builtin", LSLibrary::builtin}, {"eigen3", LSLibrary::eigen3}, {"petsc", LSLibrary::petsc}};
 }
 HIGHFIVE_REGISTER_TYPE(LSLibrary, create_enum_LSOptions_library_type)
 
diff --git a/src/utils/checkpointing/PartitionerOptionsHFType.hpp b/src/utils/checkpointing/PartitionerOptionsHFType.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e022bb66119943081d22573ac657b46736613037
--- /dev/null
+++ b/src/utils/checkpointing/PartitionerOptionsHFType.hpp
@@ -0,0 +1,15 @@
+#ifndef PARTITIONER_OPTIONS_HF_TYPE_HPP
+#define PARTITIONER_OPTIONS_HF_TYPE_HPP
+
+#include <utils/HighFivePugsUtils.hpp>
+#include <utils/PartitionerOptions.hpp>
+#include <utils/PugsMacros.hpp>
+
+HighFive::EnumType<PartitionerLibrary> PUGS_INLINE
+create_enum_PTOptions_library_type()
+{
+  return {{"ParMETIS", PartitionerLibrary::parmetis}, {"PTScotch", PartitionerLibrary::ptscotch}};
+}
+HIGHFIVE_REGISTER_TYPE(PartitionerLibrary, create_enum_PTOptions_library_type)
+
+#endif   // PARTITIONER_OPTIONS_HF_TYPE_HPP
diff --git a/src/utils/checkpointing/ReadIBoundaryConditionDescriptor.cpp b/src/utils/checkpointing/ReadIBoundaryConditionDescriptor.cpp
index 55b43c9f04bf1b3b585df208bd7a3ea86a3d2850..1f405b4d8a72a8cd15e7799732a16fc4a71ac12b 100644
--- a/src/utils/checkpointing/ReadIBoundaryConditionDescriptor.cpp
+++ b/src/utils/checkpointing/ReadIBoundaryConditionDescriptor.cpp
@@ -5,6 +5,7 @@
 #include <language/utils/EmbeddedData.hpp>
 #include <scheme/AxisBoundaryConditionDescriptor.hpp>
 #include <scheme/DirichletBoundaryConditionDescriptor.hpp>
+#include <scheme/DirichletVectorBoundaryConditionDescriptor.hpp>
 #include <scheme/ExternalBoundaryConditionDescriptor.hpp>
 #include <scheme/FixedBoundaryConditionDescriptor.hpp>
 #include <scheme/FourierBoundaryConditionDescriptor.hpp>
@@ -48,6 +49,21 @@ readIBoundaryConditionDescriptor(const HighFive::Group& iboundaryconditiondecrip
       std::make_shared<const DirichletBoundaryConditionDescriptor>(name, i_boundary_descriptor,
                                                                    *ResumingData::instance().functionSymbolId(rhs_id));
     break;
+  }
+  case IBoundaryConditionDescriptor::Type::dirichlet_vector: {
+    const std::string name = iboundaryconditiondecriptor_group.getAttribute("name").read<std::string>();
+
+    const std::vector function_id_list =
+      iboundaryconditiondecriptor_group.getAttribute("function_id_list").read<std::vector<size_t>>();
+
+    std::vector<FunctionSymbolId> function_symbol_id_list;
+    for (auto function_id : function_id_list) {
+      function_symbol_id_list.push_back(*ResumingData::instance().functionSymbolId(function_id));
+    }
+
+    bc_descriptor = std::make_shared<const DirichletVectorBoundaryConditionDescriptor>(name, i_boundary_descriptor,
+                                                                                       function_symbol_id_list);
+    break;
   }
     // LCOV_EXCL_START
   case IBoundaryConditionDescriptor::Type::external: {
diff --git a/src/utils/checkpointing/Resume.cpp b/src/utils/checkpointing/Resume.cpp
index 9d58e1d9dec4050f218d3efad71e5194ccaa5111..f342a2466caf36567840a8fc21686a28c26b4d8f 100644
--- a/src/utils/checkpointing/Resume.cpp
+++ b/src/utils/checkpointing/Resume.cpp
@@ -4,6 +4,7 @@
 
 #ifdef PUGS_HAS_HDF5
 
+#include <algebra/EigenvalueSolverOptions.hpp>
 #include <algebra/LinearSolverOptions.hpp>
 #include <language/ast/ASTExecutionStack.hpp>
 #include <language/utils/ASTCheckpointsInfo.hpp>
@@ -14,8 +15,10 @@
 #include <utils/ExecutionStatManager.hpp>
 #include <utils/HighFivePugsUtils.hpp>
 #include <utils/RandomEngine.hpp>
+#include <utils/checkpointing/EigenvalueSolverOptionsHFType.hpp>
 #include <utils/checkpointing/LinearSolverOptionsHFType.hpp>
 #include <utils/checkpointing/ParallelCheckerHFType.hpp>
+#include <utils/checkpointing/PartitionerOptionsHFType.hpp>
 #include <utils/checkpointing/ResumingData.hpp>
 #include <utils/checkpointing/ResumingManager.hpp>
 
@@ -81,6 +84,21 @@ resume()
       default_options.method()  = linear_solver_options_default_group.getAttribute("method").read<LSMethod>();
       default_options.precond() = linear_solver_options_default_group.getAttribute("precond").read<LSPrecond>();
     }
+    {
+      HighFive::Group eigenvalue_solver_options_default_group =
+        checkpoint.getGroup("singleton/eigenvalue_solver_options_default");
+
+      EigenvalueSolverOptions& default_options = EigenvalueSolverOptions::default_options;
+
+      default_options.library() = eigenvalue_solver_options_default_group.getAttribute("library").read<ESLibrary>();
+    }
+    {
+      HighFive::Group partitioner_options_default_group = checkpoint.getGroup("singleton/partitioner_options_default");
+
+      PartitionerOptions& default_options = PartitionerOptions::default_options;
+
+      default_options.library() = partitioner_options_default_group.getAttribute("library").read<PartitionerLibrary>();
+    }
 
     checkpointing::ResumingData::instance().readData(checkpoint, p_symbol_table);
 
diff --git a/src/utils/checkpointing/WriteIBoundaryConditionDescriptor.cpp b/src/utils/checkpointing/WriteIBoundaryConditionDescriptor.cpp
index 66a8670583ae63442313da42c847bbeee758a2d5..8a3c5dd062aca586bd800a19f9b0e27f4fc30e93 100644
--- a/src/utils/checkpointing/WriteIBoundaryConditionDescriptor.cpp
+++ b/src/utils/checkpointing/WriteIBoundaryConditionDescriptor.cpp
@@ -5,6 +5,7 @@
 #include <language/utils/DataHandler.hpp>
 #include <scheme/AxisBoundaryConditionDescriptor.hpp>
 #include <scheme/DirichletBoundaryConditionDescriptor.hpp>
+#include <scheme/DirichletVectorBoundaryConditionDescriptor.hpp>
 #include <scheme/FixedBoundaryConditionDescriptor.hpp>
 #include <scheme/FourierBoundaryConditionDescriptor.hpp>
 #include <scheme/FreeBoundaryConditionDescriptor.hpp>
@@ -47,6 +48,18 @@ writeIBoundaryConditionDescriptor(HighFive::Group& variable_group,
     variable_group.createAttribute("name", dirichlet_bc_descriptor.name());
     variable_group.createAttribute("rhs_function_id", dirichlet_bc_descriptor.rhsSymbolId().id());
     break;
+  }
+  case IBoundaryConditionDescriptor::Type::dirichlet_vector: {
+    const DirichletVectorBoundaryConditionDescriptor& dirichlet_vector_bc_descriptor =
+      dynamic_cast<const DirichletVectorBoundaryConditionDescriptor&>(iboundary_condition_descriptor);
+    variable_group.createAttribute("name", dirichlet_vector_bc_descriptor.name());
+    writeIBoundaryDescriptor(boundary_group, dirichlet_vector_bc_descriptor.boundaryDescriptor());
+    std::vector<size_t> function_id_list;
+    for (auto&& function_symbol_id : dirichlet_vector_bc_descriptor.rhsSymbolIdList()) {
+      function_id_list.push_back(function_symbol_id.id());
+    }
+    variable_group.createAttribute("function_id_list", function_id_list);
+    break;
   }
     // LCOV_EXCL_START
   case IBoundaryConditionDescriptor::Type::external: {
diff --git a/src/utils/pugs_config.hpp.in b/src/utils/pugs_config.hpp.in
index 7402a526d5f34505dbda8524226c971e4701d4b6..9560f7101d75033749dd89801cd560123cfbfb77 100644
--- a/src/utils/pugs_config.hpp.in
+++ b/src/utils/pugs_config.hpp.in
@@ -1,11 +1,14 @@
 #ifndef PUGS_CONFIG_HPP
 #define PUGS_CONFIG_HPP
 
+#cmakedefine PUGS_HAS_EIGEN3
 #cmakedefine PUGS_HAS_FENV_H
+#cmakedefine PUGS_HAS_HDF5
 #cmakedefine PUGS_HAS_MPI
+#cmakedefine PUGS_HAS_PARMETIS
 #cmakedefine PUGS_HAS_PETSC
+#cmakedefine PUGS_HAS_PTSCOTCH
 #cmakedefine PUGS_HAS_SLEPC
-#cmakedefine PUGS_HAS_HDF5
 #cmakedefine PUGS_HAS_SLURM
 
 #cmakedefine SYSTEM_IS_LINUX
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a276da0bc0eb0e3ce20cbd5b99161ebeedae1bbc..e4796b255e2197b682851febda673a8d55861907 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -4,6 +4,20 @@ include_directories(${PUGS_SOURCE_DIR}/src)
 include_directories(${PUGS_BINARY_DIR}/src)
 include_directories(${PUGS_SOURCE_DIR}/tests)
 
+install(
+  DIRECTORY "${PUGS_SOURCE_DIR}/packages/Catch2/src/catch2"
+  DESTINATION "include"
+  FILES_MATCHING
+  PATTERN "*.hpp"
+)
+
+install(
+  DIRECTORY "${PUGS_BINARY_DIR}/generated-includes/catch2"
+  DESTINATION "include"
+  FILES_MATCHING
+  PATTERN "*.hpp"
+)
+
 set(checkpointing_sequential_TESTS
   # this one should enventually integrate parallel tests
   test_checkpointing_Checkpoint_sequential.cpp
@@ -90,6 +104,7 @@ add_executable (unit_tests
   test_DualMeshManager.cpp
   test_DualMeshType.cpp
   test_EdgeIntegrator.cpp
+  test_Eigen3Utils.cpp
   test_EigenvalueSolver.cpp
   test_EmbeddedData.cpp
   test_EmbeddedDiscreteFunctionUtils.cpp
@@ -262,10 +277,11 @@ add_executable (mpi_unit_tests
   test_OFStream.cpp
   test_ParallelChecker_read.cpp
   test_Partitioner.cpp
-  test_PolynomialReconstruction.cpp
+  test_PolynomialReconstruction_degree_1.cpp
   test_PolynomialReconstructionDescriptor.cpp
   test_RandomEngine.cpp
-  test_StencilBuilder.cpp
+  test_StencilBuilder_cell2cell.cpp
+  test_StencilBuilder_node2cell.cpp
   test_SubItemArrayPerItemVariant.cpp
   test_SubItemValuePerItem.cpp
   test_SubItemValuePerItemVariant.cpp
diff --git a/tests/NbGhostLayersTester.hpp b/tests/NbGhostLayersTester.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0468fe18305367834c9cbd0959ba5b3f443fe870
--- /dev/null
+++ b/tests/NbGhostLayersTester.hpp
@@ -0,0 +1,27 @@
+#ifndef NB_GHOST_LAYERS_TESTER_HPP
+#define NB_GHOST_LAYERS_TESTER_HPP
+
+#include <cstddef>
+#include <utils/GlobalVariableManager.hpp>
+
+class NbGhostLayersTester
+{
+ private:
+  const size_t m_original_number_of_ghost_layers;
+
+ public:
+  PUGS_INLINE
+  NbGhostLayersTester(const size_t number_of_ghost_layers)
+    : m_original_number_of_ghost_layers{GlobalVariableManager::instance().getNumberOfGhostLayers()}
+  {
+    GlobalVariableManager::instance().m_number_of_ghost_layers = number_of_ghost_layers;
+  }
+
+  PUGS_INLINE
+  ~NbGhostLayersTester()
+  {
+    GlobalVariableManager::instance().m_number_of_ghost_layers = m_original_number_of_ghost_layers;
+  }
+};
+
+#endif   // NB_GHOST_LAYERS_TESTER_HPP
diff --git a/tests/test_ConnectivityDispatcher.cpp b/tests/test_ConnectivityDispatcher.cpp
index abe07cc21e21a1b34ae3f20dfe13ff7f52ac83b9..09ec9982e10e08d84c808f157fe4e11add3d756a 100644
--- a/tests/test_ConnectivityDispatcher.cpp
+++ b/tests/test_ConnectivityDispatcher.cpp
@@ -9,29 +9,12 @@
 #include <utils/Messenger.hpp>
 
 #include <MeshDataBaseForTests.hpp>
+#include <NbGhostLayersTester.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) {
diff --git a/tests/test_Eigen3Utils.cpp b/tests/test_Eigen3Utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0c51aad3c03ee90695f837c4a9cbff0566d566f3
--- /dev/null
+++ b/tests/test_Eigen3Utils.cpp
@@ -0,0 +1,107 @@
+#include <utils/pugs_config.hpp>
+
+#ifdef PUGS_HAS_EIGEN3
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <algebra/Eigen3Utils.hpp>
+
+#include <algebra/CRSMatrixDescriptor.hpp>
+
+#include <eigen3/Eigen/Core>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("Eigen3Utils", "[algebra]")
+{
+  SECTION("Eigen3DenseMatrixEmbedder")
+  {
+    SECTION("from TinyMatrix")
+    {
+      TinyMatrix<3> A{1, 2, 3, 4, 5, 6, 7, 8, 9};
+      Eigen3DenseMatrixEmbedder eigen3_A{A};
+
+      for (size_t i = 0; i < 3; ++i) {
+        for (size_t j = 0; j < 3; ++j) {
+          REQUIRE(eigen3_A.matrix().coeff(i, j) == 1 + i * 3 + j);
+        }
+      }
+    }
+
+    SECTION("from SmallMatrix")
+    {
+      SmallMatrix<double> A(3, 3);
+      for (size_t i = 0; i < 3; ++i) {
+        for (size_t j = 0; j < 3; ++j) {
+          A(i, j) = 1 + i * 3 + j;
+        }
+      }
+
+      Eigen3DenseMatrixEmbedder eigen3_A{A};
+
+      REQUIRE(eigen3_A.numberOfRows() == 3);
+      REQUIRE(eigen3_A.numberOfColumns() == 3);
+
+      for (size_t i = 0; i < 3; ++i) {
+        for (size_t j = 0; j < 3; ++j) {
+          REQUIRE(eigen3_A.matrix().coeff(i, j) == 1 + i * 3 + j);
+        }
+      }
+    }
+
+    SECTION("from SmallMatrix [non-square]")
+    {
+      SmallMatrix<double> A(4, 3);
+      for (size_t i = 0; i < 4; ++i) {
+        for (size_t j = 0; j < 3; ++j) {
+          A(i, j) = 1 + i * 3 + j;
+        }
+      }
+
+      Eigen3DenseMatrixEmbedder eigen3_A{A};
+
+      REQUIRE(eigen3_A.numberOfRows() == 4);
+      REQUIRE(eigen3_A.numberOfColumns() == 3);
+
+      for (size_t i = 0; i < 4; ++i) {
+        for (size_t j = 0; j < 3; ++j) {
+          REQUIRE(eigen3_A.matrix().coeff(i, j) == 1 + i * 3 + j);
+        }
+      }
+    }
+  }
+
+  SECTION("from CRSMatrix")
+  {
+    Array<int> non_zeros(4);
+    non_zeros[0] = 1;
+    non_zeros[1] = 2;
+    non_zeros[2] = 3;
+    non_zeros[3] = 2;
+
+    CRSMatrixDescriptor<double, int> A(4, 3, non_zeros);
+
+    A(0, 0) = 1;
+    A(1, 0) = 2;
+    A(1, 2) = 3;
+    A(2, 0) = 4;
+    A(2, 1) = 5;
+    A(2, 2) = 6;
+    A(3, 1) = 7;
+    A(3, 2) = 8;
+
+    Eigen3SparseMatrixEmbedder eigen3_A{A.getCRSMatrix()};
+
+    REQUIRE(eigen3_A.matrix().coeff(0, 0) == 1);
+    REQUIRE(eigen3_A.matrix().coeff(1, 0) == 2);
+    REQUIRE(eigen3_A.matrix().coeff(1, 2) == 3);
+    REQUIRE(eigen3_A.matrix().coeff(2, 0) == 4);
+    REQUIRE(eigen3_A.matrix().coeff(2, 1) == 5);
+    REQUIRE(eigen3_A.matrix().coeff(2, 2) == 6);
+    REQUIRE(eigen3_A.matrix().coeff(3, 1) == 7);
+    REQUIRE(eigen3_A.matrix().coeff(3, 2) == 8);
+  }
+}
+
+#endif   // PUGS_HAS_EIGEN3
diff --git a/tests/test_EigenvalueSolver.cpp b/tests/test_EigenvalueSolver.cpp
index fb7bf07907bf6c9e1db9333c9713365350918b06..325f3e39b0a6f34e028f99372956f67a226e39cc 100644
--- a/tests/test_EigenvalueSolver.cpp
+++ b/tests/test_EigenvalueSolver.cpp
@@ -12,109 +12,625 @@
 
 TEST_CASE("EigenvalueSolver", "[algebra]")
 {
-  SECTION("Sparse Matrices")
+  SECTION("symmetric system")
   {
-    SECTION("symmetric system")
+    SECTION("SLEPc")
     {
-      Array<int> non_zeros(3);
-      non_zeros.fill(3);
-      CRSMatrixDescriptor<double> S{3, 3, non_zeros};
+      EigenvalueSolverOptions options;
+      options.library() = ESLibrary::slepsc;
 
-      S(0, 0) = 3;
-      S(0, 1) = 2;
-      S(0, 2) = 4;
+      SECTION("Sparse Matrices")
+      {
+        Array<int> non_zeros(3);
+        non_zeros.fill(3);
+        CRSMatrixDescriptor<double> S{3, 3, non_zeros};
 
-      S(1, 0) = 2;
-      S(1, 1) = 0;
-      S(1, 2) = 2;
+        S(0, 0) = 3;
+        S(0, 1) = 2;
+        S(0, 2) = 4;
 
-      S(2, 0) = 4;
-      S(2, 1) = 2;
-      S(2, 2) = 3;
+        S(1, 0) = 2;
+        S(1, 1) = 0;
+        S(1, 2) = 2;
 
-      CRSMatrix A{S.getCRSMatrix()};
+        S(2, 0) = 4;
+        S(2, 1) = 2;
+        S(2, 2) = 3;
 
-      SECTION("eigenvalues")
-      {
-        SmallArray<double> eigenvalues;
+        CRSMatrix A{S.getCRSMatrix()};
+
+        SECTION("eigenvalues")
+        {
+          SmallArray<double> eigenvalues;
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues);
+          REQUIRE(eigenvalues[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[2] == Catch::Approx(8));
+#else    //  PUGS_HAS_SLEPC
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_SLEPC
+        }
+
+        SECTION("eigenvalues and eigenvectors")
+        {
+          SmallArray<double> eigenvalue_list;
+          std::vector<SmallVector<double>> eigenvector_list;
 
 #ifdef PUGS_HAS_SLEPC
-        EigenvalueSolver{}.computeForSymmetricMatrix(A, eigenvalues);
-        REQUIRE(eigenvalues[0] == Catch::Approx(-1));
-        REQUIRE(eigenvalues[1] == Catch::Approx(-1));
-        REQUIRE(eigenvalues[2] == Catch::Approx(8));
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> P{3};
+          SmallMatrix<double> PT{3};
+          SmallMatrix<double> D{3};
+          D = zero;
+
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              P(i, j)  = eigenvector_list[j][i];
+              PT(i, j) = eigenvector_list[i][j];
+            }
+            D(i, i) = eigenvalue_list[i];
+          }
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - S(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
 #else    //  PUGS_HAS_SLEPC
-        REQUIRE_THROWS_WITH(EigenvalueSolver{}.computeForSymmetricMatrix(A, eigenvalues),
-                            "not implemented yet: SLEPc is required to solve eigenvalue problems");
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
 #endif   // PUGS_HAS_SLEPC
+        }
+
+        SECTION("eigenvalues and transition matrix")
+        {
+          SmallArray<double> eigenvalue_list;
+          SmallMatrix<double> P{};
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> D{3};
+          D = zero;
+          for (size_t i = 0; i < 3; ++i) {
+            D(i, i) = eigenvalue_list[i];
+          }
+          SmallMatrix PT = transpose(P);
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - S(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_SLEPC
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_SLEPC
+        }
       }
 
-      SECTION("eigenvalues and eigenvectors")
+      SECTION("SmallMatrix")
       {
-        SmallArray<double> eigenvalue_list;
-        std::vector<SmallVector<double>> eigenvector_list;
+        SmallMatrix<double> A{3, 3};
+        A.fill(0);
+        A(0, 0) = 3;
+        A(0, 1) = 2;
+        A(0, 2) = 4;
+
+        A(1, 0) = 2;
+        A(1, 1) = 0;
+        A(1, 2) = 2;
+
+        A(2, 0) = 4;
+        A(2, 1) = 2;
+        A(2, 2) = 3;
+
+        SECTION("eigenvalues")
+        {
+          SmallArray<double> eigenvalues;
 
 #ifdef PUGS_HAS_SLEPC
-        EigenvalueSolver{}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues);
+          REQUIRE(eigenvalues[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[2] == Catch::Approx(8));
+#else    //  PUGS_HAS_SLEPC
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_SLEPC
+        }
 
-        REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
-        REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
-        REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+        SECTION("eigenvalues and eigenvectors")
+        {
+          SmallArray<double> eigenvalue_list;
+          std::vector<SmallVector<double>> eigenvector_list;
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
 
-        SmallMatrix<double> P{3};
-        SmallMatrix<double> PT{3};
-        SmallMatrix<double> D{3};
-        D = zero;
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
 
-        for (size_t i = 0; i < 3; ++i) {
-          for (size_t j = 0; j < 3; ++j) {
-            P(i, j)  = eigenvector_list[j][i];
-            PT(i, j) = eigenvector_list[i][j];
+          SmallMatrix<double> P{3};
+          SmallMatrix<double> PT{3};
+          SmallMatrix<double> D{3};
+          D = zero;
+
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              P(i, j)  = eigenvector_list[j][i];
+              PT(i, j) = eigenvector_list[i][j];
+            }
+            D(i, i) = eigenvalue_list[i];
           }
-          D(i, i) = eigenvalue_list[i];
-        }
 
-        SmallMatrix PDPT = P * D * PT;
-        for (size_t i = 0; i < 3; ++i) {
-          for (size_t j = 0; j < 3; ++j) {
-            REQUIRE(PDPT(i, j) - S(i, j) == Catch::Approx(0).margin(1E-13));
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
           }
+#else    //  PUGS_HAS_SLEPC
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_SLEPC
         }
+
+        SECTION("eigenvalues and transition matrix")
+        {
+          SmallArray<double> eigenvalue_list;
+          SmallMatrix<double> P{};
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> D{3};
+          D = zero;
+          for (size_t i = 0; i < 3; ++i) {
+            D(i, i) = eigenvalue_list[i];
+          }
+          SmallMatrix PT = transpose(P);
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
 #else    //  PUGS_HAS_SLEPC
-        REQUIRE_THROWS_WITH(EigenvalueSolver{}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
-                            "not implemented yet: SLEPc is required to solve eigenvalue problems");
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
 #endif   // PUGS_HAS_SLEPC
+        }
       }
 
-      SECTION("eigenvalues and passage matrix")
+      SECTION("TinyMatrix")
       {
-        SmallArray<double> eigenvalue_list;
-        SmallMatrix<double> P{};
+        TinyMatrix<3> A = zero;
 
-#ifdef PUGS_HAS_SLEPC
-        EigenvalueSolver{}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+        A(0, 0) = 3;
+        A(0, 1) = 2;
+        A(0, 2) = 4;
 
-        REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
-        REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
-        REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+        A(1, 0) = 2;
+        A(1, 1) = 0;
+        A(1, 2) = 2;
 
-        SmallMatrix<double> D{3};
-        D = zero;
-        for (size_t i = 0; i < 3; ++i) {
-          D(i, i) = eigenvalue_list[i];
+        A(2, 0) = 4;
+        A(2, 1) = 2;
+        A(2, 2) = 3;
+
+        SECTION("eigenvalues")
+        {
+          SmallArray<double> eigenvalues;
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues);
+          REQUIRE(eigenvalues[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[2] == Catch::Approx(8));
+#else    //  PUGS_HAS_SLEPC
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_SLEPC
         }
-        SmallMatrix PT = transpose(P);
 
-        SmallMatrix PDPT = P * D * PT;
-        for (size_t i = 0; i < 3; ++i) {
-          for (size_t j = 0; j < 3; ++j) {
-            REQUIRE(PDPT(i, j) - S(i, j) == Catch::Approx(0).margin(1E-13));
+        SECTION("eigenvalues and eigenvectors")
+        {
+          SmallArray<double> eigenvalue_list;
+          std::vector<SmallVector<double>> eigenvector_list;
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> P{3};
+          SmallMatrix<double> PT{3};
+          SmallMatrix<double> D{3};
+          D = zero;
+
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              P(i, j)  = eigenvector_list[j][i];
+              PT(i, j) = eigenvector_list[i][j];
+            }
+            D(i, i) = eigenvalue_list[i];
+          }
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
           }
+#else    //  PUGS_HAS_SLEPC
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_SLEPC
         }
+
+        SECTION("eigenvalues and transition matrix")
+        {
+          SmallArray<double> eigenvalue_list;
+          SmallMatrix<double> P{};
+
+#ifdef PUGS_HAS_SLEPC
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> D{3};
+          D = zero;
+          for (size_t i = 0; i < 3; ++i) {
+            D(i, i) = eigenvalue_list[i];
+          }
+          SmallMatrix PT = transpose(P);
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
 #else    //  PUGS_HAS_SLEPC
-        REQUIRE_THROWS_WITH(EigenvalueSolver{}.computeForSymmetricMatrix(A, eigenvalue_list, P),
-                            "not implemented yet: SLEPc is required to solve eigenvalue problems");
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P),
+                              "error: SLEPSc is not linked to pugs. Cannot use it!");
 #endif   // PUGS_HAS_SLEPC
+        }
+      }
+    }
+
+    SECTION("Eigen3")
+    {
+      EigenvalueSolverOptions options;
+      options.library() = ESLibrary::eigen3;
+
+      SECTION("Sparse Matrices")
+      {
+        Array<int> non_zeros(3);
+        non_zeros.fill(3);
+        CRSMatrixDescriptor<double> S{3, 3, non_zeros};
+
+        S(0, 0) = 3;
+        S(0, 1) = 2;
+        S(0, 2) = 4;
+
+        S(1, 0) = 2;
+        S(1, 1) = 0;
+        S(1, 2) = 2;
+
+        S(2, 0) = 4;
+        S(2, 1) = 2;
+        S(2, 2) = 3;
+
+        CRSMatrix A{S.getCRSMatrix()};
+
+        SECTION("eigenvalues")
+        {
+          SmallArray<double> eigenvalues;
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues);
+          REQUIRE(eigenvalues[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[2] == Catch::Approx(8));
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("eigenvalues and eigenvectors")
+        {
+          SmallArray<double> eigenvalue_list;
+          std::vector<SmallVector<double>> eigenvector_list;
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> P{3};
+          SmallMatrix<double> PT{3};
+          SmallMatrix<double> D{3};
+          D = zero;
+
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              P(i, j)  = eigenvector_list[j][i];
+              PT(i, j) = eigenvector_list[i][j];
+            }
+            D(i, i) = eigenvalue_list[i];
+          }
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - S(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("eigenvalues and transition matrix")
+        {
+          SmallArray<double> eigenvalue_list;
+          SmallMatrix<double> P{};
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> D{3};
+          D = zero;
+          for (size_t i = 0; i < 3; ++i) {
+            D(i, i) = eigenvalue_list[i];
+          }
+          SmallMatrix PT = transpose(P);
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - S(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+      }
+
+      SECTION("SmallMatrix")
+      {
+        SmallMatrix<double> A{3, 3};
+        A.fill(0);
+        A(0, 0) = 3;
+        A(0, 1) = 2;
+        A(0, 2) = 4;
+
+        A(1, 0) = 2;
+        A(1, 1) = 0;
+        A(1, 2) = 2;
+
+        A(2, 0) = 4;
+        A(2, 1) = 2;
+        A(2, 2) = 3;
+
+        SECTION("eigenvalues")
+        {
+          SmallArray<double> eigenvalues;
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues);
+          REQUIRE(eigenvalues[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[2] == Catch::Approx(8));
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("eigenvalues and eigenvectors")
+        {
+          SmallArray<double> eigenvalue_list;
+          std::vector<SmallVector<double>> eigenvector_list;
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> P{3};
+          SmallMatrix<double> PT{3};
+          SmallMatrix<double> D{3};
+          D = zero;
+
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              P(i, j)  = eigenvector_list[j][i];
+              PT(i, j) = eigenvector_list[i][j];
+            }
+            D(i, i) = eigenvalue_list[i];
+          }
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("eigenvalues and transition matrix")
+        {
+          SmallArray<double> eigenvalue_list;
+          SmallMatrix<double> P{};
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> D{3};
+          D = zero;
+          for (size_t i = 0; i < 3; ++i) {
+            D(i, i) = eigenvalue_list[i];
+          }
+          SmallMatrix PT = transpose(P);
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+      }
+
+      SECTION("TinyMatrix")
+      {
+        TinyMatrix<3> A = zero;
+
+        A(0, 0) = 3;
+        A(0, 1) = 2;
+        A(0, 2) = 4;
+
+        A(1, 0) = 2;
+        A(1, 1) = 0;
+        A(1, 2) = 2;
+
+        A(2, 0) = 4;
+        A(2, 1) = 2;
+        A(2, 2) = 3;
+
+        SECTION("eigenvalues")
+        {
+          SmallArray<double> eigenvalues;
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues);
+          REQUIRE(eigenvalues[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalues[2] == Catch::Approx(8));
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalues),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("eigenvalues and eigenvectors")
+        {
+          SmallArray<double> eigenvalue_list;
+          std::vector<SmallVector<double>> eigenvector_list;
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> P{3};
+          SmallMatrix<double> PT{3};
+          SmallMatrix<double> D{3};
+          D = zero;
+
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              P(i, j)  = eigenvector_list[j][i];
+              PT(i, j) = eigenvector_list[i][j];
+            }
+            D(i, i) = eigenvalue_list[i];
+          }
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, eigenvector_list),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("eigenvalues and transition matrix")
+        {
+          SmallArray<double> eigenvalue_list;
+          SmallMatrix<double> P{};
+
+#ifdef PUGS_HAS_EIGEN3
+          EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P);
+
+          REQUIRE(eigenvalue_list[0] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[1] == Catch::Approx(-1));
+          REQUIRE(eigenvalue_list[2] == Catch::Approx(8));
+
+          SmallMatrix<double> D{3};
+          D = zero;
+          for (size_t i = 0; i < 3; ++i) {
+            D(i, i) = eigenvalue_list[i];
+          }
+          SmallMatrix PT = transpose(P);
+
+          SmallMatrix PDPT = P * D * PT;
+          for (size_t i = 0; i < 3; ++i) {
+            for (size_t j = 0; j < 3; ++j) {
+              REQUIRE(PDPT(i, j) - A(i, j) == Catch::Approx(0).margin(1E-13));
+            }
+          }
+#else    //  PUGS_HAS_EIGEN3
+          REQUIRE_THROWS_WITH(EigenvalueSolver{options}.computeForSymmetricMatrix(A, eigenvalue_list, P),
+                              "error: Eigen3 is not linked to pugs. Cannot use it!");
+#endif   // PUGS_HAS_EIGEN3
+        }
       }
     }
   }
diff --git a/tests/test_LinearSolver.cpp b/tests/test_LinearSolver.cpp
index 11195faab3dad2f94e82dba78b9afc368d0edcd0..76ef6aa2bb75c3ea94fcf392f59ddb74350dc50a 100644
--- a/tests/test_LinearSolver.cpp
+++ b/tests/test_LinearSolver.cpp
@@ -21,6 +21,12 @@ TEST_CASE("LinearSolver", "[algebra]")
 #else    // PUGS_HAS_PETSC
     REQUIRE(linear_solver.hasLibrary(LSLibrary::petsc) == false);
 #endif   // PUGS_HAS_PETSC
+
+#ifdef PUGS_HAS_EIGEN3
+    REQUIRE(linear_solver.hasLibrary(LSLibrary::eigen3) == true);
+#else    // PUGS_HAS_PETSC
+    REQUIRE(linear_solver.hasLibrary(LSLibrary::eigen3) == false);
+#endif   // PUGS_HAS_PETSC
   }
 
   SECTION("check linear solver building")
@@ -108,7 +114,7 @@ TEST_CASE("LinearSolver", "[algebra]")
         REQUIRE_NOTHROW(linear_solver.checkOptions(options));
       }
 
-      SECTION("builtin precond")
+      SECTION("PETSc precond")
       {
         options.library() = LSLibrary::petsc;
         options.method()  = LSMethod::cg;
@@ -140,6 +146,66 @@ TEST_CASE("LinearSolver", "[algebra]")
       REQUIRE_THROWS_WITH(LinearSolver{options}, "error: PETSc is not linked to pugs. Cannot use it!");
     }
 #endif   // PUGS_HAS_PETSC
+
+    SECTION("Eigen3")
+    {
+      LinearSolverOptions always_valid;
+      always_valid.library() = LSLibrary::builtin;
+      always_valid.method()  = LSMethod::cg;
+      always_valid.precond() = LSPrecond::none;
+
+      LinearSolver linear_solver{always_valid};
+
+      SECTION("Eigen3 methods")
+      {
+        options.library() = LSLibrary::eigen3;
+        options.precond() = LSPrecond::none;
+
+        options.method() = LSMethod::cg;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.method() = LSMethod::bicgstab;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.method() = LSMethod::lu;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.method() = LSMethod::cholesky;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.method() = LSMethod::gmres;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+      }
+
+      SECTION("Eigen3 precond")
+      {
+        options.library() = LSLibrary::eigen3;
+        options.method()  = LSMethod::cg;
+
+        options.precond() = LSPrecond::none;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.precond() = LSPrecond::diagonal;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.precond() = LSPrecond::incomplete_LU;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+
+        options.precond() = LSPrecond::incomplete_cholesky;
+        REQUIRE_NOTHROW(linear_solver.checkOptions(options));
+      }
+    }
+
+#ifndef PUGS_HAS_EIGEN3
+    SECTION("not linked Eigen3")
+    {
+      options.library() = LSLibrary::eigen3;
+      options.method()  = LSMethod::cg;
+      options.precond() = LSPrecond::none;
+
+      REQUIRE_THROWS_WITH(LinearSolver{options}, "error: Eigen3 is not linked to pugs. Cannot use it!");
+    }
+#endif   // PUGS_HAS_EIGEN3
   }
 
   SECTION("Sparse Matrices")
@@ -205,6 +271,101 @@ TEST_CASE("LinearSolver", "[algebra]")
           }
         }
 
+        SECTION("Eigen3")
+        {
+#ifdef PUGS_HAS_EIGEN3
+
+          SECTION("CG")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::cg;
+            options.precond() = LSPrecond::none;
+            options.verbose() = true;
+
+            SECTION("CG no preconditioner")
+            {
+              options.precond() = LSPrecond::none;
+
+              Vector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              Vector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+
+            SECTION("CG Diagonal")
+            {
+              options.precond() = LSPrecond::diagonal;
+
+              Vector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              Vector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+
+            SECTION("CG ICholesky")
+            {
+              options.precond() = LSPrecond::incomplete_cholesky;
+
+              Vector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              Vector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+
+            SECTION("CG AMG")
+            {
+              options.precond() = LSPrecond::amg;
+
+              Vector<double> x{5};
+              x = zero;
+
+              REQUIRE_THROWS_WITH(LinearSolver{options}, "error: AMG is not an Eigen3 preconditioner!");
+            }
+          }
+
+          SECTION("Cholesky")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::cholesky;
+            options.precond() = LSPrecond::none;
+
+            Vector<double> x{5};
+            x = zero;
+
+            LinearSolver solver{options};
+
+            solver.solveLocalSystem(A, x, b);
+            Vector error = x - x_exact;
+            REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+          }
+
+#else    // PUGS_HAS_EIGEN3
+          SECTION("Eigen3 not linked")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::cg;
+            options.precond() = LSPrecond::none;
+
+            REQUIRE_THROWS_WITH(LinearSolver{options}, "error: Eigen3 is not linked to pugs. Cannot use it!");
+          }
+#endif   // PUGS_HAS_EIGEN3
+        }
+
         SECTION("PETSc")
         {
 #ifdef PUGS_HAS_PETSC
@@ -535,170 +696,150 @@ TEST_CASE("LinearSolver", "[algebra]")
           }
 #endif   // PUGS_HAS_PETSC
         }
-      }
-    }
-  }
-
-  SECTION("Dense Matrices")
-  {
-    SECTION("check linear solvers")
-    {
-      SECTION("symmetric system")
-      {
-        SmallMatrix<double> A{5};
-        A = zero;
-
-        A(0, 0) = 2;
-        A(0, 1) = -1;
-
-        A(1, 0) = -1;
-        A(1, 1) = 2;
-        A(1, 2) = -1;
-
-        A(2, 1) = -1;
-        A(2, 2) = 2;
-        A(2, 3) = -1;
-
-        A(3, 2) = -1;
-        A(3, 3) = 2;
-        A(3, 4) = -1;
-
-        A(4, 3) = -1;
-        A(4, 4) = 2;
-
-        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;
-
-        SECTION("builtin")
-        {
-          SECTION("CG no preconditioner")
-          {
-            LinearSolverOptions options;
-            options.library() = LSLibrary::builtin;
-            options.method()  = LSMethod::cg;
-            options.precond() = LSPrecond::none;
-
-            SmallVector<double> x{5};
-            x = zero;
-
-            LinearSolver solver{options};
-
-            solver.solveLocalSystem(A, x, b);
-            SmallVector error = x - x_exact;
-            REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
-          }
-        }
 
-        SECTION("PETSc")
+        SECTION("Eigen3")
         {
-#ifdef PUGS_HAS_PETSC
+#ifdef PUGS_HAS_EIGEN3
 
-          SECTION("CG")
+          SECTION("BICGStab")
           {
             LinearSolverOptions options;
-            options.library() = LSLibrary::petsc;
-            options.method()  = LSMethod::cg;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::bicgstab;
             options.precond() = LSPrecond::none;
             options.verbose() = true;
 
-            SECTION("CG no preconditioner")
+            SECTION("BICGStab no preconditioner")
             {
               options.precond() = LSPrecond::none;
 
-              SmallVector<double> x{5};
+              Vector<double> x{5};
               x = zero;
 
               LinearSolver solver{options};
 
               solver.solveLocalSystem(A, x, b);
-              SmallVector error = x - x_exact;
+              Vector error = x - x_exact;
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
 
-            SECTION("CG Diagonal")
+            SECTION("BICGStab Diagonal")
             {
               options.precond() = LSPrecond::diagonal;
 
-              SmallVector<double> x{5};
+              Vector<double> x{5};
               x = zero;
 
               LinearSolver solver{options};
 
               solver.solveLocalSystem(A, x, b);
-              SmallVector error = x - x_exact;
+              Vector error = x - x_exact;
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
 
-            SECTION("CG ICholesky")
+            SECTION("BICGStab ILU")
             {
-              options.precond() = LSPrecond::incomplete_cholesky;
+              options.precond() = LSPrecond::incomplete_LU;
 
-              SmallVector<double> x{5};
+              Vector<double> x{5};
               x = zero;
 
               LinearSolver solver{options};
 
               solver.solveLocalSystem(A, x, b);
-              SmallVector error = x - x_exact;
+              Vector error = x - x_exact;
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
+          }
 
-            SECTION("CG AMG")
+          SECTION("GMRES")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::gmres;
+            options.precond() = LSPrecond::none;
+
+            SECTION("GMRES no preconditioner")
             {
-              options.precond() = LSPrecond::amg;
+              options.precond() = LSPrecond::none;
 
-              SmallVector<double> x{5};
+              Vector<double> x{5};
               x = zero;
 
               LinearSolver solver{options};
 
               solver.solveLocalSystem(A, x, b);
-              SmallVector error = x - x_exact;
+              Vector error = x - x_exact;
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
-          }
-
-          SECTION("Cholesky")
-          {
-            LinearSolverOptions options;
-            options.library() = LSLibrary::petsc;
-            options.method()  = LSMethod::cholesky;
-            options.precond() = LSPrecond::none;
 
-            SmallVector<double> x{5};
-            x = zero;
+            SECTION("GMRES Diagonal")
+            {
+              options.precond() = LSPrecond::diagonal;
+
+              Vector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              Vector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+
+            SECTION("GMRES ILU")
+            {
+              options.precond() = LSPrecond::incomplete_LU;
+
+              Vector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              Vector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+          }
+
+          SECTION("LU")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::lu;
+            options.precond() = LSPrecond::none;
+
+            Vector<double> x{5};
+            x = zero;
 
             LinearSolver solver{options};
 
             solver.solveLocalSystem(A, x, b);
-            SmallVector error = x - x_exact;
+            Vector error = x - x_exact;
             REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
           }
 
-#else    // PUGS_HAS_PETSC
-          SECTION("PETSc not linked")
+#else    // PUGS_HAS_EIGEN3
+          SECTION("Eigen3 not linked")
           {
             LinearSolverOptions options;
-            options.library() = LSLibrary::petsc;
+            options.library() = LSLibrary::eigen3;
             options.method()  = LSMethod::cg;
             options.precond() = LSPrecond::none;
 
-            REQUIRE_THROWS_WITH(LinearSolver{options}, "error: PETSc is not linked to pugs. Cannot use it!");
+            REQUIRE_THROWS_WITH(LinearSolver{options}, "error: Eigen3 is not linked to pugs. Cannot use it!");
           }
-#endif   // PUGS_HAS_PETSC
+#endif   // PUGS_HAS_EIGEN3
         }
       }
+    }
+  }
 
-      SECTION("none symmetric system")
+  SECTION("Dense Matrices")
+  {
+    SECTION("check linear solvers")
+    {
+      SECTION("symmetric system")
       {
         SmallMatrix<double> A{5};
         A = zero;
@@ -706,20 +847,20 @@ TEST_CASE("LinearSolver", "[algebra]")
         A(0, 0) = 2;
         A(0, 1) = -1;
 
-        A(1, 0) = -0.2;
+        A(1, 0) = -1;
         A(1, 1) = 2;
         A(1, 2) = -1;
 
         A(2, 1) = -1;
-        A(2, 2) = 4;
-        A(2, 3) = -2;
+        A(2, 2) = 2;
+        A(2, 3) = -1;
 
         A(3, 2) = -1;
         A(3, 3) = 2;
-        A(3, 4) = -0.1;
+        A(3, 4) = -1;
 
-        A(4, 3) = 1;
-        A(4, 4) = 3;
+        A(4, 3) = -1;
+        A(4, 4) = 2;
 
         SmallVector<const double> x_exact = [] {
           SmallVector<double> y{5};
@@ -735,11 +876,11 @@ TEST_CASE("LinearSolver", "[algebra]")
 
         SECTION("builtin")
         {
-          SECTION("BICGStab no preconditioner")
+          SECTION("CG no preconditioner")
           {
             LinearSolverOptions options;
             options.library() = LSLibrary::builtin;
-            options.method()  = LSMethod::bicgstab;
+            options.method()  = LSMethod::cg;
             options.precond() = LSPrecond::none;
 
             SmallVector<double> x{5};
@@ -753,19 +894,19 @@ TEST_CASE("LinearSolver", "[algebra]")
           }
         }
 
-        SECTION("PETSc")
+        SECTION("Eigen3")
         {
-#ifdef PUGS_HAS_PETSC
+#ifdef PUGS_HAS_EIGEN3
 
-          SECTION("BICGStab")
+          SECTION("CG")
           {
             LinearSolverOptions options;
-            options.library() = LSLibrary::petsc;
-            options.method()  = LSMethod::bicgstab;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::cg;
             options.precond() = LSPrecond::none;
             options.verbose() = true;
 
-            SECTION("BICGStab no preconditioner")
+            SECTION("CG no preconditioner")
             {
               options.precond() = LSPrecond::none;
 
@@ -779,7 +920,7 @@ TEST_CASE("LinearSolver", "[algebra]")
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
 
-            SECTION("BICGStab Diagonal")
+            SECTION("CG Diagonal")
             {
               options.precond() = LSPrecond::diagonal;
 
@@ -793,45 +934,75 @@ TEST_CASE("LinearSolver", "[algebra]")
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
 
-            SECTION("BICGStab ILU")
+            SECTION("CG ICholesky")
             {
-              options.precond() = LSPrecond::incomplete_LU;
+              options.precond() = LSPrecond::incomplete_cholesky;
 
               SmallVector<double> x{5};
               x = zero;
 
               LinearSolver solver{options};
 
-              solver.solveLocalSystem(A, x, b);
-              SmallVector error = x - x_exact;
-              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              REQUIRE_THROWS_WITH(solver.solveLocalSystem(A, x, b),
+                                  "error: incomplete cholesky is not available for dense matrices in Eigen3");
+            }
+
+            SECTION("CG AMG")
+            {
+              options.precond() = LSPrecond::amg;
+
+              SmallVector<double> x{5};
+              x = zero;
+
+              REQUIRE_THROWS_WITH(LinearSolver{options}, "error: AMG is not an Eigen3 preconditioner!");
             }
           }
 
-          SECTION("BICGStab2")
+          SECTION("Cholesky")
           {
             LinearSolverOptions options;
-            options.library() = LSLibrary::petsc;
-            options.method()  = LSMethod::bicgstab2;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::cholesky;
             options.precond() = LSPrecond::none;
 
-            SECTION("BICGStab2 no preconditioner")
-            {
-              options.precond() = LSPrecond::none;
+            SmallVector<double> x{5};
+            x = zero;
 
-              SmallVector<double> x{5};
-              x = zero;
+            LinearSolver solver{options};
 
-              LinearSolver solver{options};
+            solver.solveLocalSystem(A, x, b);
+            SmallVector error = x - x_exact;
+            REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+          }
 
-              solver.solveLocalSystem(A, x, b);
-              SmallVector error = x - x_exact;
-              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
-            }
+#else    // PUGS_HAS_EIGEN3
+          SECTION("Eigen3 not linked")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::eigen3;
+            options.method()  = LSMethod::cg;
+            options.precond() = LSPrecond::none;
 
-            SECTION("BICGStab2 Diagonal")
+            REQUIRE_THROWS_WITH(LinearSolver{options}, "error: Eigen3 is not linked to pugs. Cannot use it!");
+          }
+#endif   // PUGS_HAS_EIGEN3
+        }
+
+        SECTION("PETSc")
+        {
+#ifdef PUGS_HAS_PETSC
+
+          SECTION("CG")
+          {
+            LinearSolverOptions options;
+            options.library() = LSLibrary::petsc;
+            options.method()  = LSMethod::cg;
+            options.precond() = LSPrecond::none;
+            options.verbose() = true;
+
+            SECTION("CG no preconditioner")
             {
-              options.precond() = LSPrecond::diagonal;
+              options.precond() = LSPrecond::none;
 
               SmallVector<double> x{5};
               x = zero;
@@ -842,18 +1013,10 @@ TEST_CASE("LinearSolver", "[algebra]")
               SmallVector error = x - x_exact;
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
-          }
 
-          SECTION("GMRES")
-          {
-            LinearSolverOptions options;
-            options.library() = LSLibrary::petsc;
-            options.method()  = LSMethod::gmres;
-            options.precond() = LSPrecond::none;
-
-            SECTION("GMRES no preconditioner")
+            SECTION("CG Diagonal")
             {
-              options.precond() = LSPrecond::none;
+              options.precond() = LSPrecond::diagonal;
 
               SmallVector<double> x{5};
               x = zero;
@@ -865,9 +1028,9 @@ TEST_CASE("LinearSolver", "[algebra]")
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
 
-            SECTION("GMRES Diagonal")
+            SECTION("CG ICholesky")
             {
-              options.precond() = LSPrecond::diagonal;
+              options.precond() = LSPrecond::incomplete_cholesky;
 
               SmallVector<double> x{5};
               x = zero;
@@ -879,9 +1042,9 @@ TEST_CASE("LinearSolver", "[algebra]")
               REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
             }
 
-            SECTION("GMRES ILU")
+            SECTION("CG AMG")
             {
-              options.precond() = LSPrecond::incomplete_LU;
+              options.precond() = LSPrecond::amg;
 
               SmallVector<double> x{5};
               x = zero;
@@ -894,11 +1057,11 @@ TEST_CASE("LinearSolver", "[algebra]")
             }
           }
 
-          SECTION("LU")
+          SECTION("Cholesky")
           {
             LinearSolverOptions options;
             options.library() = LSLibrary::petsc;
-            options.method()  = LSMethod::lu;
+            options.method()  = LSMethod::cholesky;
             options.precond() = LSPrecond::none;
 
             SmallVector<double> x{5};
@@ -924,6 +1087,387 @@ TEST_CASE("LinearSolver", "[algebra]")
 #endif   // PUGS_HAS_PETSC
         }
       }
+
+      SECTION("none symmetric system")
+      {
+        SECTION("Dense matrix")
+        {
+          SmallMatrix<double> A{5};
+          A = zero;
+
+          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;
+
+          SECTION("builtin")
+          {
+            SECTION("BICGStab no preconditioner")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::builtin;
+              options.method()  = LSMethod::bicgstab;
+              options.precond() = LSPrecond::none;
+
+              SmallVector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              SmallVector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+          }
+
+          SECTION("Eigen3")
+          {
+#ifdef PUGS_HAS_EIGEN3
+
+            SECTION("BICGStab")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::eigen3;
+              options.method()  = LSMethod::bicgstab;
+              options.precond() = LSPrecond::none;
+              options.verbose() = true;
+
+              SECTION("BICGStab no preconditioner")
+              {
+                options.precond() = LSPrecond::none;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("BICGStab Diagonal")
+              {
+                options.precond() = LSPrecond::diagonal;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("BICGStab ILU")
+              {
+                options.precond() = LSPrecond::incomplete_LU;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                REQUIRE_THROWS_WITH(solver.solveLocalSystem(A, x, b),
+                                    "error: incomplete LU is not available for dense matrices in Eigen3");
+              }
+            }
+
+            SECTION("BICGStab2")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::eigen3;
+              options.method()  = LSMethod::bicgstab2;
+              options.precond() = LSPrecond::none;
+
+              SECTION("BICGStab2 no preconditioner")
+              {
+                options.precond() = LSPrecond::none;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                REQUIRE_THROWS_WITH(LinearSolver{options}, "error: BICGStab2 is not an Eigen3 linear solver!");
+              }
+            }
+
+            SECTION("GMRES")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::eigen3;
+              options.method()  = LSMethod::gmres;
+              options.precond() = LSPrecond::none;
+
+              SECTION("GMRES no preconditioner")
+              {
+                options.precond() = LSPrecond::none;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("GMRES Diagonal")
+              {
+                options.precond() = LSPrecond::diagonal;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("GMRES ILU")
+              {
+                options.precond() = LSPrecond::incomplete_LU;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                REQUIRE_THROWS_WITH(solver.solveLocalSystem(A, x, b),
+                                    "error: incomplete LU is not available for dense matrices in Eigen3");
+              }
+            }
+
+            SECTION("LU")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::eigen3;
+              options.method()  = LSMethod::lu;
+              options.precond() = LSPrecond::none;
+
+              SmallVector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              SmallVector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+
+#else    // PUGS_HAS_EIGEN3
+            SECTION("Eigen3 not linked")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::eigen3;
+              options.method()  = LSMethod::cg;
+              options.precond() = LSPrecond::none;
+
+              REQUIRE_THROWS_WITH(LinearSolver{options}, "error: Eigen3 is not linked to pugs. Cannot use it!");
+            }
+#endif   // PUGS_HAS_EIGEN3
+          }
+
+          SECTION("PETSc")
+          {
+#ifdef PUGS_HAS_PETSC
+
+            SECTION("BICGStab")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::petsc;
+              options.method()  = LSMethod::bicgstab;
+              options.precond() = LSPrecond::none;
+              options.verbose() = true;
+
+              SECTION("BICGStab no preconditioner")
+              {
+                options.precond() = LSPrecond::none;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("BICGStab Diagonal")
+              {
+                options.precond() = LSPrecond::diagonal;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("BICGStab ILU")
+              {
+                options.precond() = LSPrecond::incomplete_LU;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+            }
+
+            SECTION("BICGStab2")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::petsc;
+              options.method()  = LSMethod::bicgstab2;
+              options.precond() = LSPrecond::none;
+
+              SECTION("BICGStab2 no preconditioner")
+              {
+                options.precond() = LSPrecond::none;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("BICGStab2 Diagonal")
+              {
+                options.precond() = LSPrecond::diagonal;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+            }
+
+            SECTION("GMRES")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::petsc;
+              options.method()  = LSMethod::gmres;
+              options.precond() = LSPrecond::none;
+
+              SECTION("GMRES no preconditioner")
+              {
+                options.precond() = LSPrecond::none;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("GMRES Diagonal")
+              {
+                options.precond() = LSPrecond::diagonal;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+
+              SECTION("GMRES ILU")
+              {
+                options.precond() = LSPrecond::incomplete_LU;
+
+                SmallVector<double> x{5};
+                x = zero;
+
+                LinearSolver solver{options};
+
+                solver.solveLocalSystem(A, x, b);
+                SmallVector error = x - x_exact;
+                REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+              }
+            }
+
+            SECTION("LU")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::petsc;
+              options.method()  = LSMethod::lu;
+              options.precond() = LSPrecond::none;
+
+              SmallVector<double> x{5};
+              x = zero;
+
+              LinearSolver solver{options};
+
+              solver.solveLocalSystem(A, x, b);
+              SmallVector error = x - x_exact;
+              REQUIRE(std::sqrt(dot(error, error)) < 1E-10 * std::sqrt(dot(x_exact, x_exact)));
+            }
+
+#else    // PUGS_HAS_PETSC
+            SECTION("PETSc not linked")
+            {
+              LinearSolverOptions options;
+              options.library() = LSLibrary::petsc;
+              options.method()  = LSMethod::cg;
+              options.precond() = LSPrecond::none;
+
+              REQUIRE_THROWS_WITH(LinearSolver{options}, "error: PETSc is not linked to pugs. Cannot use it!");
+            }
+#endif   // PUGS_HAS_PETSC
+          }
+        }
+      }
     }
   }
 }
diff --git a/tests/test_LinearSolverOptions.cpp b/tests/test_LinearSolverOptions.cpp
index 90519ddf9797f0d6fa745cbc34e2f80443eed33d..7d078a7fb73e094ca09f888ab43ed12980081f2f 100644
--- a/tests/test_LinearSolverOptions.cpp
+++ b/tests/test_LinearSolverOptions.cpp
@@ -68,6 +68,7 @@ TEST_CASE("LinearSolverOptions", "[algebra]")
   SECTION("library name")
   {
     REQUIRE(name(LSLibrary::builtin) == "builtin");
+    REQUIRE(name(LSLibrary::eigen3) == "Eigen3");
     REQUIRE(name(LSLibrary::petsc) == "PETSc");
     REQUIRE_THROWS_WITH(name(LSLibrary::LS__end), "unexpected error: Linear system library name is not defined!");
   }
@@ -135,6 +136,7 @@ TEST_CASE("LinearSolverOptions", "[algebra]")
 
     const std::string library_list = R"(
   - builtin
+  - Eigen3
   - PETSc
 )";
 
diff --git a/tests/test_PolynomialReconstruction.cpp b/tests/test_PolynomialReconstruction.cpp
deleted file mode 100644
index cdfb532f2cd6a30327f09c5f0b1b8e54e6917a29..0000000000000000000000000000000000000000
--- a/tests/test_PolynomialReconstruction.cpp
+++ /dev/null
@@ -1,983 +0,0 @@
-#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_PolynomialReconstruction_degree_1.cpp b/tests/test_PolynomialReconstruction_degree_1.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7c4f07266514eaf8b277af5f26751c2ec17bc8ae
--- /dev/null
+++ b/tests/test_PolynomialReconstruction_degree_1.cpp
@@ -0,0 +1,1912 @@
+#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/GaussQuadratureDescriptor.hpp>
+#include <analysis/QuadratureFormula.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <scheme/DiscreteFunctionDPkVariant.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionVariant.hpp>
+#include <scheme/PolynomialReconstruction.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("PolynomialReconstruction_degree_1", "[scheme]")
+{
+  SECTION("without symmetries")
+  {
+    std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+      {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, 1},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::element, 1},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, 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();
+                auto xr       = mesh.xr();
+
+                auto cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
+
+                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>>();
+
+                auto qf = QuadratureManager::instance().getLineFormula(GaussQuadratureDescriptor{2});
+                {
+                  double max_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    auto cell_nodes = cell_to_node_matrix[cell_id];
+                    LineTransformation<1> T{xr[cell_nodes[0]], xr[cell_nodes[1]]};
+                    for (size_t i_quadrarture = 0; i_quadrarture < qf.numberOfPoints(); ++i_quadrarture) {
+                      max_error = std::max(max_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+                    }
+                  }
+
+                  REQUIRE(parallel::allReduceMax(max_error) == 0   // Catch::Approx(0).margin(0 * 1E-14)
+                  );
+                }
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    const double reconstructed_slope =
+                      (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1})) / 0.2;
+
+                    max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+
+          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("R3 vector data")
+          {
+            using R3 = TinyVector<3>;
+
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto vector_affine0 = [](const R1& x) -> R3 {
+                  return R3{+2.3 + 1.7 * x[0], -1.7 + 2.1 * x[0], +1.4 - 0.6 * x[0]};
+                };
+
+                auto vector_affine1 = [](const R1& x) -> R3 {
+                  return R3{+1.6 + 0.7 * x[0], -2.1 + 1.2 * x[0], +1.1 - 0.3 * x[0]};
+                };
+
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0Vector<R3> Vh{p_mesh, 2};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                    Vh[cell_id][0] = vector_affine0(xj[cell_id]);
+                    Vh[cell_id][1] = vector_affine1(xj[cell_id]);
+                  });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R3>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 0)(xj[cell_id]) - vector_affine0(xj[cell_id])));
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 1)(xj[cell_id]) - vector_affine1(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  {
+                    const TinyVector<3> slope0{+1.7, +2.1, -0.6};
+
+                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                      for (size_t i = 0; i < R3::Dimension; ++i) {
+                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 0)(R1{0.1} + xj[cell_id])[i] -
+                                                                        dpk_Vh(cell_id, 0)(xj[cell_id] - R1{0.1})[i]);
+
+                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope0[i]));
+                      }
+                    }
+                  }
+
+                  {
+                    const TinyVector<3> slope1{+0.7, +1.2, -0.3};
+
+                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                      for (size_t i = 0; i < R3::Dimension; ++i) {
+                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 1)(R1{0.1} + xj[cell_id])[i] -
+                                                                        dpk_Vh(cell_id, 1)(xj[cell_id] - R1{0.1})[i]);
+
+                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope1[i]));
+                      }
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+
+          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");
+        }
+      }
+    }
+  }
+
+  SECTION("with symmetries")
+  {
+    SECTION("errors")
+    {
+      SECTION("1D")
+      {
+        auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 1,
+                                                      std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                        std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<2>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<3>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<2>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<3>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 1");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 1");
+      }
+
+      SECTION("2D")
+      {
+        auto p_mesh = MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 1,
+                                                      std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                        std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<1>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<3>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 3 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<1>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<3>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 2");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<3>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 3x3 using a mesh of dimension 2");
+      }
+
+      SECTION("3D")
+      {
+        auto p_mesh = MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+        PolynomialReconstructionDescriptor descriptor{IntegrationMethodType::element, 1,
+                                                      std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                                        std::make_shared<NamedBoundaryDescriptor>("XMIN")}};
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<1>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyVector<2>>{p_mesh}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyVector<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize vectors of dimension 2 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<1>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(DiscreteFunctionP0<TinyMatrix<2>>{p_mesh}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<1>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 1x1 using a mesh of dimension 3");
+
+        REQUIRE_THROWS_WITH(PolynomialReconstruction{descriptor}.build(
+                              DiscreteFunctionP0Vector<TinyMatrix<2>>{p_mesh, 1}),
+                            "error: cannot symmetrize matrices of dimensions 2x2 using a mesh of dimension 3");
+      }
+    }
+
+    std::vector<PolynomialReconstructionDescriptor> descriptor_list =
+      {PolynomialReconstructionDescriptor{IntegrationMethodType::cell_center, 1,
+                                          std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                            std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::element, 1,
+                                          std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                            std::make_shared<NamedBoundaryDescriptor>("XMIN")}},
+       PolynomialReconstructionDescriptor{IntegrationMethodType::boundary, 1,
+                                          std::vector<std::shared_ptr<const IBoundaryDescriptor>>{
+                                            std::make_shared<NamedBoundaryDescriptor>("XMIN")}}};
+
+    for (auto descriptor : descriptor_list) {
+      SECTION(name(descriptor.integrationMethodType()))
+      {
+        SECTION("1D")
+        {
+          using R1 = TinyVector<1>;
+
+          SECTION("R^1 data")
+          {
+            auto p_mesh = MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+            auto& mesh = *p_mesh;
+
+            auto R1_affine = [](const R1& x) { return R1{1.7 * (x[0] + 1)}; };
+            auto xj        = MeshDataManager::instance().getMeshData(mesh).xj();
+
+            DiscreteFunctionP0<R1> fh{p_mesh};
+
+            parallel_for(
+              mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R1_affine(xj[cell_id]); });
+
+            auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+            auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<1, const R1>>();
+
+            {
+              double max_mean_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                max_mean_error =
+                  std::max(max_mean_error, std::abs((dpk_fh[cell_id](xj[cell_id]) - R1_affine(xj[cell_id]))[0]));
+              }
+              REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+            }
+
+            {
+              double max_slope_error = 0;
+              for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                const double reconstructed_slope =
+                  (dpk_fh[cell_id](R1{0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R1{0.1}))[0] / 0.2;
+
+                max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - 1.7));
+              }
+              REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+            }
+          }
+
+          SECTION("R1 vector data")
+          {
+            for (auto named_mesh : MeshDataBaseForTests::get().all1DMeshes()) {
+              SECTION(named_mesh.name())
+              {
+                auto p_mesh = named_mesh.mesh()->get<Mesh<1>>();
+                auto& mesh  = *p_mesh;
+
+                auto vector_affine0 = [](const R1& x) -> R1 { return R1{+1.7 * (x[0] + 1)}; };
+
+                auto vector_affine1 = [](const R1& x) -> R1 { return R1{-0.3 * (x[0] + 1)}; };
+
+                auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+                DiscreteFunctionP0Vector<R1> Vh{p_mesh, 2};
+
+                parallel_for(
+                  mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+                    Vh[cell_id][0] = vector_affine0(xj[cell_id]);
+                    Vh[cell_id][1] = vector_affine1(xj[cell_id]);
+                  });
+
+                auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+                auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<1, const R1>>();
+
+                {
+                  double max_mean_error = 0;
+                  for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 0)(xj[cell_id]) - vector_affine0(xj[cell_id])));
+                    max_mean_error =
+                      std::max(max_mean_error, l2Norm(dpk_Vh(cell_id, 1)(xj[cell_id]) - vector_affine1(xj[cell_id])));
+                  }
+                  REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+                }
+
+                {
+                  double max_slope_error = 0;
+                  {
+                    const TinyVector<1> slope0{+1.7};
+
+                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                      for (size_t i = 0; i < R1::Dimension; ++i) {
+                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 0)(R1{0.1} + xj[cell_id])[i] -
+                                                                        dpk_Vh(cell_id, 0)(xj[cell_id] - R1{0.1})[i]);
+
+                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope0[i]));
+                      }
+                    }
+                  }
+
+                  {
+                    const TinyVector<1> slope1{-0.3};
+
+                    for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+                      for (size_t i = 0; i < R1::Dimension; ++i) {
+                        const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, 1)(R1{0.1} + xj[cell_id])[i] -
+                                                                        dpk_Vh(cell_id, 1)(xj[cell_id] - R1{0.1})[i]);
+
+                        max_slope_error = std::max(max_slope_error, std::abs(reconstructed_slope - slope1[i]));
+                      }
+                    }
+                  }
+                  REQUIRE(parallel::allReduceMax(max_slope_error) == Catch::Approx(0).margin(1E-14));
+                }
+              }
+            }
+          }
+        }
+
+        SECTION("2D")
+        {
+#warning not done
+          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>;
+#warning not done
+
+          // SECTION("R data")
+          // {
+          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+          //     SECTION(named_mesh.name())
+          //     {
+          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+          //       auto& mesh  = *p_mesh;
+
+          //       auto R_affine = [](const R3& x) { return 2.3 + 1.7 * x[0] - 1.3 * x[1] + 2.1 * x[2]; };
+          //       auto xj       = MeshDataManager::instance().getMeshData(mesh).xj();
+
+          //       DiscreteFunctionP0<double> fh{p_mesh};
+
+          //       parallel_for(
+          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { fh[cell_id] = R_affine(xj[cell_id]); });
+
+          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(fh);
+
+          //       auto dpk_fh = reconstructions[0]->get<DiscreteFunctionDPk<3, const double>>();
+
+          //       {
+          //         double max_mean_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           max_mean_error =
+          //             std::max(max_mean_error, std::abs(dpk_fh[cell_id](xj[cell_id]) - R_affine(xj[cell_id])));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+          //       }
+
+          //       {
+          //         double max_x_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const double reconstructed_slope =
+          //             (dpk_fh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0.1, 0, 0})) /
+          //             0.2;
+
+          //           max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - 1.7));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+          //       }
+
+          //       {
+          //         double max_y_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const double reconstructed_slope =
+          //             (dpk_fh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0.1, 0})) /
+          //             0.2;
+
+          //           max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - (-1.3)));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-13));
+          //       }
+
+          //       {
+          //         double max_z_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const double reconstructed_slope =
+          //             (dpk_fh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) - dpk_fh[cell_id](xj[cell_id] - R3{0, 0, 0.1})) /
+          //             0.2;
+
+          //           max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - 2.1));
+          //         }//         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+          //     }
+          //   }
+          // }
+
+          // SECTION("R^3 data")
+          // {
+          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+          //     SECTION(named_mesh.name())
+          //     {
+          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+          //       auto& mesh  = *p_mesh;
+
+          //       auto R3_affine = [](const R3& x) -> R3 {
+          //         return R3{+2.3 + 1.7 * x[0] - 2.2 * x[1] + 1.8 * x[2],   //
+          //                   +1.4 - 0.6 * x[0] + 1.3 * x[1] - 3.7 * x[2],   //
+          //                   -0.2 + 3.1 * x[0] - 1.1 * x[1] + 1.9 * x[2]};
+          //       };
+          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+          //       DiscreteFunctionP0<R3> uh{p_mesh};
+
+          //       parallel_for(
+          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { uh[cell_id] = R3_affine(xj[cell_id]); });
+
+          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(uh);
+
+          //       auto dpk_uh = reconstructions[0]->get<DiscreteFunctionDPk<3, const R3>>();
+
+          //       {
+          //         double max_mean_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           max_mean_error =
+          //             std::max(max_mean_error, l2Norm(dpk_uh[cell_id](xj[cell_id]) - R3_affine(xj[cell_id])));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+          //       }
+
+          //       {
+          //         double max_x_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
+          //                                                       dpk_uh[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
+
+          //           max_x_slope_error = std::max(max_x_slope_error, l2Norm(reconstructed_slope - R3{1.7,
+          //           -0.6, 3.1}));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+
+          //       {
+          //         double max_y_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
+          //                                                       dpk_uh[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
+
+          //           max_y_slope_error = std::max(max_y_slope_error, l2Norm(reconstructed_slope - R3{-2.2, 1.3,
+          //           -1.1}));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+
+          //       {
+          //         double max_z_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const R3 reconstructed_slope = (1 / 0.2) * (dpk_uh[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
+          //                                                       dpk_uh[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
+
+          //           max_z_slope_error = std::max(max_z_slope_error, l2Norm(reconstructed_slope - R3{1.8,
+          //           -3.7, 1.9}));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+          //     }
+          //   }
+          // }
+
+          // SECTION("R^2x2 data")
+          // {
+          //   using R2x2 = TinyMatrix<2, 2>;
+
+          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+          //     SECTION(named_mesh.name())
+          //     {
+          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+          //       auto& mesh  = *p_mesh;
+
+          //       auto R2x2_affine = [](const R3& x) -> R2x2 {
+          //         return R2x2{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 *
+          //         x[2],
+          //                     //
+          //                     +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 *
+          //                     x[2]};
+          //       };
+          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+          //       DiscreteFunctionP0<R2x2> Ah{p_mesh};
+
+          //       parallel_for(
+          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { Ah[cell_id] = R2x2_affine(xj[cell_id]);
+          //         });
+
+          //       descriptor.setRowWeighting(false);
+          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(Ah);
+
+          //       auto dpk_Ah = reconstructions[0]->get<DiscreteFunctionDPk<3, const R2x2>>();
+
+          //       {
+          //         double max_mean_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           max_mean_error =
+          //             std::max(max_mean_error, frobeniusNorm(dpk_Ah[cell_id](xj[cell_id]) -
+          //             R2x2_affine(xj[cell_id])));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+          //       }
+
+          //       {
+          //         double max_x_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0.1, 0, 0} + xj[cell_id]) -
+          //                                                         dpk_Ah[cell_id](xj[cell_id] - R3{0.1, 0, 0}));
+
+          //           max_x_slope_error =
+          //             std::max(max_x_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.7, 2.1,   //
+          //                                                                                  -2.3, +3.1}));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+          //       }
+
+          //       {
+          //         double max_y_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0.1, 0} + xj[cell_id]) -
+          //                                                         dpk_Ah[cell_id](xj[cell_id] - R3{0, 0.1, 0}));
+
+          //           max_y_slope_error =
+          //             std::max(max_y_slope_error, frobeniusNorm(reconstructed_slope - R2x2{+1.2, -2.2,   //
+          //                                                                                  1.3, +0.8}));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+
+          //       {
+          //         double max_z_slope_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           const R2x2 reconstructed_slope = (1 / 0.2) * (dpk_Ah[cell_id](R3{0, 0, 0.1} + xj[cell_id]) -
+          //                                                         dpk_Ah[cell_id](xj[cell_id] - R3{0, 0, 0.1}));
+
+          //           max_z_slope_error =
+          //             std::max(max_z_slope_error, frobeniusNorm(reconstructed_slope - R2x2{-1.3, -2.4,   //
+          //                                                                                  +1.4, -1.8}));
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+          //     }
+          //   }
+          // }
+
+          // SECTION("vector data")
+          // {
+          //   using R4 = TinyVector<4>;
+
+          //   for (auto named_mesh : MeshDataBaseForTests::get().all3DMeshes()) {
+          //     SECTION(named_mesh.name())
+          //     {
+          //       auto p_mesh = named_mesh.mesh()->get<Mesh<3>>();
+          //       auto& mesh  = *p_mesh;
+
+          //       auto vector_affine = [](const R3& x) -> R4 {
+          //         return R4{+2.3 + 1.7 * x[0] + 1.2 * x[1] - 1.3 * x[2], -1.7 + 2.1 * x[0] - 2.2 * x[1] - 2.4 * x[2],
+          //                   //
+          //                   +2.4 - 2.3 * x[0] + 1.3 * x[1] + 1.4 * x[2], -0.2 + 3.1 * x[0] + 0.8 * x[1] - 1.8 *
+          //                   x[2]};
+          //       };
+          //       auto xj = MeshDataManager::instance().getMeshData(mesh).xj();
+
+          //       DiscreteFunctionP0Vector<double> Vh{p_mesh, 4};
+
+          //       parallel_for(
+          //         mesh.numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+          //           auto vector = vector_affine(xj[cell_id]);
+          //           for (size_t i = 0; i < vector.dimension(); ++i) {
+          //             Vh[cell_id][i] = vector[i];
+          //           }
+          //         });
+
+          //       descriptor.setPreconditioning(false);
+          //       auto reconstructions = PolynomialReconstruction{descriptor}.build(Vh);
+
+          //       auto dpk_Vh = reconstructions[0]->get<DiscreteFunctionDPkVector<3, const double>>();
+
+          //       {
+          //         double max_mean_error = 0;
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           auto vector = vector_affine(xj[cell_id]);
+          //           for (size_t i = 0; i < vector.dimension(); ++i) {
+          //             max_mean_error = std::max(max_mean_error, std::abs(dpk_Vh(cell_id, i)(xj[cell_id]) -
+          //             vector[i]));
+          //           }
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_mean_error) == Catch::Approx(0).margin(1E-14));
+          //       }
+
+          //       {
+          //         double max_x_slope_error = 0;
+          //         const R4 slope{+1.7, 2.1, -2.3, +3.1};
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           for (size_t i = 0; i < slope.dimension(); ++i) {
+          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0.1, 0, 0} + xj[cell_id])
+          //             -
+          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R3{0.1, 0,
+          //                                                             0}));
+
+          //             max_x_slope_error = std::max(max_x_slope_error, std::abs(reconstructed_slope - slope[i]));
+          //           }
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_x_slope_error) == Catch::Approx(0).margin(1E-13));
+          //       }
+
+          //       {
+          //         double max_y_slope_error = 0;
+          //         const R4 slope{+1.2, -2.2, 1.3, +0.8};
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           for (size_t i = 0; i < slope.dimension(); ++i) {
+          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0.1, 0} + xj[cell_id])
+          //             -
+          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0.1,
+          //                                                             0}));
+
+          //             max_y_slope_error = std::max(max_y_slope_error, std::abs(reconstructed_slope - slope[i]));
+          //           }
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_y_slope_error) == Catch::Approx(0).margin(1E-12));
+          //       }
+
+          //       {
+          //         double max_z_slope_error = 0;
+          //         const R4 slope{-1.3, -2.4, +1.4, -1.8};
+          //         for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          //           for (size_t i = 0; i < slope.dimension(); ++i) {
+          //             const double reconstructed_slope = (1 / 0.2) * (dpk_Vh(cell_id, i)(R3{0, 0, 0.1} + xj[cell_id])
+          //             -
+          //                                                             dpk_Vh(cell_id, i)(xj[cell_id] - R3{0, 0,
+          //                                                             0.1}));
+
+          //             max_z_slope_error = std::max(max_z_slope_error, std::abs(reconstructed_slope - slope[i]));
+          //           }
+          //         }
+          //         REQUIRE(parallel::allReduceMax(max_z_slope_error) == Catch::Approx(0).margin(1E-13));
+          //       }
+          //     }
+          //   }
+          // }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_PugsUtils.cpp b/tests/test_PugsUtils.cpp
index fb86486527e3f1d91ce5c2f1e7d2653a1dea3bde..5ba8817fcaa34ddf31f0955ed9fd118f841bd87d 100644
--- a/tests/test_PugsUtils.cpp
+++ b/tests/test_PugsUtils.cpp
@@ -51,6 +51,7 @@ TEST_CASE("PugsUtils", "[utils]")
       os << "MPI:      " << rang::style::bold << BuildInfo::mpiLibrary() << rang::style::reset << '\n';
       os << "PETSc:    " << rang::style::bold << BuildInfo::petscLibrary() << rang::style::reset << '\n';
       os << "SLEPc:    " << rang::style::bold << BuildInfo::slepcLibrary() << rang::style::reset << '\n';
+      os << "Eigen3:   " << rang::style::bold << BuildInfo::eigen3Library() << rang::style::reset << '\n';
       os << "HDF5:     " << rang::style::bold << BuildInfo::hdf5Library() << rang::style::reset << '\n';
       os << "SLURM:    " << rang::style::bold << BuildInfo::slurmLibrary() << rang::style::reset << '\n';
       os << "-------------------------------------------------------";
diff --git a/tests/test_StencilBuilder.cpp b/tests/test_StencilBuilder.cpp
deleted file mode 100644
index b99ba218aa1e3eb5d8ec884e974cfad2f4a157db..0000000000000000000000000000000000000000
--- a/tests/test_StencilBuilder.cpp
+++ /dev/null
@@ -1,349 +0,0 @@
-#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_StencilBuilder_cell2cell.cpp b/tests/test_StencilBuilder_cell2cell.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..42ceb6a5da583a9869035db09f5239d008c6f780
--- /dev/null
+++ b/tests/test_StencilBuilder_cell2cell.cpp
@@ -0,0 +1,1081 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/CartesianMeshBuilder.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ConnectivityUtils.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/ItemValueUtils.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFaceBoundary.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/StencilManager.hpp>
+#include <utils/Messenger.hpp>
+
+#include <NbGhostLayersTester.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("StencilBuilder cell2cell", "[mesh]")
+{
+  auto is_valid = []<ItemType connecting_item_type>(const auto& connectivity, const auto& stencil_array,
+                                                    const size_t number_of_layers) {
+    auto cell_to_connecting_item_matrix =
+      connectivity.template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+    auto connecting_to_cell_matrix = connectivity.template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+    auto cell_is_owned             = connectivity.cellIsOwned();
+    auto cell_number               = connectivity.cellNumber();
+
+    using ConnectingItemId = ItemIdT<connecting_item_type>;
+
+    for (CellId cell_id = 0; cell_id < connectivity.numberOfCells(); ++cell_id) {
+      if (cell_is_owned[cell_id]) {
+        std::vector<CellId> expected_stencil;
+
+        std::set<CellId> marked_cell_set;
+        marked_cell_set.insert(cell_id);
+
+        std::set<ConnectingItemId> marked_connecting_item_set;
+        std::set<ConnectingItemId> layer_connecting_item_set;
+        {
+          auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[cell_id];
+          for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+               ++i_connecting_item) {
+            const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+            layer_connecting_item_set.insert(connecting_item_id);
+            marked_connecting_item_set.insert(connecting_item_id);
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+            [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+          for (auto&& connecting_item_id : layer_connecting_item_set) {
+            auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+            for (size_t i_connecting_item_of_cell = 0; i_connecting_item_of_cell < connecting_item_to_cell_list.size();
+                 ++i_connecting_item_of_cell) {
+              const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+              if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                cell_set.insert(connecting_item_id_of_cell);
+                marked_cell_set.insert(connecting_item_id_of_cell);
+              }
+            }
+          }
+
+          layer_connecting_item_set.clear();
+          for (auto layer_cell_id : cell_set) {
+            auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+              if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+          }
+
+          for (auto&& set_cell_id : cell_set) {
+            expected_stencil.push_back(set_cell_id);
+          }
+        }
+
+        auto cell_stencil = stencil_array[cell_id];
+
+        auto i_set_cell = expected_stencil.begin();
+        if (cell_stencil.size() != expected_stencil.size()) {
+          return false;
+        }
+
+        for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+          if (*i_set_cell != cell_stencil[index]) {
+            return false;
+          }
+        }
+      }
+    }
+    return true;
+  };
+
+  SECTION("inner stencil")
+  {
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getCellToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+    }
+  }
+
+  SECTION("Stencil using symmetries")
+  {
+    auto check_ghost_cells_have_empty_stencils = [](const auto& stencil_array, const auto& connecticity) {
+      bool is_empty = true;
+
+      auto cell_is_owned = connecticity.cellIsOwned();
+
+      for (CellId cell_id = 0; cell_id < connecticity.numberOfCells(); ++cell_id) {
+        if (not cell_is_owned[cell_id]) {
+          is_empty &= (stencil_array[cell_id].size() == 0);
+          for (size_t i_symmetry_stencil = 0;
+               i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry_stencil) {
+            is_empty &=
+              (stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray()[cell_id].size() ==
+               0);
+          }
+        }
+      }
+
+      return is_empty;
+    };
+
+    auto are_symmetry_stencils_valid = []<ItemType connecting_item_type>(const auto& stencil_array, const auto& mesh,
+                                                                         const size_t number_of_layers) {
+      bool are_valid_symmetries = true;
+
+      auto connecting_to_cell_matrix =
+        mesh.connectivity().template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+      auto cell_to_connecting_item_matrix =
+        mesh.connectivity().template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+      auto cell_is_owned = mesh.connectivity().cellIsOwned();
+      auto cell_number   = mesh.connectivity().cellNumber();
+
+      auto connecting_number = mesh.connectivity().template number<connecting_item_type>();
+
+      using ConnectingItemId = ItemIdT<connecting_item_type>;
+      using MeshType         = std::decay_t<decltype(mesh)>;
+
+      for (size_t i_symmetry_stencil = 0; i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size();
+           ++i_symmetry_stencil) {
+        const IBoundaryDescriptor& boundary_descriptor =
+          stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].boundaryDescriptor();
+
+        auto boundary_stencil   = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray();
+        auto boundary_face_list = getMeshFaceBoundary(mesh, boundary_descriptor);
+
+        std::set<ConnectingItemId> sym_connecting_item_set;
+
+        if constexpr (ItemTypeId<MeshType::Dimension>::itemTId(connecting_item_type) ==
+                      ItemTypeId<MeshType::Dimension>::itemTId(ItemType::face)) {
+          for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+            const FaceId face_id = boundary_face_list.faceList()[i_face];
+            sym_connecting_item_set.insert(ConnectingItemId{FaceId::base_type{face_id}});
+          }
+
+        } else {
+          auto face_to_connecting_item_matrix =
+            mesh.connectivity().template getItemToItemMatrix<ItemType::face, connecting_item_type>();
+
+          for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+            const FaceId face_id      = boundary_face_list.faceList()[i_face];
+            auto connecting_item_list = face_to_connecting_item_matrix[face_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < connecting_item_list.size(); ++i_connecting_item) {
+              sym_connecting_item_set.insert(connecting_item_list[i_connecting_item]);
+            }
+          }
+        }
+
+        for (CellId cell_id = 0; cell_id < mesh.numberOfCells(); ++cell_id) {
+          if (not cell_is_owned[cell_id]) {
+            are_valid_symmetries &= (boundary_stencil[cell_id].size() == 0);
+          } else {
+            std::vector<CellId> expected_stencil;
+
+            std::set<CellId> marked_cell_set;
+            marked_cell_set.insert(cell_id);
+
+            std::set<ConnectingItemId> marked_connecting_item_set;
+            std::set<ConnectingItemId> layer_connecting_item_set;
+            {
+              auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[cell_id];
+              for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                   ++i_connecting_item) {
+                const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+
+            std::set<ConnectingItemId> marked_sym_connecting_item_set;
+            std::set<ConnectingItemId> layer_sym_connecting_item_set;
+
+            for (auto&& connecting_item_id : marked_connecting_item_set) {
+              if (sym_connecting_item_set.contains(connecting_item_id)) {
+                marked_sym_connecting_item_set.insert(connecting_item_id);
+                layer_sym_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+
+            std::set<CellId> marked_sym_cell_set;
+
+            for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+              std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+                [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+              for (auto&& connecting_item_id : layer_connecting_item_set) {
+                auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                for (size_t i_connecting_item_of_cell = 0;
+                     i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                  const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                  if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                    cell_set.insert(connecting_item_id_of_cell);
+                    marked_cell_set.insert(connecting_item_id_of_cell);
+                  }
+                }
+              }
+
+              std::set<CellId, std::function<bool(CellId, CellId)>> sym_cell_set(
+                [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+              for (auto&& connecting_item_id : layer_sym_connecting_item_set) {
+                auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                for (size_t i_connecting_item_of_cell = 0;
+                     i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                  const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                  if (not marked_sym_cell_set.contains(connecting_item_id_of_cell)) {
+                    sym_cell_set.insert(connecting_item_id_of_cell);
+                    marked_sym_cell_set.insert(connecting_item_id_of_cell);
+                  }
+                }
+              }
+
+              for (auto&& set_sym_cell_id : sym_cell_set) {
+                expected_stencil.push_back(set_sym_cell_id);
+              }
+
+              layer_connecting_item_set.clear();
+              for (auto&& layer_cell_id : cell_set) {
+                auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+                for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                     ++i_connecting_item) {
+                  const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                  if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                    layer_connecting_item_set.insert(connecting_item_id);
+                    marked_connecting_item_set.insert(connecting_item_id);
+                  }
+                }
+              }
+
+              layer_sym_connecting_item_set.clear();
+
+              for (auto&& connecting_item_id : layer_connecting_item_set) {
+                if (sym_connecting_item_set.contains(connecting_item_id)) {
+                  if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                    marked_sym_connecting_item_set.insert(connecting_item_id);
+                    layer_sym_connecting_item_set.insert(connecting_item_id);
+                  }
+                }
+              }
+
+              for (auto layer_sym_cell_id : sym_cell_set) {
+                auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_sym_cell_id];
+                for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                     ++i_connecting_item) {
+                  const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                  if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                    marked_sym_connecting_item_set.insert(connecting_item_id);
+                    layer_sym_connecting_item_set.insert(connecting_item_id);
+                  }
+                }
+              }
+            }
+
+            auto cell_stencil = boundary_stencil[cell_id];
+
+            if (cell_stencil.size() != expected_stencil.size()) {
+              are_valid_symmetries = false;
+            }
+
+            auto i_set_cell = expected_stencil.begin();
+            for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+              if (*i_set_cell != cell_stencil[index]) {
+                are_valid_symmetries = false;
+              }
+            }
+          }
+        }
+      }
+
+      return are_valid_symmetries;
+    };
+
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(not(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1)));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(not(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1)));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(not(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2)));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getCellToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_cells_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(are_symmetry_stencils_valid.template operator()<ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_StencilBuilder_node2cell.cpp b/tests/test_StencilBuilder_node2cell.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d9c429c6d4ba903185c90a1e6c37d63bf9c9feb
--- /dev/null
+++ b/tests/test_StencilBuilder_node2cell.cpp
@@ -0,0 +1,1184 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/CartesianMeshBuilder.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ConnectivityUtils.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/ItemValueUtils.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFaceBoundary.hpp>
+#include <mesh/MeshVariant.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/StencilManager.hpp>
+#include <utils/Messenger.hpp>
+
+#include <NbGhostLayersTester.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("StencilBuilder node2cell", "[mesh]")
+{
+  auto is_valid = []<ItemType source_item_type, ItemType connecting_item_type>(const auto& connectivity,
+                                                                               const auto& stencil_array,
+                                                                               const size_t number_of_layers) {
+    constexpr size_t Dimension = std::decay_t<decltype(connectivity)>::Dimension;
+
+    auto cell_to_connecting_item_matrix =
+      connectivity.template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+
+    auto source_item_to_connecting_item_matrix =
+      [](const auto& given_connectivity) -> ItemToItemMatrix<source_item_type, connecting_item_type> {
+      if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                    ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+        return ConnectivityMatrix{};
+      } else {
+        return given_connectivity.template getItemToItemMatrix<source_item_type, connecting_item_type>();
+      }
+    }(connectivity);
+
+    auto connecting_to_cell_matrix = connectivity.template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+    auto cell_is_owned             = connectivity.cellIsOwned();
+    auto cell_number               = connectivity.cellNumber();
+    auto source_item_is_owned      = connectivity.template isOwned<source_item_type>();
+
+    using SourceItemId     = ItemIdT<source_item_type>;
+    using ConnectingItemId = ItemIdT<connecting_item_type>;
+
+    for (SourceItemId source_item_id = 0; source_item_id < connectivity.template numberOf<source_item_type>();
+         ++source_item_id) {
+      if (source_item_is_owned[source_item_id]) {
+        std::vector<CellId> expected_stencil;
+
+        std::set<CellId> marked_cell_set;
+        if constexpr (source_item_type == ItemType::cell) {
+          marked_cell_set.insert(source_item_id);
+        }
+
+        std::set<ConnectingItemId> marked_connecting_item_set;
+        std::set<ConnectingItemId> layer_connecting_item_set;
+        {
+          if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                        ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+            ConnectingItemId connecting_id =
+              ConnectingItemId{static_cast<typename SourceItemId::base_type>(source_item_id)};
+            layer_connecting_item_set.insert(connecting_id);
+            marked_connecting_item_set.insert(connecting_id);
+          } else {
+            auto source_item_to_connecting_item_list = source_item_to_connecting_item_matrix[source_item_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < source_item_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = source_item_to_connecting_item_list[i_connecting_item];
+              layer_connecting_item_set.insert(connecting_item_id);
+              marked_connecting_item_set.insert(connecting_item_id);
+            }
+          }
+        }
+
+        for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+          std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+            [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+          for (auto&& connecting_item_id : layer_connecting_item_set) {
+            auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+            for (size_t i_connecting_item_of_cell = 0; i_connecting_item_of_cell < connecting_item_to_cell_list.size();
+                 ++i_connecting_item_of_cell) {
+              const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+              if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                cell_set.insert(connecting_item_id_of_cell);
+                marked_cell_set.insert(connecting_item_id_of_cell);
+              }
+            }
+          }
+
+          layer_connecting_item_set.clear();
+          for (auto layer_cell_id : cell_set) {
+            auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+            for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                 ++i_connecting_item) {
+              const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+              if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              }
+            }
+          }
+
+          for (auto&& set_cell_id : cell_set) {
+            expected_stencil.push_back(set_cell_id);
+          }
+        }
+
+        auto cell_stencil = stencil_array[source_item_id];
+
+        auto i_set_cell = expected_stencil.begin();
+        if (cell_stencil.size() != expected_stencil.size()) {
+          return false;
+        }
+
+        for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+          if (*i_set_cell != cell_stencil[index]) {
+            return false;
+          }
+        }
+      }
+    }
+    return true;
+  };
+
+  SECTION("inner stencil")
+  {
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       1));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(connectivity,
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(connectivity,
+                                                                    StencilDescriptor{1, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       1));
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("2D")
+      {
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+
+      SECTION("3D")
+      {
+        SECTION("carteian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::node>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_nodes}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::edge>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_edges}),
+                                       2));
+
+          REQUIRE(
+            is_valid.template
+            operator()<ItemType::node,
+                       ItemType::face>(mesh.connectivity(),
+                                       StencilManager::instance()
+                                         .getNodeToCellStencilArray(mesh.connectivity(),
+                                                                    StencilDescriptor{2, StencilDescriptor::
+                                                                                           ConnectionType::by_faces}),
+                                       2));
+        }
+      }
+    }
+  }
+
+  SECTION("Stencil using symmetries")
+  {
+    auto check_ghost_nodes_have_empty_stencils = [](const auto& stencil_array, const auto& connecticity) {
+      bool is_empty = true;
+
+      auto node_is_owned = connecticity.nodeIsOwned();
+
+      for (NodeId node_id = 0; node_id < connecticity.numberOfNodes(); ++node_id) {
+        if (not node_is_owned[node_id]) {
+          is_empty &= (stencil_array[node_id].size() == 0);
+          for (size_t i_symmetry_stencil = 0;
+               i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry_stencil) {
+            is_empty &=
+              (stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray()[node_id].size() ==
+               0);
+          }
+        }
+      }
+
+      return is_empty;
+    };
+
+    auto are_symmetry_stencils_valid =
+      []<ItemType source_item_type, ItemType connecting_item_type>(const auto& stencil_array, const auto& mesh,
+                                                                   const size_t number_of_layers) {
+        bool are_valid_symmetries = true;
+
+        constexpr const size_t Dimension = std::decay_t<decltype(mesh)>::Dimension;
+
+        auto connecting_to_cell_matrix =
+          mesh.connectivity().template getItemToItemMatrix<connecting_item_type, ItemType::cell>();
+        auto cell_to_connecting_item_matrix =
+          mesh.connectivity().template getItemToItemMatrix<ItemType::cell, connecting_item_type>();
+        //        auto source_item_is_owned        = mesh.connectivity().cellIsOwned();
+        auto cell_number          = mesh.connectivity().cellNumber();
+        auto source_item_is_owned = mesh.connectivity().template isOwned<source_item_type>();
+
+        auto connecting_number = mesh.connectivity().template number<connecting_item_type>();
+
+        auto source_item_to_connecting_item_matrix =
+          [](const auto& given_connectivity) -> ItemToItemMatrix<source_item_type, connecting_item_type> {
+          if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                        ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+            return ConnectivityMatrix{};
+          } else {
+            return given_connectivity.template getItemToItemMatrix<source_item_type, connecting_item_type>();
+          }
+        }(mesh.connectivity());
+
+        using SourceItemId     = ItemIdT<source_item_type>;
+        using ConnectingItemId = ItemIdT<connecting_item_type>;
+        using MeshType         = std::decay_t<decltype(mesh)>;
+
+        for (size_t i_symmetry_stencil = 0;
+             i_symmetry_stencil < stencil_array.symmetryBoundaryStencilArrayList().size(); ++i_symmetry_stencil) {
+          const IBoundaryDescriptor& boundary_descriptor =
+            stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].boundaryDescriptor();
+
+          auto boundary_stencil   = stencil_array.symmetryBoundaryStencilArrayList()[i_symmetry_stencil].stencilArray();
+          auto boundary_face_list = getMeshFaceBoundary(mesh, boundary_descriptor);
+
+          std::set<ConnectingItemId> sym_connecting_item_set;
+
+          if constexpr (ItemTypeId<MeshType::Dimension>::itemTId(connecting_item_type) ==
+                        ItemTypeId<MeshType::Dimension>::itemTId(ItemType::face)) {
+            for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+              const FaceId face_id = boundary_face_list.faceList()[i_face];
+              sym_connecting_item_set.insert(ConnectingItemId{FaceId::base_type{face_id}});
+            }
+
+          } else {
+            auto face_to_connecting_item_matrix =
+              mesh.connectivity().template getItemToItemMatrix<ItemType::face, connecting_item_type>();
+
+            for (size_t i_face = 0; i_face < boundary_face_list.faceList().size(); ++i_face) {
+              const FaceId face_id      = boundary_face_list.faceList()[i_face];
+              auto connecting_item_list = face_to_connecting_item_matrix[face_id];
+              for (size_t i_connecting_item = 0; i_connecting_item < connecting_item_list.size(); ++i_connecting_item) {
+                sym_connecting_item_set.insert(connecting_item_list[i_connecting_item]);
+              }
+            }
+          }
+
+          for (SourceItemId source_item_id = 0; source_item_id < mesh.template numberOf<source_item_type>();
+               ++source_item_id) {
+            if (not source_item_is_owned[source_item_id]) {
+              are_valid_symmetries &= (boundary_stencil[source_item_id].size() == 0);
+            } else {
+              std::vector<CellId> expected_stencil;
+
+              std::set<CellId> marked_cell_set;
+              if constexpr (source_item_type == ItemType::cell) {
+                marked_cell_set.insert(source_item_id);
+              }
+
+              std::set<ConnectingItemId> marked_connecting_item_set;
+              std::set<ConnectingItemId> layer_connecting_item_set;
+              if constexpr (ItemTypeId<Dimension>::itemTId(source_item_type) ==
+                            ItemTypeId<Dimension>::itemTId(connecting_item_type)) {
+                const ConnectingItemId connecting_item_id =
+                  static_cast<typename SourceItemId::base_type>(source_item_id);
+                layer_connecting_item_set.insert(connecting_item_id);
+                marked_connecting_item_set.insert(connecting_item_id);
+              } else {
+                auto source_item_to_connecting_item_list = source_item_to_connecting_item_matrix[source_item_id];
+                for (size_t i_connecting_item = 0; i_connecting_item < source_item_to_connecting_item_list.size();
+                     ++i_connecting_item) {
+                  const ConnectingItemId connecting_item_id = source_item_to_connecting_item_list[i_connecting_item];
+                  layer_connecting_item_set.insert(connecting_item_id);
+                  marked_connecting_item_set.insert(connecting_item_id);
+                }
+              }
+
+              std::set<ConnectingItemId> marked_sym_connecting_item_set;
+              std::set<ConnectingItemId> layer_sym_connecting_item_set;
+
+              for (auto&& connecting_item_id : marked_connecting_item_set) {
+                if (sym_connecting_item_set.contains(connecting_item_id)) {
+                  marked_sym_connecting_item_set.insert(connecting_item_id);
+                  layer_sym_connecting_item_set.insert(connecting_item_id);
+                }
+              }
+
+              std::set<CellId> marked_sym_cell_set;
+
+              for (size_t i_layer = 0; i_layer < number_of_layers; ++i_layer) {
+                std::set<CellId, std::function<bool(CellId, CellId)>> cell_set(
+                  [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+                for (auto&& connecting_item_id : layer_connecting_item_set) {
+                  auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                  for (size_t i_connecting_item_of_cell = 0;
+                       i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                    const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                    if (not marked_cell_set.contains(connecting_item_id_of_cell)) {
+                      cell_set.insert(connecting_item_id_of_cell);
+                      marked_cell_set.insert(connecting_item_id_of_cell);
+                    }
+                  }
+                }
+
+                std::set<CellId, std::function<bool(CellId, CellId)>> sym_cell_set(
+                  [=](CellId cell_0, CellId cell_1) { return cell_number[cell_0] < cell_number[cell_1]; });
+
+                for (auto&& connecting_item_id : layer_sym_connecting_item_set) {
+                  auto connecting_item_to_cell_list = connecting_to_cell_matrix[connecting_item_id];
+                  for (size_t i_connecting_item_of_cell = 0;
+                       i_connecting_item_of_cell < connecting_item_to_cell_list.size(); ++i_connecting_item_of_cell) {
+                    const CellId connecting_item_id_of_cell = connecting_item_to_cell_list[i_connecting_item_of_cell];
+                    if (not marked_sym_cell_set.contains(connecting_item_id_of_cell)) {
+                      sym_cell_set.insert(connecting_item_id_of_cell);
+                      marked_sym_cell_set.insert(connecting_item_id_of_cell);
+                    }
+                  }
+                }
+
+                for (auto&& set_sym_cell_id : sym_cell_set) {
+                  expected_stencil.push_back(set_sym_cell_id);
+                }
+
+                layer_connecting_item_set.clear();
+                for (auto&& layer_cell_id : cell_set) {
+                  auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_cell_id];
+                  for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                       ++i_connecting_item) {
+                    const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                    if (not marked_connecting_item_set.contains(connecting_item_id)) {
+                      layer_connecting_item_set.insert(connecting_item_id);
+                      marked_connecting_item_set.insert(connecting_item_id);
+                    }
+                  }
+                }
+
+                layer_sym_connecting_item_set.clear();
+
+                for (auto&& connecting_item_id : layer_connecting_item_set) {
+                  if (sym_connecting_item_set.contains(connecting_item_id)) {
+                    if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                      marked_sym_connecting_item_set.insert(connecting_item_id);
+                      layer_sym_connecting_item_set.insert(connecting_item_id);
+                    }
+                  }
+                }
+
+                for (auto layer_sym_cell_id : sym_cell_set) {
+                  auto cell_to_connecting_item_list = cell_to_connecting_item_matrix[layer_sym_cell_id];
+                  for (size_t i_connecting_item = 0; i_connecting_item < cell_to_connecting_item_list.size();
+                       ++i_connecting_item) {
+                    const ConnectingItemId connecting_item_id = cell_to_connecting_item_list[i_connecting_item];
+                    if (not marked_sym_connecting_item_set.contains(connecting_item_id)) {
+                      marked_sym_connecting_item_set.insert(connecting_item_id);
+                      layer_sym_connecting_item_set.insert(connecting_item_id);
+                    }
+                  }
+                }
+              }
+
+              auto cell_stencil = boundary_stencil[source_item_id];
+
+              if (cell_stencil.size() != expected_stencil.size()) {
+                are_valid_symmetries = false;
+              }
+
+              auto i_set_cell = expected_stencil.begin();
+              for (size_t index = 0; index < cell_stencil.size(); ++index, ++i_set_cell) {
+                if (*i_set_cell != cell_stencil[index]) {
+                  are_valid_symmetries = false;
+                }
+              }
+            }
+          }
+        }
+
+        return are_valid_symmetries;
+      };
+
+    SECTION("1 layer")
+    {
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("unordered")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().unordered1DMesh()->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid2DMesh()->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().cartesian3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+
+        SECTION("hybrid")
+        {
+          const auto& mesh = *MeshDataBaseForTests::get().hybrid3DMesh()->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 1));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{1, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 1));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 1));
+          }
+        }
+      }
+    }
+
+    SECTION("2 layers")
+    {
+      NbGhostLayersTester nb_ghost_layers_tester(2);
+
+      SECTION("1D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v = CartesianMeshBuilder(TinyVector<1>{0}, TinyVector<1>{1}, TinyVector<1, uint64_t>(20)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<1>>();
+
+          const Connectivity<1>& connectivity = mesh.connectivity();
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 2);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("2D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<2>{0, 0}, TinyVector<2>{1, 2}, TinyVector<2, uint64_t>(5, 7)).mesh();
+          const auto& mesh = *mesh_v->get<Mesh<2>>();
+
+          const Connectivity<2>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 4);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(not(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2)));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+
+      SECTION("3D")
+      {
+        StencilManager::BoundaryDescriptorList list;
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("XMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("YMAX"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMIN"));
+        list.emplace_back(std::make_shared<NamedBoundaryDescriptor>("ZMAX"));
+
+        SECTION("cartesian")
+        {
+          auto mesh_v =
+            CartesianMeshBuilder(TinyVector<3>{0, 0, 0}, TinyVector<3>{1, 1, 2}, TinyVector<3, uint64_t>(3, 4, 5))
+              .mesh();
+          const auto& mesh = *mesh_v->get<Mesh<3>>();
+
+          const Connectivity<3>& connectivity = mesh.connectivity();
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_nodes}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::node>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::node>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_edges}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::edge>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::edge>(connectivity, stencil_array, 2));
+          }
+
+          {
+            auto stencil_array =
+              StencilManager::instance()
+                .getNodeToCellStencilArray(connectivity,
+                                           StencilDescriptor{2, StencilDescriptor::ConnectionType::by_faces}, list);
+
+            REQUIRE(stencil_array.symmetryBoundaryStencilArrayList().size() == 6);
+            REQUIRE(check_ghost_nodes_have_empty_stencils(stencil_array, connectivity));
+            REQUIRE(
+              are_symmetry_stencils_valid.template operator()<ItemType::node, ItemType::face>(stencil_array, mesh, 2));
+            REQUIRE(is_valid.template operator()<ItemType::node, ItemType::face>(connectivity, stencil_array, 2));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_checkpointing_HFTypes.cpp b/tests/test_checkpointing_HFTypes.cpp
index 41fc6948c382e3d8f4a3f0bf100cd94405c50875..1a5f02b4a4328910fe9caca1ff3c780954f77197 100644
--- a/tests/test_checkpointing_HFTypes.cpp
+++ b/tests/test_checkpointing_HFTypes.cpp
@@ -3,6 +3,7 @@
 
 #include <utils/checkpointing/DiscreteFunctionTypeHFType.hpp>
 #include <utils/checkpointing/DualMeshTypeHFType.hpp>
+#include <utils/checkpointing/EigenvalueSolverOptionsHFType.hpp>
 #include <utils/checkpointing/IBoundaryConditionDescriptorHFType.hpp>
 #include <utils/checkpointing/IBoundaryDescriptorHFType.hpp>
 #include <utils/checkpointing/IInterfaceDescriptorHFType.hpp>
@@ -160,6 +161,7 @@ TEST_CASE("HFTypes", "[utils/checkpointing]")
     SECTION("LinearSolverOptionsHFType")
     {
       file.createAttribute("builtin", LSLibrary::builtin);
+      file.createAttribute("eigen3", LSLibrary::eigen3);
       file.createAttribute("petsc", LSLibrary::petsc);
 
       file.createAttribute("cg", LSMethod::cg);
@@ -177,6 +179,7 @@ TEST_CASE("HFTypes", "[utils/checkpointing]")
 
       REQUIRE(file.getAttribute("builtin").read<LSLibrary>() == LSLibrary::builtin);
       REQUIRE(file.getAttribute("petsc").read<LSLibrary>() == LSLibrary::petsc);
+      REQUIRE(file.getAttribute("eigen3").read<LSLibrary>() == LSLibrary::eigen3);
 
       REQUIRE(file.getAttribute("cg").read<LSMethod>() == LSMethod::cg);
       REQUIRE(file.getAttribute("bicgstab").read<LSMethod>() == LSMethod::bicgstab);
diff --git a/tests/test_checkpointing_IBoundaryConditionDescriptor.cpp b/tests/test_checkpointing_IBoundaryConditionDescriptor.cpp
index 34505db62eff529b480c912ce5ca36714c7bc17b..ab192f387231dba8c5a1802f8d503303852c7b13 100644
--- a/tests/test_checkpointing_IBoundaryConditionDescriptor.cpp
+++ b/tests/test_checkpointing_IBoundaryConditionDescriptor.cpp
@@ -9,6 +9,7 @@
 #include <mesh/NumberedBoundaryDescriptor.hpp>
 #include <scheme/AxisBoundaryConditionDescriptor.hpp>
 #include <scheme/DirichletBoundaryConditionDescriptor.hpp>
+#include <scheme/DirichletVectorBoundaryConditionDescriptor.hpp>
 #include <scheme/ExternalBoundaryConditionDescriptor.hpp>
 #include <scheme/FixedBoundaryConditionDescriptor.hpp>
 #include <scheme/FourierBoundaryConditionDescriptor.hpp>
@@ -179,6 +180,17 @@ let i: R -> R, x -> x+3;
                                                          p_dirichlet_bc_descriptor)},
                                                        file, useless_group, symbol_table_group);
 
+      const std::vector<FunctionSymbolId> dirichlet_vector_function_id_list{FunctionSymbolId{2, symbol_table},
+                                                                            FunctionSymbolId{3, symbol_table}};
+      auto p_dirichlet_vector_bc_descriptor =
+        std::make_shared<const DirichletVectorBoundaryConditionDescriptor>("dirichlet_vector_name", p_boundary_1,
+                                                                           dirichlet_vector_function_id_list);
+      checkpointing::writeIBoundaryConditionDescriptor("dirichlet_vector_bc_descriptor",
+                                                       EmbeddedData{std::make_shared<
+                                                         DataHandler<const IBoundaryConditionDescriptor>>(
+                                                         p_dirichlet_vector_bc_descriptor)},
+                                                       file, useless_group, symbol_table_group);
+
       const FunctionSymbolId neumann_function_id{1, symbol_table};
       auto p_neumann_bc_descriptor =
         std::make_shared<const NeumannBoundaryConditionDescriptor>("neumann_name", p_boundary_1, neumann_function_id);
@@ -245,6 +257,9 @@ let i: R -> R, x -> x+3;
       EmbeddedData read_dirichlet_bc_descriptor =
         checkpointing::readIBoundaryConditionDescriptor("dirichlet_bc_descriptor", symbol_table_group);
 
+      EmbeddedData read_dirichlet_vector_bc_descriptor =
+        checkpointing::readIBoundaryConditionDescriptor("dirichlet_vector_bc_descriptor", symbol_table_group);
+
       EmbeddedData read_neumann_bc_descriptor =
         checkpointing::readIBoundaryConditionDescriptor("neumann_bc_descriptor", symbol_table_group);
 
@@ -268,6 +283,7 @@ let i: R -> R, x -> x+3;
       REQUIRE_NOTHROW(get_value(read_free_bc_descriptor));
       REQUIRE_NOTHROW(get_value(read_fixed_bc_descriptor));
       REQUIRE_NOTHROW(get_value(read_dirichlet_bc_descriptor));
+      REQUIRE_NOTHROW(get_value(read_dirichlet_vector_bc_descriptor));
       REQUIRE_NOTHROW(get_value(read_neumann_bc_descriptor));
       REQUIRE_NOTHROW(get_value(read_fourier_bc_descriptor));
       REQUIRE_NOTHROW(get_value(read_inflow_bc_descriptor));
@@ -280,6 +296,8 @@ let i: R -> R, x -> x+3;
       REQUIRE(get_value(read_free_bc_descriptor).type() == IBoundaryConditionDescriptor::Type::free);
       REQUIRE(get_value(read_fixed_bc_descriptor).type() == IBoundaryConditionDescriptor::Type::fixed);
       REQUIRE(get_value(read_dirichlet_bc_descriptor).type() == IBoundaryConditionDescriptor::Type::dirichlet);
+      REQUIRE(get_value(read_dirichlet_vector_bc_descriptor).type() ==
+              IBoundaryConditionDescriptor::Type::dirichlet_vector);
       REQUIRE(get_value(read_neumann_bc_descriptor).type() == IBoundaryConditionDescriptor::Type::neumann);
       REQUIRE(get_value(read_fourier_bc_descriptor).type() == IBoundaryConditionDescriptor::Type::fourier);
       REQUIRE(get_value(read_inflow_bc_descriptor).type() == IBoundaryConditionDescriptor::Type::inflow);
@@ -293,6 +311,8 @@ let i: R -> R, x -> x+3;
       REQUIRE_NOTHROW(dynamic_cast<const FixedBoundaryConditionDescriptor&>(get_value(read_fixed_bc_descriptor)));
       REQUIRE_NOTHROW(
         dynamic_cast<const DirichletBoundaryConditionDescriptor&>(get_value(read_dirichlet_bc_descriptor)));
+      REQUIRE_NOTHROW(dynamic_cast<const DirichletVectorBoundaryConditionDescriptor&>(
+        get_value(read_dirichlet_vector_bc_descriptor)));
       REQUIRE_NOTHROW(dynamic_cast<const NeumannBoundaryConditionDescriptor&>(get_value(read_neumann_bc_descriptor)));
       REQUIRE_NOTHROW(dynamic_cast<const FourierBoundaryConditionDescriptor&>(get_value(read_fourier_bc_descriptor)));
       REQUIRE_NOTHROW(dynamic_cast<const InflowBoundaryConditionDescriptor&>(get_value(read_inflow_bc_descriptor)));
@@ -308,6 +328,8 @@ let i: R -> R, x -> x+3;
       auto& read_fixed_bc = dynamic_cast<const FixedBoundaryConditionDescriptor&>(get_value(read_fixed_bc_descriptor));
       auto& read_dirichlet_bc =
         dynamic_cast<const DirichletBoundaryConditionDescriptor&>(get_value(read_dirichlet_bc_descriptor));
+      auto& read_dirichlet_vector_bc =
+        dynamic_cast<const DirichletVectorBoundaryConditionDescriptor&>(get_value(read_dirichlet_vector_bc_descriptor));
       auto& read_neumann_bc =
         dynamic_cast<const NeumannBoundaryConditionDescriptor&>(get_value(read_neumann_bc_descriptor));
       auto& read_fourier_bc =
@@ -326,6 +348,15 @@ let i: R -> R, x -> x+3;
       REQUIRE(read_dirichlet_bc.boundaryDescriptor().type() == p_dirichlet_bc_descriptor->boundaryDescriptor().type());
       REQUIRE(read_dirichlet_bc.name() == p_dirichlet_bc_descriptor->name());
       REQUIRE(read_dirichlet_bc.rhsSymbolId().id() == p_dirichlet_bc_descriptor->rhsSymbolId().id());
+      REQUIRE(read_dirichlet_vector_bc.boundaryDescriptor().type() ==
+              p_dirichlet_vector_bc_descriptor->boundaryDescriptor().type());
+      REQUIRE(read_dirichlet_vector_bc.name() == p_dirichlet_vector_bc_descriptor->name());
+      REQUIRE(read_dirichlet_vector_bc.rhsSymbolIdList().size() ==
+              p_dirichlet_vector_bc_descriptor->rhsSymbolIdList().size());
+      for (size_t i = 0; i < read_dirichlet_vector_bc.rhsSymbolIdList().size(); ++i) {
+        REQUIRE(read_dirichlet_vector_bc.rhsSymbolIdList()[i].id() ==
+                p_dirichlet_vector_bc_descriptor->rhsSymbolIdList()[i].id());
+      }
       REQUIRE(read_neumann_bc.boundaryDescriptor().type() == p_neumann_bc_descriptor->boundaryDescriptor().type());
       REQUIRE(read_neumann_bc.name() == p_neumann_bc_descriptor->name());
       REQUIRE(read_neumann_bc.rhsSymbolId().id() == p_neumann_bc_descriptor->rhsSymbolId().id());
@@ -337,6 +368,8 @@ let i: R -> R, x -> x+3;
       REQUIRE(read_inflow_bc.functionSymbolId().id() == p_inflow_bc_descriptor->functionSymbolId().id());
       REQUIRE(read_inflow_list_bc.boundaryDescriptor().type() ==
               p_inflow_list_bc_descriptor->boundaryDescriptor().type());
+      REQUIRE(read_inflow_list_bc.functionSymbolIdList().size() ==
+              p_inflow_list_bc_descriptor->functionSymbolIdList().size());
       for (size_t i = 0; i < read_inflow_list_bc.functionSymbolIdList().size(); ++i) {
         REQUIRE(read_inflow_list_bc.functionSymbolIdList()[i].id() ==
                 p_inflow_list_bc_descriptor->functionSymbolIdList()[i].id());
diff --git a/tools/generate-plugin.sh b/tools/generate-plugin.sh
new file mode 100755
index 0000000000000000000000000000000000000000..275d1795f79753fbd259a2079119d977c89b7d82
--- /dev/null
+++ b/tools/generate-plugin.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+BOLD='\e[1m'
+RESET='\e[0m'
+
+GREEN='\e[92m'
+RED='\e[91m'
+YELLOW='\e[93m'
+
+echo -ne ${BOLD}
+echo -e "---------------------"
+echo -e "pugs plugin generator"
+echo -e "---------------------"
+echo -e ${RESET}
+
+CURRENT_DIR="$(pwd -P)"
+SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
+PUGS_DIR="$(dirname ${SCRIPT_DIR})"
+
+if [[ "${CURRENT_DIR}" =~ "${PUGS_DIR}" ]]
+then
+    echo -e ${RED}"Aborting..."${RESET}
+    echo -e "run this script outside of pugs sources"
+    exit 1
+fi
+
+NAME_RE='^[A-Z][a-zA-Z0-9]*$'
+
+echo "   Plugin name must fulfill the following constrains:"
+echo "   - be a single word that starts by an upper case,"
+echo "   - contains only letters or numbers,"
+echo "   and preferably separate words with caps."
+echo
+echo "   ex.: MyFirstPlugin"
+echo
+
+while [[ ! "${PLUGIN_NAME}" =~ $NAME_RE ]]
+do
+    echo -n "Give plugin name: "
+    read -r PLUGIN_NAME
+
+    if [[ ! "${PLUGIN_NAME}" =~ $NAME_RE ]]
+    then
+	echo -e ${RED}"  invalid name!"${RESET}
+	echo
+	unset PLUGIN_NAME
+    fi
+
+done
+
+PLUGIN_UP="${PLUGIN_NAME^^}"
+PLUGIN_LOW="${PLUGIN_NAME,,}"
+echo
+echo -e "creating plugin ${YELLOW}${PLUGIN_NAME}${RESET} in directory ${YELLOW}${PLUGIN_LOW}${RESET}"
+echo
+
+if [[ -e ${PLUGIN_LOW} ]]
+then
+    echo -e ${RED}"Aborting..."${RESET}
+    echo -e "directory \"${PLUGIN_LOW}\" ${YELLOW}already exists${RESET}!"
+    exit 1
+fi
+
+function substitute()
+{
+    sed s/_PLUGIN_NAME_/${PLUGIN_NAME}/g | sed s/_PLUGIN_LOW_/${PLUGIN_LOW}/g | sed s/_PLUGIN_UP_/${PLUGIN_UP}/g
+}
+
+mkdir -p "${PLUGIN_LOW}/cmake"
+mkdir -p "${PLUGIN_LOW}/tests"
+
+cp "${PUGS_DIR}"/tests/MeshDataBaseForTests.hpp "${PLUGIN_LOW}"/tests/
+cp "${PUGS_DIR}"/tests/MeshDataBaseForTests.cpp "${PLUGIN_LOW}"/tests/
+cp "${PUGS_DIR}"/tests/ParallelCheckerTester.hpp "${PLUGIN_LOW}"/tests/
+cp "${PUGS_DIR}"/tests/ParallelCheckerTester.cpp "${PLUGIN_LOW}"/tests/
+cp "${PUGS_DIR}"/tests/test_main.cpp "${PLUGIN_LOW}"/tests/
+cp "${PUGS_DIR}"/tests/mpi_test_main.cpp "${PLUGIN_LOW}"/tests/
+
+cp "${PUGS_DIR}"/cmake/CheckNotInSources.cmake "${PLUGIN_LOW}"/cmake/
+cp "${PUGS_DIR}"/tools/plugin-template/FindPugs.cmake "${PLUGIN_LOW}"/cmake/
+cp "${PUGS_DIR}"/.gitignore "${PLUGIN_LOW}"
+cp "${PUGS_DIR}"/.clang-format "${PLUGIN_LOW}"
+
+cat "${PUGS_DIR}"/tools/plugin-template/CMakeLists.txt-template | substitute > "${PLUGIN_LOW}"/CMakeLists.txt
+cat "${PUGS_DIR}"/tools/plugin-template/Module.hpp-template | substitute > "${PLUGIN_LOW}"/${PLUGIN_NAME}Module.hpp
+cat "${PUGS_DIR}"/tools/plugin-template/Module.cpp-template | substitute > "${PLUGIN_LOW}"/${PLUGIN_NAME}Module.cpp
+cat "${PUGS_DIR}"/tools/plugin-template/README.md-template | substitute > "${PLUGIN_LOW}"/README.md
+
+cat "${PUGS_DIR}"/tools/plugin-template/tests-CMakeLists.txt-template | substitute > "${PLUGIN_LOW}"/tests/CMakeLists.txt
+
+(cd "${PLUGIN_LOW}"; git init -q)
+(cd "${PLUGIN_LOW}"; git add .)
+(cd "${PLUGIN_LOW}"; git commit -m "init" -q)
+
+echo -e ${GREEN}"Creation finished successfully!"${RESET}
diff --git a/tools/plugin-template/CMakeLists.txt-template b/tools/plugin-template/CMakeLists.txt-template
new file mode 100644
index 0000000000000000000000000000000000000000..9c6478b0711cbbd5e38a41f3eaa6c4736a300e67
--- /dev/null
+++ b/tools/plugin-template/CMakeLists.txt-template
@@ -0,0 +1,152 @@
+cmake_minimum_required (VERSION 3.19)
+
+project("_PLUGIN_LOW_")
+
+set(PUGS_PREFIX_PATH "" CACHE STRING "pugs intall dir")
+
+string(COMPARE EQUAL "${PUGS_PREFIX_PATH}" "" pugs_prefix_undefined)
+
+if (pugs_prefix_undefined)
+  message(FATAL_ERROR "PUGS_PREFIX_PATH must be defined")
+endif()
+
+# CMake utils
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+
+# Forbids in-source builds
+include(CheckNotInSources)
+
+# use PkgConfig to find packages
+find_package(PkgConfig REQUIRED)
+find_package(Pugs REQUIRED)
+
+list(APPEND CMAKE_MODULE_PATH "${PUGS_PREFIX_PATH}/lib/cmake/Kokkos")
+include(KokkosConfig)
+
+set(HDF5_PREFER_PARALLEL TRUE)
+list(APPEND CMAKE_MODULE_PATH "${PUGS_PREFIX_PATH}/lib/cmake/HighFive")
+include(HighFiveConfig)
+
+list(APPEND CMAKE_MODULE_PATH "${PUGS_PREFIX_PATH}/lib/cmake/pugs")
+include(PugsTargets)
+include(PugsCompileFlags)
+
+#------------------------------------------------------
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${PUGS_CMAKE_CXX_FLAGS}")
+set(CMAKE_CXX_STANDARD "${PUGS_CMAKE_CXX_STANDARD}")
+
+# -----------------------------------------------------
+# dynamic libraries
+
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+link_libraries("-rdynamic")
+set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
+
+#------------------------------------------------------
+
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+#------------------------------------------------------
+
+set(_PLUGIN_UP__SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+set(_PLUGIN_UP__BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+
+#------------------------------------------------------
+# use pugs compilation settings
+
+set(CMAKE_BUILD_TYPE "${PUGS_CMAKE_BUILD_TYPE}" CACHE STRING "" FORCE)
+set(CMAKE_CXX_COMPILER "${PUGS_CMAKE_CXX_COMPILER}" CACHE STRING "" FORCE)
+set(CMAKE_C_COMPILER "${PUGS_CMAKE_C_COMPILER}" CACHE STRING "" FORCE)
+
+mark_as_advanced(CMAKE_BUILD_TYPE CMAKE_CXX_COMPILER CMAKE_C_COMPILER)
+
+#------------------------------------------------------
+# default build shared libraries
+set(BUILD_SHARED_LIBS ON CACHE STRING "" FORCE)
+
+#------------------------------------------------------
+
+# Checks if compiler version is compatible with Pugs sources
+set(GNU_CXX_MIN_VERSION "10.0.0")
+set(CLANG_CXX_MIN_VERSION "11.0.0")
+
+#------------------------------------------------------
+
+include_directories("${CMAKE_CURRENT_SOURCE_DIR}")
+include_directories(SYSTEM "${PUGS_PREFIX_PATH}/include")
+include_directories(SYSTEM "${PUGS_PREFIX_PATH}/include/kokkos")
+include_directories(SYSTEM "${PUGS_PREFIX_PATH}/include/tao/")
+include_directories(SYSTEM "${MPI_CXX_INCLUDE_DIRS}")
+
+get_target_property(_prop Pugs::PugsAlgebra INTERFACE_INCLUDE_DIRECTORIES)
+set(PUGS_INC_DIR "${PUGS_INC_DIR};${_prop}")
+get_target_property(_prop Pugs::PugsUtils INTERFACE_INCLUDE_DIRECTORIES)
+set(PUGS_INC_DIR "${PUGS_INC_DIR};${_prop}")
+get_target_property(_prop Pugs::pugs INTERFACE_INCLUDE_DIRECTORIES)
+set(PUGS_INC_DIR "${PUGS_INC_DIR};${_prop}")
+
+include_directories(SYSTEM ${PUGS_INC_DIR})
+link_directories(${PUGS_PREFIX_PATH}/lib)
+
+#------------------------------------------------------
+
+if(${PUGS_HAS_MPI})
+  set(MPIEXEC_OPTION_FLAGS --oversubscribe)
+  if (NOT "$ENV{GITLAB_CI}" STREQUAL "")
+    set(MPIEXEC_OPTION_FLAGS ${MPIEXEC_OPTION_FLAGS} --allow-run-as-root)
+  endif()
+  set(MPIEXEC_NUMPROC 3)
+  set(MPIEXEC_PATH_FLAG --path)
+  set(MPI_LAUNCHER ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${MPIEXEC_NUMPROC} ${MPIEXEC_OPTION_FLAGS})
+endif()
+
+add_custom_target(all_unit_tests
+  DEPENDS unit_tests mpi_unit_tests
+)
+
+add_custom_target(check
+  DEPENDS test
+  )
+
+add_custom_target(test
+  DEPENDS run_all_unit_tests
+  )
+
+add_custom_target(run_all_unit_tests
+  DEPENDS run_mpi_unit_tests
+  )
+
+if(PUGS_HAS_MPI)
+  set(RUN_MPI_UNIT_TESTS_COMMENT "Running mpi_unit_tests [using ${MPIEXEC_NUMPROC} procs]")
+else()
+  set(RUN_MPI_UNIT_TESTS_COMMENT "Running mpi_unit_tests [sequentially]")
+endif()
+
+add_custom_target(run_mpi_unit_tests
+  COMMAND ${MPI_LAUNCHER} "${_PLUGIN_UP__BINARY_DIR}/mpi_unit_tests" --allow-running-no-tests
+  WORKING_DIRECTORY ${_PLUGIN_UP__BINARY_DIR}
+  DEPENDS run_unit_tests
+  COMMENT ${RUN_MPI_UNIT_TESTS_COMMENT}
+  )
+
+add_custom_target(run_unit_tests
+  COMMAND "${_PLUGIN_UP__BINARY_DIR}/unit_tests" --allow-running-no-tests
+  DEPENDS all_unit_tests
+  COMMENT "Running unit_tests"
+  )
+
+#------------------------------------------------------
+
+add_library(_PLUGIN_NAME_
+  _PLUGIN_NAME_Module.cpp
+  # add cpp sources files here
+)
+
+#------------------------------------------------------
+
+add_subdirectory(tests)
+
+#------------------------------------------------------
+
+install(TARGETS _PLUGIN_NAME_)
diff --git a/tools/plugin-template/FindPugs.cmake b/tools/plugin-template/FindPugs.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..6269bed8a9055339894cdd9fa9e63b2b479c9b70
--- /dev/null
+++ b/tools/plugin-template/FindPugs.cmake
@@ -0,0 +1,10 @@
+# Finds for the pugs installation directory
+
+find_path(PUGS_PREFIX_PATH include/utils/pugs_version.hpp
+  HINTS
+  $ENV{PUGS_INSTALL_DIR}
+  /usr/local/pugs
+  NO_DEFAULT_PATH
+)
+
+find_package_handle_standard_args(Pugs REQUIRED_VARS PUGS_PREFIX_PATH )
diff --git a/tools/plugin-template/Module.cpp-template b/tools/plugin-template/Module.cpp-template
new file mode 100644
index 0000000000000000000000000000000000000000..dd264e03631590e3cf0db152779e2118df86d474
--- /dev/null
+++ b/tools/plugin-template/Module.cpp-template
@@ -0,0 +1,28 @@
+#include <_PLUGIN_NAME_Module.hpp>
+
+#include <language/modules/ModuleRepository.hpp>
+#include <language/utils/BuiltinFunctionEmbedder.hpp>
+
+_PLUGIN_NAME_Module::_PLUGIN_NAME_Module() : BuiltinModule(false)
+{
+  // Simple hello world example
+  this->_addBuiltinFunction("_PLUGIN_LOW__hello", std::function(
+
+                                                    []() -> void { std::cout << "_PLUGIN_NAME_: Hello world\n"; }
+
+                                                    ));
+}
+
+void
+_PLUGIN_NAME_Module::registerOperators() const
+{
+  // Kept empty for basic use
+}
+
+void
+_PLUGIN_NAME_Module::registerCheckpointResume() const
+{
+  // kept empty for basic use
+}
+
+ModuleRepository::Subscribe<_PLUGIN_NAME_Module> _PLUGIN_LOW__module;
diff --git a/tools/plugin-template/Module.hpp-template b/tools/plugin-template/Module.hpp-template
new file mode 100644
index 0000000000000000000000000000000000000000..757e3b5e650390251d15515cfdcf26a9c970a102
--- /dev/null
+++ b/tools/plugin-template/Module.hpp-template
@@ -0,0 +1,22 @@
+#ifndef _PLUGIN_UP__MODULE_HPP
+#define _PLUGIN_UP__MODULE_HPP
+
+#include <language/modules/BuiltinModule.hpp>
+
+class _PLUGIN_NAME_Module : public BuiltinModule
+{
+ public:
+  std::string_view
+  name() const final
+  {
+    return "_PLUGIN_LOW_";
+  }
+
+  void registerOperators() const final;
+  void registerCheckpointResume() const final;
+
+  _PLUGIN_NAME_Module();
+  ~_PLUGIN_NAME_Module() = default;
+};
+
+#endif   // _PLUGIN_UP__MODULE_HPP
diff --git a/tools/plugin-template/README.md-template b/tools/plugin-template/README.md-template
new file mode 100644
index 0000000000000000000000000000000000000000..48485137f1b8c9f8063fec872aa38e15916d9350
--- /dev/null
+++ b/tools/plugin-template/README.md-template
@@ -0,0 +1,70 @@
+`pugs`'s plugin `_PLUGIN_NAME_`
+===============================
+
+# Building `_PLUGIN_NAME_`
+
+## `pugs` installation
+
+Building this plugin requires an **installed** version of `pugs`.
+`pugs` follows standard `cmake` installation recipes.
+
+Before building `pugs` one should define its installation directory.
+In the `pugs` compilation directory one should execute
+```shell
+cmake -DCMAKE_INSTALL_PREFIX=[pugs_install_dir] [pugs_src_path] [...]
+```
+where `[pugs_install_dir]` is the chosen installation directory.
+
+Then one simply runs
+```shell
+make install
+```
+
+## building the plugin `_PLUGIN_NAME_`
+
+> **Warning**:<br>
+> Building `_PLUGIN_NAME_` in its source directory is
+> **forbidden**. Trying to do so will result in a failure. However it
+> generally leaves some garbage files in the source directory, namely
+> the `CMakeCache.txt` and the `CMakeFiles` directory. `CMake` itself
+> is not able to remove them, to avoid the risk of compilation issues,
+> one has to dot it manually...
+
+In the plugin build directory one runs
+```shell
+cmake -DPUGS_PREFIX_PATH=[pugs_install_dir] [_PLUGIN_LOW__src_dir]
+```
+where `[pugs_install_dir]` has the same value as above and `[_PLUGIN_LOW__src_dir]`
+is the plugin source directory that contains this `README.md` file.
+
+Then to build the plugin, one runs
+```shell
+make
+```
+
+If anything runs fine, the dynamic library `lib_PLUGIN_NAME_.so` is
+built.
+
+# Using `_PLUGIN_NAME_`
+
+In order to use the created plugin, one simply has to give the
+location of `lib_PLUGIN_NAME_.so` to `pugs`. This is done by means of
+environment variables. There are two possibilities:
+- `PUGS_PLUGIN` contains a semicolumn separated list of plugin
+  libraries,
+- `PUGS_PLUGIN_DIR` contains a semicolumn separated list of path to
+  plugin libraries.
+
+Example
+```shell
+export PUGS_PLUGIN="/pathtoplugin/lib_PLUGIN_NAME_.so"
+```
+or
+```shell
+export PUGS_PLUGIN_DIR="/pathtoplugin1;/pathtoplugin2"
+```
+
+Then one launches `pugs` classically.
+```shell
+[pugs_install_dir]/bin/pugs [script.pgs]
+```
diff --git a/tools/plugin-template/tests-CMakeLists.txt-template b/tools/plugin-template/tests-CMakeLists.txt-template
new file mode 100644
index 0000000000000000000000000000000000000000..026ff2c2962ed988d35e2680b8b97919801c7a4b
--- /dev/null
+++ b/tools/plugin-template/tests-CMakeLists.txt-template
@@ -0,0 +1,95 @@
+set(EXECUTABLE_OUTPUT_PATH ${_PLUGIN_UP__BINARY_DIR})
+
+include_directories(${_PLUGIN_UP__SOURCE_DIR}/tests)
+
+add_executable (unit_tests
+  test_main.cpp
+
+  # add unit tests here
+  )
+
+  set(_PLUGIN_UP__checkpointing_TESTS
+  )
+
+if(PUGS_HAS_HDF5)
+  list(APPEND _PLUGIN_UP__checkpointing_TESTS
+  )
+endif(PUGS_HAS_HDF5)
+
+add_executable (mpi_unit_tests
+  mpi_test_main.cpp
+  ${_PLUGIN_UP__checkpointing_TESTS}
+
+  # add mpi unit tests here
+)
+
+add_library(test_MeshDataBase
+  MeshDataBaseForTests.cpp)
+
+add_library(test_ParallelCheckerTester
+  ParallelCheckerTester.cpp)
+
+target_link_libraries (test_ParallelCheckerTester
+  ${HIGHFIVE_TARGET})
+
+target_link_libraries (unit_tests
+  test_MeshDataBase
+  test_ParallelCheckerTester
+  PugsAlgebra
+  PugsAnalysis
+  PugsUtils
+  PugsLanguage
+  PugsLanguageAST
+  PugsLanguageModules
+  PugsMesh
+  PugsAlgebra
+  PugsUtils
+  PugsLanguageUtils
+  PugsScheme
+  PugsOutput
+  PugsUtils
+  PugsCheckpointing
+  PugsDev
+  PugsAlgebra
+  PugsMesh
+  Kokkos::kokkos
+  ${PARMETIS_LIBRARIES}
+  ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
+  ${PETSC_LIBRARIES}
+  Catch2
+  ${PUGS_STD_LINK_FLAGS}
+  ${HIGHFIVE_TARGET}
+  ${SLURM_LIBRARY}
+  stdc++fs
+  )
+
+target_link_libraries (mpi_unit_tests
+  test_MeshDataBase
+  test_ParallelCheckerTester
+  PugsAlgebra
+  PugsAnalysis
+  PugsUtils
+  PugsLanguage
+  PugsLanguageAST
+  PugsLanguageModules
+  PugsMesh
+  PugsAlgebra
+  PugsUtils
+  PugsLanguageUtils
+  PugsScheme
+  PugsOutput
+  PugsUtils
+  PugsCheckpointing
+  PugsDev
+  PugsAlgebra
+  PugsMesh
+  Kokkos::kokkos
+  ${PARMETIS_LIBRARIES}
+  ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
+  ${PETSC_LIBRARIES}
+  Catch2
+  ${PUGS_STD_LINK_FLAGS}
+  ${HIGHFIVE_TARGET}
+  ${SLURM_LIBRARY}
+  stdc++fs
+  )