diff --git a/src/algebra/TinyMatrix.hpp b/src/algebra/TinyMatrix.hpp
index 0d3b9c0e5fef46f85fa576b68f940708c8ed529d..e347650b362e32a9e37ccc58841b02674967aead 100644
--- a/src/algebra/TinyMatrix.hpp
+++ b/src/algebra/TinyMatrix.hpp
@@ -14,6 +14,10 @@ template <size_t M, size_t N = M, typename T = double>
 class [[nodiscard]] TinyMatrix
 {
  public:
+  inline static constexpr size_t Dimension       = M * N;
+  inline static constexpr size_t NumberOfRows    = M;
+  inline static constexpr size_t NumberOfColumns = N;
+
   using data_type = T;
 
  private:
diff --git a/src/algebra/TinyVector.hpp b/src/algebra/TinyVector.hpp
index 382741093f351cea80ff1e261399df9583dc4d0c..029b259f568866208919f5864980fdd3079dc975 100644
--- a/src/algebra/TinyVector.hpp
+++ b/src/algebra/TinyVector.hpp
@@ -15,7 +15,8 @@ class [[nodiscard]] TinyVector
 {
  public:
   inline static constexpr size_t Dimension = N;
-  using data_type                          = T;
+
+  using data_type = T;
 
  private:
   T m_values[N];
diff --git a/src/language/modules/MathModule.cpp b/src/language/modules/MathModule.cpp
index 69f6890fbc5e8c3e3515901acd5b5102100e8c35..2217424e43bb9b276cf2260ce88c547252f88982 100644
--- a/src/language/modules/MathModule.cpp
+++ b/src/language/modules/MathModule.cpp
@@ -70,6 +70,18 @@ MathModule::MathModule()
   this->_addBuiltinFunction("round", std::make_shared<BuiltinFunctionEmbedder<int64_t(double)>>(
                                        [](double x) -> int64_t { return std::lround(x); }));
 
+  this->_addBuiltinFunction("min", std::make_shared<BuiltinFunctionEmbedder<double(double, double)>>(
+                                     [](double x, double y) -> double { return std::min(x, y); }));
+
+  this->_addBuiltinFunction("min", std::make_shared<BuiltinFunctionEmbedder<int64_t(int64_t, int64_t)>>(
+                                     [](int64_t x, int64_t y) -> int64_t { return std::min(x, y); }));
+
+  this->_addBuiltinFunction("max", std::make_shared<BuiltinFunctionEmbedder<double(double, double)>>(
+                                     [](double x, double y) -> double { return std::max(x, y); }));
+
+  this->_addBuiltinFunction("max", std::make_shared<BuiltinFunctionEmbedder<int64_t(int64_t, int64_t)>>(
+                                     [](int64_t x, int64_t y) -> int64_t { return std::max(x, y); }));
+
   this->_addBuiltinFunction("dot",
                             std::make_shared<BuiltinFunctionEmbedder<double(const TinyVector<1>, const TinyVector<1>)>>(
                               [](const TinyVector<1> x, const TinyVector<1> y) -> double { return dot(x, y); }));
diff --git a/src/language/node_processor/BinaryExpressionProcessor.hpp b/src/language/node_processor/BinaryExpressionProcessor.hpp
index 8cc1a430f96d2892110afaff0b1ac8bc6ccaf347..7f278d4c7d4a007ad5db4c9f5d444665ccca56d6 100644
--- a/src/language/node_processor/BinaryExpressionProcessor.hpp
+++ b/src/language/node_processor/BinaryExpressionProcessor.hpp
@@ -260,7 +260,7 @@ struct BinaryExpressionProcessor<BinaryOpT, std::shared_ptr<ValueT>, std::shared
       return this->_eval(m_node.children[0]->execute(exec_policy), m_node.children[1]->execute(exec_policy));
     }
     catch (const NormalError& error) {
-      throw ParseError(error.what(), m_node.begin());
+      throw ParseError(error.what(), m_node.begin());   // LCOV_EXCL_LINE
     }
   }
 
@@ -297,7 +297,7 @@ struct BinaryExpressionProcessor<BinaryOpT, std::shared_ptr<ValueT>, A_DataT, st
       return this->_eval(m_node.children[0]->execute(exec_policy), m_node.children[1]->execute(exec_policy));
     }
     catch (const NormalError& error) {
-      throw ParseError(error.what(), m_node.begin());
+      throw ParseError(error.what(), m_node.begin());   // LCOV_EXCL_LINE
     }
   }
 
@@ -335,7 +335,7 @@ struct BinaryExpressionProcessor<BinaryOpT, std::shared_ptr<ValueT>, std::shared
       return this->_eval(m_node.children[0]->execute(exec_policy), m_node.children[1]->execute(exec_policy));
     }
     catch (const NormalError& error) {
-      throw ParseError(error.what(), m_node.begin());
+      throw ParseError(error.what(), m_node.begin());   // LCOV_EXCL_LINE
     }
   }
 
diff --git a/src/language/utils/CMakeLists.txt b/src/language/utils/CMakeLists.txt
index c8928003d045b7216b8edd8ea13b989d697d3268..cd3f8af2314e96e87bacec25ac1dfc08d015192b 100644
--- a/src/language/utils/CMakeLists.txt
+++ b/src/language/utils/CMakeLists.txt
@@ -23,8 +23,9 @@ add_library(PugsLanguageUtils
   BuiltinFunctionEmbedderUtils.cpp
   DataVariant.cpp
   EmbeddedData.cpp
-  EmbeddedIDiscreteFunctionOperators.cpp
   EmbeddedIDiscreteFunctionMathFunctions.cpp
+  EmbeddedIDiscreteFunctionOperators.cpp
+  EmbeddedIDiscreteFunctionUtils.cpp
   FunctionSymbolId.cpp
   IncDecOperatorRegisterForN.cpp
   IncDecOperatorRegisterForR.cpp
diff --git a/src/language/utils/EmbeddedIDiscreteFunctionMathFunctions.cpp b/src/language/utils/EmbeddedIDiscreteFunctionMathFunctions.cpp
index d4d10c6c7c531add6cd9f32c2ad376fcd97100a6..f49c4c033251bf46d7cd91e7c524af4d146db9d3 100644
--- a/src/language/utils/EmbeddedIDiscreteFunctionMathFunctions.cpp
+++ b/src/language/utils/EmbeddedIDiscreteFunctionMathFunctions.cpp
@@ -8,7 +8,7 @@
 #include <scheme/IDiscreteFunction.hpp>
 #include <scheme/IDiscreteFunctionDescriptor.hpp>
 
-#define DISCRETE_FUNCTION_CALL(FUNCTION, ARG)                                                                         \
+#define DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(FUNCTION, ARG)                                                           \
   if (ARG->dataType() == ASTNodeDataType::double_t and ARG->descriptor().type() == DiscreteFunctionType::P0) {        \
     switch (ARG->mesh()->dimension()) {                                                                               \
     case 1: {                                                                                                         \
@@ -28,55 +28,55 @@
     }                                                                                                                 \
     }                                                                                                                 \
   } else {                                                                                                            \
-    throw NormalError("invalid operand type " + operand_type_name(ARG));                                              \
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(ARG));                                       \
   }
 
 std::shared_ptr<const IDiscreteFunction>
 sqrt(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(sqrt, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(sqrt, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 abs(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(abs, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(abs, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 sin(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(sin, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(sin, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 cos(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(cos, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(cos, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 tan(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(tan, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(tan, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 asin(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(asin, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(asin, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 acos(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(acos, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(acos, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 atan(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(atan, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(atan, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
@@ -106,14 +106,14 @@ atan2(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<c
       return std::make_shared<const DiscreteFunctionType>(
         atan2(dynamic_cast<const DiscreteFunctionType&>(*f), dynamic_cast<const DiscreteFunctionType&>(*g)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(g);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
@@ -134,14 +134,14 @@ atan2(const double a, const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(atan2(a, dynamic_cast<const DiscreteFunctionType&>(*f)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(a) << " and " << operand_type_name(f);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -162,63 +162,63 @@ atan2(const std::shared_ptr<const IDiscreteFunction>& f, const double a)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(atan2(dynamic_cast<const DiscreteFunctionType&>(*f), a));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(a);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 }
 
 std::shared_ptr<const IDiscreteFunction>
 sinh(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(sinh, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(sinh, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 cosh(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(cosh, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(cosh, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 tanh(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(tanh, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(tanh, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 asinh(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(asinh, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(asinh, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 acosh(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(acosh, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(acosh, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 atanh(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(atanh, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(atanh, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 exp(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(exp, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(exp, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
 log(const std::shared_ptr<const IDiscreteFunction>& f)
 {
-  DISCRETE_FUNCTION_CALL(log, f);
+  DISCRETE_VH_TO_VH_REAL_FUNCTION_CALL(log, f);
 }
 
 std::shared_ptr<const IDiscreteFunction>
@@ -248,14 +248,14 @@ pow(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<con
       return std::make_shared<const DiscreteFunctionType>(
         pow(dynamic_cast<const DiscreteFunctionType&>(*f), dynamic_cast<const DiscreteFunctionType&>(*g)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(g);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
@@ -276,14 +276,14 @@ pow(const double a, const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(pow(a, dynamic_cast<const DiscreteFunctionType&>(*f)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(a) << " and " << operand_type_name(f);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -304,14 +304,14 @@ pow(const std::shared_ptr<const IDiscreteFunction>& f, const double a)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(pow(dynamic_cast<const DiscreteFunctionType&>(*f), a));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(a);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 }
 
@@ -361,9 +361,11 @@ dot(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<con
       return std::make_shared<const DiscreteFunctionResultType>(
         dot(dynamic_cast<const DiscreteFunctionType&>(*f), dynamic_cast<const DiscreteFunctionType&>(*g)));
     }
+      // LCOV_EXCL_START
     default: {
-      throw UnexpectedError("invalid data dimension " + operand_type_name(f));
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
     }
+      // LCOV_EXCL_STOP
     }
   }
 }
@@ -392,14 +394,14 @@ dot(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<con
     case 3: {
       return dot<3>(f, g);
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(g);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
@@ -432,14 +434,14 @@ dot(const std::shared_ptr<const IDiscreteFunction>& f, const TinyVector<VectorDi
     case 3: {
       return dot<3, VectorDimension>(f, a);
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(a);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 }
 
@@ -472,14 +474,14 @@ dot(const TinyVector<VectorDimension>& a, const std::shared_ptr<const IDiscreteF
     case 3: {
       return dot<3, VectorDimension>(a, f);
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(a) << " and " << operand_type_name(f);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -518,12 +520,14 @@ min(const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return min(dynamic_cast<const DiscreteFunctionType&>(*f));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    throw NormalError("invalid operand type " + operand_type_name(f));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
   }
 }
 
@@ -554,14 +558,14 @@ min(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<con
       return std::make_shared<const DiscreteFunctionType>(
         min(dynamic_cast<const DiscreteFunctionType&>(*f), dynamic_cast<const DiscreteFunctionType&>(*g)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(g);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
@@ -582,14 +586,14 @@ min(const double a, const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(min(a, dynamic_cast<const DiscreteFunctionType&>(*f)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(a) << " and " << operand_type_name(f);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -610,14 +614,14 @@ min(const std::shared_ptr<const IDiscreteFunction>& f, const double a)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(min(dynamic_cast<const DiscreteFunctionType&>(*f), a));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(a);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 }
 
@@ -638,12 +642,14 @@ max(const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return max(dynamic_cast<const DiscreteFunctionType&>(*f));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    throw NormalError("invalid operand type " + operand_type_name(f));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
   }
 }
 
@@ -674,14 +680,14 @@ max(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<con
       return std::make_shared<const DiscreteFunctionType>(
         max(dynamic_cast<const DiscreteFunctionType&>(*f), dynamic_cast<const DiscreteFunctionType&>(*g)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(g);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
@@ -702,14 +708,14 @@ max(const double a, const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(max(a, dynamic_cast<const DiscreteFunctionType&>(*f)));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(a) << " and " << operand_type_name(f);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -730,14 +736,14 @@ max(const std::shared_ptr<const IDiscreteFunction>& f, const double a)
       using DiscreteFunctionType = DiscreteFunctionP0<3, double>;
       return std::make_shared<const DiscreteFunctionType>(max(dynamic_cast<const DiscreteFunctionType&>(*f), a));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    std::stringstream os;
-    os << "incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(a);
-    throw NormalError(os.str());
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 }
 
@@ -759,12 +765,14 @@ sum_of(const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, ValueT>;
       return sum(dynamic_cast<const DiscreteFunctionType&>(*f));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    throw NormalError("invalid operand type " + operand_type_name(f));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
   }
 }
 
@@ -800,12 +808,14 @@ integral_of(const std::shared_ptr<const IDiscreteFunction>& f)
       using DiscreteFunctionType = DiscreteFunctionP0<3, ValueT>;
       return integrate(dynamic_cast<const DiscreteFunctionType&>(*f));
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid mesh dimension");
     }
+      // LCOV_EXCL_STOP
     }
   } else {
-    throw NormalError("invalid operand type " + operand_type_name(f));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
   }
 }
 
diff --git a/src/language/utils/EmbeddedIDiscreteFunctionOperators.cpp b/src/language/utils/EmbeddedIDiscreteFunctionOperators.cpp
index 13def86d07990370bb7dce658dde09f8d06b565a..656164d6fc012ef2469f846d3e54f32a9c1d4927 100644
--- a/src/language/utils/EmbeddedIDiscreteFunctionOperators.cpp
+++ b/src/language/utils/EmbeddedIDiscreteFunctionOperators.cpp
@@ -9,16 +9,6 @@
 #include <scheme/IDiscreteFunction.hpp>
 #include <utils/Exceptions.hpp>
 
-template <typename LHS_T, typename RHS_T>
-PUGS_INLINE std::string
-invalid_operands(const LHS_T& f, const RHS_T& g)
-{
-  std::ostringstream os;
-  os << "undefined binary operator\n";
-  os << "note: incompatible operand types " << operand_type_name(f) << " and " << operand_type_name(g);
-  return os.str();
-}
-
 // unary operators
 template <typename UnaryOperatorT, typename DiscreteFunctionT>
 std::shared_ptr<const IDiscreteFunction>
@@ -52,9 +42,11 @@ applyUnaryOperation(const std::shared_ptr<const IDiscreteFunction>& f)
         auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<3>>&>(*f);
         return applyUnaryOperation<UnaryOperatorT>(fh);
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid operand type " + operand_type_name(f));
+        throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
       }
+        // LCOV_EXCL_STOP
       }
     }
     case ASTNodeDataType::matrix_t: {
@@ -72,14 +64,18 @@ applyUnaryOperation(const std::shared_ptr<const IDiscreteFunction>& f)
         auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
         return applyUnaryOperation<UnaryOperatorT>(fh);
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid operand type " + operand_type_name(f));
+        throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
       }
+        // LCOV_EXCL_STOP
       }
     }
+      // LCOV_EXCL_START
     default: {
-      throw UnexpectedError("invalid operand type " + operand_type_name(f));
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
     }
+      // LCOV_EXCL_STOP
     }
     break;
   }
@@ -89,15 +85,19 @@ applyUnaryOperation(const std::shared_ptr<const IDiscreteFunction>& f)
       auto fh = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*f);
       return applyUnaryOperation<UnaryOperatorT>(fh);
     }
+      // LCOV_EXCL_START
     default: {
-      throw UnexpectedError("invalid operand type " + operand_type_name(f));
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
     }
+      // LCOV_EXCL_STOP
     }
     break;
   }
+    // LCOV_EXCL_START
   default: {
-    throw UnexpectedError("invalid operand type " + operand_type_name(f));
+    throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -115,9 +115,11 @@ applyUnaryOperation(const std::shared_ptr<const IDiscreteFunction>& f)
   case 3: {
     return applyUnaryOperation<UnaryOperatorT, 3>(f);
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid mesh dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -137,7 +139,7 @@ innerCompositionLaw(const DiscreteFunctionT& lhs, const DiscreteFunctionT& rhs)
   using data_type = typename DiscreteFunctionT::data_type;
   if constexpr ((std::is_same_v<language::multiply_op, BinOperatorT> and is_tiny_vector_v<data_type>) or
                 (std::is_same_v<language::divide_op, BinOperatorT> and not std::is_arithmetic_v<data_type>)) {
-    throw NormalError(invalid_operands(lhs, rhs));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(lhs, rhs));
   } else {
     return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(lhs, rhs))>(BinOp<BinOperatorT>{}.eval(lhs, rhs));
   }
@@ -149,7 +151,7 @@ innerCompositionLaw(const std::shared_ptr<const IDiscreteFunction>& f,
                     const std::shared_ptr<const IDiscreteFunction>& g)
 {
   Assert(f->mesh() == g->mesh());
-  Assert(isSameDiscretization(f, g));
+  Assert(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g));
 
   switch (f->dataType()) {
   case ASTNodeDataType::double_t: {
@@ -166,15 +168,17 @@ innerCompositionLaw(const std::shared_ptr<const IDiscreteFunction>& f,
         auto gh = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*g);
 
         if (fh.size() != gh.size()) {
-          throw NormalError(operand_type_name(f) + " spaces have different sizes");
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f) + " spaces have different sizes");
         }
 
         return innerCompositionLaw<BinOperatorT>(fh, gh);
       } else {
-        throw NormalError(invalid_operands(f, g));
+        throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
       }
     } else {
-      throw UnexpectedError(invalid_operands(f, g));
+      // LCOV_EXCL_START
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
+      // LCOV_EXCL_STOP
     }
   }
   case ASTNodeDataType::vector_t: {
@@ -198,9 +202,11 @@ innerCompositionLaw(const std::shared_ptr<const IDiscreteFunction>& f,
 
       return innerCompositionLaw<BinOperatorT>(fh, gh);
     }
+      // LCOV_EXCL_START
     default: {
-      throw NormalError(invalid_operands(f, g));
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
     }
+      // LCOV_EXCL_STOP
     }
   }
   case ASTNodeDataType::matrix_t: {
@@ -225,14 +231,18 @@ innerCompositionLaw(const std::shared_ptr<const IDiscreteFunction>& f,
 
       return innerCompositionLaw<BinOperatorT>(fh, gh);
     }
+      // LCOV_EXCL_START
     default: {
-      throw UnexpectedError("invalid data type " + operand_type_name(f));
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
     }
+      // LCOV_EXCL_STOP
     }
   }
+    // LCOV_EXCL_START
   default: {
-    throw UnexpectedError("invalid data type " + operand_type_name(f));
+    throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::invalidOperandType(f));
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -246,9 +256,7 @@ innerCompositionLaw(const std::shared_ptr<const IDiscreteFunction>& f,
     throw NormalError("operands are defined on different meshes");
   }
 
-  if (not isSameDiscretization(f, g)) {
-    throw NormalError(invalid_operands(f, g));
-  }
+  Assert(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g));
 
   switch (mesh->dimension()) {
   case 1: {
@@ -260,9 +268,11 @@ innerCompositionLaw(const std::shared_ptr<const IDiscreteFunction>& f,
   case 3: {
     return innerCompositionLaw<BinOperatorT, 3>(f, g);
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid mesh dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -283,29 +293,23 @@ std::shared_ptr<const IDiscreteFunction>
 applyBinaryOperation(const DiscreteFunctionT& fh, const std::shared_ptr<const IDiscreteFunction>& g)
 {
   Assert(fh.mesh() == g->mesh());
-  Assert(not isSameDiscretization(fh, *g));
+  Assert(not EmbeddedIDiscreteFunctionUtils::isSameDiscretization(fh, *g));
   using lhs_data_type = std::decay_t<typename DiscreteFunctionT::data_type>;
 
   switch (g->dataType()) {
   case ASTNodeDataType::double_t: {
-    if constexpr (not std::is_same_v<lhs_data_type, double>) {
-      if constexpr (not is_tiny_matrix_v<lhs_data_type>) {
-        auto gh = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*g);
-
-        return applyBinaryOperation<BinOperatorT>(fh, gh);
-      } else {
-        throw NormalError(invalid_operands(fh, g));
-      }
-    } else if constexpr (std::is_same_v<BinOperatorT, language::multiply_op> and
-                         std::is_same_v<DiscreteFunctionT, DiscreteFunctionP0<Dimension, double>>) {
+    if constexpr (std::is_same_v<BinOperatorT, language::multiply_op> and
+                  std::is_same_v<DiscreteFunctionT, DiscreteFunctionP0<Dimension, double>>) {
       if (g->descriptor().type() == DiscreteFunctionType::P0Vector) {
         auto gh = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*g);
         return applyBinaryOperation<BinOperatorT>(fh, gh);
       } else {
-        throw NormalError(invalid_operands(fh, g));
+        // LCOV_EXCL_START
+        throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
+        // LCOV_EXCL_STOP
       }
     } else {
-      throw UnexpectedError("should have called innerCompositionLaw");
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
     }
   }
   case ASTNodeDataType::vector_t: {
@@ -318,7 +322,7 @@ applyBinaryOperation(const DiscreteFunctionT& fh, const std::shared_ptr<const ID
 
           return applyBinaryOperation<BinOperatorT>(fh, gh);
         } else {
-          throw NormalError(invalid_operands(fh, g));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
         }
       }
       case 2: {
@@ -328,7 +332,7 @@ applyBinaryOperation(const DiscreteFunctionT& fh, const std::shared_ptr<const ID
 
           return applyBinaryOperation<BinOperatorT>(fh, gh);
         } else {
-          throw NormalError(invalid_operands(fh, g));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
         }
       }
       case 3: {
@@ -338,15 +342,17 @@ applyBinaryOperation(const DiscreteFunctionT& fh, const std::shared_ptr<const ID
 
           return applyBinaryOperation<BinOperatorT>(fh, gh);
         } else {
-          throw NormalError(invalid_operands(fh, g));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
         }
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid rhs data type " + operand_type_name(g));
+        throw UnexpectedError("invalid rhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(g));
       }
+        // LCOV_EXCL_STOP
       }
     } else {
-      throw NormalError(invalid_operands(fh, g));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
     }
   }
   case ASTNodeDataType::matrix_t: {
@@ -368,17 +374,21 @@ applyBinaryOperation(const DiscreteFunctionT& fh, const std::shared_ptr<const ID
 
         return applyBinaryOperation<BinOperatorT>(fh, gh);
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid rhs data type " + operand_type_name(g));
+        throw UnexpectedError("invalid rhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(g));
       }
+        // LCOV_EXCL_STOP
       }
     } else {
-      throw NormalError(invalid_operands(fh, g));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(fh, g));
     }
   }
+    // LCOV_EXCL_START
   default: {
-    throw UnexpectedError("invalid rhs data type " + operand_type_name(g));
+    throw UnexpectedError("invalid rhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(g));
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -388,40 +398,45 @@ applyBinaryOperation(const std::shared_ptr<const IDiscreteFunction>& f,
                      const std::shared_ptr<const IDiscreteFunction>& g)
 {
   Assert(f->mesh() == g->mesh());
-  Assert(not isSameDiscretization(f, g));
-
-  switch (f->dataType()) {
-  case ASTNodeDataType::double_t: {
-    auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*f);
-
-    return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
-  }
-  case ASTNodeDataType::matrix_t: {
-    Assert(f->dataType().numberOfRows() == f->dataType().numberOfColumns());
-    switch (f->dataType().numberOfRows()) {
-    case 1: {
-      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>&>(*f);
+  Assert(not EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g));
 
+  if (f->descriptor().type() == DiscreteFunctionType::P0) {
+    switch (f->dataType()) {
+    case ASTNodeDataType::double_t: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*f);
       return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
     }
-    case 2: {
-      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>&>(*f);
+    case ASTNodeDataType::matrix_t: {
+      Assert(f->dataType().numberOfRows() == f->dataType().numberOfColumns());
+      switch (f->dataType().numberOfRows()) {
+      case 1: {
+        auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>&>(*f);
 
-      return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
-    }
-    case 3: {
-      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
+        return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
+      }
+      case 2: {
+        auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>&>(*f);
 
-      return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
+        return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
+      }
+      case 3: {
+        auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
+
+        return applyBinaryOperation<BinOperatorT, Dimension>(fh, g);
+      }
+        // LCOV_EXCL_START
+      default: {
+        throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
+      }
+        // LCOV_EXCL_STOP
+      }
     }
     default: {
-      throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
     }
     }
-  }
-  default: {
-    throw NormalError(invalid_operands(f, g));
-  }
+  } else {
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
@@ -435,7 +450,7 @@ applyBinaryOperation(const std::shared_ptr<const IDiscreteFunction>& f,
     throw NormalError("operands are defined on different meshes");
   }
 
-  Assert(not isSameDiscretization(f, g), "should call inner composition instead");
+  Assert(not EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g), "should call inner composition instead");
 
   switch (mesh->dimension()) {
   case 1: {
@@ -447,36 +462,38 @@ applyBinaryOperation(const std::shared_ptr<const IDiscreteFunction>& f,
   case 3: {
     return applyBinaryOperation<BinOperatorT, 3>(f, g);
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid mesh dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
 
 std::shared_ptr<const IDiscreteFunction>
 operator+(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<const IDiscreteFunction>& g)
 {
-  if (isSameDiscretization(f, g)) {
+  if (EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g)) {
     return innerCompositionLaw<language::plus_op>(f, g);
   } else {
-    throw NormalError(invalid_operands(f, g));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
 std::shared_ptr<const IDiscreteFunction>
 operator-(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<const IDiscreteFunction>& g)
 {
-  if (isSameDiscretization(f, g)) {
+  if (EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g)) {
     return innerCompositionLaw<language::minus_op>(f, g);
   } else {
-    throw NormalError(invalid_operands(f, g));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, g));
   }
 }
 
 std::shared_ptr<const IDiscreteFunction>
 operator*(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<const IDiscreteFunction>& g)
 {
-  if (isSameDiscretization(f, g)) {
+  if (EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g)) {
     return innerCompositionLaw<language::multiply_op>(f, g);
   } else {
     return applyBinaryOperation<language::multiply_op>(f, g);
@@ -486,7 +503,7 @@ operator*(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_p
 std::shared_ptr<const IDiscreteFunction>
 operator/(const std::shared_ptr<const IDiscreteFunction>& f, const std::shared_ptr<const IDiscreteFunction>& g)
 {
-  if (isSameDiscretization(f, g)) {
+  if (EmbeddedIDiscreteFunctionUtils::isSameDiscretization(f, g)) {
     return innerCompositionLaw<language::divide_op>(f, g);
   } else {
     return applyBinaryOperation<language::divide_op>(f, g);
@@ -507,7 +524,7 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const DiscreteFunctionT&
                          (is_tiny_matrix_v<rhs_data_type> or is_tiny_vector_v<rhs_data_type>)) {
       return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(a, f))>(BinOp<BinOperatorT>{}.eval(a, f));
     } else {
-      throw NormalError(invalid_operands(a, f));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
     }
   } else if constexpr (std::is_same_v<language::plus_op, BinOperatorT> or
                        std::is_same_v<language::minus_op, BinOperatorT>) {
@@ -516,16 +533,18 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const DiscreteFunctionT&
     } else if constexpr (std::is_same_v<lhs_data_type, rhs_data_type>) {
       return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(a, f))>(BinOp<BinOperatorT>{}.eval(a, f));
     } else {
-      throw NormalError(invalid_operands(a, f));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
     }
   } else if constexpr (std::is_same_v<language::divide_op, BinOperatorT>) {
     if constexpr (std::is_same_v<lhs_data_type, double> and std::is_arithmetic_v<rhs_data_type>) {
       return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(a, f))>(BinOp<BinOperatorT>{}.eval(a, f));
     } else {
-      throw NormalError(invalid_operands(a, f));
+      // LCOV_EXCL_START
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
+      // LCOV_EXCL_STOP
     }
   } else {
-    throw NormalError(invalid_operands(a, f));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -543,10 +562,10 @@ applyBinaryOperationToVectorWithLeftConstant(const DataType& a, const DiscreteFu
                          (is_tiny_matrix_v<rhs_data_type> or is_tiny_vector_v<rhs_data_type>)) {
       return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(a, f))>(BinOp<BinOperatorT>{}.eval(a, f));
     } else {
-      throw NormalError(invalid_operands(a, f));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
     }
   } else {
-    throw NormalError(invalid_operands(a, f));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
 }
 
@@ -566,7 +585,9 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
       auto fh = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*f);
       return applyBinaryOperationToVectorWithLeftConstant<BinOperatorT>(a, fh);
     } else {
-      throw NormalError(invalid_operands(a, f));
+      // LCOV_EXCL_START
+      throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
+      // LCOV_EXCL_STOP
     }
   }
   case ASTNodeDataType::vector_t: {
@@ -577,7 +598,7 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
           auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<1>>&>(*f);
           return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
         } else {
-          throw NormalError(invalid_operands(a, f));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
         }
       }
       case 2: {
@@ -585,7 +606,7 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
           auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<2>>&>(*f);
           return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
         } else {
-          throw NormalError(invalid_operands(a, f));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
         }
       }
       case 3: {
@@ -593,12 +614,14 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
           auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<3>>&>(*f);
           return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
         } else {
-          throw NormalError(invalid_operands(a, f));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
         }
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
+        throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
       }
+        // LCOV_EXCL_STOP
       }
     } else {
       switch (f->dataType().dimension()) {
@@ -614,9 +637,11 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
         auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<3>>&>(*f);
         return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
+        throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
       }
+        // LCOV_EXCL_STOP
       }
     }
   }
@@ -629,7 +654,7 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
           auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>&>(*f);
           return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
         } else {
-          throw NormalError(invalid_operands(a, f));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
         }
       }
       case 2: {
@@ -637,7 +662,7 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
           auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>&>(*f);
           return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
         } else {
-          throw NormalError(invalid_operands(a, f));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
         }
       }
       case 3: {
@@ -645,12 +670,14 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
           auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
           return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
         } else {
-          throw NormalError(invalid_operands(a, f));
+          throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
         }
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
+        throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
       }
+        // LCOV_EXCL_STOP
       }
     } else {
       switch (f->dataType().numberOfRows()) {
@@ -666,15 +693,19 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
         auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
         return applyBinaryOperationWithLeftConstant<BinOperatorT>(a, fh);
       }
+        // LCOV_EXCL_START
       default: {
-        throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
+        throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
       }
+        // LCOV_EXCL_STOP
       }
     }
   }
+    // LCOV_EXCL_START
   default: {
-    throw NormalError(invalid_operands(a, f));
+    throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(a, f));
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -692,9 +723,11 @@ applyBinaryOperationWithLeftConstant(const DataType& a, const std::shared_ptr<co
   case 3: {
     return applyBinaryOperationWithLeftConstant<BinOperatorT, 3>(a, f);
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid mesh dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -709,13 +742,17 @@ applyBinaryOperationWithRightConstant(const DiscreteFunctionT& f, const DataType
 
   if constexpr (std::is_same_v<language::multiply_op, BinOperatorT>) {
     if constexpr (is_tiny_matrix_v<lhs_data_type> and is_tiny_matrix_v<rhs_data_type>) {
-      return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(f, a))>(BinOp<BinOperatorT>{}.eval(f, a));
+      if constexpr (lhs_data_type::NumberOfColumns == rhs_data_type::NumberOfRows) {
+        return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(f, a))>(BinOp<BinOperatorT>{}.eval(f, a));
+      } else {
+        throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
+      }
     } else if constexpr (std::is_same_v<lhs_data_type, double> and
                          (is_tiny_matrix_v<rhs_data_type> or is_tiny_vector_v<rhs_data_type> or
                           std::is_arithmetic_v<rhs_data_type>)) {
       return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(f, a))>(BinOp<BinOperatorT>{}.eval(f, a));
     } else {
-      throw NormalError(invalid_operands(f, a));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
     }
   } else if constexpr (std::is_same_v<language::plus_op, BinOperatorT> or
                        std::is_same_v<language::minus_op, BinOperatorT>) {
@@ -723,10 +760,10 @@ applyBinaryOperationWithRightConstant(const DiscreteFunctionT& f, const DataType
                   (std::is_arithmetic_v<lhs_data_type> and std::is_arithmetic_v<rhs_data_type>)) {
       return std::make_shared<decltype(BinOp<BinOperatorT>{}.eval(f, a))>(BinOp<BinOperatorT>{}.eval(f, a));
     } else {
-      throw NormalError(invalid_operands(f, a));
+      throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
     }
   } else {
-    throw NormalError(invalid_operands(f, a));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 }
 
@@ -735,7 +772,7 @@ std::shared_ptr<const IDiscreteFunction>
 applyBinaryOperationWithRightConstant(const std::shared_ptr<const IDiscreteFunction>& f, const DataType& a)
 {
   if (f->descriptor().type() != DiscreteFunctionType::P0) {
-    throw NormalError(invalid_operands(f, a));
+    throw NormalError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
 
   switch (f->dataType()) {
@@ -746,61 +783,54 @@ applyBinaryOperationWithRightConstant(const std::shared_ptr<const IDiscreteFunct
     auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*f);
     return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
   }
+  case ASTNodeDataType::vector_t: {
+    switch (f->dataType().dimension()) {
+    case 1: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<1>>&>(*f);
+      return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
+    }
+    case 2: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<2>>&>(*f);
+      return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
+    }
+    case 3: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyVector<3>>&>(*f);
+      return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
+    }
+      // LCOV_EXCL_START
+    default: {
+      throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
+    }
+      // LCOV_EXCL_STOP
+    }
+  }
   case ASTNodeDataType::matrix_t: {
     Assert(f->dataType().numberOfRows() == f->dataType().numberOfColumns());
-    if constexpr (is_tiny_matrix_v<DataType>) {
-      switch (f->dataType().numberOfRows()) {
-      case 1: {
-        if constexpr (std::is_same_v<DataType, TinyMatrix<1>>) {
-          auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>&>(*f);
-          return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
-        } else {
-          throw NormalError(invalid_operands(f, a));
-        }
-      }
-      case 2: {
-        if constexpr (std::is_same_v<DataType, TinyMatrix<2>>) {
-          auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>&>(*f);
-          return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
-        } else {
-          throw NormalError(invalid_operands(f, a));
-        }
-      }
-      case 3: {
-        if constexpr (std::is_same_v<DataType, TinyMatrix<3>>) {
-          auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
-          return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
-        } else {
-          throw NormalError(invalid_operands(f, a));
-        }
-      }
-      default: {
-        throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
-      }
-      }
-    } else {
-      switch (f->dataType().numberOfRows()) {
-      case 1: {
-        auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>&>(*f);
-        return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
-      }
-      case 2: {
-        auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>&>(*f);
-        return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
-      }
-      case 3: {
-        auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
-        return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
-      }
-      default: {
-        throw UnexpectedError("invalid lhs data type " + operand_type_name(f));
-      }
-      }
+    switch (f->dataType().numberOfRows()) {
+    case 1: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>&>(*f);
+      return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
+    }
+    case 2: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>&>(*f);
+      return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
+    }
+    case 3: {
+      auto fh = dynamic_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>&>(*f);
+      return applyBinaryOperationWithRightConstant<BinOperatorT>(fh, a);
+    }
+      // LCOV_EXCL_START
+    default: {
+      throw UnexpectedError("invalid lhs data type " + EmbeddedIDiscreteFunctionUtils::getOperandTypeName(f));
+    }
+      // LCOV_EXCL_STOP
     }
   }
+    // LCOV_EXCL_START
   default: {
-    throw NormalError(invalid_operands(f, a));
+    throw UnexpectedError(EmbeddedIDiscreteFunctionUtils::incompatibleOperandTypes(f, a));
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -818,9 +848,11 @@ applyBinaryOperationWithRightConstant(const std::shared_ptr<const IDiscreteFunct
   case 3: {
     return applyBinaryOperationWithRightConstant<BinOperatorT, 3>(f, a);
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid mesh dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
 
diff --git a/src/language/utils/EmbeddedIDiscreteFunctionUtils.cpp b/src/language/utils/EmbeddedIDiscreteFunctionUtils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..286988b9b2849fb34dc84948be05414ebbad6d5b
--- /dev/null
+++ b/src/language/utils/EmbeddedIDiscreteFunctionUtils.cpp
@@ -0,0 +1,27 @@
+#include <language/utils/EmbeddedIDiscreteFunctionUtils.hpp>
+
+#include <utils/Exceptions.hpp>
+
+bool
+EmbeddedIDiscreteFunctionUtils::isSameDiscretization(const IDiscreteFunction& f, const IDiscreteFunction& g)
+{
+  if ((f.dataType() == g.dataType()) and (f.descriptor().type() == g.descriptor().type())) {
+    switch (f.dataType()) {
+    case ASTNodeDataType::double_t: {
+      return true;
+    }
+    case ASTNodeDataType::vector_t: {
+      return f.dataType().dimension() == g.dataType().dimension();
+    }
+    case ASTNodeDataType::matrix_t: {
+      return (f.dataType().numberOfRows() == g.dataType().numberOfRows()) and
+             (f.dataType().numberOfColumns() == g.dataType().numberOfColumns());
+    }
+    default: {
+      throw UnexpectedError("invalid data type " + getOperandTypeName(f));
+    }
+    }
+  } else {
+    return false;
+  }
+}
diff --git a/src/language/utils/EmbeddedIDiscreteFunctionUtils.hpp b/src/language/utils/EmbeddedIDiscreteFunctionUtils.hpp
index 981cd8be9ca166038b5aa4b913dae63488868b64..1e61f917dfa448add18b562dd403a70101dbec3b 100644
--- a/src/language/utils/EmbeddedIDiscreteFunctionUtils.hpp
+++ b/src/language/utils/EmbeddedIDiscreteFunctionUtils.hpp
@@ -1,58 +1,50 @@
 #ifndef EMBEDDED_I_DISCRETE_FUNCTION_UTILS_HPP
 #define EMBEDDED_I_DISCRETE_FUNCTION_UTILS_HPP
 
+#include <language/utils/ASTNodeDataType.hpp>
 #include <scheme/IDiscreteFunction.hpp>
 #include <scheme/IDiscreteFunctionDescriptor.hpp>
-#include <utils/Exceptions.hpp>
 
-#include <sstream>
 #include <string>
 
-template <typename T>
-PUGS_INLINE std::string
-operand_type_name(const T& t)
+struct EmbeddedIDiscreteFunctionUtils
 {
-  if constexpr (is_shared_ptr_v<T>) {
-    Assert(t.use_count() > 0);
-    return operand_type_name(*t);
-  } else if constexpr (std::is_base_of_v<IDiscreteFunction, std::decay_t<T>>) {
-    return "Vh(" + name(t.descriptor().type()) + ':' + dataTypeName(t.dataType()) + ')';
-  } else {
-    return dataTypeName(ast_node_data_type_from<T>);
+  template <typename T>
+  static PUGS_INLINE std::string
+  getOperandTypeName(const T& t)
+  {
+    if constexpr (is_shared_ptr_v<T>) {
+      Assert(t.use_count() > 0, "dangling shared_ptr");
+      return getOperandTypeName(*t);
+    } else if constexpr (std::is_base_of_v<IDiscreteFunction, std::decay_t<T>>) {
+      return "Vh(" + name(t.descriptor().type()) + ':' + dataTypeName(t.dataType()) + ')';
+    } else {
+      return dataTypeName(ast_node_data_type_from<T>);
+    }
   }
-}
 
-PUGS_INLINE
-bool
-isSameDiscretization(const IDiscreteFunction& f, const IDiscreteFunction& g)
-{
-  if ((f.dataType() == g.dataType()) and (f.descriptor().type() == g.descriptor().type())) {
-    switch (f.dataType()) {
-    case ASTNodeDataType::double_t: {
-      return true;
-    }
-    case ASTNodeDataType::vector_t: {
-      return f.dataType().dimension() == g.dataType().dimension();
-    }
-    case ASTNodeDataType::matrix_t: {
-      return (f.dataType().numberOfRows() == g.dataType().numberOfRows()) and
-             (f.dataType().numberOfColumns() == g.dataType().numberOfColumns());
-    }
-    default: {
-      throw UnexpectedError("invalid data type " + operand_type_name(f));
-    }
-    }
-  } else {
-    return false;
+  static bool isSameDiscretization(const IDiscreteFunction& f, const IDiscreteFunction& g);
+
+  static PUGS_INLINE bool
+  isSameDiscretization(const std::shared_ptr<const IDiscreteFunction>& f,
+                       const std::shared_ptr<const IDiscreteFunction>& g)
+  {
+    return isSameDiscretization(*f, *g);
   }
-}
 
-PUGS_INLINE
-bool
-isSameDiscretization(const std::shared_ptr<const IDiscreteFunction>& f,
-                     const std::shared_ptr<const IDiscreteFunction>& g)
-{
-  return isSameDiscretization(*f, *g);
-}
+  template <typename T1, typename T2>
+  PUGS_INLINE static std::string
+  incompatibleOperandTypes(const T1& t1, const T2& t2)
+  {
+    return "incompatible operand types " + getOperandTypeName(t1) + " and " + getOperandTypeName(t2);
+  }
+
+  template <typename T>
+  PUGS_INLINE static std::string
+  invalidOperandType(const T& t)
+  {
+    return "invalid operand type " + getOperandTypeName(t);
+  }
+};
 
 #endif   // EMBEDDED_I_DISCRETE_FUNCTION_UTILS_HPP
diff --git a/src/mesh/CartesianMeshBuilder.cpp b/src/mesh/CartesianMeshBuilder.cpp
index d6a4589cced3958ca7401a5a80d881d23fe87749..6b7ef2b26f8aa8fbe1d78ab24f606eb8c335340d 100644
--- a/src/mesh/CartesianMeshBuilder.cpp
+++ b/src/mesh/CartesianMeshBuilder.cpp
@@ -125,14 +125,14 @@ CartesianMeshBuilder::CartesianMeshBuilder(const TinyVector<Dimension>& a,
                                            const TinyVector<Dimension>& b,
                                            const TinyVector<Dimension, uint64_t>& size)
 {
-  if (parallel::rank() == 0) {
-    TinyVector lenght = b - a;
-    for (size_t i = 0; i < Dimension; ++i) {
-      if (lenght[i] == 0) {
-        throw NormalError("invalid box definition corners share a component");
-      }
+  TinyVector lenght = b - a;
+  for (size_t i = 0; i < Dimension; ++i) {
+    if (lenght[i] == 0) {
+      throw NormalError("invalid box definition corners share a component");
     }
+  }
 
+  if (parallel::rank() == 0) {
     TinyVector<Dimension> corner0 = a;
     TinyVector<Dimension> corner1 = b;
 
diff --git a/src/scheme/DiscreteFunctionInterpoler.cpp b/src/scheme/DiscreteFunctionInterpoler.cpp
index 11b19835eab3af91c0ea125359364831916cbc18..c39b6ee9d7020b1ffee3582ee36bd461a0c03435 100644
--- a/src/scheme/DiscreteFunctionInterpoler.cpp
+++ b/src/scheme/DiscreteFunctionInterpoler.cpp
@@ -4,7 +4,7 @@
 #include <scheme/DiscreteFunctionP0.hpp>
 #include <utils/Exceptions.hpp>
 
-template <size_t Dimension, typename DataType>
+template <size_t Dimension, typename DataType, typename ValueType>
 std::shared_ptr<IDiscreteFunction>
 DiscreteFunctionInterpoler::_interpolate() const
 {
@@ -12,10 +12,25 @@ DiscreteFunctionInterpoler::_interpolate() const
   using MeshDataType      = MeshData<Dimension>;
   MeshDataType& mesh_data = MeshDataManager::instance().getMeshData(*mesh);
 
-  return std::make_shared<
-    DiscreteFunctionP0<Dimension, DataType>>(mesh,
-                                             InterpolateItemValue<DataType(TinyVector<Dimension>)>::
-                                               template interpolate<ItemType::cell>(m_function_id, mesh_data.xj()));
+  if constexpr (std::is_same_v<DataType, ValueType>) {
+    return std::make_shared<
+      DiscreteFunctionP0<Dimension, ValueType>>(mesh,
+                                                InterpolateItemValue<DataType(TinyVector<Dimension>)>::
+                                                  template interpolate<ItemType::cell>(m_function_id, mesh_data.xj()));
+  } else {
+    static_assert(std::is_convertible_v<DataType, ValueType>);
+
+    CellValue<DataType> cell_data =
+      InterpolateItemValue<DataType(TinyVector<Dimension>)>::template interpolate<ItemType::cell>(m_function_id,
+                                                                                                  mesh_data.xj());
+
+    CellValue<ValueType> cell_value{mesh->connectivity()};
+
+    parallel_for(
+      mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_data[cell_id]; });
+
+    return std::make_shared<DiscreteFunctionP0<Dimension, ValueType>>(mesh, cell_value);
+  }
 }
 
 template <size_t Dimension>
@@ -29,13 +44,13 @@ DiscreteFunctionInterpoler::_interpolate() const
 
   switch (data_type) {
   case ASTNodeDataType::bool_t: {
-    return this->_interpolate<Dimension, bool>();
+    return this->_interpolate<Dimension, bool, double>();
   }
   case ASTNodeDataType::unsigned_int_t: {
-    return this->_interpolate<Dimension, uint64_t>();
+    return this->_interpolate<Dimension, uint64_t, double>();
   }
   case ASTNodeDataType::int_t: {
-    return this->_interpolate<Dimension, int64_t>();
+    return this->_interpolate<Dimension, int64_t, double>();
   }
   case ASTNodeDataType::double_t: {
     return this->_interpolate<Dimension, double>();
@@ -51,12 +66,14 @@ DiscreteFunctionInterpoler::_interpolate() const
     case 3: {
       return this->_interpolate<Dimension, TinyVector<3>>();
     }
+      // LCOV_EXCL_START
     default: {
       std::ostringstream os;
       os << "invalid vector dimension " << rang::fgB::red << data_type.dimension() << rang::style::reset;
 
       throw UnexpectedError(os.str());
     }
+      // LCOV_EXCL_STOP
     }
   }
   case ASTNodeDataType::matrix_t: {
@@ -71,20 +88,24 @@ DiscreteFunctionInterpoler::_interpolate() const
     case 3: {
       return this->_interpolate<Dimension, TinyMatrix<3>>();
     }
+      // LCOV_EXCL_START
     default: {
       std::ostringstream os;
       os << "invalid vector dimension " << rang::fgB::red << data_type.dimension() << rang::style::reset;
 
       throw UnexpectedError(os.str());
     }
+      // LCOV_EXCL_STOP
     }
   }
+    // LCOV_EXCL_START
   default: {
     std::ostringstream os;
     os << "invalid interpolation value type: " << rang::fgB::red << dataTypeName(data_type) << rang::style::reset;
 
     throw UnexpectedError(os.str());
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -102,9 +123,10 @@ DiscreteFunctionInterpoler::interpolate() const
   case 3: {
     return this->_interpolate<3>();
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid dimension");
   }
+    // LCOV_EXCL_STOP
   }
-  return nullptr;
 }
diff --git a/src/scheme/DiscreteFunctionInterpoler.hpp b/src/scheme/DiscreteFunctionInterpoler.hpp
index 758ff2454ec673ba2cc46c0c561eca1a4a256b51..e5724e3fec3ea75098772827537a1267f68328b7 100644
--- a/src/scheme/DiscreteFunctionInterpoler.hpp
+++ b/src/scheme/DiscreteFunctionInterpoler.hpp
@@ -15,7 +15,7 @@ class DiscreteFunctionInterpoler
   std::shared_ptr<const IDiscreteFunctionDescriptor> m_discrete_function_descriptor;
   const FunctionSymbolId m_function_id;
 
-  template <size_t Dimension, typename DataType>
+  template <size_t Dimension, typename DataType, typename ValueType = DataType>
   std::shared_ptr<IDiscreteFunction> _interpolate() const;
 
   template <size_t Dimension>
diff --git a/src/scheme/DiscreteFunctionUtils.cpp b/src/scheme/DiscreteFunctionUtils.cpp
index 6d2e62da4fcdd9d1c785b01894ca3d4776dfae8c..f7057c4abcf920eea37d678eb410e7edf58006b3 100644
--- a/src/scheme/DiscreteFunctionUtils.cpp
+++ b/src/scheme/DiscreteFunctionUtils.cpp
@@ -10,6 +10,10 @@ std::shared_ptr<const IDiscreteFunction>
 shallowCopy(const std::shared_ptr<const Mesh<Connectivity<Dimension>>>& mesh,
             const std::shared_ptr<const DiscreteFunctionP0<Dimension, DataType>>& discrete_function)
 {
+  Assert(mesh->shared_connectivity() ==
+           dynamic_cast<const Mesh<Connectivity<Dimension>>&>(*discrete_function->mesh()).shared_connectivity(),
+         "connectivities should be the same");
+
   return std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh, discrete_function->cellValues());
 }
 
@@ -22,7 +26,7 @@ shallowCopy(const std::shared_ptr<const Mesh<Connectivity<Dimension>>>& mesh,
     std::dynamic_pointer_cast<const Mesh<Connectivity<Dimension>>>(discrete_function->mesh());
 
   if (mesh->shared_connectivity() != function_mesh->shared_connectivity()) {
-    throw NormalError("incompatible connectivities");
+    throw NormalError("cannot shallow copy when connectivity changes");
   }
 
   switch (discrete_function->descriptor().type()) {
@@ -46,17 +50,21 @@ shallowCopy(const std::shared_ptr<const Mesh<Connectivity<Dimension>>>& mesh,
         return shallowCopy(mesh, std::dynamic_pointer_cast<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(
                                    discrete_function));
       }
+        // LCOV_EXCL_START
       default: {
         throw UnexpectedError("invalid data vector dimension: " +
                               std::to_string(discrete_function->dataType().dimension()));
       }
+        // LCOV_EXCL_STOP
       }
     }
     case ASTNodeDataType::matrix_t: {
       if (discrete_function->dataType().numberOfRows() != discrete_function->dataType().numberOfColumns()) {
+        // LCOV_EXCL_START
         throw UnexpectedError(
           "invalid data matrix dimensions: " + std::to_string(discrete_function->dataType().numberOfRows()) + "x" +
           std::to_string(discrete_function->dataType().numberOfColumns()));
+        // LCOV_EXCL_STOP
       }
       switch (discrete_function->dataType().numberOfRows()) {
       case 1: {
@@ -71,21 +79,27 @@ shallowCopy(const std::shared_ptr<const Mesh<Connectivity<Dimension>>>& mesh,
         return shallowCopy(mesh, std::dynamic_pointer_cast<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(
                                    discrete_function));
       }
+        // LCOV_EXCL_START
       default: {
         throw UnexpectedError(
           "invalid data matrix dimensions: " + std::to_string(discrete_function->dataType().numberOfRows()) + "x" +
           std::to_string(discrete_function->dataType().numberOfColumns()));
       }
+        // LCOV_EXCL_STOP
       }
     }
+      // LCOV_EXCL_START
     default: {
       throw UnexpectedError("invalid kind of P0 function: invalid data type");
     }
+      // LCOV_EXCL_STOP
     }
   }
+    // LCOV_EXCL_START
   default: {
-    throw NormalError("invalid discretization type");
+    throw UnexpectedError("invalid discretization type");
   }
+    // LCOV_EXCL_STOP
   }
 }
 
@@ -108,8 +122,10 @@ shallowCopy(const std::shared_ptr<const IMesh>& mesh, const std::shared_ptr<cons
   case 3: {
     return shallowCopy(std::dynamic_pointer_cast<const Mesh<Connectivity<3>>>(mesh), discrete_function);
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid mesh dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
diff --git a/src/scheme/DiscreteFunctionVectorInterpoler.cpp b/src/scheme/DiscreteFunctionVectorInterpoler.cpp
index 4827345a5864ccae5425f6463e00163d455a4aa8..98d6513c9a896b81d8e6909e057a8300b66d6dc7 100644
--- a/src/scheme/DiscreteFunctionVectorInterpoler.cpp
+++ b/src/scheme/DiscreteFunctionVectorInterpoler.cpp
@@ -38,7 +38,7 @@ DiscreteFunctionVectorInterpoler::_interpolate() const
     default: {
       std::ostringstream os;
       os << "vector functions require scalar value type.\n"
-         << "Invalid interpolation value type:" << rang::fgB::red << dataTypeName(data_type) << rang::style::reset;
+         << "Invalid interpolation value type: " << rang::fgB::red << dataTypeName(data_type) << rang::style::reset;
       throw NormalError(os.str());
     }
     }
@@ -53,7 +53,6 @@ DiscreteFunctionVectorInterpoler::interpolate() const
     throw NormalError("invalid discrete function type for vector interpolation");
   }
 
-  std::shared_ptr<IDiscreteFunction> discrete_function;
   switch (m_mesh->dimension()) {
   case 1: {
     return this->_interpolate<1>();
@@ -64,8 +63,10 @@ DiscreteFunctionVectorInterpoler::interpolate() const
   case 3: {
     return this->_interpolate<3>();
   }
+    // LCOV_EXCL_START
   default: {
     throw UnexpectedError("invalid dimension");
   }
+    // LCOV_EXCL_STOP
   }
 }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 4bac187608a6a5bfd19f058b6f8a00683f852f05..25bcaa008af00dd141bfc4a0ba1fc65bb11a2003 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -44,6 +44,9 @@ add_executable (unit_tests
   test_BinaryExpressionProcessor_comparison.cpp
   test_BinaryExpressionProcessor_equality.cpp
   test_BinaryExpressionProcessor_logic.cpp
+  test_BinaryExpressionProcessor_raw.cpp
+  test_BinaryExpressionProcessor_shift.cpp
+  test_BinaryOperatorMangler.cpp
   test_BiCGStab.cpp
   test_BuildInfo.cpp
   test_BuiltinFunctionEmbedder.cpp
@@ -63,9 +66,11 @@ add_executable (unit_tests
   test_DiscreteFunctionDescriptorP0.cpp
   test_DiscreteFunctionDescriptorP0Vector.cpp
   test_DiscreteFunctionType.cpp
+  test_DiscreteFunctionUtils.cpp
   test_DoWhileProcessor.cpp
   test_EigenvalueSolver.cpp
   test_EmbeddedData.cpp
+  test_EmbeddedIDiscreteFunctionUtils.cpp
   test_EscapedString.cpp
   test_Exceptions.cpp
   test_ExecutionPolicy.cpp
@@ -93,6 +98,7 @@ add_executable (unit_tests
   test_PugsUtils.cpp
   test_RevisionInfo.cpp
   test_SmallArray.cpp
+  test_SmallVector.cpp
   test_SymbolTable.cpp
   test_Table.cpp
   test_Timer.cpp
@@ -100,14 +106,19 @@ add_executable (unit_tests
   test_TinyVector.cpp
   test_TupleToVectorProcessor.cpp
   test_UnaryExpressionProcessor.cpp
+  test_UnaryOperatorMangler.cpp
   test_Vector.cpp
   test_WhileProcessor.cpp
   )
 
 add_executable (mpi_unit_tests
   mpi_test_main.cpp
+  test_DiscreteFunctionInterpoler.cpp
   test_DiscreteFunctionP0.cpp
   test_DiscreteFunctionP0Vector.cpp
+  test_DiscreteFunctionVectorInterpoler.cpp
+  test_EmbeddedIDiscreteFunctionMathFunctions.cpp
+  test_EmbeddedIDiscreteFunctionOperators.cpp
   test_InterpolateItemArray.cpp
   test_InterpolateItemValue.cpp
   test_ItemArray.cpp
diff --git a/tests/FixturesForBuiltinT.hpp b/tests/FixturesForBuiltinT.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d2ff0fcb3b0db1e37a9a5f00487eb8288f1aec3d
--- /dev/null
+++ b/tests/FixturesForBuiltinT.hpp
@@ -0,0 +1,68 @@
+#ifndef FIXTURES_FOR_BUILTIN_T_HPP
+#define FIXTURES_FOR_BUILTIN_T_HPP
+
+#include <language/utils/ASTNodeDataTypeTraits.hpp>
+
+#include <memory>
+#include <stdexcept>
+
+template <>
+inline ASTNodeDataType ast_node_data_type_from<std::shared_ptr<const double>> =
+  ASTNodeDataType::build<ASTNodeDataType::type_id_t>("builtin_t");
+const auto builtin_data_type = ast_node_data_type_from<std::shared_ptr<const double>>;
+
+inline std::shared_ptr<const double>
+operator-(const std::shared_ptr<const double>& p_a)
+{
+  return std::make_shared<double>(-*p_a);
+}
+
+inline std::shared_ptr<const double>
+operator+(const std::shared_ptr<const double>& p_a, const std::shared_ptr<const double>& p_b)
+{
+  return std::make_shared<double>(*p_a + *p_b);
+}
+
+inline std::shared_ptr<const double>
+operator+(std::shared_ptr<const double> p_a, double b)
+{
+  return std::make_shared<double>(*p_a + b);
+}
+
+inline std::shared_ptr<const double>
+operator+(double a, const std::shared_ptr<const double>& p_b)
+{
+  return std::make_shared<double>(a + *p_b);
+}
+
+inline std::shared_ptr<const double>
+operator/(const std::shared_ptr<const double>&, const std::shared_ptr<const double>&)
+{
+  throw std::runtime_error("runtime error both");
+}
+
+inline std::shared_ptr<const double>
+operator/(const std::shared_ptr<const double>&, double)
+{
+  throw std::runtime_error("runtime error lhs");
+}
+
+inline std::shared_ptr<const double>
+operator/(double, const std::shared_ptr<const double>&)
+{
+  throw std::runtime_error("runtime error rhs");
+}
+
+inline std::shared_ptr<const double>
+operator<<(const std::shared_ptr<const double>& p_a, const std::shared_ptr<const double>& p_b)
+{
+  return std::make_shared<double>(static_cast<int>(*p_a) << static_cast<int>(*p_b));
+}
+
+inline std::shared_ptr<const double>
+operator>>(const std::shared_ptr<const double>& p_a, const std::shared_ptr<const double>& p_b)
+{
+  return std::make_shared<double>(static_cast<int>(*p_a) >> static_cast<int>(*p_b));
+}
+
+#endif   // FIXTURES_FOR_BUILTIN_T_HPP
diff --git a/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp b/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp
index b99aa045958dc356cb03f2b548e8c632d58b124d..4a9f2bb8d64b047f3355d3f16b81f1e1dada2143 100644
--- a/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp
+++ b/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp
@@ -1,6 +1,8 @@
 #include <catch2/catch_test_macros.hpp>
 #include <catch2/matchers/catch_matchers_all.hpp>
 
+#include <FixturesForBuiltinT.hpp>
+
 #include <language/ast/ASTBuilder.hpp>
 #include <language/ast/ASTModulesImporter.hpp>
 #include <language/ast/ASTNodeBinaryOperatorExpressionBuilder.hpp>
@@ -10,6 +12,11 @@
 #include <language/ast/ASTNodeTypeCleaner.hpp>
 #include <language/ast/ASTSymbolTableBuilder.hpp>
 #include <language/utils/ASTPrinter.hpp>
+#include <language/utils/BasicAffectationRegistrerFor.hpp>
+#include <language/utils/BinaryOperatorProcessorBuilder.hpp>
+#include <language/utils/DataHandler.hpp>
+#include <language/utils/OperatorRepository.hpp>
+#include <language/utils/TypeDescriptor.hpp>
 #include <utils/Demangle.hpp>
 
 #include <pegtl/string_input.hpp>
@@ -1417,6 +1424,111 @@ x!=y;
     }
   }
 
+  SECTION("shift")
+  {
+#define CHECK_AST_BUILTIN_SHIFT_EXPRESSION(data, expected_output)                                               \
+  {                                                                                                             \
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};                                                  \
+    auto ast = ASTBuilder::build(input);                                                                        \
+                                                                                                                \
+    ASTModulesImporter{*ast};                                                                                   \
+                                                                                                                \
+    BasicAffectationRegisterFor<EmbeddedData>{ASTNodeDataType::build<ASTNodeDataType::type_id_t>("builtin_t")}; \
+                                                                                                                \
+    OperatorRepository& repository = OperatorRepository::instance();                                            \
+                                                                                                                \
+    repository.addBinaryOperator<language::shift_left_op>(                                                      \
+      std::make_shared<                                                                                         \
+        BinaryOperatorProcessorBuilder<language::shift_left_op, std::shared_ptr<const double>,                  \
+                                       std::shared_ptr<const double>, std::shared_ptr<const double>>>());       \
+                                                                                                                \
+    repository.addBinaryOperator<language::shift_right_op>(                                                     \
+      std::make_shared<                                                                                         \
+        BinaryOperatorProcessorBuilder<language::shift_right_op, std::shared_ptr<const double>,                 \
+                                       std::shared_ptr<const double>, std::shared_ptr<const double>>>());       \
+                                                                                                                \
+    SymbolTable& symbol_table = *ast->m_symbol_table;                                                           \
+    auto [i_symbol, success]  = symbol_table.add(builtin_data_type.nameOfTypeId(), ast->begin());               \
+    if (not success) {                                                                                          \
+      throw UnexpectedError("cannot add '" + builtin_data_type.nameOfTypeId() + "' type for testing");          \
+    }                                                                                                           \
+                                                                                                                \
+    i_symbol->attributes().setDataType(ASTNodeDataType::build<ASTNodeDataType::type_name_id_t>());              \
+    i_symbol->attributes().setIsInitialized();                                                                  \
+    i_symbol->attributes().value() = symbol_table.typeEmbedderTable().size();                                   \
+    symbol_table.typeEmbedderTable().add(std::make_shared<TypeDescriptor>(builtin_data_type.nameOfTypeId()));   \
+                                                                                                                \
+    auto [i_symbol_bt_a, success_bt_a] = symbol_table.add("bt_a", ast->begin());                                \
+    if (not success_bt_a) {                                                                                     \
+      throw UnexpectedError("cannot add 'bt_a' of type builtin_t for testing");                                 \
+    }                                                                                                           \
+    i_symbol_bt_a->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);            \
+    i_symbol_bt_a->attributes().setIsInitialized();                                                             \
+    i_symbol_bt_a->attributes().value() =                                                                       \
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(3.2)));                 \
+                                                                                                                \
+    auto [i_symbol_bt_b, success_bt_b] = symbol_table.add("bt_b", ast->begin());                                \
+    if (not success_bt_b) {                                                                                     \
+      throw UnexpectedError("cannot add 'bt_b' of type builtin_t for testing");                                 \
+    }                                                                                                           \
+    i_symbol_bt_b->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);            \
+    i_symbol_bt_b->attributes().setIsInitialized();                                                             \
+    i_symbol_bt_b->attributes().value() =                                                                       \
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(5.3)));                 \
+                                                                                                                \
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};                                                     \
+                                                                                                                \
+    ASTSymbolTableBuilder{*ast};                                                                                \
+    ASTNodeDataTypeBuilder{*ast};                                                                               \
+                                                                                                                \
+    ASTNodeDeclarationToAffectationConverter{*ast};                                                             \
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};                                                        \
+                                                                                                                \
+    ASTNodeExpressionBuilder{*ast};                                                                             \
+                                                                                                                \
+    std::stringstream ast_output;                                                                               \
+    ast_output << '\n' << ASTPrinter{*ast, ASTPrinter::Format::raw, {ASTPrinter::Info::exec_type}};             \
+                                                                                                                \
+    REQUIRE(ast_output.str() == expected_output);                                                               \
+  }
+
+    SECTION("shift left (builtin)")
+    {
+      std::string_view data = R"(
+let m : builtin_t;
+let n : builtin_t;
+n << m;
+)";
+
+      std::string_view result = R"(
+(root:ASTNodeListProcessor)
+ `-(language::shift_left_op:BinaryExpressionProcessor<language::shift_left_op, std::shared_ptr<double const>, std::shared_ptr<double const>, std::shared_ptr<double const> >)
+     +-(language::name:n:NameProcessor)
+     `-(language::name:m:NameProcessor)
+)";
+
+      CHECK_AST_BUILTIN_SHIFT_EXPRESSION(data, result);
+    }
+
+    SECTION("shift right (builtin)")
+    {
+      std::string_view data = R"(
+let m : builtin_t;
+let n : builtin_t;
+n >> m;
+)";
+
+      std::string_view result = R"(
+(root:ASTNodeListProcessor)
+ `-(language::shift_right_op:BinaryExpressionProcessor<language::shift_right_op, std::shared_ptr<double const>, std::shared_ptr<double const>, std::shared_ptr<double const> >)
+     +-(language::name:n:NameProcessor)
+     `-(language::name:m:NameProcessor)
+)";
+
+      CHECK_AST_BUILTIN_SHIFT_EXPRESSION(data, result);
+    }
+  }
+
   SECTION("Errors")
   {
     SECTION("Invalid binary operator type")
diff --git a/tests/test_BinaryExpressionProcessor_arithmetic.cpp b/tests/test_BinaryExpressionProcessor_arithmetic.cpp
index 2c91674b2fd300bb59ef4faae100c4ea0423df33..6ded2c991a25508ae89ce90fca602aad71901547 100644
--- a/tests/test_BinaryExpressionProcessor_arithmetic.cpp
+++ b/tests/test_BinaryExpressionProcessor_arithmetic.cpp
@@ -1,10 +1,161 @@
 #include <catch2/catch_test_macros.hpp>
 #include <catch2/matchers/catch_matchers_all.hpp>
 
+#include <FixturesForBuiltinT.hpp>
+
+#include <language/utils/BasicAffectationRegistrerFor.hpp>
+#include <language/utils/BinaryOperatorProcessorBuilder.hpp>
+#include <language/utils/DataHandler.hpp>
+#include <language/utils/OperatorRepository.hpp>
+#include <language/utils/TypeDescriptor.hpp>
+
 #include <test_BinaryExpressionProcessor_utils.hpp>
 
 // clazy:excludeall=non-pod-global-static
 
+#define CHECK_BUILTIN_BINARY_EXPRESSION_RESULT(data, result)                                                    \
+  {                                                                                                             \
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};                                                  \
+    auto ast = ASTBuilder::build(input);                                                                        \
+                                                                                                                \
+    ASTModulesImporter{*ast};                                                                                   \
+                                                                                                                \
+    BasicAffectationRegisterFor<EmbeddedData>{ASTNodeDataType::build<ASTNodeDataType::type_id_t>("builtin_t")}; \
+                                                                                                                \
+    OperatorRepository& repository = OperatorRepository::instance();                                            \
+                                                                                                                \
+    repository.addBinaryOperator<language::plus_op>(                                                            \
+      std::make_shared<                                                                                         \
+        BinaryOperatorProcessorBuilder<language::plus_op, std::shared_ptr<const double>,                        \
+                                       std::shared_ptr<const double>, std::shared_ptr<const double>>>());       \
+                                                                                                                \
+    repository.addBinaryOperator<language::plus_op>(                                                            \
+      std::make_shared<BinaryOperatorProcessorBuilder<language::plus_op, std::shared_ptr<const double>,         \
+                                                      std::shared_ptr<const double>, double>>());               \
+                                                                                                                \
+    repository.addBinaryOperator<language::plus_op>(                                                            \
+      std::make_shared<BinaryOperatorProcessorBuilder<language::plus_op, std::shared_ptr<const double>, double, \
+                                                      std::shared_ptr<const double>>>());                       \
+                                                                                                                \
+    SymbolTable& symbol_table = *ast->m_symbol_table;                                                           \
+    auto [i_symbol, success]  = symbol_table.add(builtin_data_type.nameOfTypeId(), ast->begin());               \
+    if (not success) {                                                                                          \
+      throw UnexpectedError("cannot add '" + builtin_data_type.nameOfTypeId() + "' type for testing");          \
+    }                                                                                                           \
+                                                                                                                \
+    i_symbol->attributes().setDataType(ASTNodeDataType::build<ASTNodeDataType::type_name_id_t>());              \
+    i_symbol->attributes().setIsInitialized();                                                                  \
+    i_symbol->attributes().value() = symbol_table.typeEmbedderTable().size();                                   \
+    symbol_table.typeEmbedderTable().add(std::make_shared<TypeDescriptor>(builtin_data_type.nameOfTypeId()));   \
+                                                                                                                \
+    auto [i_symbol_bt_a, success_bt_a] = symbol_table.add("bt_a", ast->begin());                                \
+    if (not success_bt_a) {                                                                                     \
+      throw UnexpectedError("cannot add 'bt_a' of type builtin_t for testing");                                 \
+    }                                                                                                           \
+    i_symbol_bt_a->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);            \
+    i_symbol_bt_a->attributes().setIsInitialized();                                                             \
+    i_symbol_bt_a->attributes().value() =                                                                       \
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(3.2)));                 \
+                                                                                                                \
+    auto [i_symbol_bt_b, success_bt_b] = symbol_table.add("bt_b", ast->begin());                                \
+    if (not success_bt_b) {                                                                                     \
+      throw UnexpectedError("cannot add 'bt_b' of type builtin_t for testing");                                 \
+    }                                                                                                           \
+    i_symbol_bt_b->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);            \
+    i_symbol_bt_b->attributes().setIsInitialized();                                                             \
+    i_symbol_bt_b->attributes().value() =                                                                       \
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(5.3)));                 \
+                                                                                                                \
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};                                                     \
+                                                                                                                \
+    ASTSymbolTableBuilder{*ast};                                                                                \
+    ASTNodeDataTypeBuilder{*ast};                                                                               \
+                                                                                                                \
+    ASTNodeDeclarationToAffectationConverter{*ast};                                                             \
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};                                                        \
+                                                                                                                \
+    ASTNodeExpressionBuilder{*ast};                                                                             \
+    ExecutionPolicy exec_policy;                                                                                \
+    ast->execute(exec_policy);                                                                                  \
+                                                                                                                \
+    using namespace TAO_PEGTL_NAMESPACE;                                                                        \
+    position use_position{internal::iterator{"fixture"}, "fixture"};                                            \
+    use_position.byte    = 10000;                                                                               \
+    auto [symbol, found] = symbol_table.find("r", use_position);                                                \
+                                                                                                                \
+    auto attributes     = symbol->attributes();                                                                 \
+    auto embedded_value = std::get<EmbeddedData>(attributes.value());                                           \
+                                                                                                                \
+    double value = *dynamic_cast<const DataHandler<const double>&>(embedded_value.get()).data_ptr();            \
+    REQUIRE(value == expected);                                                                                 \
+  }
+
+#define CHECK_BUILTIN_BINARY_EXPRESSION_ERROR(data, error_msg)                                                    \
+  {                                                                                                               \
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};                                                    \
+    auto ast = ASTBuilder::build(input);                                                                          \
+                                                                                                                  \
+    ASTModulesImporter{*ast};                                                                                     \
+                                                                                                                  \
+    BasicAffectationRegisterFor<EmbeddedData>{ASTNodeDataType::build<ASTNodeDataType::type_id_t>("builtin_t")};   \
+                                                                                                                  \
+    OperatorRepository& repository = OperatorRepository::instance();                                              \
+                                                                                                                  \
+    repository.addBinaryOperator<language::divide_op>(                                                            \
+      std::make_shared<                                                                                           \
+        BinaryOperatorProcessorBuilder<language::divide_op, std::shared_ptr<const double>,                        \
+                                       std::shared_ptr<const double>, std::shared_ptr<const double>>>());         \
+                                                                                                                  \
+    repository.addBinaryOperator<language::divide_op>(                                                            \
+      std::make_shared<BinaryOperatorProcessorBuilder<language::divide_op, std::shared_ptr<const double>,         \
+                                                      std::shared_ptr<const double>, double>>());                 \
+                                                                                                                  \
+    repository.addBinaryOperator<language::divide_op>(                                                            \
+      std::make_shared<BinaryOperatorProcessorBuilder<language::divide_op, std::shared_ptr<const double>, double, \
+                                                      std::shared_ptr<const double>>>());                         \
+                                                                                                                  \
+    SymbolTable& symbol_table = *ast->m_symbol_table;                                                             \
+    auto [i_symbol, success]  = symbol_table.add(builtin_data_type.nameOfTypeId(), ast->begin());                 \
+    if (not success) {                                                                                            \
+      throw UnexpectedError("cannot add '" + builtin_data_type.nameOfTypeId() + "' type for testing");            \
+    }                                                                                                             \
+                                                                                                                  \
+    i_symbol->attributes().setDataType(ASTNodeDataType::build<ASTNodeDataType::type_name_id_t>());                \
+    i_symbol->attributes().setIsInitialized();                                                                    \
+    i_symbol->attributes().value() = symbol_table.typeEmbedderTable().size();                                     \
+    symbol_table.typeEmbedderTable().add(std::make_shared<TypeDescriptor>(builtin_data_type.nameOfTypeId()));     \
+                                                                                                                  \
+    auto [i_symbol_bt_a, success_bt_a] = symbol_table.add("bt_a", ast->begin());                                  \
+    if (not success_bt_a) {                                                                                       \
+      throw UnexpectedError("cannot add 'bt_a' of type builtin_t for testing");                                   \
+    }                                                                                                             \
+    i_symbol_bt_a->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);              \
+    i_symbol_bt_a->attributes().setIsInitialized();                                                               \
+    i_symbol_bt_a->attributes().value() =                                                                         \
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(3.2)));                   \
+                                                                                                                  \
+    auto [i_symbol_bt_b, success_bt_b] = symbol_table.add("bt_b", ast->begin());                                  \
+    if (not success_bt_b) {                                                                                       \
+      throw UnexpectedError("cannot add 'bt_b' of type builtin_t for testing");                                   \
+    }                                                                                                             \
+    i_symbol_bt_b->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);              \
+    i_symbol_bt_b->attributes().setIsInitialized();                                                               \
+    i_symbol_bt_b->attributes().value() =                                                                         \
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(5.3)));                   \
+                                                                                                                  \
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};                                                       \
+                                                                                                                  \
+    ASTSymbolTableBuilder{*ast};                                                                                  \
+    ASTNodeDataTypeBuilder{*ast};                                                                                 \
+                                                                                                                  \
+    ASTNodeDeclarationToAffectationConverter{*ast};                                                               \
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};                                                          \
+                                                                                                                  \
+    ASTNodeExpressionBuilder{*ast};                                                                               \
+    ExecutionPolicy exec_policy;                                                                                  \
+    REQUIRE_THROWS_WITH(ast->execute(exec_policy), error_msg);                                                    \
+  }
+
 TEST_CASE("BinaryExpressionProcessor arithmetic", "[language]")
 {
   SECTION("+")
@@ -151,4 +302,43 @@ TEST_CASE("BinaryExpressionProcessor arithmetic", "[language]")
       CHECK_BINARY_EXPRESSION_RESULT(R"(let r:R, r = -1.2 / 2.3;)", "r", (-1.2 / 2.3));
     }
   }
+
+  SECTION("binary operator [builtin]")
+  {
+    SECTION("builtin both side")
+    {
+      std::string_view data = R"(let r:builtin_t, r = bt_a + bt_b;)";
+      const double expected = double{3.2} + double{5.3};
+
+      CHECK_BUILTIN_BINARY_EXPRESSION_RESULT(data, expected);
+    }
+
+    SECTION("builtin lhs")
+    {
+      std::string_view data = R"(let r:builtin_t, r = bt_a + 5.;)";
+      const double expected = double{3.2} + double{5};
+
+      CHECK_BUILTIN_BINARY_EXPRESSION_RESULT(data, expected);
+    }
+
+    SECTION("builtin rhs")
+    {
+      std::string_view data = R"(let r:builtin_t, r = 5. + bt_a;)";
+      const double expected = double{3.2} + double{5};
+
+      CHECK_BUILTIN_BINARY_EXPRESSION_RESULT(data, expected);
+    }
+
+    SECTION("runtime error")
+    {
+      std::string_view data_both = R"(let r:builtin_t, r = bt_a / bt_b;)";
+      CHECK_BUILTIN_BINARY_EXPRESSION_ERROR(data_both, "runtime error both");
+
+      std::string_view data_lhs = R"(let r:builtin_t, r = bt_a / 2.3;)";
+      CHECK_BUILTIN_BINARY_EXPRESSION_ERROR(data_lhs, "runtime error lhs");
+
+      std::string_view data_rhs = R"(let r:builtin_t, r = 2.3/ bt_a;)";
+      CHECK_BUILTIN_BINARY_EXPRESSION_ERROR(data_rhs, "runtime error rhs");
+    }
+  }
 }
diff --git a/tests/test_BinaryExpressionProcessor_raw.cpp b/tests/test_BinaryExpressionProcessor_raw.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c115c67ca070c0610e5225c022fc527e4cff8a1a
--- /dev/null
+++ b/tests/test_BinaryExpressionProcessor_raw.cpp
@@ -0,0 +1,86 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <language/node_processor/BinaryExpressionProcessor.hpp>
+#include <language/utils/OFStream.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("BinaryExpressionProcessor raw operators", "[language]")
+{
+  REQUIRE(BinOp<language::and_op>{}.eval(true, true) == true);
+  REQUIRE(BinOp<language::and_op>{}.eval(true, false) == false);
+  REQUIRE(BinOp<language::and_op>{}.eval(false, true) == false);
+  REQUIRE(BinOp<language::and_op>{}.eval(false, false) == false);
+
+  REQUIRE(BinOp<language::or_op>{}.eval(true, true) == true);
+  REQUIRE(BinOp<language::or_op>{}.eval(true, false) == true);
+  REQUIRE(BinOp<language::or_op>{}.eval(false, true) == true);
+  REQUIRE(BinOp<language::or_op>{}.eval(false, false) == false);
+
+  REQUIRE(BinOp<language::xor_op>{}.eval(true, true) == false);
+  REQUIRE(BinOp<language::xor_op>{}.eval(true, false) == true);
+  REQUIRE(BinOp<language::xor_op>{}.eval(false, true) == true);
+  REQUIRE(BinOp<language::xor_op>{}.eval(false, false) == false);
+
+  REQUIRE(BinOp<language::eqeq_op>{}.eval(3, 3) == true);
+  REQUIRE(BinOp<language::eqeq_op>{}.eval(2, 3) == false);
+
+  REQUIRE(BinOp<language::not_eq_op>{}.eval(3, 3) == false);
+  REQUIRE(BinOp<language::not_eq_op>{}.eval(2, 3) == true);
+
+  REQUIRE(BinOp<language::lesser_op>{}.eval(2.9, 3) == true);
+  REQUIRE(BinOp<language::lesser_op>{}.eval(3, 3) == false);
+  REQUIRE(BinOp<language::lesser_op>{}.eval(3.1, 3) == false);
+
+  REQUIRE(BinOp<language::lesser_or_eq_op>{}.eval(2.9, 3) == true);
+  REQUIRE(BinOp<language::lesser_or_eq_op>{}.eval(3, 3) == true);
+  REQUIRE(BinOp<language::lesser_or_eq_op>{}.eval(3.1, 3) == false);
+
+  REQUIRE(BinOp<language::greater_op>{}.eval(2.9, 3) == false);
+  REQUIRE(BinOp<language::greater_op>{}.eval(3, 3) == false);
+  REQUIRE(BinOp<language::greater_op>{}.eval(3.1, 3) == true);
+
+  REQUIRE(BinOp<language::greater_or_eq_op>{}.eval(2.9, 3) == false);
+  REQUIRE(BinOp<language::greater_or_eq_op>{}.eval(3, 3) == true);
+  REQUIRE(BinOp<language::greater_or_eq_op>{}.eval(3.1, 3) == true);
+
+  REQUIRE(BinOp<language::plus_op>{}.eval(2.9, 3) == (2.9 + 3));
+  REQUIRE(BinOp<language::minus_op>{}.eval(2.9, 3) == (2.9 - 3));
+  REQUIRE(BinOp<language::multiply_op>{}.eval(2.9, 3) == (2.9 * 3));
+  REQUIRE(BinOp<language::divide_op>{}.eval(2.9, 3) == (2.9 / 3));
+
+  {
+    std::filesystem::path path = std::filesystem::temp_directory_path();
+    path.append(std::string{"binary_expression_processor_shift_left_"} + std::to_string(getpid()));
+
+    std::string filename = path.string();
+
+    std::shared_ptr<const OStream> p_fout = std::make_shared<OFStream>(filename);
+    BinOp<language::shift_left_op>{}.eval(p_fout, true);
+    BinOp<language::shift_left_op>{}.eval(p_fout, std::string{" bar\n"});
+    p_fout.reset();
+
+    REQUIRE(std::filesystem::exists(filename));
+
+    {
+      std::string file_content;
+      std::ifstream fin(filename.c_str());
+
+      do {
+        char c = fin.get();
+        if (c != EOF) {
+          file_content += c;
+        }
+      } while (fin);
+
+      REQUIRE(file_content == "true bar\n");
+    }
+
+    std::filesystem::remove(filename);
+    REQUIRE(not std::filesystem::exists(filename));
+  }
+
+  REQUIRE(BinOp<language::shift_left_op>{}.eval(3, 2) == (3 << 2));
+  REQUIRE(BinOp<language::shift_right_op>{}.eval(17, 2) == (17 >> 2));
+}
diff --git a/tests/test_BinaryExpressionProcessor_shift.cpp b/tests/test_BinaryExpressionProcessor_shift.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..082d5fc5e829e11ac14e700e226f07278fca13c8
--- /dev/null
+++ b/tests/test_BinaryExpressionProcessor_shift.cpp
@@ -0,0 +1,62 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <test_BinaryExpressionProcessor_utils.hpp>
+
+#include <fstream>
+#include <unistd.h>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("BinaryExpressionProcessor shift", "[language]")
+{
+  SECTION("<<")
+  {
+    std::filesystem::path path = std::filesystem::temp_directory_path();
+
+    path.append(std::string{"binary_expression_processor_"} + std::to_string(getpid()));
+
+    std::string filename = path.string();
+
+    {
+      std::ostringstream data;
+      data << "let fout:ostream, fout = ofstream(\"" << path.string() << "\");\n";
+      data << R"(fout << 2 << " " << true << " " << 2 + 3 << "\n";)";
+
+      TAO_PEGTL_NAMESPACE::string_input input{data.str(), "test.pgs"};
+      auto ast = ASTBuilder::build(input);
+
+      ASTModulesImporter{*ast};
+      ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+      ASTSymbolTableBuilder{*ast};
+      ASTNodeDataTypeBuilder{*ast};
+
+      ASTNodeDeclarationToAffectationConverter{*ast};
+      ASTNodeTypeCleaner<language::var_declaration>{*ast};
+
+      ASTNodeExpressionBuilder{*ast};
+      ExecutionPolicy exec_policy;
+      ast->execute(exec_policy);
+    }
+
+    REQUIRE(std::filesystem::exists(filename));
+
+    {
+      std::string file_content;
+      std::ifstream fin(filename.c_str());
+
+      do {
+        char c = fin.get();
+        if (c != EOF) {
+          file_content += c;
+        }
+      } while (fin);
+
+      REQUIRE(file_content == "2 true 5\n");
+    }
+
+    std::filesystem::remove(filename);
+    REQUIRE(not std::filesystem::exists(filename));
+  }
+}
diff --git a/tests/test_BinaryOperatorMangler.cpp b/tests/test_BinaryOperatorMangler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..329c59dcb9fd5fadfae97f39dcbb9e7a6ee3e582
--- /dev/null
+++ b/tests/test_BinaryOperatorMangler.cpp
@@ -0,0 +1,31 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <language/utils/BinaryOperatorMangler.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("BinaryOperatorMangler", "[language]")
+{
+  SECTION("binary operators")
+  {
+    const ASTNodeDataType R = ASTNodeDataType::build<ASTNodeDataType::double_t>();
+    const ASTNodeDataType Z = ASTNodeDataType::build<ASTNodeDataType::int_t>();
+
+    REQUIRE(binaryOperatorMangler<language::multiply_op>(R, Z) == "R * Z");
+    REQUIRE(binaryOperatorMangler<language::divide_op>(R, Z) == "R / Z");
+    REQUIRE(binaryOperatorMangler<language::plus_op>(R, Z) == "R + Z");
+    REQUIRE(binaryOperatorMangler<language::minus_op>(R, Z) == "R - Z");
+    REQUIRE(binaryOperatorMangler<language::or_op>(R, Z) == "R or Z");
+    REQUIRE(binaryOperatorMangler<language::and_op>(R, Z) == "R and Z");
+    REQUIRE(binaryOperatorMangler<language::xor_op>(R, Z) == "R xor Z");
+    REQUIRE(binaryOperatorMangler<language::greater_op>(R, Z) == "R > Z");
+    REQUIRE(binaryOperatorMangler<language::greater_or_eq_op>(R, Z) == "R >= Z");
+    REQUIRE(binaryOperatorMangler<language::lesser_op>(R, Z) == "R < Z");
+    REQUIRE(binaryOperatorMangler<language::lesser_or_eq_op>(R, Z) == "R <= Z");
+    REQUIRE(binaryOperatorMangler<language::eqeq_op>(R, Z) == "R == Z");
+    REQUIRE(binaryOperatorMangler<language::not_eq_op>(R, Z) == "R != Z");
+    REQUIRE(binaryOperatorMangler<language::shift_left_op>(R, Z) == "R << Z");
+    REQUIRE(binaryOperatorMangler<language::shift_right_op>(R, Z) == "R >> Z");
+  }
+}
diff --git a/tests/test_BuiltinFunctionProcessor.cpp b/tests/test_BuiltinFunctionProcessor.cpp
index a1fb3fe82b982bc50d2b5247c1158941da24ce0d..bbc5ac52e1a6e8034b68b591adff83c1d8701ff5 100644
--- a/tests/test_BuiltinFunctionProcessor.cpp
+++ b/tests/test_BuiltinFunctionProcessor.cpp
@@ -278,6 +278,42 @@ let z:Z, z = round(-1.2);
       CHECK_BUILTIN_FUNCTION_EVALUATION_RESULT(data, "z", int64_t{-1});
     }
 
+    {   // min
+      tested_function_set.insert("min:R*R");
+      std::string_view data = R"(
+import math;
+let x:R, x = min(-2,2.3);
+)";
+      CHECK_BUILTIN_FUNCTION_EVALUATION_RESULT(data, "x", double{-2});
+    }
+
+    {   // min
+      tested_function_set.insert("min:Z*Z");
+      std::string_view data = R"(
+import math;
+let z:Z, z = min(-1,2);
+)";
+      CHECK_BUILTIN_FUNCTION_EVALUATION_RESULT(data, "z", int64_t{-1});
+    }
+
+    {   // max
+      tested_function_set.insert("max:R*R");
+      std::string_view data = R"(
+import math;
+let x:R, x = max(-1,2.3);
+)";
+      CHECK_BUILTIN_FUNCTION_EVALUATION_RESULT(data, "x", double{2.3});
+    }
+
+    {   // max
+      tested_function_set.insert("max:Z*Z");
+      std::string_view data = R"(
+import math;
+let z:Z, z = max(-1,2);
+)";
+      CHECK_BUILTIN_FUNCTION_EVALUATION_RESULT(data, "z", int64_t{2});
+    }
+
     {   // dot
       tested_function_set.insert("dot:R^1*R^1");
       std::string_view data = R"(
diff --git a/tests/test_CRSMatrixDescriptor.cpp b/tests/test_CRSMatrixDescriptor.cpp
index 28276aaadb747f01933fda8ce302cb853af5cf6d..4d31a7397aee4fa031179a912f143b3cf3f32578 100644
--- a/tests/test_CRSMatrixDescriptor.cpp
+++ b/tests/test_CRSMatrixDescriptor.cpp
@@ -12,6 +12,34 @@ template class CRSMatrixDescriptor<int>;
 
 TEST_CASE("CRSMatrixDescriptor", "[algebra]")
 {
+  SECTION("sizes")
+  {
+    SECTION("rectangle")
+    {
+      const size_t nb_lines   = 2;
+      const size_t nb_columns = 5;
+
+      Array<int> non_zeros{nb_lines};
+      non_zeros.fill(2);
+      CRSMatrixDescriptor<int> S(nb_lines, nb_columns, non_zeros);
+
+      REQUIRE(S.numberOfRows() == 2);
+      REQUIRE(S.numberOfColumns() == 5);
+    }
+
+    SECTION("square")
+    {
+      const size_t nb_lines = 3;
+
+      Array<int> non_zeros{nb_lines};
+      non_zeros.fill(2);
+      CRSMatrixDescriptor<int> S(nb_lines, non_zeros);
+
+      REQUIRE(S.numberOfRows() == 3);
+      REQUIRE(S.numberOfColumns() == 3);
+    }
+  }
+
   SECTION("has overflow / not filled")
   {
     const size_t nb_lines   = 2;
diff --git a/tests/test_DataVariant.cpp b/tests/test_DataVariant.cpp
index 0db6af9b2fbe55081a8dc43d359a189cae4f5d5a..dab7d6436cc547b7163baccd369068f20103f7c5 100644
--- a/tests/test_DataVariant.cpp
+++ b/tests/test_DataVariant.cpp
@@ -11,11 +11,12 @@ TEST_CASE("DataVariant", "[language]")
 {
   SECTION("AggregateDataVariant")
   {
-    AggregateDataVariant aggregate{std::vector<DataVariant>{double{1.3}, int64_t{-3}, std::vector<double>{1, 2.7}}};
+    AggregateDataVariant aggregate{
+      std::vector<DataVariant>{double{1.3}, int64_t{-3}, std::vector<double>{1, 2.7}, bool{true}}};
 
     SECTION("size")
     {
-      REQUIRE(aggregate.size() == 3);
+      REQUIRE(aggregate.size() == 4);
     }
 
     SECTION("output")
@@ -24,7 +25,8 @@ TEST_CASE("DataVariant", "[language]")
       aggregate_output << aggregate;
 
       std::stringstream expected_output;
-      expected_output << '(' << double{1.3} << ", " << int64_t{-3} << ", (" << 1 << ", " << 2.7 << "))";
+      expected_output << '(' << double{1.3} << ", " << int64_t{-3} << ", (" << 1 << ", " << 2.7 << "), "
+                      << std::boolalpha << true << ")";
       REQUIRE(aggregate_output.str() == expected_output.str());
     }
 
diff --git a/tests/test_DiscreteFunctionInterpoler.cpp b/tests/test_DiscreteFunctionInterpoler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d24bae5f065ba88afd225268c425b92f9211bcf5
--- /dev/null
+++ b/tests/test_DiscreteFunctionInterpoler.cpp
@@ -0,0 +1,916 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <language/ast/ASTBuilder.hpp>
+#include <language/ast/ASTModulesImporter.hpp>
+#include <language/ast/ASTNodeDataTypeBuilder.hpp>
+#include <language/ast/ASTNodeExpressionBuilder.hpp>
+#include <language/ast/ASTNodeFunctionEvaluationExpressionBuilder.hpp>
+#include <language/ast/ASTNodeFunctionExpressionBuilder.hpp>
+#include <language/ast/ASTNodeTypeCleaner.hpp>
+#include <language/ast/ASTSymbolTableBuilder.hpp>
+#include <language/utils/PugsFunctionAdapter.hpp>
+#include <language/utils/SymbolTable.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshData.hpp>
+#include <mesh/MeshDataManager.hpp>
+
+#include <scheme/DiscreteFunctionDescriptorP0.hpp>
+#include <scheme/DiscreteFunctionInterpoler.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+
+#include <pegtl/string_input.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("DiscreteFunctionInterpoler", "[scheme]")
+{
+  auto same_cell_value = [](auto f, auto g) -> bool {
+    using ItemIdType = typename decltype(f)::index_type;
+    for (ItemIdType item_id = 0; item_id < f.numberOfItems(); ++item_id) {
+      if (f[item_id] != g[item_id]) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  SECTION("1D")
+  {
+    constexpr size_t Dimension = 1;
+
+    const auto& mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_1d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_1d: R^1 -> B, x -> (exp(2 * x[0]) + 3 > 4);
+let N_scalar_non_linear_1d: R^1 -> N, x -> floor(3 * x[0] * x[0] + 2);
+let Z_scalar_non_linear_1d: R^1 -> Z, x -> floor(exp(2 * x[0]) - 1);
+let R_scalar_non_linear_1d: R^1 -> R, x -> 2 * exp(x[0]) + 3;
+let R1_non_linear_1d: R^1 -> R^1, x -> 2 * exp(x[0]);
+let R2_non_linear_1d: R^1 -> R^2, x -> (2 * exp(x[0]), -3*x[0]);
+let R3_non_linear_1d: R^1 -> R^3, x -> (2 * exp(x[0]) + 3, x[0] - 2, 3);
+let R1x1_non_linear_1d: R^1 -> R^1x1, x -> (2 * exp(x[0]) * sin(x[0]) + 3);
+let R2x2_non_linear_1d: R^1 -> R^2x2, x -> (2 * exp(x[0]) * sin(x[0]) + 3, sin(x[0] - 2 * x[0]), 3, x[0] * x[0]);
+let R3x3_non_linear_1d: R^1 -> R^3x3, x -> (2 * exp(x[0]) * sin(x[0]) + 3, sin(x[0] - 2 * x[0]), 3, x[0] * x[0], -4*x[0], 2*x[0]+1, 3, -6*x[0], exp(x[0]));
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    SECTION("B_scalar_non_linear_1d")
+    {
+      auto [i_symbol, found] = symbol_table->find("B_scalar_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::exp(2 * x[0]) + 3 > 4;
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("N_scalar_non_linear_1d")
+    {
+      auto [i_symbol, found] = symbol_table->find("N_scalar_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(3 * x[0] * x[0] + 2);
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("Z_scalar_non_linear_1d")
+    {
+      auto [i_symbol, found] = symbol_table->find("Z_scalar_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(std::exp(2 * x[0]) - 1);
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("R_scalar_non_linear_1d")
+    {
+      auto [i_symbol, found] = symbol_table->find("R_scalar_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = 2 * std::exp(x[0]) + 3;
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("R1_non_linear_1d")
+    {
+      using DataType = TinyVector<1>;
+
+      auto [i_symbol, found] = symbol_table->find("R1_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0])};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R2_non_linear_1d")
+    {
+      using DataType = TinyVector<2>;
+
+      auto [i_symbol, found] = symbol_table->find("R2_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]), -3 * x[0]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R3_non_linear_1d")
+    {
+      using DataType = TinyVector<3>;
+
+      auto [i_symbol, found] = symbol_table->find("R3_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) + 3, x[0] - 2, 3};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R1x1_non_linear_1d")
+    {
+      using DataType = TinyMatrix<1>;
+
+      auto [i_symbol, found] = symbol_table->find("R1x1_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) * std::sin(x[0]) + 3};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R2x2_non_linear_1d")
+    {
+      using DataType = TinyMatrix<2>;
+
+      auto [i_symbol, found] = symbol_table->find("R2x2_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id] =
+            DataType{2 * std::exp(x[0]) * std::sin(x[0]) + 3, std::sin(x[0] - 2 * x[0]), 3, x[0] * x[0]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R3x3_non_linear_1d")
+    {
+      using DataType = TinyMatrix<3>;
+
+      auto [i_symbol, found] = symbol_table->find("R3x3_non_linear_1d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * exp(x[0]) * std::sin(x[0]) + 3,
+                                         std::sin(x[0] - 2 * x[0]),
+                                         3,
+                                         x[0] * x[0],
+                                         -4 * x[0],
+                                         2 * x[0] + 1,
+                                         3,
+                                         -6 * x[0],
+                                         std::exp(x[0])};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+  }
+
+  SECTION("2D")
+  {
+    constexpr size_t Dimension = 2;
+
+    const auto& mesh_2d = MeshDataBaseForTests::get().cartesianMesh2D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_2d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_2d: R^2 -> B, x -> (exp(2 * x[0])< 2*x[1]);
+let N_scalar_non_linear_2d: R^2 -> N, x -> floor(3 * (x[0] + x[1]) * (x[0] + x[1]) + 2);
+let Z_scalar_non_linear_2d: R^2 -> Z, x -> floor(exp(2 * x[0]) - 3 * x[1]);
+let R_scalar_non_linear_2d: R^2 -> R, x -> 2 * exp(x[0]) + 3 * x[1];
+let R1_non_linear_2d: R^2 -> R^1, x -> 2 * exp(x[0]);
+let R2_non_linear_2d: R^2 -> R^2, x -> (2 * exp(x[0]), -3*x[1]);
+let R3_non_linear_2d: R^2 -> R^3, x -> (2 * exp(x[0]) + 3, x[1] - 2, 3);
+let R1x1_non_linear_2d: R^2 -> R^1x1, x -> (2 * exp(x[0]) * sin(x[1]) + 3);
+let R2x2_non_linear_2d: R^2 -> R^2x2, x -> (2 * exp(x[0]) * sin(x[1]) + 3, sin(x[1] - 2 * x[0]), 3, x[1] * x[0]);
+let R3x3_non_linear_2d: R^2 -> R^3x3, x -> (2 * exp(x[0]) * sin(x[1]) + 3, sin(x[1] - 2 * x[0]), 3, x[1] * x[0], -4*x[1], 2*x[0]+1, 3, -6*x[0], exp(x[1]));
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    SECTION("B_scalar_non_linear_2d")
+    {
+      auto [i_symbol, found] = symbol_table->find("B_scalar_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::exp(2 * x[0]) < 2 * x[1];
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("N_scalar_non_linear_2d")
+    {
+      auto [i_symbol, found] = symbol_table->find("N_scalar_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(3 * (x[0] + x[1]) * (x[0] + x[1]) + 2);
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("Z_scalar_non_linear_2d")
+    {
+      auto [i_symbol, found] = symbol_table->find("Z_scalar_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(std::exp(2 * x[0]) - 3 * x[1]);
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("R_scalar_non_linear_2d")
+    {
+      auto [i_symbol, found] = symbol_table->find("R_scalar_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = 2 * std::exp(x[0]) + 3 * x[1];
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("R1_non_linear_2d")
+    {
+      using DataType = TinyVector<1>;
+
+      auto [i_symbol, found] = symbol_table->find("R1_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0])};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R2_non_linear_2d")
+    {
+      using DataType = TinyVector<2>;
+
+      auto [i_symbol, found] = symbol_table->find("R2_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]), -3 * x[1]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R3_non_linear_2d")
+    {
+      using DataType = TinyVector<3>;
+
+      auto [i_symbol, found] = symbol_table->find("R3_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) + 3, x[1] - 2, 3};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R1x1_non_linear_2d")
+    {
+      using DataType = TinyMatrix<1>;
+
+      auto [i_symbol, found] = symbol_table->find("R1x1_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) * std::sin(x[1]) + 3};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R2x2_non_linear_2d")
+    {
+      using DataType = TinyMatrix<2>;
+
+      auto [i_symbol, found] = symbol_table->find("R2x2_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id] =
+            DataType{2 * std::exp(x[0]) * std::sin(x[1]) + 3, std::sin(x[1] - 2 * x[0]), 3, x[1] * x[0]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R3x3_non_linear_2d")
+    {
+      using DataType = TinyMatrix<3>;
+
+      auto [i_symbol, found] = symbol_table->find("R3x3_non_linear_2d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+
+          cell_value[cell_id] = DataType{2 * std::exp(x[0]) * std::sin(x[1]) + 3,
+                                         std::sin(x[1] - 2 * x[0]),
+                                         3,
+                                         x[1] * x[0],
+                                         -4 * x[1],
+                                         2 * x[0] + 1,
+                                         3,
+                                         -6 * x[0],
+                                         std::exp(x[1])};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+  }
+
+  SECTION("3D")
+  {
+    constexpr size_t Dimension = 3;
+
+    const auto& mesh_3d = MeshDataBaseForTests::get().cartesianMesh3D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_3d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_3d: R^3 -> B, x -> (exp(2 * x[0])< 2*x[1]+x[2]);
+let N_scalar_non_linear_3d: R^3 -> N, x -> floor(3 * (x[0] + x[1]) * (x[0] + x[1]) + x[2] * x[2]);
+let Z_scalar_non_linear_3d: R^3 -> Z, x -> floor(exp(2 * x[0]) - 3 * x[1] + x[2]);
+let R_scalar_non_linear_3d: R^3 -> R, x -> 2 * exp(x[0]+x[2]) + 3 * x[1];
+let R1_non_linear_3d: R^3 -> R^1, x -> 2 * exp(x[0])+sin(x[1] + x[2]);
+let R2_non_linear_3d: R^3 -> R^2, x -> (2 * exp(x[0]), -3*x[1] * x[2]);
+let R3_non_linear_3d: R^3 -> R^3, x -> (2 * exp(x[0]) + 3, x[1] - 2, 3 * x[2]);
+let R1x1_non_linear_3d: R^3 -> R^1x1, x -> (2 * exp(x[0]) * sin(x[1]) + 3 * x[2]);
+let R2x2_non_linear_3d: R^3 -> R^2x2, x -> (2 * exp(x[0]) * sin(x[1]) + 3, sin(x[2] - 2 * x[0]), 3, x[1] * x[0] - x[2]);
+let R3x3_non_linear_3d: R^3 -> R^3x3, x -> (2 * exp(x[0]) * sin(x[1]) + 3, sin(x[1] - 2 * x[2]), 3, x[1] * x[2], -4*x[1], 2*x[2]+1, 3, -6*x[2], exp(x[1] + x[2]));
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    SECTION("B_scalar_non_linear_3d")
+    {
+      auto [i_symbol, found] = symbol_table->find("B_scalar_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::exp(2 * x[0]) < 2 * x[1] + x[2];
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("N_scalar_non_linear_3d")
+    {
+      auto [i_symbol, found] = symbol_table->find("N_scalar_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(3 * (x[0] + x[1]) * (x[0] + x[1]) + x[2] * x[2]);
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("Z_scalar_non_linear_3d")
+    {
+      auto [i_symbol, found] = symbol_table->find("Z_scalar_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(std::exp(2 * x[0]) - 3 * x[1] + x[2]);
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("R_scalar_non_linear_3d")
+    {
+      auto [i_symbol, found] = symbol_table->find("R_scalar_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = 2 * std::exp(x[0] + x[2]) + 3 * x[1];
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*discrete_function)));
+    }
+
+    SECTION("R1_non_linear_3d")
+    {
+      using DataType = TinyVector<1>;
+
+      auto [i_symbol, found] = symbol_table->find("R1_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) + std::sin(x[1] + x[2])};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R2_non_linear_3d")
+    {
+      using DataType = TinyVector<2>;
+
+      auto [i_symbol, found] = symbol_table->find("R2_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]), -3 * x[1] * x[2]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R3_non_linear_3d")
+    {
+      using DataType = TinyVector<3>;
+
+      auto [i_symbol, found] = symbol_table->find("R3_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) + 3, x[1] - 2, 3 * x[2]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R1x1_non_linear_3d")
+    {
+      using DataType = TinyMatrix<1>;
+
+      auto [i_symbol, found] = symbol_table->find("R1x1_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = DataType{2 * std::exp(x[0]) * std::sin(x[1]) + 3 * x[2]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R2x2_non_linear_3d")
+    {
+      using DataType = TinyMatrix<2>;
+
+      auto [i_symbol, found] = symbol_table->find("R2x2_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id] =
+            DataType{2 * std::exp(x[0]) * std::sin(x[1]) + 3, std::sin(x[2] - 2 * x[0]), 3, x[1] * x[0] - x[2]};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+
+    SECTION("R3x3_non_linear_3d")
+    {
+      using DataType = TinyMatrix<3>;
+
+      auto [i_symbol, found] = symbol_table->find("R3x3_non_linear_3d", position);
+      REQUIRE(found);
+      REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+      FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+
+      CellValue<DataType> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+
+          cell_value[cell_id] = DataType{2 * std::exp(x[0]) * std::sin(x[1]) + 3,
+                                         std::sin(x[1] - 2 * x[2]),
+                                         3,
+                                         x[1] * x[2],
+                                         -4 * x[1],
+                                         2 * x[2] + 1,
+                                         3,
+                                         -6 * x[2],
+                                         std::exp(x[1] + x[2])};
+        });
+
+      DiscreteFunctionInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(),
+                                            function_symbol_id);
+      std::shared_ptr discrete_function = interpoler.interpolate();
+
+      REQUIRE(
+        same_cell_value(cell_value, dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*discrete_function)));
+    }
+  }
+}
diff --git a/tests/test_DiscreteFunctionP0.cpp b/tests/test_DiscreteFunctionP0.cpp
index 8b7e48ea8f999b7d3d166a2f0b6eefc2a363bc75..f8d7bb6ef78d027287cf7d721299f051ba7c7a98 100644
--- a/tests/test_DiscreteFunctionP0.cpp
+++ b/tests/test_DiscreteFunctionP0.cpp
@@ -2309,6 +2309,1073 @@ TEST_CASE("DiscreteFunctionP0", "[scheme]")
     }
   }
 
+  SECTION("math functions")
+  {
+#define CHECK_STD_MATH_FUNCTION(data_expression, FCT)                           \
+  {                                                                             \
+    DiscreteFunctionP0 data   = data_expression;                                \
+    DiscreteFunctionP0 result = FCT(data);                                      \
+    bool is_same              = true;                                           \
+    parallel_for(data.cellValues().numberOfItems(), [&](const CellId cell_id) { \
+      if (result[cell_id] != std::FCT(data[cell_id])) {                         \
+        is_same = false;                                                        \
+      }                                                                         \
+    });                                                                         \
+    REQUIRE(is_same);                                                           \
+  }
+
+#define CHECK_STD_BINARY_MATH_FUNCTION(lhs_expression, rhs_expression, FCT)    \
+  {                                                                            \
+    DiscreteFunctionP0 lhs    = lhs_expression;                                \
+    DiscreteFunctionP0 rhs    = rhs_expression;                                \
+    DiscreteFunctionP0 result = FCT(lhs, rhs);                                 \
+    using namespace std;                                                       \
+    bool is_same = true;                                                       \
+    parallel_for(lhs.cellValues().numberOfItems(), [&](const CellId cell_id) { \
+      if (result[cell_id] != FCT(lhs[cell_id], rhs[cell_id])) {                \
+        is_same = false;                                                       \
+      }                                                                        \
+    });                                                                        \
+    REQUIRE(is_same);                                                          \
+  }
+
+#define CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(lhs, rhs_expression, FCT) \
+  {                                                                             \
+    DiscreteFunctionP0 rhs    = rhs_expression;                                 \
+    DiscreteFunctionP0 result = FCT(lhs, rhs);                                  \
+    bool is_same              = true;                                           \
+    using namespace std;                                                        \
+    parallel_for(rhs.cellValues().numberOfItems(), [&](const CellId cell_id) {  \
+      if (result[cell_id] != FCT(lhs, rhs[cell_id])) {                          \
+        is_same = false;                                                        \
+      }                                                                         \
+    });                                                                         \
+    REQUIRE(is_same);                                                           \
+  }
+
+#define CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(lhs_expression, rhs, FCT) \
+  {                                                                             \
+    DiscreteFunctionP0 lhs    = lhs_expression;                                 \
+    DiscreteFunctionP0 result = FCT(lhs, rhs);                                  \
+    bool is_same              = true;                                           \
+    using namespace std;                                                        \
+    parallel_for(lhs.cellValues().numberOfItems(), [&](const CellId cell_id) {  \
+      if (result[cell_id] != FCT(lhs[cell_id], rhs)) {                          \
+        is_same = false;                                                        \
+      }                                                                         \
+    });                                                                         \
+    REQUIRE(is_same);                                                           \
+  }
+
+    SECTION("1D")
+    {
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      constexpr size_t Dimension = 1;
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      DiscreteFunctionP0<Dimension, double> positive_function{mesh};
+
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(const CellId cell_id) { positive_function[cell_id] = 1 + std::abs(xj[cell_id][0]); });
+
+      const double min_value = min(positive_function);
+      SECTION("min")
+      {
+        double local_min = std::numeric_limits<double>::max();
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          local_min = std::min(local_min, positive_function[cell_id]);
+        }
+        REQUIRE(min_value == parallel::allReduceMin(local_min));
+      }
+
+      const double max_value = max(positive_function);
+      SECTION("max")
+      {
+        double local_max = -std::numeric_limits<double>::max();
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          local_max = std::max(local_max, positive_function[cell_id]);
+        }
+        REQUIRE(max_value == parallel::allReduceMax(local_max));
+      }
+
+      REQUIRE(min_value < max_value);
+
+      DiscreteFunctionP0 unsigned_function = positive_function - 0.5 * (min_value + max_value);
+
+      SECTION("sqrt")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sqrt);
+      }
+
+      SECTION("abs")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, abs);
+      }
+
+      SECTION("cos")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, cos);
+      }
+
+      SECTION("sin")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sin);
+      }
+
+      SECTION("tan")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, tan);
+      }
+
+      DiscreteFunctionP0<Dimension, double> unit_function{mesh};
+
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+          unit_function[cell_id] = (2 * (positive_function[cell_id] - min_value) / (max_value - min_value) - 1) * 0.95;
+        });
+
+      SECTION("acos")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, acos);
+      }
+
+      SECTION("asin")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, asin);
+      }
+
+      SECTION("atan")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, atan);
+      }
+
+      SECTION("cosh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, cosh);
+      }
+
+      SECTION("sinh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sinh);
+      }
+
+      SECTION("tanh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, tanh);
+      }
+
+      SECTION("acosh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, acosh);
+      }
+
+      SECTION("asinh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, asinh);
+      }
+
+      SECTION("atanh")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, atanh);
+      }
+
+      SECTION("exp")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, exp);
+      }
+
+      SECTION("log")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, log);
+      }
+
+      SECTION("max(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(cos(positive_function), sin(positive_function), max);
+      }
+
+      SECTION("max(0.2,vh)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.2, sin(positive_function), max);
+      }
+
+      SECTION("max(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(cos(positive_function), 0.2, max);
+      }
+
+      SECTION("atan2(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(positive_function, 2 + positive_function, atan2);
+      }
+
+      SECTION("atan2(0.5,uh)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, 2 + positive_function, atan2);
+      }
+
+      SECTION("atan2(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(2 + cos(positive_function), 0.2, atan2);
+      }
+
+      SECTION("pow(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(positive_function, 0.5 * positive_function, pow);
+      }
+
+      SECTION("pow(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, positive_function, pow);
+      }
+
+      SECTION("pow(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(positive_function, 1.3, pow);
+      }
+
+      SECTION("min(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(sin(positive_function), cos(positive_function), min);
+      }
+
+      SECTION("min(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, cos(positive_function), min);
+      }
+
+      SECTION("min(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(sin(positive_function), 0.5, min);
+      }
+
+      SECTION("max(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(sin(positive_function), cos(positive_function), max);
+      }
+
+      SECTION("min(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.1, cos(positive_function), max);
+      }
+
+      SECTION("min(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(sin(positive_function), 0.1, max);
+      }
+
+      SECTION("dot(uh,hv)")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        DiscreteFunctionP0<Dimension, TinyVector<2>> vh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            vh[cell_id]    = TinyVector<2>{2.3 * x, 1 - x};
+          });
+
+        CHECK_STD_BINARY_MATH_FUNCTION(uh, vh, dot);
+      }
+
+      SECTION("dot(uh,v)")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        const TinyVector<2> v{1, 2};
+
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(uh, v, dot);
+      }
+
+      SECTION("dot(u,hv)")
+      {
+        const TinyVector<2> u{3, -2};
+
+        DiscreteFunctionP0<Dimension, TinyVector<2>> vh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            vh[cell_id]    = TinyVector<2>{2.3 * x, 1 - x};
+          });
+
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(u, vh, dot);
+      }
+
+      SECTION("scalar sum")
+      {
+        const CellValue<const double> cell_value = positive_function.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(positive_function));
+      }
+
+      SECTION("vector sum")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+        const CellValue<const TinyVector<2>> cell_value = uh.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(uh));
+      }
+
+      SECTION("matrix sum")
+      {
+        DiscreteFunctionP0<Dimension, TinyMatrix<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyMatrix<2>{x + 1, 2 * x - 3, 2 * x, 3 * x - 1};
+          });
+        const CellValue<const TinyMatrix<2>> cell_value = uh.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(uh));
+      }
+
+      SECTION("integrate scalar")
+      {
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<double> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            cell_value[cell_id] = cell_volume[cell_id] * positive_function[cell_id];
+          });
+
+        REQUIRE(integrate(positive_function) == Catch::Approx(sum(cell_value)));
+      }
+
+      SECTION("integrate vector")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<TinyVector<2>> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(),
+          PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_volume[cell_id] * uh[cell_id]; });
+
+        REQUIRE(integrate(uh)[0] == Catch::Approx(sum(cell_value)[0]));
+        REQUIRE(integrate(uh)[1] == Catch::Approx(sum(cell_value)[1]));
+      }
+
+      SECTION("integrate matrix")
+      {
+        DiscreteFunctionP0<Dimension, TinyMatrix<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyMatrix<2>{x + 1, 2 * x - 3, 2 * x, 1 - x};
+          });
+
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<TinyMatrix<2>> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(),
+          PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_volume[cell_id] * uh[cell_id]; });
+
+        REQUIRE(integrate(uh)(0, 0) == Catch::Approx(sum(cell_value)(0, 0)));
+        REQUIRE(integrate(uh)(0, 1) == Catch::Approx(sum(cell_value)(0, 1)));
+        REQUIRE(integrate(uh)(1, 0) == Catch::Approx(sum(cell_value)(1, 0)));
+        REQUIRE(integrate(uh)(1, 1) == Catch::Approx(sum(cell_value)(1, 1)));
+      }
+    }
+
+    SECTION("2D")
+    {
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh2D();
+
+      constexpr size_t Dimension = 2;
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      DiscreteFunctionP0<Dimension, double> positive_function{mesh};
+
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(const CellId cell_id) { positive_function[cell_id] = 1 + std::abs(xj[cell_id][0]); });
+
+      const double min_value = min(positive_function);
+      SECTION("min")
+      {
+        double local_min = std::numeric_limits<double>::max();
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          local_min = std::min(local_min, positive_function[cell_id]);
+        }
+        REQUIRE(min_value == parallel::allReduceMin(local_min));
+      }
+
+      const double max_value = max(positive_function);
+      SECTION("max")
+      {
+        double local_max = -std::numeric_limits<double>::max();
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          local_max = std::max(local_max, positive_function[cell_id]);
+        }
+        REQUIRE(max_value == parallel::allReduceMax(local_max));
+      }
+
+      REQUIRE(min_value < max_value);
+
+      DiscreteFunctionP0 unsigned_function = positive_function - 0.5 * (min_value + max_value);
+
+      SECTION("sqrt")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sqrt);
+      }
+
+      SECTION("abs")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, abs);
+      }
+
+      SECTION("cos")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, cos);
+      }
+
+      SECTION("sin")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sin);
+      }
+
+      SECTION("tan")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, tan);
+      }
+
+      DiscreteFunctionP0<Dimension, double> unit_function{mesh};
+
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+          unit_function[cell_id] = (2 * (positive_function[cell_id] - min_value) / (max_value - min_value) - 1) * 0.95;
+        });
+
+      SECTION("acos")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, acos);
+      }
+
+      SECTION("asin")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, asin);
+      }
+
+      SECTION("atan")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, atan);
+      }
+
+      SECTION("cosh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, cosh);
+      }
+
+      SECTION("sinh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sinh);
+      }
+
+      SECTION("tanh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, tanh);
+      }
+
+      SECTION("acosh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, acosh);
+      }
+
+      SECTION("asinh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, asinh);
+      }
+
+      SECTION("atanh")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, atanh);
+      }
+
+      SECTION("exp")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, exp);
+      }
+
+      SECTION("log")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, log);
+      }
+
+      SECTION("max(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(cos(positive_function), sin(positive_function), max);
+      }
+
+      SECTION("max(0.2,vh)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.2, sin(positive_function), max);
+      }
+
+      SECTION("max(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(cos(positive_function), 0.2, max);
+      }
+
+      SECTION("atan2(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(positive_function, 2 + positive_function, atan2);
+      }
+
+      SECTION("atan2(0.5,uh)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, 2 + positive_function, atan2);
+      }
+
+      SECTION("atan2(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(2 + cos(positive_function), 0.2, atan2);
+      }
+
+      SECTION("pow(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(positive_function, 0.5 * positive_function, pow);
+      }
+
+      SECTION("pow(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, positive_function, pow);
+      }
+
+      SECTION("pow(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(positive_function, 1.3, pow);
+      }
+
+      SECTION("min(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(sin(positive_function), cos(positive_function), min);
+      }
+
+      SECTION("min(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, cos(positive_function), min);
+      }
+
+      SECTION("min(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(sin(positive_function), 0.5, min);
+      }
+
+      SECTION("max(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(sin(positive_function), cos(positive_function), max);
+      }
+
+      SECTION("min(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.1, cos(positive_function), max);
+      }
+
+      SECTION("min(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(sin(positive_function), 0.1, max);
+      }
+
+      SECTION("dot(uh,hv)")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        DiscreteFunctionP0<Dimension, TinyVector<2>> vh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            vh[cell_id]    = TinyVector<2>{2.3 * x, 1 - x};
+          });
+
+        CHECK_STD_BINARY_MATH_FUNCTION(uh, vh, dot);
+      }
+
+      SECTION("dot(uh,v)")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        const TinyVector<2> v{1, 2};
+
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(uh, v, dot);
+      }
+
+      SECTION("dot(u,hv)")
+      {
+        const TinyVector<2> u{3, -2};
+
+        DiscreteFunctionP0<Dimension, TinyVector<2>> vh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            vh[cell_id]    = TinyVector<2>{2.3 * x, 1 - x};
+          });
+
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(u, vh, dot);
+      }
+
+      SECTION("scalar sum")
+      {
+        const CellValue<const double> cell_value = positive_function.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(positive_function));
+      }
+
+      SECTION("vector sum")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+        const CellValue<const TinyVector<2>> cell_value = uh.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(uh));
+      }
+
+      SECTION("matrix sum")
+      {
+        DiscreteFunctionP0<Dimension, TinyMatrix<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyMatrix<2>{x + 1, 2 * x - 3, 2 * x, 3 * x - 1};
+          });
+        const CellValue<const TinyMatrix<2>> cell_value = uh.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(uh));
+      }
+
+      SECTION("integrate scalar")
+      {
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<double> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            cell_value[cell_id] = cell_volume[cell_id] * positive_function[cell_id];
+          });
+
+        REQUIRE(integrate(positive_function) == Catch::Approx(sum(cell_value)));
+      }
+
+      SECTION("integrate vector")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<TinyVector<2>> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(),
+          PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_volume[cell_id] * uh[cell_id]; });
+
+        REQUIRE(integrate(uh)[0] == Catch::Approx(sum(cell_value)[0]));
+        REQUIRE(integrate(uh)[1] == Catch::Approx(sum(cell_value)[1]));
+      }
+
+      SECTION("integrate matrix")
+      {
+        DiscreteFunctionP0<Dimension, TinyMatrix<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyMatrix<2>{x + 1, 2 * x - 3, 2 * x, 1 - x};
+          });
+
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<TinyMatrix<2>> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(),
+          PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_volume[cell_id] * uh[cell_id]; });
+
+        REQUIRE(integrate(uh)(0, 0) == Catch::Approx(sum(cell_value)(0, 0)));
+        REQUIRE(integrate(uh)(0, 1) == Catch::Approx(sum(cell_value)(0, 1)));
+        REQUIRE(integrate(uh)(1, 0) == Catch::Approx(sum(cell_value)(1, 0)));
+        REQUIRE(integrate(uh)(1, 1) == Catch::Approx(sum(cell_value)(1, 1)));
+      }
+    }
+
+    SECTION("3D")
+    {
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh3D();
+
+      constexpr size_t Dimension = 3;
+
+      auto xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      DiscreteFunctionP0<Dimension, double> positive_function{mesh};
+
+      parallel_for(
+        mesh->numberOfCells(),
+        PUGS_LAMBDA(const CellId cell_id) { positive_function[cell_id] = 1 + std::abs(xj[cell_id][0]); });
+
+      const double min_value = min(positive_function);
+      SECTION("min")
+      {
+        double local_min = std::numeric_limits<double>::max();
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          local_min = std::min(local_min, positive_function[cell_id]);
+        }
+        REQUIRE(min_value == parallel::allReduceMin(local_min));
+      }
+
+      const double max_value = max(positive_function);
+      SECTION("max")
+      {
+        double local_max = -std::numeric_limits<double>::max();
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          local_max = std::max(local_max, positive_function[cell_id]);
+        }
+        REQUIRE(max_value == parallel::allReduceMax(local_max));
+      }
+
+      REQUIRE(min_value < max_value);
+
+      DiscreteFunctionP0 unsigned_function = positive_function - 0.5 * (min_value + max_value);
+
+      SECTION("sqrt")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sqrt);
+      }
+
+      SECTION("abs")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, abs);
+      }
+
+      SECTION("cos")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, cos);
+      }
+
+      SECTION("sin")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sin);
+      }
+
+      SECTION("tan")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, tan);
+      }
+
+      DiscreteFunctionP0<Dimension, double> unit_function{mesh};
+
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+          unit_function[cell_id] = (2 * (positive_function[cell_id] - min_value) / (max_value - min_value) - 1) * 0.95;
+        });
+
+      SECTION("acos")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, acos);
+      }
+
+      SECTION("asin")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, asin);
+      }
+
+      SECTION("atan")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, atan);
+      }
+
+      SECTION("cosh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, cosh);
+      }
+
+      SECTION("sinh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, sinh);
+      }
+
+      SECTION("tanh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, tanh);
+      }
+
+      SECTION("acosh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, acosh);
+      }
+
+      SECTION("asinh")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, asinh);
+      }
+
+      SECTION("atanh")
+      {
+        CHECK_STD_MATH_FUNCTION(unit_function, atanh);
+      }
+
+      SECTION("exp")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, exp);
+      }
+
+      SECTION("log")
+      {
+        CHECK_STD_MATH_FUNCTION(positive_function, log);
+      }
+
+      SECTION("max(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(cos(positive_function), sin(positive_function), max);
+      }
+
+      SECTION("max(0.2,vh)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.2, sin(positive_function), max);
+      }
+
+      SECTION("max(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(cos(positive_function), 0.2, max);
+      }
+
+      SECTION("atan2(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(positive_function, 2 + positive_function, atan2);
+      }
+
+      SECTION("atan2(0.5,uh)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, 2 + positive_function, atan2);
+      }
+
+      SECTION("atan2(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(2 + cos(positive_function), 0.2, atan2);
+      }
+
+      SECTION("pow(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(positive_function, 0.5 * positive_function, pow);
+      }
+
+      SECTION("pow(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, positive_function, pow);
+      }
+
+      SECTION("pow(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(positive_function, 1.3, pow);
+      }
+
+      SECTION("min(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(sin(positive_function), cos(positive_function), min);
+      }
+
+      SECTION("min(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.5, cos(positive_function), min);
+      }
+
+      SECTION("min(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(sin(positive_function), 0.5, min);
+      }
+
+      SECTION("max(uh,hv)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION(sin(positive_function), cos(positive_function), max);
+      }
+
+      SECTION("min(uh,0.5)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(0.1, cos(positive_function), max);
+      }
+
+      SECTION("min(uh,0.2)")
+      {
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(sin(positive_function), 0.1, max);
+      }
+
+      SECTION("dot(uh,hv)")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        DiscreteFunctionP0<Dimension, TinyVector<2>> vh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            vh[cell_id]    = TinyVector<2>{2.3 * x, 1 - x};
+          });
+
+        CHECK_STD_BINARY_MATH_FUNCTION(uh, vh, dot);
+      }
+
+      SECTION("dot(uh,v)")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        const TinyVector<2> v{1, 2};
+
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_RHS_VALUE(uh, v, dot);
+      }
+
+      SECTION("dot(u,hv)")
+      {
+        const TinyVector<2> u{3, -2};
+
+        DiscreteFunctionP0<Dimension, TinyVector<2>> vh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            vh[cell_id]    = TinyVector<2>{2.3 * x, 1 - x};
+          });
+
+        CHECK_STD_BINARY_MATH_FUNCTION_WITH_LHS_VALUE(u, vh, dot);
+      }
+
+      SECTION("scalar sum")
+      {
+        const CellValue<const double> cell_value = positive_function.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(positive_function));
+      }
+
+      SECTION("vector sum")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+        const CellValue<const TinyVector<2>> cell_value = uh.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(uh));
+      }
+
+      SECTION("matrix sum")
+      {
+        DiscreteFunctionP0<Dimension, TinyMatrix<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyMatrix<2>{x + 1, 2 * x - 3, 2 * x, 3 * x - 1};
+          });
+        const CellValue<const TinyMatrix<2>> cell_value = uh.cellValues();
+
+        REQUIRE(sum(cell_value) == sum(uh));
+      }
+
+      SECTION("integrate scalar")
+      {
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<double> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            cell_value[cell_id] = cell_volume[cell_id] * positive_function[cell_id];
+          });
+
+        REQUIRE(integrate(positive_function) == Catch::Approx(sum(cell_value)));
+      }
+
+      SECTION("integrate vector")
+      {
+        DiscreteFunctionP0<Dimension, TinyVector<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyVector<2>{x + 1, 2 * x - 3};
+          });
+
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<TinyVector<2>> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(),
+          PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_volume[cell_id] * uh[cell_id]; });
+
+        REQUIRE(integrate(uh)[0] == Catch::Approx(sum(cell_value)[0]));
+        REQUIRE(integrate(uh)[1] == Catch::Approx(sum(cell_value)[1]));
+      }
+
+      SECTION("integrate matrix")
+      {
+        DiscreteFunctionP0<Dimension, TinyMatrix<2>> uh{mesh};
+        parallel_for(
+          mesh->numberOfCells(), PUGS_LAMBDA(const CellId cell_id) {
+            const double x = xj[cell_id][0];
+            uh[cell_id]    = TinyMatrix<2>{x + 1, 2 * x - 3, 2 * x, 1 - x};
+          });
+
+        const CellValue<const double> cell_volume = MeshDataManager::instance().getMeshData(*mesh).Vj();
+
+        CellValue<TinyMatrix<2>> cell_value{mesh->connectivity()};
+        parallel_for(
+          mesh->numberOfCells(),
+          PUGS_LAMBDA(const CellId cell_id) { cell_value[cell_id] = cell_volume[cell_id] * uh[cell_id]; });
+
+        REQUIRE(integrate(uh)(0, 0) == Catch::Approx(sum(cell_value)(0, 0)));
+        REQUIRE(integrate(uh)(0, 1) == Catch::Approx(sum(cell_value)(0, 1)));
+        REQUIRE(integrate(uh)(1, 0) == Catch::Approx(sum(cell_value)(1, 0)));
+        REQUIRE(integrate(uh)(1, 1) == Catch::Approx(sum(cell_value)(1, 1)));
+      }
+    }
+  }
+
 #ifndef NDEBUG
   SECTION("error")
   {
diff --git a/tests/test_DiscreteFunctionUtils.cpp b/tests/test_DiscreteFunctionUtils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6587fcba0e201539552e78f4550523b9dbe277eb
--- /dev/null
+++ b/tests/test_DiscreteFunctionUtils.cpp
@@ -0,0 +1,477 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionP0Vector.hpp>
+#include <scheme/DiscreteFunctionUtils.hpp>
+
+#include <mesh/CartesianMeshBuilder.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("DiscreteFunctionUtils", "[scheme]")
+{
+  SECTION("1D")
+  {
+    constexpr size_t Dimension = 1;
+
+    std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh1D();
+    std::shared_ptr mesh_copy =
+      std::make_shared<std::decay_t<decltype(*mesh)>>(mesh->shared_connectivity(), mesh->xr());
+
+    SECTION("common mesh")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr wh = std::make_shared<DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh);
+
+      std::shared_ptr qh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_copy);
+
+      REQUIRE(getCommonMesh({uh, vh, wh}).get() == mesh.get());
+      REQUIRE(getCommonMesh({uh, vh, wh, qh}).use_count() == 0);
+    }
+
+    SECTION("check discretization type")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+
+      std::shared_ptr qh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_copy);
+
+      std::shared_ptr Uh = std::make_shared<DiscreteFunctionP0Vector<Dimension, double>>(mesh, 3);
+      std::shared_ptr Vh = std::make_shared<DiscreteFunctionP0Vector<Dimension, double>>(mesh, 3);
+
+      REQUIRE(checkDiscretizationType({uh}, DiscreteFunctionType::P0));
+      REQUIRE(checkDiscretizationType({uh, vh, qh}, DiscreteFunctionType::P0));
+      REQUIRE(not checkDiscretizationType({uh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(not checkDiscretizationType({uh, vh, qh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(checkDiscretizationType({Uh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(checkDiscretizationType({Uh, Vh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(not checkDiscretizationType({Uh, Vh}, DiscreteFunctionType::P0));
+      REQUIRE(not checkDiscretizationType({Uh}, DiscreteFunctionType::P0));
+    }
+
+    SECTION("scalar function shallow copy")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^1 function shallow copy")
+    {
+      using DataType     = TinyVector<1>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^2 function shallow copy")
+    {
+      using DataType     = TinyVector<2>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^3 function shallow copy")
+    {
+      using DataType     = TinyVector<3>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^1x1 function shallow copy")
+    {
+      using DataType     = TinyMatrix<1>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^2x2 function shallow copy")
+    {
+      using DataType     = TinyMatrix<2>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^3x3 function shallow copy")
+    {
+      using DataType     = TinyMatrix<3>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+  }
+
+  SECTION("2D")
+  {
+    constexpr size_t Dimension = 2;
+
+    std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh2D();
+    std::shared_ptr mesh_copy =
+      std::make_shared<std::decay_t<decltype(*mesh)>>(mesh->shared_connectivity(), mesh->xr());
+
+    SECTION("common mesh")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr wh = std::make_shared<DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh);
+
+      std::shared_ptr qh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_copy);
+
+      REQUIRE(getCommonMesh({uh, vh, wh}).get() == mesh.get());
+      REQUIRE(getCommonMesh({uh, vh, wh, qh}).use_count() == 0);
+    }
+
+    SECTION("check discretization type")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+
+      std::shared_ptr qh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_copy);
+
+      std::shared_ptr Uh = std::make_shared<DiscreteFunctionP0Vector<Dimension, double>>(mesh, 3);
+      std::shared_ptr Vh = std::make_shared<DiscreteFunctionP0Vector<Dimension, double>>(mesh, 3);
+
+      REQUIRE(checkDiscretizationType({uh}, DiscreteFunctionType::P0));
+      REQUIRE(checkDiscretizationType({uh, vh, qh}, DiscreteFunctionType::P0));
+      REQUIRE(not checkDiscretizationType({uh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(not checkDiscretizationType({uh, vh, qh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(checkDiscretizationType({Uh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(checkDiscretizationType({Uh, Vh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(not checkDiscretizationType({Uh, Vh}, DiscreteFunctionType::P0));
+      REQUIRE(not checkDiscretizationType({Uh}, DiscreteFunctionType::P0));
+    }
+
+    SECTION("scalar function shallow copy")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^1 function shallow copy")
+    {
+      using DataType     = TinyVector<1>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^2 function shallow copy")
+    {
+      using DataType     = TinyVector<2>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^3 function shallow copy")
+    {
+      using DataType     = TinyVector<3>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^1x1 function shallow copy")
+    {
+      using DataType     = TinyMatrix<1>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^2x2 function shallow copy")
+    {
+      using DataType     = TinyMatrix<2>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^3x3 function shallow copy")
+    {
+      using DataType     = TinyMatrix<3>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+  }
+
+  SECTION("3D")
+  {
+    constexpr size_t Dimension = 3;
+
+    std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh3D();
+    std::shared_ptr mesh_copy =
+      std::make_shared<std::decay_t<decltype(*mesh)>>(mesh->shared_connectivity(), mesh->xr());
+
+    SECTION("common mesh")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr wh = std::make_shared<DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh);
+
+      std::shared_ptr qh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_copy);
+
+      REQUIRE(getCommonMesh({uh, vh, wh}).get() == mesh.get());
+      REQUIRE(getCommonMesh({uh, vh, wh, qh}).use_count() == 0);
+    }
+
+    SECTION("check discretization type")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+
+      std::shared_ptr qh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_copy);
+
+      std::shared_ptr Uh = std::make_shared<DiscreteFunctionP0Vector<Dimension, double>>(mesh, 3);
+      std::shared_ptr Vh = std::make_shared<DiscreteFunctionP0Vector<Dimension, double>>(mesh, 3);
+
+      REQUIRE(checkDiscretizationType({uh}, DiscreteFunctionType::P0));
+      REQUIRE(checkDiscretizationType({uh, vh, qh}, DiscreteFunctionType::P0));
+      REQUIRE(not checkDiscretizationType({uh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(not checkDiscretizationType({uh, vh, qh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(checkDiscretizationType({Uh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(checkDiscretizationType({Uh, Vh}, DiscreteFunctionType::P0Vector));
+      REQUIRE(not checkDiscretizationType({Uh, Vh}, DiscreteFunctionType::P0));
+      REQUIRE(not checkDiscretizationType({Uh}, DiscreteFunctionType::P0));
+    }
+
+    SECTION("scalar function shallow copy")
+    {
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^1 function shallow copy")
+    {
+      using DataType     = TinyVector<1>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^2 function shallow copy")
+    {
+      using DataType     = TinyVector<2>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^3 function shallow copy")
+    {
+      using DataType     = TinyVector<3>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^1x1 function shallow copy")
+    {
+      using DataType     = TinyMatrix<1>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^2x2 function shallow copy")
+    {
+      using DataType     = TinyMatrix<2>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+
+    SECTION("R^3x3 function shallow copy")
+    {
+      using DataType     = TinyMatrix<3>;
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, DataType>>(mesh);
+      std::shared_ptr vh = shallowCopy(mesh, uh);
+
+      REQUIRE(uh == vh);
+
+      std::shared_ptr wh = shallowCopy(mesh_copy, uh);
+
+      REQUIRE(uh != wh);
+      REQUIRE(&(uh->cellValues()[CellId{0}]) ==
+              &(dynamic_cast<const DiscreteFunctionP0<Dimension, DataType>&>(*wh).cellValues()[CellId{0}]));
+    }
+  }
+
+  SECTION("errors")
+  {
+    SECTION("different connectivities")
+    {
+      constexpr size_t Dimension = 1;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh1D();
+      std::shared_ptr other_mesh =
+        CartesianMeshBuilder{TinyVector<1>{-1}, TinyVector<1>{3}, TinyVector<1, size_t>{19}}.mesh();
+
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh);
+
+      REQUIRE_THROWS_WITH(shallowCopy(other_mesh, uh), "error: cannot shallow copy when connectivity changes");
+    }
+
+    SECTION("incompatible mesh dimension")
+    {
+      constexpr size_t Dimension = 1;
+
+      std::shared_ptr mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+      std::shared_ptr mesh_2d = MeshDataBaseForTests::get().cartesianMesh2D();
+
+      std::shared_ptr uh = std::make_shared<DiscreteFunctionP0<Dimension, double>>(mesh_1d);
+
+      REQUIRE_THROWS_WITH(shallowCopy(mesh_2d, uh), "error: incompatible mesh dimensions");
+    }
+  }
+}
diff --git a/tests/test_DiscreteFunctionVectorInterpoler.cpp b/tests/test_DiscreteFunctionVectorInterpoler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6a1a5a94b55302aa36fbd85b7d6e7b1522a4ef6b
--- /dev/null
+++ b/tests/test_DiscreteFunctionVectorInterpoler.cpp
@@ -0,0 +1,414 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <language/ast/ASTBuilder.hpp>
+#include <language/ast/ASTModulesImporter.hpp>
+#include <language/ast/ASTNodeDataTypeBuilder.hpp>
+#include <language/ast/ASTNodeExpressionBuilder.hpp>
+#include <language/ast/ASTNodeFunctionEvaluationExpressionBuilder.hpp>
+#include <language/ast/ASTNodeFunctionExpressionBuilder.hpp>
+#include <language/ast/ASTNodeTypeCleaner.hpp>
+#include <language/ast/ASTSymbolTableBuilder.hpp>
+#include <language/utils/PugsFunctionAdapter.hpp>
+#include <language/utils/SymbolTable.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshData.hpp>
+#include <mesh/MeshDataManager.hpp>
+
+#include <scheme/DiscreteFunctionDescriptorP0Vector.hpp>
+#include <scheme/DiscreteFunctionP0Vector.hpp>
+#include <scheme/DiscreteFunctionVectorInterpoler.hpp>
+
+#include <pegtl/string_input.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("DiscreteFunctionVectorInterpoler", "[scheme]")
+{
+  auto same_cell_value = [](const CellValue<const double>& fi, const size_t i, const auto& f) -> bool {
+    for (CellId cell_id = 0; cell_id < fi.numberOfItems(); ++cell_id) {
+      if (fi[cell_id] != f[cell_id][i]) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  auto register_function = [](const TAO_PEGTL_NAMESPACE::position& position,
+                              const std::shared_ptr<SymbolTable>& symbol_table, const std::string& name,
+                              std::vector<FunctionSymbolId>& function_id_list) {
+    auto [i_symbol, found] = symbol_table->find(name, position);
+    REQUIRE(found);
+    REQUIRE(i_symbol->attributes().dataType() == ASTNodeDataType::function_t);
+
+    FunctionSymbolId function_symbol_id(std::get<uint64_t>(i_symbol->attributes().value()), symbol_table);
+    function_id_list.push_back(function_symbol_id);
+  };
+
+  SECTION("1D")
+  {
+    constexpr size_t Dimension = 1;
+
+    const auto& mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_1d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_1d: R^1 -> B, x -> (exp(2 * x[0]) + 3 > 4);
+let N_scalar_non_linear_1d: R^1 -> N, x -> floor(3 * x[0] * x[0] + 2);
+let Z_scalar_non_linear_1d: R^1 -> Z, x -> floor(exp(2 * x[0]) - 1);
+let R_scalar_non_linear_1d: R^1 -> R, x -> 2 * exp(x[0]) + 3;
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    std::vector<FunctionSymbolId> function_id_list;
+    register_function(position, symbol_table, "B_scalar_non_linear_1d", function_id_list);
+    register_function(position, symbol_table, "N_scalar_non_linear_1d", function_id_list);
+    register_function(position, symbol_table, "Z_scalar_non_linear_1d", function_id_list);
+    register_function(position, symbol_table, "R_scalar_non_linear_1d", function_id_list);
+
+    DiscreteFunctionVectorInterpoler interpoler(mesh_1d, std::make_shared<DiscreteFunctionDescriptorP0Vector>(),
+                                                function_id_list);
+    std::shared_ptr discrete_function = interpoler.interpolate();
+
+    size_t i = 0;
+
+    {
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::exp(2 * x[0]) + 3 > 4;
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(3 * x[0] * x[0] + 2);
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(std::exp(2 * x[0]) - 1);
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_1d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = 2 * std::exp(x[0]) + 3;
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    REQUIRE(i == function_id_list.size());
+  }
+
+  SECTION("2D")
+  {
+    constexpr size_t Dimension = 2;
+
+    const auto& mesh_2d = MeshDataBaseForTests::get().cartesianMesh2D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_2d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_2d: R^2 -> B, x -> (exp(2 * x[0]) + 3 > 4);
+let N_scalar_non_linear_2d: R^2 -> N, x -> floor(3 * (x[0] * x[1]) * (x[0] * x[1]) + 2);
+let Z_scalar_non_linear_2d: R^2 -> Z, x -> floor(exp(2 * x[1]) - 1);
+let R_scalar_non_linear_2d: R^2 -> R, x -> 2 * exp(x[0] + x[1]) + 3;
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    std::vector<FunctionSymbolId> function_id_list;
+    register_function(position, symbol_table, "B_scalar_non_linear_2d", function_id_list);
+    register_function(position, symbol_table, "N_scalar_non_linear_2d", function_id_list);
+    register_function(position, symbol_table, "Z_scalar_non_linear_2d", function_id_list);
+    register_function(position, symbol_table, "R_scalar_non_linear_2d", function_id_list);
+
+    DiscreteFunctionVectorInterpoler interpoler(mesh_2d, std::make_shared<DiscreteFunctionDescriptorP0Vector>(),
+                                                function_id_list);
+    std::shared_ptr discrete_function = interpoler.interpolate();
+
+    size_t i = 0;
+
+    {
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::exp(2 * x[0]) + 3 > 4;
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(3 * (x[0] * x[1]) * (x[0] * x[1]) + 2);
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(std::exp(2 * x[1]) - 1);
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_2d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = 2 * std::exp(x[0] + x[1]) + 3;
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    REQUIRE(i == function_id_list.size());
+  }
+
+  SECTION("3D")
+  {
+    constexpr size_t Dimension = 3;
+
+    const auto& mesh_3d = MeshDataBaseForTests::get().cartesianMesh3D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_3d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_3d: R^3 -> B, x -> (exp(2 * x[0] + x[2]) + 3 > 4);
+let N_scalar_non_linear_3d: R^3 -> N, x -> floor(3 * (x[0] * x[1]) * (x[0] * x[1]) + 2);
+let Z_scalar_non_linear_3d: R^3 -> Z, x -> floor(exp(2 * x[1]) - x[2]);
+let R_scalar_non_linear_3d: R^3 -> R, x -> 2 * exp(x[0] + x[1]) + 3 * x[2];
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    std::vector<FunctionSymbolId> function_id_list;
+    register_function(position, symbol_table, "B_scalar_non_linear_3d", function_id_list);
+    register_function(position, symbol_table, "N_scalar_non_linear_3d", function_id_list);
+    register_function(position, symbol_table, "Z_scalar_non_linear_3d", function_id_list);
+    register_function(position, symbol_table, "R_scalar_non_linear_3d", function_id_list);
+
+    DiscreteFunctionVectorInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0Vector>(),
+                                                function_id_list);
+    std::shared_ptr discrete_function = interpoler.interpolate();
+
+    size_t i = 0;
+
+    {
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::exp(2 * x[0] + x[2]) + 3 > 4;
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(3 * (x[0] * x[1]) * (x[0] * x[1]) + 2);
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = std::floor(std::exp(2 * x[1]) - x[2]);
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    {
+      CellValue<double> cell_value{mesh_3d->connectivity()};
+      parallel_for(
+        cell_value.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<Dimension>& x = xj[cell_id];
+          cell_value[cell_id]            = 2 * std::exp(x[0] + x[1]) + 3 * x[2];
+        });
+
+      REQUIRE(same_cell_value(cell_value, i++,
+                              dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*discrete_function)));
+    }
+
+    REQUIRE(i == function_id_list.size());
+  }
+
+  SECTION("errors")
+  {
+    const auto& mesh_3d = MeshDataBaseForTests::get().cartesianMesh3D();
+    auto xj             = MeshDataManager::instance().getMeshData(*mesh_3d).xj();
+
+    std::string_view data = R"(
+import math;
+let B_scalar_non_linear_2d: R^2 -> B, x -> (exp(2 * x[0] + x[1]) + 3 > 4);
+let N_scalar_non_linear_1d: R^1 -> N, x -> floor(3 * x[0] * x[0] + 2);
+let Z_scalar_non_linear_3d: R^3 -> Z, x -> floor(exp(2 * x[1]) - x[2]);
+let R_scalar_non_linear_3d: R^3 -> R, x -> 2 * exp(x[0] + x[1]) + 3 * x[2];
+let R2_scalar_non_linear_3d: R^3 -> R^2, x -> (2 * exp(x[0] + x[1]) + 3 * x[2], x[0] - x[1]);
+)";
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+    ASTNodeTypeCleaner<language::fct_declaration>{*ast};
+    ASTNodeExpressionBuilder{*ast};
+
+    TAO_PEGTL_NAMESPACE::position position{TAO_PEGTL_NAMESPACE::internal::iterator{"fixture"}, "fixture"};
+    position.byte = data.size();   // ensure that variables are declared at this point
+
+    std::shared_ptr<SymbolTable> symbol_table = ast->m_symbol_table;
+
+    SECTION("invalid function type")
+    {
+      std::vector<FunctionSymbolId> function_id_list;
+      register_function(position, symbol_table, "B_scalar_non_linear_2d", function_id_list);
+      register_function(position, symbol_table, "N_scalar_non_linear_1d", function_id_list);
+      register_function(position, symbol_table, "Z_scalar_non_linear_3d", function_id_list);
+      register_function(position, symbol_table, "R_scalar_non_linear_3d", function_id_list);
+
+      DiscreteFunctionVectorInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0Vector>(),
+                                                  function_id_list);
+
+      const std::string error_msg = R"(error: invalid function type
+note: expecting R^3 -> R
+note: provided function B_scalar_non_linear_2d: R^2 -> B)";
+
+      REQUIRE_THROWS_WITH(interpoler.interpolate(), error_msg);
+    }
+
+    SECTION("invalid value type")
+    {
+      std::vector<FunctionSymbolId> function_id_list;
+      register_function(position, symbol_table, "Z_scalar_non_linear_3d", function_id_list);
+      register_function(position, symbol_table, "R_scalar_non_linear_3d", function_id_list);
+      register_function(position, symbol_table, "R2_scalar_non_linear_3d", function_id_list);
+
+      DiscreteFunctionVectorInterpoler interpoler(mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0Vector>(),
+                                                  function_id_list);
+
+      const std::string error_msg = R"(error: vector functions require scalar value type.
+Invalid interpolation value type: R^2)";
+
+      REQUIRE_THROWS_WITH(interpoler.interpolate(), error_msg);
+    }
+
+    SECTION("invalid discrete function type")
+    {
+      const std::string error_msg = "error: invalid discrete function type for vector interpolation";
+
+      DiscreteFunctionVectorInterpoler interpoler{mesh_3d, std::make_shared<DiscreteFunctionDescriptorP0>(), {}};
+      REQUIRE_THROWS_WITH(interpoler.interpolate(), error_msg);
+    }
+  }
+}
diff --git a/tests/test_EmbeddedIDiscreteFunctionMathFunctions.cpp b/tests/test_EmbeddedIDiscreteFunctionMathFunctions.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d41be6566f5bcbc1205c5561abf474f02780b6af
--- /dev/null
+++ b/tests/test_EmbeddedIDiscreteFunctionMathFunctions.cpp
@@ -0,0 +1,1596 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <language/utils/EmbeddedIDiscreteFunctionMathFunctions.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionP0Vector.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+#define CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(P_U, FCT)          \
+  {                                                                         \
+    using DiscreteFunctionType = const std::decay_t<decltype(*P_U)>;        \
+    std::shared_ptr p_fu       = ::FCT(P_U);                                \
+                                                                            \
+    REQUIRE(p_fu.use_count() > 0);                                          \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fu));      \
+                                                                            \
+    const auto& fu = dynamic_cast<const DiscreteFunctionType&>(*p_fu);      \
+                                                                            \
+    auto values  = P_U->cellValues();                                       \
+    bool is_same = true;                                                    \
+    for (CellId cell_id = 0; cell_id < values.numberOfItems(); ++cell_id) { \
+      if (fu[cell_id] != std::FCT(values[cell_id])) {                       \
+        is_same = false;                                                    \
+        break;                                                              \
+      }                                                                     \
+    }                                                                       \
+                                                                            \
+    REQUIRE(is_same);                                                       \
+  }
+
+#define CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(P_LHS, P_RHS, FCT)             \
+  {                                                                                 \
+    using DiscreteFunctionType = const std::decay_t<decltype(FCT(*P_LHS, *P_RHS))>; \
+    std::shared_ptr p_fuv      = ::FCT(P_LHS, P_RHS);                               \
+                                                                                    \
+    REQUIRE(p_fuv.use_count() > 0);                                                 \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));             \
+                                                                                    \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);            \
+                                                                                    \
+    auto lhs_values = P_LHS->cellValues();                                          \
+    auto rhs_values = P_RHS->cellValues();                                          \
+    bool is_same    = true;                                                         \
+    for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {     \
+      using namespace std;                                                          \
+      if (fuv[cell_id] != FCT(lhs_values[cell_id], rhs_values[cell_id])) {          \
+        is_same = false;                                                            \
+        break;                                                                      \
+      }                                                                             \
+    }                                                                               \
+                                                                                    \
+    REQUIRE(is_same);                                                               \
+  }
+
+#define CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(P_LHS, RHS, FCT)           \
+  {                                                                              \
+    using DiscreteFunctionType = const std::decay_t<decltype(FCT(*P_LHS, RHS))>; \
+    std::shared_ptr p_fuv      = ::FCT(P_LHS, RHS);                              \
+                                                                                 \
+    REQUIRE(p_fuv.use_count() > 0);                                              \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));          \
+                                                                                 \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);         \
+                                                                                 \
+    auto lhs_values = P_LHS->cellValues();                                       \
+    bool is_same    = true;                                                      \
+    for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {  \
+      using namespace std;                                                       \
+      if (fuv[cell_id] != FCT(lhs_values[cell_id], RHS)) {                       \
+        is_same = false;                                                         \
+        break;                                                                   \
+      }                                                                          \
+    }                                                                            \
+                                                                                 \
+    REQUIRE(is_same);                                                            \
+  }
+
+#define CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(LHS, P_RHS, FCT)           \
+  {                                                                              \
+    using DiscreteFunctionType = const std::decay_t<decltype(FCT(LHS, *P_RHS))>; \
+    std::shared_ptr p_fuv      = ::FCT(LHS, P_RHS);                              \
+                                                                                 \
+    REQUIRE(p_fuv.use_count() > 0);                                              \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));          \
+                                                                                 \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);         \
+                                                                                 \
+    auto rhs_values = P_RHS->cellValues();                                       \
+    bool is_same    = true;                                                      \
+    for (CellId cell_id = 0; cell_id < rhs_values.numberOfItems(); ++cell_id) {  \
+      using namespace std;                                                       \
+      if (fuv[cell_id] != FCT(LHS, rhs_values[cell_id])) {                       \
+        is_same = false;                                                         \
+        break;                                                                   \
+      }                                                                          \
+    }                                                                            \
+                                                                                 \
+    REQUIRE(is_same);                                                            \
+  }
+
+TEST_CASE("EmbeddedIDiscreteFunctionMathFunctions", "[scheme]")
+{
+  SECTION("1D")
+  {
+    constexpr size_t Dimension = 1;
+
+    using Rd = TinyVector<Dimension>;
+
+    std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh1D();
+
+    std::shared_ptr other_mesh =
+      std::make_shared<Mesh<Connectivity<Dimension>>>(mesh->shared_connectivity(), mesh->xr());
+
+    CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+    CellValue<double> values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    CellValue<double> positive_values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 2 + std::sin(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    CellValue<double> bounded_values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.9 * std::sin(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    std::shared_ptr p_u            = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, values);
+    std::shared_ptr p_other_mesh_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(other_mesh, values);
+    std::shared_ptr p_positive_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, positive_values);
+    std::shared_ptr p_bounded_u  = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, bounded_values);
+
+    std::shared_ptr p_R1_u = [=] {
+      CellValue<TinyVector<1>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R1_v = [=] {
+      CellValue<TinyVector<1>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { vj[cell_id][0] = xj[cell_id][0] * xj[cell_id][0] + 1; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R1_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(other_mesh, p_R1_u->cellValues());
+
+    constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+      if constexpr (Dimension == 1) {
+        return {x[0], 1 + x[0] * x[0]};
+      } else if constexpr (Dimension == 2) {
+        return {x[0], x[1]};
+      } else if constexpr (Dimension == 3) {
+        return {x[0], x[1] + x[2]};
+      }
+    };
+
+    std::shared_ptr p_R2_u = [=] {
+      CellValue<TinyVector<2>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+          uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R2_v = [=] {
+      CellValue<TinyVector<2>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+          vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R2_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(other_mesh, p_R2_u->cellValues());
+
+    constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+      if constexpr (Dimension == 1) {
+        return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+      } else if constexpr (Dimension == 2) {
+        return {x[0], x[1], x[0] + x[1]};
+      } else if constexpr (Dimension == 3) {
+        return {x[0], x[1], x[2]};
+      }
+    };
+
+    std::shared_ptr p_R3_u = [=] {
+      CellValue<TinyVector<3>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R3_v = [=] {
+      CellValue<TinyVector<3>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1], x[2] * x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R3_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(other_mesh, p_R3_u->cellValues());
+
+    std::shared_ptr p_R1x1_u = [=] {
+      CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R2x2_u = [=] {
+      CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+
+          uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                         2 * x[1], -x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R3x3_u = [=] {
+      CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+
+          uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                         2 * x[1],        -x[0],           x[0] - x[1],   //
+                         3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_Vector3_u = [=] {
+      CellArray<double> uj_vector{mesh->connectivity(), 3};
+      parallel_for(
+        uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          uj_vector[cell_id][0] = 2 * x[0] + 1;
+          uj_vector[cell_id][1] = 1 - x[1] * x[2];
+          uj_vector[cell_id][2] = x[0] + x[2];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+    }();
+
+    std::shared_ptr p_Vector3_v = [=] {
+      CellArray<double> vj_vector{mesh->connectivity(), 3};
+      parallel_for(
+        vj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          vj_vector[cell_id][0] = x[0] * x[1] + 1;
+          vj_vector[cell_id][1] = 2 * x[1];
+          vj_vector[cell_id][2] = x[2] * x[0];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, vj_vector);
+    }();
+
+    std::shared_ptr p_Vector2_w = [=] {
+      CellArray<double> wj_vector{mesh->connectivity(), 2};
+      parallel_for(
+        wj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          wj_vector[cell_id][0] = x[0] + x[1] * 2;
+          wj_vector[cell_id][1] = x[0] * x[1];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, wj_vector);
+    }();
+
+    SECTION("sqrt Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, sqrt);
+      REQUIRE_THROWS_WITH(sqrt(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("abs Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, abs);
+      REQUIRE_THROWS_WITH(abs(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("sin Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, sin);
+      REQUIRE_THROWS_WITH(sin(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("cos Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, cos);
+      REQUIRE_THROWS_WITH(cos(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("tan Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, tan);
+      REQUIRE_THROWS_WITH(tan(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("asin Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, asin);
+      REQUIRE_THROWS_WITH(asin(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("acos Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, acos);
+      REQUIRE_THROWS_WITH(acos(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atan Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, atan);
+      REQUIRE_THROWS_WITH(atan(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("sinh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, sinh);
+      REQUIRE_THROWS_WITH(sinh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("cosh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, cosh);
+      REQUIRE_THROWS_WITH(cosh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("tanh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, tanh);
+      REQUIRE_THROWS_WITH(tanh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("asinh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, asinh);
+      REQUIRE_THROWS_WITH(asinh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("acosh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, acosh);
+      REQUIRE_THROWS_WITH(acosh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atanh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, atanh);
+      REQUIRE_THROWS_WITH(atanh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("exp Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, exp);
+      REQUIRE_THROWS_WITH(exp(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("log Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, log);
+      REQUIRE_THROWS_WITH(log(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atan2 Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_positive_u, p_bounded_u, atan2);
+      REQUIRE_THROWS_WITH(atan2(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(atan2(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+      REQUIRE_THROWS_WITH(atan2(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+    }
+
+    SECTION("atan2 Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 3.6, atan2);
+      REQUIRE_THROWS_WITH(atan2(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("atan2 R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(2.4, p_u, atan2);
+      REQUIRE_THROWS_WITH(atan2(2.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("min Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_u, p_bounded_u, min);
+      REQUIRE_THROWS_WITH(::min(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(::min(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(::min(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("min Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 1.2, min);
+      REQUIRE_THROWS_WITH(min(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("min R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(0.4, p_u, min);
+      REQUIRE_THROWS_WITH(min(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("min Vh -> R")
+    {
+      REQUIRE(min(std::shared_ptr<const IDiscreteFunction>{p_u}) == min(p_u->cellValues()));
+      REQUIRE_THROWS_WITH(min(std::shared_ptr<const IDiscreteFunction>{p_R1_u}),
+                          "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("max Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_u, p_bounded_u, max);
+      REQUIRE_THROWS_WITH(::max(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(::max(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(::max(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("max Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 1.2, max);
+      REQUIRE_THROWS_WITH(max(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("max Vh -> R")
+    {
+      REQUIRE(max(std::shared_ptr<const IDiscreteFunction>{p_u}) == max(p_u->cellValues()));
+      REQUIRE_THROWS_WITH(max(std::shared_ptr<const IDiscreteFunction>{p_R1_u}),
+                          "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("max R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(0.4, p_u, max);
+      REQUIRE_THROWS_WITH(max(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("pow Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_positive_u, p_bounded_u, pow);
+      REQUIRE_THROWS_WITH(pow(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(pow(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(pow(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("pow Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_positive_u, 3.3, pow);
+      REQUIRE_THROWS_WITH(pow(p_R1_u, 3.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("pow R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(2.1, p_u, pow);
+      REQUIRE_THROWS_WITH(pow(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("dot Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R1_u, p_R1_v, dot);
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R2_u, p_R2_v, dot);
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R3_u, p_R3_v, dot);
+
+      {
+        auto p_UV = dot(p_Vector3_u, p_Vector3_v);
+        REQUIRE(p_UV.use_count() == 1);
+
+        auto UV        = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*p_UV);
+        auto direct_UV = dot(*p_Vector3_u, *p_Vector3_v);
+
+        bool is_same = true;
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          if (UV[cell_id] != direct_UV[cell_id]) {
+            is_same = false;
+            break;
+          }
+        }
+
+        REQUIRE(is_same);
+      }
+
+      REQUIRE_THROWS_WITH(dot(p_R1_u, p_other_mesh_R1_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R2_u, p_other_mesh_R2_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R3_u, p_other_mesh_R3_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R1_u, p_R3_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(dot(p_Vector3_u, p_Vector2_w), "error: operands have different dimension");
+    }
+
+    SECTION("dot Vh*Rd -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R1_u, (TinyVector<1>{3}), dot);
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R2_u, (TinyVector<2>{-6, 2}), dot);
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R3_u, (TinyVector<3>{-1, 5, 2}), dot);
+      REQUIRE_THROWS_WITH(dot(p_R1_u, (TinyVector<2>{-6, 2})), "error: incompatible operand types Vh(P0:R^1) and R^2");
+      REQUIRE_THROWS_WITH(dot(p_R2_u, (TinyVector<3>{-1, 5, 2})),
+                          "error: incompatible operand types Vh(P0:R^2) and R^3");
+      REQUIRE_THROWS_WITH(dot(p_R3_u, (TinyVector<1>{-1})), "error: incompatible operand types Vh(P0:R^3) and R^1");
+    }
+
+    SECTION("dot Rd*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<1>{3}), p_R1_u, dot);
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<2>{-6, 2}), p_R2_u, dot);
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<3>{-1, 5, 2}), p_R3_u, dot);
+      REQUIRE_THROWS_WITH(dot((TinyVector<2>{-6, 2}), p_R1_u), "error: incompatible operand types R^2 and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(dot((TinyVector<3>{-1, 5, 2}), p_R2_u),
+                          "error: incompatible operand types R^3 and Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(dot((TinyVector<1>{-1}), p_R3_u), "error: incompatible operand types R^1 and Vh(P0:R^3)");
+    }
+
+    SECTION("sum_of_R* Vh -> R*")
+    {
+      REQUIRE(sum_of<double>(p_u) == sum(p_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<1>>(p_R1_u) == sum(p_R1_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<2>>(p_R2_u) == sum(p_R2_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<3>>(p_R3_u) == sum(p_R3_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<1>>(p_R1x1_u) == sum(p_R1x1_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<2>>(p_R2x2_u) == sum(p_R2x2_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<3>>(p_R3x3_u) == sum(p_R3x3_u->cellValues()));
+
+      REQUIRE_THROWS_WITH(sum_of<TinyVector<1>>(p_u), "error: invalid operand type Vh(P0:R)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R2_u), "error: invalid operand type Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R3_u), "error: invalid operand type Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R1x1_u), "error: invalid operand type Vh(P0:R^1x1)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R2x2_u), "error: invalid operand type Vh(P0:R^2x2)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R3x3_u), "error: invalid operand type Vh(P0:R^3x3)");
+    }
+
+    SECTION("integral_of_R* Vh -> R*")
+    {
+      auto integrate_locally = [&](const auto& cell_values) {
+        const auto& Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+        using DataType = decltype(double{} * cell_values[CellId{0}]);
+        CellValue<DataType> local_integral{mesh->connectivity()};
+        parallel_for(
+          local_integral.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { local_integral[cell_id] = Vj[cell_id] * cell_values[cell_id]; });
+        return local_integral;
+      };
+
+      REQUIRE(integral_of<double>(p_u) == sum(integrate_locally(p_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<1>>(p_R1_u) == sum(integrate_locally(p_R1_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<2>>(p_R2_u) == sum(integrate_locally(p_R2_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<3>>(p_R3_u) == sum(integrate_locally(p_R3_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<1>>(p_R1x1_u) == sum(integrate_locally(p_R1x1_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<2>>(p_R2x2_u) == sum(integrate_locally(p_R2x2_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<3>>(p_R3x3_u) == sum(integrate_locally(p_R3x3_u->cellValues())));
+
+      REQUIRE_THROWS_WITH(integral_of<TinyVector<1>>(p_u), "error: invalid operand type Vh(P0:R)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R2_u), "error: invalid operand type Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R3_u), "error: invalid operand type Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R1x1_u), "error: invalid operand type Vh(P0:R^1x1)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R2x2_u), "error: invalid operand type Vh(P0:R^2x2)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R3x3_u), "error: invalid operand type Vh(P0:R^3x3)");
+    }
+  }
+
+  SECTION("2D")
+  {
+    constexpr size_t Dimension = 2;
+
+    using Rd = TinyVector<Dimension>;
+
+    std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh2D();
+
+    std::shared_ptr other_mesh =
+      std::make_shared<Mesh<Connectivity<Dimension>>>(mesh->shared_connectivity(), mesh->xr());
+
+    CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+    CellValue<double> values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    CellValue<double> positive_values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 2 + std::sin(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    CellValue<double> bounded_values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.9 * std::sin(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    std::shared_ptr p_u            = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, values);
+    std::shared_ptr p_other_mesh_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(other_mesh, values);
+    std::shared_ptr p_positive_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, positive_values);
+    std::shared_ptr p_bounded_u  = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, bounded_values);
+
+    std::shared_ptr p_R1_u = [=] {
+      CellValue<TinyVector<1>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R1_v = [=] {
+      CellValue<TinyVector<1>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { vj[cell_id][0] = xj[cell_id][0] * xj[cell_id][0] + 1; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R1_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(other_mesh, p_R1_u->cellValues());
+
+    constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+      if constexpr (Dimension == 1) {
+        return {x[0], 1 + x[0] * x[0]};
+      } else if constexpr (Dimension == 2) {
+        return {x[0], x[1]};
+      } else if constexpr (Dimension == 3) {
+        return {x[0], x[1] + x[2]};
+      }
+    };
+
+    std::shared_ptr p_R2_u = [=] {
+      CellValue<TinyVector<2>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+          uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R2_v = [=] {
+      CellValue<TinyVector<2>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+          vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R2_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(other_mesh, p_R2_u->cellValues());
+
+    constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+      if constexpr (Dimension == 1) {
+        return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+      } else if constexpr (Dimension == 2) {
+        return {x[0], x[1], x[0] + x[1]};
+      } else if constexpr (Dimension == 3) {
+        return {x[0], x[1], x[2]};
+      }
+    };
+
+    std::shared_ptr p_R3_u = [=] {
+      CellValue<TinyVector<3>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R3_v = [=] {
+      CellValue<TinyVector<3>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1], x[2] * x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R3_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(other_mesh, p_R3_u->cellValues());
+
+    std::shared_ptr p_R1x1_u = [=] {
+      CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R2x2_u = [=] {
+      CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+
+          uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                         2 * x[1], -x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R3x3_u = [=] {
+      CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+
+          uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                         2 * x[1],        -x[0],           x[0] - x[1],   //
+                         3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_Vector3_u = [=] {
+      CellArray<double> uj_vector{mesh->connectivity(), 3};
+      parallel_for(
+        uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          uj_vector[cell_id][0] = 2 * x[0] + 1;
+          uj_vector[cell_id][1] = 1 - x[1] * x[2];
+          uj_vector[cell_id][2] = x[0] + x[2];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+    }();
+
+    std::shared_ptr p_Vector3_v = [=] {
+      CellArray<double> vj_vector{mesh->connectivity(), 3};
+      parallel_for(
+        vj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          vj_vector[cell_id][0] = x[0] * x[1] + 1;
+          vj_vector[cell_id][1] = 2 * x[1];
+          vj_vector[cell_id][2] = x[2] * x[0];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, vj_vector);
+    }();
+
+    std::shared_ptr p_Vector2_w = [=] {
+      CellArray<double> wj_vector{mesh->connectivity(), 2};
+      parallel_for(
+        wj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          wj_vector[cell_id][0] = x[0] + x[1] * 2;
+          wj_vector[cell_id][1] = x[0] * x[1];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, wj_vector);
+    }();
+
+    SECTION("sqrt Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, sqrt);
+      REQUIRE_THROWS_WITH(sqrt(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("abs Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, abs);
+      REQUIRE_THROWS_WITH(abs(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("sin Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, sin);
+      REQUIRE_THROWS_WITH(sin(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("cos Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, cos);
+      REQUIRE_THROWS_WITH(cos(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("tan Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, tan);
+      REQUIRE_THROWS_WITH(tan(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("asin Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, asin);
+      REQUIRE_THROWS_WITH(asin(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("acos Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, acos);
+      REQUIRE_THROWS_WITH(acos(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atan Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, atan);
+      REQUIRE_THROWS_WITH(atan(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("sinh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, sinh);
+      REQUIRE_THROWS_WITH(sinh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("cosh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, cosh);
+      REQUIRE_THROWS_WITH(cosh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("tanh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, tanh);
+      REQUIRE_THROWS_WITH(tanh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("asinh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, asinh);
+      REQUIRE_THROWS_WITH(asinh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("acosh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, acosh);
+      REQUIRE_THROWS_WITH(acosh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atanh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, atanh);
+      REQUIRE_THROWS_WITH(atanh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("exp Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, exp);
+      REQUIRE_THROWS_WITH(exp(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("log Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, log);
+      REQUIRE_THROWS_WITH(log(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atan2 Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_positive_u, p_bounded_u, atan2);
+      REQUIRE_THROWS_WITH(atan2(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(atan2(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+      REQUIRE_THROWS_WITH(atan2(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+    }
+
+    SECTION("atan2 Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 3.6, atan2);
+      REQUIRE_THROWS_WITH(atan2(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("atan2 R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(2.4, p_u, atan2);
+      REQUIRE_THROWS_WITH(atan2(2.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("min Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_u, p_bounded_u, min);
+      REQUIRE_THROWS_WITH(::min(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(::min(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(::min(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("min Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 1.2, min);
+      REQUIRE_THROWS_WITH(min(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("min R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(0.4, p_u, min);
+      REQUIRE_THROWS_WITH(min(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("min Vh -> R")
+    {
+      REQUIRE(min(std::shared_ptr<const IDiscreteFunction>{p_u}) == min(p_u->cellValues()));
+      REQUIRE_THROWS_WITH(min(std::shared_ptr<const IDiscreteFunction>{p_R1_u}),
+                          "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("max Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_u, p_bounded_u, max);
+      REQUIRE_THROWS_WITH(::max(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(::max(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(::max(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("max Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 1.2, max);
+      REQUIRE_THROWS_WITH(max(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("max Vh -> R")
+    {
+      REQUIRE(max(std::shared_ptr<const IDiscreteFunction>{p_u}) == max(p_u->cellValues()));
+      REQUIRE_THROWS_WITH(max(std::shared_ptr<const IDiscreteFunction>{p_R1_u}),
+                          "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("max R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(0.4, p_u, max);
+      REQUIRE_THROWS_WITH(max(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("pow Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_positive_u, p_bounded_u, pow);
+      REQUIRE_THROWS_WITH(pow(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(pow(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(pow(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("pow Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_positive_u, 3.3, pow);
+      REQUIRE_THROWS_WITH(pow(p_R1_u, 3.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("pow R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(2.1, p_u, pow);
+      REQUIRE_THROWS_WITH(pow(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("dot Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R1_u, p_R1_v, dot);
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R2_u, p_R2_v, dot);
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R3_u, p_R3_v, dot);
+
+      {
+        auto p_UV = dot(p_Vector3_u, p_Vector3_v);
+        REQUIRE(p_UV.use_count() == 1);
+
+        auto UV        = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*p_UV);
+        auto direct_UV = dot(*p_Vector3_u, *p_Vector3_v);
+
+        bool is_same = true;
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          if (UV[cell_id] != direct_UV[cell_id]) {
+            is_same = false;
+            break;
+          }
+        }
+
+        REQUIRE(is_same);
+      }
+
+      REQUIRE_THROWS_WITH(dot(p_R1_u, p_other_mesh_R1_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R2_u, p_other_mesh_R2_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R3_u, p_other_mesh_R3_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R1_u, p_R3_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(dot(p_Vector3_u, p_Vector2_w), "error: operands have different dimension");
+    }
+
+    SECTION("dot Vh*Rd -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R1_u, (TinyVector<1>{3}), dot);
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R2_u, (TinyVector<2>{-6, 2}), dot);
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R3_u, (TinyVector<3>{-1, 5, 2}), dot);
+      REQUIRE_THROWS_WITH(dot(p_R1_u, (TinyVector<2>{-6, 2})), "error: incompatible operand types Vh(P0:R^1) and R^2");
+      REQUIRE_THROWS_WITH(dot(p_R2_u, (TinyVector<3>{-1, 5, 2})),
+                          "error: incompatible operand types Vh(P0:R^2) and R^3");
+      REQUIRE_THROWS_WITH(dot(p_R3_u, (TinyVector<1>{-1})), "error: incompatible operand types Vh(P0:R^3) and R^1");
+    }
+
+    SECTION("dot Rd*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<1>{3}), p_R1_u, dot);
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<2>{-6, 2}), p_R2_u, dot);
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<3>{-1, 5, 2}), p_R3_u, dot);
+      REQUIRE_THROWS_WITH(dot((TinyVector<2>{-6, 2}), p_R1_u), "error: incompatible operand types R^2 and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(dot((TinyVector<3>{-1, 5, 2}), p_R2_u),
+                          "error: incompatible operand types R^3 and Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(dot((TinyVector<1>{-1}), p_R3_u), "error: incompatible operand types R^1 and Vh(P0:R^3)");
+    }
+
+    SECTION("sum_of_R* Vh -> R*")
+    {
+      REQUIRE(sum_of<double>(p_u) == sum(p_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<1>>(p_R1_u) == sum(p_R1_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<2>>(p_R2_u) == sum(p_R2_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<3>>(p_R3_u) == sum(p_R3_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<1>>(p_R1x1_u) == sum(p_R1x1_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<2>>(p_R2x2_u) == sum(p_R2x2_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<3>>(p_R3x3_u) == sum(p_R3x3_u->cellValues()));
+
+      REQUIRE_THROWS_WITH(sum_of<TinyVector<1>>(p_u), "error: invalid operand type Vh(P0:R)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R2_u), "error: invalid operand type Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R3_u), "error: invalid operand type Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R1x1_u), "error: invalid operand type Vh(P0:R^1x1)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R2x2_u), "error: invalid operand type Vh(P0:R^2x2)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R3x3_u), "error: invalid operand type Vh(P0:R^3x3)");
+    }
+
+    SECTION("integral_of_R* Vh -> R*")
+    {
+      auto integrate_locally = [&](const auto& cell_values) {
+        const auto& Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+        using DataType = decltype(double{} * cell_values[CellId{0}]);
+        CellValue<DataType> local_integral{mesh->connectivity()};
+        parallel_for(
+          local_integral.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { local_integral[cell_id] = Vj[cell_id] * cell_values[cell_id]; });
+        return local_integral;
+      };
+
+      REQUIRE(integral_of<double>(p_u) == sum(integrate_locally(p_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<1>>(p_R1_u) == sum(integrate_locally(p_R1_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<2>>(p_R2_u) == sum(integrate_locally(p_R2_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<3>>(p_R3_u) == sum(integrate_locally(p_R3_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<1>>(p_R1x1_u) == sum(integrate_locally(p_R1x1_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<2>>(p_R2x2_u) == sum(integrate_locally(p_R2x2_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<3>>(p_R3x3_u) == sum(integrate_locally(p_R3x3_u->cellValues())));
+
+      REQUIRE_THROWS_WITH(integral_of<TinyVector<1>>(p_u), "error: invalid operand type Vh(P0:R)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R2_u), "error: invalid operand type Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R3_u), "error: invalid operand type Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R1x1_u), "error: invalid operand type Vh(P0:R^1x1)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R2x2_u), "error: invalid operand type Vh(P0:R^2x2)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R3x3_u), "error: invalid operand type Vh(P0:R^3x3)");
+    }
+  }
+
+  SECTION("3D")
+  {
+    constexpr size_t Dimension = 3;
+
+    using Rd = TinyVector<Dimension>;
+
+    std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh3D();
+
+    std::shared_ptr other_mesh =
+      std::make_shared<Mesh<Connectivity<Dimension>>>(mesh->shared_connectivity(), mesh->xr());
+
+    CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+    CellValue<double> values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    CellValue<double> positive_values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 2 + std::sin(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    CellValue<double> bounded_values = [=] {
+      CellValue<double> build_values{mesh->connectivity()};
+      parallel_for(
+        build_values.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.9 * std::sin(l2Norm(xj[cell_id])); });
+      return build_values;
+    }();
+
+    std::shared_ptr p_u            = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, values);
+    std::shared_ptr p_other_mesh_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(other_mesh, values);
+    std::shared_ptr p_positive_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, positive_values);
+    std::shared_ptr p_bounded_u  = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, bounded_values);
+
+    std::shared_ptr p_R1_u = [=] {
+      CellValue<TinyVector<1>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R1_v = [=] {
+      CellValue<TinyVector<1>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(),
+        PUGS_LAMBDA(const CellId cell_id) { vj[cell_id][0] = xj[cell_id][0] * xj[cell_id][0] + 1; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R1_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(other_mesh, p_R1_u->cellValues());
+
+    constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+      if constexpr (Dimension == 1) {
+        return {x[0], 1 + x[0] * x[0]};
+      } else if constexpr (Dimension == 2) {
+        return {x[0], x[1]};
+      } else if constexpr (Dimension == 3) {
+        return {x[0], x[1] + x[2]};
+      }
+    };
+
+    std::shared_ptr p_R2_u = [=] {
+      CellValue<TinyVector<2>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+          uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R2_v = [=] {
+      CellValue<TinyVector<2>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+          vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R2_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(other_mesh, p_R2_u->cellValues());
+
+    constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+      if constexpr (Dimension == 1) {
+        return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+      } else if constexpr (Dimension == 2) {
+        return {x[0], x[1], x[0] + x[1]};
+      } else if constexpr (Dimension == 3) {
+        return {x[0], x[1], x[2]};
+      }
+    };
+
+    std::shared_ptr p_R3_u = [=] {
+      CellValue<TinyVector<3>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R3_v = [=] {
+      CellValue<TinyVector<3>> vj{mesh->connectivity()};
+      parallel_for(
+        vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1], x[2] * x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, vj);
+    }();
+
+    std::shared_ptr p_other_mesh_R3_u =
+      std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(other_mesh, p_R3_u->cellValues());
+
+    std::shared_ptr p_R1x1_u = [=] {
+      CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R2x2_u = [=] {
+      CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<2> x = to_2d(xj[cell_id]);
+
+          uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                         2 * x[1], -x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_R3x3_u = [=] {
+      CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+      parallel_for(
+        uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+
+          uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                         2 * x[1],        -x[0],           x[0] - x[1],   //
+                         3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+        });
+
+      return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+    }();
+
+    std::shared_ptr p_Vector3_u = [=] {
+      CellArray<double> uj_vector{mesh->connectivity(), 3};
+      parallel_for(
+        uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          uj_vector[cell_id][0] = 2 * x[0] + 1;
+          uj_vector[cell_id][1] = 1 - x[1] * x[2];
+          uj_vector[cell_id][2] = x[0] + x[2];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+    }();
+
+    std::shared_ptr p_Vector3_v = [=] {
+      CellArray<double> vj_vector{mesh->connectivity(), 3};
+      parallel_for(
+        vj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          vj_vector[cell_id][0] = x[0] * x[1] + 1;
+          vj_vector[cell_id][1] = 2 * x[1];
+          vj_vector[cell_id][2] = x[2] * x[0];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, vj_vector);
+    }();
+
+    std::shared_ptr p_Vector2_w = [=] {
+      CellArray<double> wj_vector{mesh->connectivity(), 2};
+      parallel_for(
+        wj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+          const TinyVector<3> x = to_3d(xj[cell_id]);
+          wj_vector[cell_id][0] = x[0] + x[1] * 2;
+          wj_vector[cell_id][1] = x[0] * x[1];
+        });
+
+      return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, wj_vector);
+    }();
+
+    SECTION("sqrt Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, sqrt);
+      REQUIRE_THROWS_WITH(sqrt(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("abs Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, abs);
+      REQUIRE_THROWS_WITH(abs(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("sin Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, sin);
+      REQUIRE_THROWS_WITH(sin(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("cos Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, cos);
+      REQUIRE_THROWS_WITH(cos(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("tan Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, tan);
+      REQUIRE_THROWS_WITH(tan(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("asin Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, asin);
+      REQUIRE_THROWS_WITH(asin(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("acos Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, acos);
+      REQUIRE_THROWS_WITH(acos(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atan Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, atan);
+      REQUIRE_THROWS_WITH(atan(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("sinh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, sinh);
+      REQUIRE_THROWS_WITH(sinh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("cosh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, cosh);
+      REQUIRE_THROWS_WITH(cosh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("tanh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, tanh);
+      REQUIRE_THROWS_WITH(tanh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("asinh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, asinh);
+      REQUIRE_THROWS_WITH(asinh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("acosh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, acosh);
+      REQUIRE_THROWS_WITH(acosh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atanh Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_bounded_u, atanh);
+      REQUIRE_THROWS_WITH(atanh(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("exp Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_u, exp);
+      REQUIRE_THROWS_WITH(exp(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("log Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH_TO_VH_REAL_FUNCTION_EVALUATION(p_positive_u, log);
+      REQUIRE_THROWS_WITH(log(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("atan2 Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_positive_u, p_bounded_u, atan2);
+      REQUIRE_THROWS_WITH(atan2(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(atan2(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+      REQUIRE_THROWS_WITH(atan2(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+    }
+
+    SECTION("atan2 Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 3.6, atan2);
+      REQUIRE_THROWS_WITH(atan2(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("atan2 R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(2.4, p_u, atan2);
+      REQUIRE_THROWS_WITH(atan2(2.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("min Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_u, p_bounded_u, min);
+      REQUIRE_THROWS_WITH(::min(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(::min(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(::min(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("min Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 1.2, min);
+      REQUIRE_THROWS_WITH(min(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("min R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(0.4, p_u, min);
+      REQUIRE_THROWS_WITH(min(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("min Vh -> R")
+    {
+      REQUIRE(min(std::shared_ptr<const IDiscreteFunction>{p_u}) == min(p_u->cellValues()));
+      REQUIRE_THROWS_WITH(min(std::shared_ptr<const IDiscreteFunction>{p_R1_u}),
+                          "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("max Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_u, p_bounded_u, max);
+      REQUIRE_THROWS_WITH(::max(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(::max(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(::max(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("max Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_u, 1.2, max);
+      REQUIRE_THROWS_WITH(max(p_R1_u, 2.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("max Vh -> R")
+    {
+      REQUIRE(max(std::shared_ptr<const IDiscreteFunction>{p_u}) == max(p_u->cellValues()));
+      REQUIRE_THROWS_WITH(max(std::shared_ptr<const IDiscreteFunction>{p_R1_u}),
+                          "error: invalid operand type Vh(P0:R^1)");
+    }
+
+    SECTION("max R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(0.4, p_u, max);
+      REQUIRE_THROWS_WITH(max(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("pow Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_positive_u, p_bounded_u, pow);
+      REQUIRE_THROWS_WITH(pow(p_u, p_other_mesh_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(pow(p_u, p_R1_u), "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(pow(p_R1_u, p_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R)");
+    }
+
+    SECTION("pow Vh*R -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_positive_u, 3.3, pow);
+      REQUIRE_THROWS_WITH(pow(p_R1_u, 3.1), "error: incompatible operand types Vh(P0:R^1) and R");
+    }
+
+    SECTION("pow R*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION(2.1, p_u, pow);
+      REQUIRE_THROWS_WITH(pow(3.1, p_R1_u), "error: incompatible operand types R and Vh(P0:R^1)");
+    }
+
+    SECTION("dot Vh*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R1_u, p_R1_v, dot);
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R2_u, p_R2_v, dot);
+      CHECK_EMBEDDED_VH2_TO_VH_FUNCTION_EVALUATION(p_R3_u, p_R3_v, dot);
+
+      {
+        auto p_UV = dot(p_Vector3_u, p_Vector3_v);
+        REQUIRE(p_UV.use_count() == 1);
+
+        auto UV        = dynamic_cast<const DiscreteFunctionP0<Dimension, double>&>(*p_UV);
+        auto direct_UV = dot(*p_Vector3_u, *p_Vector3_v);
+
+        bool is_same = true;
+        for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+          if (UV[cell_id] != direct_UV[cell_id]) {
+            is_same = false;
+            break;
+          }
+        }
+
+        REQUIRE(is_same);
+      }
+
+      REQUIRE_THROWS_WITH(dot(p_R1_u, p_other_mesh_R1_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R2_u, p_other_mesh_R2_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R3_u, p_other_mesh_R3_u), "error: operands are defined on different meshes");
+      REQUIRE_THROWS_WITH(dot(p_R1_u, p_R3_u), "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(dot(p_Vector3_u, p_Vector2_w), "error: operands have different dimension");
+    }
+
+    SECTION("dot Vh*Rd -> Vh")
+    {
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R1_u, (TinyVector<1>{3}), dot);
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R2_u, (TinyVector<2>{-6, 2}), dot);
+      CHECK_EMBEDDED_VHxW_TO_VH_FUNCTION_EVALUATION(p_R3_u, (TinyVector<3>{-1, 5, 2}), dot);
+      REQUIRE_THROWS_WITH(dot(p_R1_u, (TinyVector<2>{-6, 2})), "error: incompatible operand types Vh(P0:R^1) and R^2");
+      REQUIRE_THROWS_WITH(dot(p_R2_u, (TinyVector<3>{-1, 5, 2})),
+                          "error: incompatible operand types Vh(P0:R^2) and R^3");
+      REQUIRE_THROWS_WITH(dot(p_R3_u, (TinyVector<1>{-1})), "error: incompatible operand types Vh(P0:R^3) and R^1");
+    }
+
+    SECTION("dot Rd*Vh -> Vh")
+    {
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<1>{3}), p_R1_u, dot);
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<2>{-6, 2}), p_R2_u, dot);
+      CHECK_EMBEDDED_WxVH_TO_VH_FUNCTION_EVALUATION((TinyVector<3>{-1, 5, 2}), p_R3_u, dot);
+      REQUIRE_THROWS_WITH(dot((TinyVector<2>{-6, 2}), p_R1_u), "error: incompatible operand types R^2 and Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(dot((TinyVector<3>{-1, 5, 2}), p_R2_u),
+                          "error: incompatible operand types R^3 and Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(dot((TinyVector<1>{-1}), p_R3_u), "error: incompatible operand types R^1 and Vh(P0:R^3)");
+    }
+
+    SECTION("sum_of_R* Vh -> R*")
+    {
+      REQUIRE(sum_of<double>(p_u) == sum(p_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<1>>(p_R1_u) == sum(p_R1_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<2>>(p_R2_u) == sum(p_R2_u->cellValues()));
+      REQUIRE(sum_of<TinyVector<3>>(p_R3_u) == sum(p_R3_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<1>>(p_R1x1_u) == sum(p_R1x1_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<2>>(p_R2x2_u) == sum(p_R2x2_u->cellValues()));
+      REQUIRE(sum_of<TinyMatrix<3>>(p_R3x3_u) == sum(p_R3x3_u->cellValues()));
+
+      REQUIRE_THROWS_WITH(sum_of<TinyVector<1>>(p_u), "error: invalid operand type Vh(P0:R)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R2_u), "error: invalid operand type Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R3_u), "error: invalid operand type Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R1x1_u), "error: invalid operand type Vh(P0:R^1x1)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R2x2_u), "error: invalid operand type Vh(P0:R^2x2)");
+      REQUIRE_THROWS_WITH(sum_of<double>(p_R3x3_u), "error: invalid operand type Vh(P0:R^3x3)");
+    }
+
+    SECTION("integral_of_R* Vh -> R*")
+    {
+      auto integrate_locally = [&](const auto& cell_values) {
+        const auto& Vj = MeshDataManager::instance().getMeshData(*mesh).Vj();
+        using DataType = decltype(double{} * cell_values[CellId{0}]);
+        CellValue<DataType> local_integral{mesh->connectivity()};
+        parallel_for(
+          local_integral.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { local_integral[cell_id] = Vj[cell_id] * cell_values[cell_id]; });
+        return local_integral;
+      };
+
+      REQUIRE(integral_of<double>(p_u) == sum(integrate_locally(p_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<1>>(p_R1_u) == sum(integrate_locally(p_R1_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<2>>(p_R2_u) == sum(integrate_locally(p_R2_u->cellValues())));
+      REQUIRE(integral_of<TinyVector<3>>(p_R3_u) == sum(integrate_locally(p_R3_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<1>>(p_R1x1_u) == sum(integrate_locally(p_R1x1_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<2>>(p_R2x2_u) == sum(integrate_locally(p_R2x2_u->cellValues())));
+      REQUIRE(integral_of<TinyMatrix<3>>(p_R3x3_u) == sum(integrate_locally(p_R3x3_u->cellValues())));
+
+      REQUIRE_THROWS_WITH(integral_of<TinyVector<1>>(p_u), "error: invalid operand type Vh(P0:R)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R1_u), "error: invalid operand type Vh(P0:R^1)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R2_u), "error: invalid operand type Vh(P0:R^2)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R3_u), "error: invalid operand type Vh(P0:R^3)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R1x1_u), "error: invalid operand type Vh(P0:R^1x1)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R2x2_u), "error: invalid operand type Vh(P0:R^2x2)");
+      REQUIRE_THROWS_WITH(integral_of<double>(p_R3x3_u), "error: invalid operand type Vh(P0:R^3x3)");
+    }
+  }
+}
diff --git a/tests/test_EmbeddedIDiscreteFunctionOperators.cpp b/tests/test_EmbeddedIDiscreteFunctionOperators.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5232e8fe8d05aca29d5d56440071c0718a206627
--- /dev/null
+++ b/tests/test_EmbeddedIDiscreteFunctionOperators.cpp
@@ -0,0 +1,2622 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <language/utils/EmbeddedIDiscreteFunctionOperators.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionP0Vector.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+#define CHECK_SCALAR_VH2_TO_VH(P_LHS, OPERATOR, P_RHS)                                  \
+  {                                                                                     \
+    using DiscreteFunctionType = const std::decay_t<decltype(*P_LHS OPERATOR * P_RHS)>; \
+                                                                                        \
+    std::shared_ptr p_fuv = P_LHS OPERATOR P_RHS;                                       \
+                                                                                        \
+    REQUIRE(p_fuv.use_count() > 0);                                                     \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));                 \
+                                                                                        \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);                \
+                                                                                        \
+    auto lhs_values = P_LHS->cellValues();                                              \
+    auto rhs_values = P_RHS->cellValues();                                              \
+    bool is_same    = true;                                                             \
+    for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {         \
+      if (fuv[cell_id] != (lhs_values[cell_id] OPERATOR rhs_values[cell_id])) {         \
+        is_same = false;                                                                \
+        break;                                                                          \
+      }                                                                                 \
+    }                                                                                   \
+                                                                                        \
+    REQUIRE(is_same);                                                                   \
+  }
+
+#define CHECK_SCALAR_VHxX_TO_VH(P_LHS, OPERATOR, RHS)                               \
+  {                                                                                 \
+    using DiscreteFunctionType = const std::decay_t<decltype(*P_LHS OPERATOR RHS)>; \
+                                                                                    \
+    std::shared_ptr p_fuv = P_LHS OPERATOR RHS;                                     \
+                                                                                    \
+    REQUIRE(p_fuv.use_count() > 0);                                                 \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));             \
+                                                                                    \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);            \
+                                                                                    \
+    auto lhs_values = P_LHS->cellValues();                                          \
+    bool is_same    = true;                                                         \
+    for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {     \
+      if (fuv[cell_id] != (lhs_values[cell_id] OPERATOR RHS)) {                     \
+        is_same = false;                                                            \
+        break;                                                                      \
+      }                                                                             \
+    }                                                                               \
+                                                                                    \
+    REQUIRE(is_same);                                                               \
+  }
+
+#define CHECK_SCALAR_XxVH_TO_VH(LHS, OPERATOR, P_RHS)                                \
+  {                                                                                  \
+    using DiscreteFunctionType = const std::decay_t<decltype(LHS OPERATOR * P_RHS)>; \
+                                                                                     \
+    std::shared_ptr p_fuv = LHS OPERATOR P_RHS;                                      \
+                                                                                     \
+    REQUIRE(p_fuv.use_count() > 0);                                                  \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));              \
+                                                                                     \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);             \
+                                                                                     \
+    auto rhs_values = P_RHS->cellValues();                                           \
+    bool is_same    = true;                                                          \
+    for (CellId cell_id = 0; cell_id < rhs_values.numberOfItems(); ++cell_id) {      \
+      if (fuv[cell_id] != (LHS OPERATOR rhs_values[cell_id])) {                      \
+        is_same = false;                                                             \
+        break;                                                                       \
+      }                                                                              \
+    }                                                                                \
+                                                                                     \
+    REQUIRE(is_same);                                                                \
+  }
+
+#define CHECK_VECTOR_VH2_TO_VH(P_LHS, OPERATOR, P_RHS)                                     \
+  {                                                                                        \
+    using DiscreteFunctionType = const std::decay_t<decltype(*P_LHS OPERATOR * P_RHS)>;    \
+                                                                                           \
+    std::shared_ptr p_fuv = P_LHS OPERATOR P_RHS;                                          \
+                                                                                           \
+    REQUIRE(p_fuv.use_count() > 0);                                                        \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));                    \
+                                                                                           \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);                   \
+                                                                                           \
+    auto lhs_arrays = P_LHS->cellArrays();                                                 \
+    auto rhs_arrays = P_RHS->cellArrays();                                                 \
+    bool is_same    = true;                                                                \
+    REQUIRE(rhs_arrays.sizeOfArrays() > 0);                                                \
+    REQUIRE(lhs_arrays.sizeOfArrays() == rhs_arrays.sizeOfArrays());                       \
+    REQUIRE(lhs_arrays.sizeOfArrays() == fuv.size());                                      \
+    for (CellId cell_id = 0; cell_id < lhs_arrays.numberOfItems(); ++cell_id) {            \
+      for (size_t i = 0; i < fuv.size(); ++i) {                                            \
+        if (fuv[cell_id][i] != (lhs_arrays[cell_id][i] OPERATOR rhs_arrays[cell_id][i])) { \
+          is_same = false;                                                                 \
+          break;                                                                           \
+        }                                                                                  \
+      }                                                                                    \
+    }                                                                                      \
+                                                                                           \
+    REQUIRE(is_same);                                                                      \
+  }
+
+#define CHECK_VECTOR_XxVH_TO_VH(LHS, OPERATOR, P_RHS)                                \
+  {                                                                                  \
+    using DiscreteFunctionType = const std::decay_t<decltype(LHS OPERATOR * P_RHS)>; \
+                                                                                     \
+    std::shared_ptr p_fuv = LHS OPERATOR P_RHS;                                      \
+                                                                                     \
+    REQUIRE(p_fuv.use_count() > 0);                                                  \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fuv));              \
+                                                                                     \
+    const auto& fuv = dynamic_cast<const DiscreteFunctionType&>(*p_fuv);             \
+                                                                                     \
+    auto rhs_arrays = P_RHS->cellArrays();                                           \
+    bool is_same    = true;                                                          \
+    REQUIRE(rhs_arrays.sizeOfArrays() > 0);                                          \
+    REQUIRE(fuv.size() == rhs_arrays.sizeOfArrays());                                \
+    for (CellId cell_id = 0; cell_id < rhs_arrays.numberOfItems(); ++cell_id) {      \
+      for (size_t i = 0; i < fuv.size(); ++i) {                                      \
+        if (fuv[cell_id][i] != (LHS OPERATOR rhs_arrays[cell_id][i])) {              \
+          is_same = false;                                                           \
+          break;                                                                     \
+        }                                                                            \
+      }                                                                              \
+    }                                                                                \
+                                                                                     \
+    REQUIRE(is_same);                                                                \
+  }
+
+#define CHECK_SCALAR_VH_TO_VH(OPERATOR, P_RHS)                                   \
+  {                                                                              \
+    using DiscreteFunctionType = const std::decay_t<decltype(OPERATOR * P_RHS)>; \
+                                                                                 \
+    std::shared_ptr p_fu = OPERATOR P_RHS;                                       \
+                                                                                 \
+    REQUIRE(p_fu.use_count() > 0);                                               \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fu));           \
+                                                                                 \
+    const auto& fu = dynamic_cast<const DiscreteFunctionType&>(*p_fu);           \
+                                                                                 \
+    auto rhs_values = P_RHS->cellValues();                                       \
+    bool is_same    = true;                                                      \
+    for (CellId cell_id = 0; cell_id < rhs_values.numberOfItems(); ++cell_id) {  \
+      if (fu[cell_id] != (OPERATOR rhs_values[cell_id])) {                       \
+        is_same = false;                                                         \
+        break;                                                                   \
+      }                                                                          \
+    }                                                                            \
+                                                                                 \
+    REQUIRE(is_same);                                                            \
+  }
+
+#define CHECK_VECTOR_VH_TO_VH(OPERATOR, P_RHS)                                   \
+  {                                                                              \
+    using DiscreteFunctionType = const std::decay_t<decltype(OPERATOR * P_RHS)>; \
+                                                                                 \
+    std::shared_ptr p_fu = OPERATOR P_RHS;                                       \
+                                                                                 \
+    REQUIRE(p_fu.use_count() > 0);                                               \
+    REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionType&>(*p_fu));           \
+                                                                                 \
+    const auto& fu = dynamic_cast<const DiscreteFunctionType&>(*p_fu);           \
+                                                                                 \
+    auto rhs_arrays = P_RHS->cellArrays();                                       \
+    REQUIRE(rhs_arrays.sizeOfArrays() > 0);                                      \
+    REQUIRE(rhs_arrays.sizeOfArrays() == fu.size());                             \
+    bool is_same = true;                                                         \
+    for (CellId cell_id = 0; cell_id < rhs_arrays.numberOfItems(); ++cell_id) {  \
+      for (size_t i = 0; i < rhs_arrays.sizeOfArrays(); ++i) {                   \
+        if (fu[cell_id][i] != (OPERATOR rhs_arrays[cell_id][i])) {               \
+          is_same = false;                                                       \
+          break;                                                                 \
+        }                                                                        \
+      }                                                                          \
+    }                                                                            \
+                                                                                 \
+    REQUIRE(is_same);                                                            \
+  }
+
+TEST_CASE("EmbeddedIDiscreteFunctionOperators", "[scheme]")
+{
+  SECTION("binary operators")
+  {
+    SECTION("1D")
+    {
+      constexpr size_t Dimension = 1;
+
+      using Rd = TinyVector<Dimension>;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      std::shared_ptr other_mesh =
+        std::make_shared<Mesh<Connectivity<Dimension>>>(mesh->shared_connectivity(), mesh->xr());
+
+      CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      CellValue<double> u_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      CellValue<double> v_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.6 + std::sin(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      std::shared_ptr p_R_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, u_R_values);
+      std::shared_ptr p_other_mesh_R_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, double>>(other_mesh, u_R_values);
+      std::shared_ptr p_R_v = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, v_R_values);
+
+      std::shared_ptr p_R1_u = [=] {
+        CellValue<TinyVector<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R1_v = [=] {
+        CellValue<TinyVector<1>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { vj[cell_id][0] = xj[cell_id][0] * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R1_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(other_mesh, p_R1_u->cellValues());
+
+      constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1] + x[2]};
+        }
+      };
+
+      std::shared_ptr p_R2_u = [=] {
+        CellValue<TinyVector<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R2_v = [=] {
+        CellValue<TinyVector<2>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R2_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(other_mesh, p_R2_u->cellValues());
+
+      constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1], x[0] + x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1], x[2]};
+        }
+      };
+
+      std::shared_ptr p_R3_u = [=] {
+        CellValue<TinyVector<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R3_v = [=] {
+        CellValue<TinyVector<3>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1], x[2] * x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R3_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(other_mesh, p_R3_u->cellValues());
+
+      std::shared_ptr p_R1x1_u = [=] {
+        CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R1x1_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(other_mesh, p_R1x1_u->cellValues());
+
+      std::shared_ptr p_R1x1_v = [=] {
+        CellValue<TinyMatrix<1>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { vj[cell_id] = {0.3 - xj[cell_id][0]}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_R2x2_u = [=] {
+        CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                           2 * x[1], -x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R2x2_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(other_mesh, p_R2x2_u->cellValues());
+
+      std::shared_ptr p_R2x2_v = [=] {
+        CellValue<TinyMatrix<2>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            vj[cell_id] = {x[0] + 0.3, 1 - x[1] - x[0],   //
+                           2 * x[1] + x[0], x[1] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_R3x3_u = [=] {
+        CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                           2 * x[1],        -x[0],           x[0] - x[1],   //
+                           3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R3x3_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(other_mesh, p_R3x3_u->cellValues());
+
+      std::shared_ptr p_R3x3_v = [=] {
+        CellValue<TinyMatrix<3>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            vj[cell_id] = {0.2 * x[0] + 1,  2 + x[1],          3 - x[2],      //
+                           2.3 * x[2],      x[1] - x[0],       x[2] - x[1],   //
+                           2 * x[2] + x[0], x[1] + 0.2 * x[2], x[2] - 2 * x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_Vector3_u = [=] {
+        CellArray<double> uj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj_vector[cell_id][0] = 2 * x[0] + 1;
+            uj_vector[cell_id][1] = 1 - x[1] * x[2];
+            uj_vector[cell_id][2] = x[0] + x[2];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+      }();
+
+      std::shared_ptr p_other_mesh_Vector3_u =
+        std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(other_mesh, p_Vector3_u->cellArrays());
+
+      std::shared_ptr p_Vector3_v = [=] {
+        CellArray<double> vj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          vj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            vj_vector[cell_id][0] = x[0] * x[1] + 1;
+            vj_vector[cell_id][1] = 2 * x[1];
+            vj_vector[cell_id][2] = x[2] * x[0];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, vj_vector);
+      }();
+
+      std::shared_ptr p_Vector2_w = [=] {
+        CellArray<double> wj_vector{mesh->connectivity(), 2};
+        parallel_for(
+          wj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            wj_vector[cell_id][0] = x[0] + x[1] * 2;
+            wj_vector[cell_id][1] = x[0] * x[1];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, wj_vector);
+      }();
+
+      SECTION("sum")
+      {
+        SECTION("Vh + Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, +, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1_u, +, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2_u, +, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3_u, +, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, +, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, +, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, +, p_R3x3_v);
+
+          CHECK_VECTOR_VH2_TO_VH(p_Vector3_u, +, p_Vector3_v);
+
+          REQUIRE_THROWS_WITH(p_R_u + p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u + p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u + p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u + p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_R_u + p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_R_v, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_Vector2_w, "error: Vh(P0Vector:R) spaces have different sizes");
+
+          REQUIRE_THROWS_WITH(p_R_u + p_other_mesh_R_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1_u + p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2_u + p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3_u + p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u + p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u + p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u + p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh + X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1_u, +, (TinyVector<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2_u, +, (TinyVector<2>{1.2, 2.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3_u, +, (TinyVector<3>{3.2, 7.1, 5.2}));
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, +, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, +, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, +,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R) and R^1");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R) and R^2");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<3>{2, 3, 2}), "error: incompatible operand types Vh(P0:R) and R^3");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u + (double{1}), "error: incompatible operand types Vh(P0Vector:R) and R");
+          REQUIRE_THROWS_WITH(p_Vector3_u + (TinyVector<1>{1}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^1");
+          REQUIRE_THROWS_WITH(p_Vector3_u + (TinyVector<2>{1, 2}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^2");
+        }
+
+        SECTION("X + Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, +, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<1>{1.3}), +, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<2>{1.2, 2.3}), +, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<3>{3.2, 7.1, 5.2}), +, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), +, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), +, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}),
+                                  +, p_R3x3_u);
+
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) + p_R_u, "error: incompatible operand types R^1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) + p_R_u, "error: incompatible operand types R^2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<3>{2, 3, 2}) + p_R_u, "error: incompatible operand types R^3 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) + p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) + p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) + p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((double{1}) + p_Vector3_u, "error: incompatible operand types R and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) + p_Vector3_u,
+                              "error: incompatible operand types R^1 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) + p_Vector3_u,
+                              "error: incompatible operand types R^2 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("difference")
+      {
+        SECTION("Vh - Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, -, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1_u, -, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2_u, -, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3_u, -, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, -, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, -, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, -, p_R3x3_v);
+
+          CHECK_VECTOR_VH2_TO_VH(p_Vector3_u, -, p_Vector3_v);
+
+          REQUIRE_THROWS_WITH(p_R_u - p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u - p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u - p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u - p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_R_v, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_Vector2_w, "error: Vh(P0Vector:R) spaces have different sizes");
+
+          REQUIRE_THROWS_WITH(p_R_u - p_other_mesh_R_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1_u - p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2_u - p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3_u - p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u - p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u - p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u - p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh - X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1_u, -, (TinyVector<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2_u, -, (TinyVector<2>{1.2, 2.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3_u, -, (TinyVector<3>{3.2, 7.1, 5.2}));
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, -, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, -, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, -,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R) and R^1");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R) and R^2");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<3>{2, 3, 2}), "error: incompatible operand types Vh(P0:R) and R^3");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u - (double{1}), "error: incompatible operand types Vh(P0Vector:R) and R");
+          REQUIRE_THROWS_WITH(p_Vector3_u - (TinyVector<1>{1}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^1");
+          REQUIRE_THROWS_WITH(p_Vector3_u - (TinyVector<2>{1, 2}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^2");
+        }
+
+        SECTION("X - Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, -, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<1>{1.3}), -, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<2>{1.2, 2.3}), -, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<3>{3.2, 7.1, 5.2}), -, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), -, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), -, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}),
+                                  -, p_R3x3_u);
+
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) - p_R_u, "error: incompatible operand types R^1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) - p_R_u, "error: incompatible operand types R^2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<3>{2, 3, 2}) - p_R_u, "error: incompatible operand types R^3 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) - p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) - p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) - p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((double{1}) - p_Vector3_u, "error: incompatible operand types R and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) - p_Vector3_u,
+                              "error: incompatible operand types R^1 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) - p_Vector3_u,
+                              "error: incompatible operand types R^2 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("product")
+      {
+        SECTION("Vh * Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, *, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, *, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, *, p_R3x3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R3x3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, *, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, *, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, *, p_R3_v);
+
+          {
+            std::shared_ptr p_fuv = p_R_u * p_Vector3_v;
+
+            REQUIRE(p_fuv.use_count() > 0);
+            REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*p_fuv));
+
+            const auto& fuv = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*p_fuv);
+
+            auto lhs_values = p_R_u->cellValues();
+            auto rhs_arrays = p_Vector3_v->cellArrays();
+            bool is_same    = true;
+            for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {
+              for (size_t i = 0; i < fuv.size(); ++i) {
+                if (fuv[cell_id][i] != (lhs_values[cell_id] * rhs_arrays[cell_id][i])) {
+                  is_same = false;
+                  break;
+                }
+              }
+            }
+
+            REQUIRE(is_same);
+          }
+
+          REQUIRE_THROWS_WITH(p_R1_u * p_R1_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u * p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u * p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R1_u * p_R2x2_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^2x2)");
+
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_R2x2_v, "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_R3x3_v, "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0:R^3x3)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_R1x1_v, "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0:R^1x1)");
+
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_R2_v, "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_R3_v, "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_R1_v, "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0:R^1)");
+
+          REQUIRE_THROWS_WITH(p_R1_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R2_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R3_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0Vector:R)");
+
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R1_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R2_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R3_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R1x1_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R2x2_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R3x3_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^3x3)");
+
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh * X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, *, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, *, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, *,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R1_u * (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R^1) and R^1");
+          REQUIRE_THROWS_WITH(p_R2_u * (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R^2) and R^2");
+          REQUIRE_THROWS_WITH(p_R3_u * (TinyVector<3>{2, 3, 2}),
+                              "error: incompatible operand types Vh(P0:R^3) and R^3");
+          REQUIRE_THROWS_WITH(p_R1_u * (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R^1) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R2_u * (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R^2) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R3_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R^3) and R^3x3");
+          REQUIRE_THROWS_WITH(p_R2x2_u * (TinyMatrix<1>{2}),
+                              "error: incompatible operand types Vh(P0:R^2x2) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R1x1_u * (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R^1x1) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R2x2_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R^2x2) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^3x3");
+          REQUIRE_THROWS_WITH(p_Vector3_u * (double{2}), "error: incompatible operand types Vh(P0Vector:R) and R");
+        }
+
+        SECTION("X * Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R1x1_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R2x2_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R3x3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), *, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), *, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                                     4.7, 2.3, 7.1,   //
+                                                                     9.7, 3.2, 6.8}),
+                                                      *, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                                     4.7, 2.3, 7.1,   //
+                                                                     9.7, 3.2, 6.8}),
+                                                      *, p_R3x3_u);
+
+          CHECK_VECTOR_XxVH_TO_VH(bool{true}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(uint64_t{1}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(int64_t{2}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(double{1.3}, *, p_Vector3_u);
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R2_u, "error: incompatible operand types R^1x1 and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R3_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R2_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R1_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^1)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R2x2_u,
+                              "error: incompatible operand types R^1x1 and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R3x3_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^3x3)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R2x2_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R1x1_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^1x1)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_Vector3_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_Vector3_u,
+                              "error: incompatible operand types R^1x1 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("ratio")
+      {
+        SECTION("Vh / Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, /, p_R_v);
+
+          REQUIRE_THROWS_WITH(p_R_u / p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u / p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u / p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u / p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+
+          REQUIRE_THROWS_WITH(p_R_u / p_other_mesh_R_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("X / Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, /, p_R_u);
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      constexpr size_t Dimension = 2;
+
+      using Rd = TinyVector<Dimension>;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh2D();
+
+      std::shared_ptr other_mesh =
+        std::make_shared<Mesh<Connectivity<Dimension>>>(mesh->shared_connectivity(), mesh->xr());
+
+      CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      CellValue<double> u_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      CellValue<double> v_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.6 + std::sin(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      std::shared_ptr p_R_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, u_R_values);
+      std::shared_ptr p_other_mesh_R_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, double>>(other_mesh, u_R_values);
+      std::shared_ptr p_R_v = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, v_R_values);
+
+      std::shared_ptr p_R1_u = [=] {
+        CellValue<TinyVector<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R1_v = [=] {
+        CellValue<TinyVector<1>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { vj[cell_id][0] = xj[cell_id][0] * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R1_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(other_mesh, p_R1_u->cellValues());
+
+      constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1] + x[2]};
+        }
+      };
+
+      std::shared_ptr p_R2_u = [=] {
+        CellValue<TinyVector<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R2_v = [=] {
+        CellValue<TinyVector<2>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R2_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(other_mesh, p_R2_u->cellValues());
+
+      constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1], x[0] + x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1], x[2]};
+        }
+      };
+
+      std::shared_ptr p_R3_u = [=] {
+        CellValue<TinyVector<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R3_v = [=] {
+        CellValue<TinyVector<3>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1], x[2] * x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R3_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(other_mesh, p_R3_u->cellValues());
+
+      std::shared_ptr p_R1x1_u = [=] {
+        CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R1x1_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(other_mesh, p_R1x1_u->cellValues());
+
+      std::shared_ptr p_R1x1_v = [=] {
+        CellValue<TinyMatrix<1>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { vj[cell_id] = {0.3 - xj[cell_id][0]}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_R2x2_u = [=] {
+        CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                           2 * x[1], -x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R2x2_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(other_mesh, p_R2x2_u->cellValues());
+
+      std::shared_ptr p_R2x2_v = [=] {
+        CellValue<TinyMatrix<2>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            vj[cell_id] = {x[0] + 0.3, 1 - x[1] - x[0],   //
+                           2 * x[1] + x[0], x[1] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_R3x3_u = [=] {
+        CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                           2 * x[1],        -x[0],           x[0] - x[1],   //
+                           3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R3x3_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(other_mesh, p_R3x3_u->cellValues());
+
+      std::shared_ptr p_R3x3_v = [=] {
+        CellValue<TinyMatrix<3>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            vj[cell_id] = {0.2 * x[0] + 1,  2 + x[1],          3 - x[2],      //
+                           2.3 * x[2],      x[1] - x[0],       x[2] - x[1],   //
+                           2 * x[2] + x[0], x[1] + 0.2 * x[2], x[2] - 2 * x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_Vector3_u = [=] {
+        CellArray<double> uj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj_vector[cell_id][0] = 2 * x[0] + 1;
+            uj_vector[cell_id][1] = 1 - x[1] * x[2];
+            uj_vector[cell_id][2] = x[0] + x[2];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+      }();
+
+      std::shared_ptr p_other_mesh_Vector3_u =
+        std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(other_mesh, p_Vector3_u->cellArrays());
+
+      std::shared_ptr p_Vector3_v = [=] {
+        CellArray<double> vj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          vj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            vj_vector[cell_id][0] = x[0] * x[1] + 1;
+            vj_vector[cell_id][1] = 2 * x[1];
+            vj_vector[cell_id][2] = x[2] * x[0];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, vj_vector);
+      }();
+
+      std::shared_ptr p_Vector2_w = [=] {
+        CellArray<double> wj_vector{mesh->connectivity(), 2};
+        parallel_for(
+          wj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            wj_vector[cell_id][0] = x[0] + x[1] * 2;
+            wj_vector[cell_id][1] = x[0] * x[1];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, wj_vector);
+      }();
+
+      SECTION("sum")
+      {
+        SECTION("Vh + Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, +, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1_u, +, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2_u, +, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3_u, +, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, +, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, +, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, +, p_R3x3_v);
+
+          CHECK_VECTOR_VH2_TO_VH(p_Vector3_u, +, p_Vector3_v);
+
+          REQUIRE_THROWS_WITH(p_R_u + p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u + p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u + p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u + p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_R_u + p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_R_v, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_Vector2_w, "error: Vh(P0Vector:R) spaces have different sizes");
+
+          REQUIRE_THROWS_WITH(p_R_u + p_other_mesh_R_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1_u + p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2_u + p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3_u + p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u + p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u + p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u + p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh + X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1_u, +, (TinyVector<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2_u, +, (TinyVector<2>{1.2, 2.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3_u, +, (TinyVector<3>{3.2, 7.1, 5.2}));
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, +, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, +, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, +,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R) and R^1");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R) and R^2");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<3>{2, 3, 2}), "error: incompatible operand types Vh(P0:R) and R^3");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u + (double{1}), "error: incompatible operand types Vh(P0Vector:R) and R");
+          REQUIRE_THROWS_WITH(p_Vector3_u + (TinyVector<1>{1}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^1");
+          REQUIRE_THROWS_WITH(p_Vector3_u + (TinyVector<2>{1, 2}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^2");
+        }
+
+        SECTION("X + Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, +, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<1>{1.3}), +, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<2>{1.2, 2.3}), +, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<3>{3.2, 7.1, 5.2}), +, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), +, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), +, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}),
+                                  +, p_R3x3_u);
+
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) + p_R_u, "error: incompatible operand types R^1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) + p_R_u, "error: incompatible operand types R^2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<3>{2, 3, 2}) + p_R_u, "error: incompatible operand types R^3 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) + p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) + p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) + p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((double{1}) + p_Vector3_u, "error: incompatible operand types R and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) + p_Vector3_u,
+                              "error: incompatible operand types R^1 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) + p_Vector3_u,
+                              "error: incompatible operand types R^2 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("difference")
+      {
+        SECTION("Vh - Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, -, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1_u, -, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2_u, -, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3_u, -, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, -, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, -, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, -, p_R3x3_v);
+
+          CHECK_VECTOR_VH2_TO_VH(p_Vector3_u, -, p_Vector3_v);
+
+          REQUIRE_THROWS_WITH(p_R_u - p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u - p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u - p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u - p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_R_v, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_Vector2_w, "error: Vh(P0Vector:R) spaces have different sizes");
+
+          REQUIRE_THROWS_WITH(p_R_u - p_other_mesh_R_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1_u - p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2_u - p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3_u - p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u - p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u - p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u - p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh - X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1_u, -, (TinyVector<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2_u, -, (TinyVector<2>{1.2, 2.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3_u, -, (TinyVector<3>{3.2, 7.1, 5.2}));
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, -, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, -, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, -,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R) and R^1");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R) and R^2");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<3>{2, 3, 2}), "error: incompatible operand types Vh(P0:R) and R^3");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u - (double{1}), "error: incompatible operand types Vh(P0Vector:R) and R");
+          REQUIRE_THROWS_WITH(p_Vector3_u - (TinyVector<1>{1}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^1");
+          REQUIRE_THROWS_WITH(p_Vector3_u - (TinyVector<2>{1, 2}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^2");
+        }
+
+        SECTION("X - Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, -, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<1>{1.3}), -, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<2>{1.2, 2.3}), -, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<3>{3.2, 7.1, 5.2}), -, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), -, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), -, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}),
+                                  -, p_R3x3_u);
+
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) - p_R_u, "error: incompatible operand types R^1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) - p_R_u, "error: incompatible operand types R^2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<3>{2, 3, 2}) - p_R_u, "error: incompatible operand types R^3 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) - p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) - p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) - p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((double{1}) - p_Vector3_u, "error: incompatible operand types R and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) - p_Vector3_u,
+                              "error: incompatible operand types R^1 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) - p_Vector3_u,
+                              "error: incompatible operand types R^2 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("product")
+      {
+        SECTION("Vh * Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, *, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, *, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, *, p_R3x3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R3x3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, *, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, *, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, *, p_R3_v);
+
+          {
+            std::shared_ptr p_fuv = p_R_u * p_Vector3_v;
+
+            REQUIRE(p_fuv.use_count() > 0);
+            REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*p_fuv));
+
+            const auto& fuv = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*p_fuv);
+
+            auto lhs_values = p_R_u->cellValues();
+            auto rhs_arrays = p_Vector3_v->cellArrays();
+            bool is_same    = true;
+            for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {
+              for (size_t i = 0; i < fuv.size(); ++i) {
+                if (fuv[cell_id][i] != (lhs_values[cell_id] * rhs_arrays[cell_id][i])) {
+                  is_same = false;
+                  break;
+                }
+              }
+            }
+
+            REQUIRE(is_same);
+          }
+
+          REQUIRE_THROWS_WITH(p_R1_u * p_R1_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u * p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u * p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R1_u * p_R2x2_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^2x2)");
+
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_R2x2_v, "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_R3x3_v, "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0:R^3x3)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_R1x1_v, "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0:R^1x1)");
+
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_R2_v, "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_R3_v, "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_R1_v, "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0:R^1)");
+
+          REQUIRE_THROWS_WITH(p_R1_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R2_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R3_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0Vector:R)");
+
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R1_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R2_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R3_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R1x1_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R2x2_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R3x3_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^3x3)");
+
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh * X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, *, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, *, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, *,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R1_u * (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R^1) and R^1");
+          REQUIRE_THROWS_WITH(p_R2_u * (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R^2) and R^2");
+          REQUIRE_THROWS_WITH(p_R3_u * (TinyVector<3>{2, 3, 2}),
+                              "error: incompatible operand types Vh(P0:R^3) and R^3");
+          REQUIRE_THROWS_WITH(p_R1_u * (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R^1) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R2_u * (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R^2) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R3_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R^3) and R^3x3");
+          REQUIRE_THROWS_WITH(p_R2x2_u * (TinyMatrix<1>{2}),
+                              "error: incompatible operand types Vh(P0:R^2x2) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R1x1_u * (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R^1x1) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R2x2_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R^2x2) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^3x3");
+          REQUIRE_THROWS_WITH(p_Vector3_u * (double{2}), "error: incompatible operand types Vh(P0Vector:R) and R");
+        }
+
+        SECTION("X * Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R1x1_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R2x2_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R3x3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), *, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), *, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                                     4.7, 2.3, 7.1,   //
+                                                                     9.7, 3.2, 6.8}),
+                                                      *, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                                     4.7, 2.3, 7.1,   //
+                                                                     9.7, 3.2, 6.8}),
+                                                      *, p_R3x3_u);
+
+          CHECK_VECTOR_XxVH_TO_VH(bool{true}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(uint64_t{1}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(int64_t{2}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(double{1.3}, *, p_Vector3_u);
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R2_u, "error: incompatible operand types R^1x1 and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R3_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R2_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R1_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^1)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R2x2_u,
+                              "error: incompatible operand types R^1x1 and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R3x3_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^3x3)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R2x2_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R1x1_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^1x1)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_Vector3_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_Vector3_u,
+                              "error: incompatible operand types R^1x1 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("ratio")
+      {
+        SECTION("Vh / Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, /, p_R_v);
+
+          REQUIRE_THROWS_WITH(p_R_u / p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u / p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u / p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u / p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+
+          REQUIRE_THROWS_WITH(p_R_u / p_other_mesh_R_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("X / Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, /, p_R_u);
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      constexpr size_t Dimension = 3;
+
+      using Rd = TinyVector<Dimension>;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh3D();
+
+      std::shared_ptr other_mesh =
+        std::make_shared<Mesh<Connectivity<Dimension>>>(mesh->shared_connectivity(), mesh->xr());
+
+      CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      CellValue<double> u_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      CellValue<double> v_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.6 + std::sin(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      std::shared_ptr p_R_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, u_R_values);
+      std::shared_ptr p_other_mesh_R_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, double>>(other_mesh, u_R_values);
+      std::shared_ptr p_R_v = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, v_R_values);
+
+      std::shared_ptr p_R1_u = [=] {
+        CellValue<TinyVector<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R1_v = [=] {
+        CellValue<TinyVector<1>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { vj[cell_id][0] = xj[cell_id][0] * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R1_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(other_mesh, p_R1_u->cellValues());
+
+      constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1] + x[2]};
+        }
+      };
+
+      std::shared_ptr p_R2_u = [=] {
+        CellValue<TinyVector<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R2_v = [=] {
+        CellValue<TinyVector<2>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R2_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(other_mesh, p_R2_u->cellValues());
+
+      constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1], x[0] + x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1], x[2]};
+        }
+      };
+
+      std::shared_ptr p_R3_u = [=] {
+        CellValue<TinyVector<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R3_v = [=] {
+        CellValue<TinyVector<3>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            vj[cell_id]           = {x[0] * x[1] + 1, 2 * x[1], x[2] * x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_other_mesh_R3_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(other_mesh, p_R3_u->cellValues());
+
+      std::shared_ptr p_R1x1_u = [=] {
+        CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R1x1_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(other_mesh, p_R1x1_u->cellValues());
+
+      std::shared_ptr p_R1x1_v = [=] {
+        CellValue<TinyMatrix<1>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { vj[cell_id] = {0.3 - xj[cell_id][0]}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_R2x2_u = [=] {
+        CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                           2 * x[1], -x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R2x2_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(other_mesh, p_R2x2_u->cellValues());
+
+      std::shared_ptr p_R2x2_v = [=] {
+        CellValue<TinyMatrix<2>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            vj[cell_id] = {x[0] + 0.3, 1 - x[1] - x[0],   //
+                           2 * x[1] + x[0], x[1] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_R3x3_u = [=] {
+        CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                           2 * x[1],        -x[0],           x[0] - x[1],   //
+                           3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_other_mesh_R3x3_u =
+        std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(other_mesh, p_R3x3_u->cellValues());
+
+      std::shared_ptr p_R3x3_v = [=] {
+        CellValue<TinyMatrix<3>> vj{mesh->connectivity()};
+        parallel_for(
+          vj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            vj[cell_id] = {0.2 * x[0] + 1,  2 + x[1],          3 - x[2],      //
+                           2.3 * x[2],      x[1] - x[0],       x[2] - x[1],   //
+                           2 * x[2] + x[0], x[1] + 0.2 * x[2], x[2] - 2 * x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, vj);
+      }();
+
+      std::shared_ptr p_Vector3_u = [=] {
+        CellArray<double> uj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj_vector[cell_id][0] = 2 * x[0] + 1;
+            uj_vector[cell_id][1] = 1 - x[1] * x[2];
+            uj_vector[cell_id][2] = x[0] + x[2];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+      }();
+
+      std::shared_ptr p_other_mesh_Vector3_u =
+        std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(other_mesh, p_Vector3_u->cellArrays());
+
+      std::shared_ptr p_Vector3_v = [=] {
+        CellArray<double> vj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          vj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            vj_vector[cell_id][0] = x[0] * x[1] + 1;
+            vj_vector[cell_id][1] = 2 * x[1];
+            vj_vector[cell_id][2] = x[2] * x[0];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, vj_vector);
+      }();
+
+      std::shared_ptr p_Vector2_w = [=] {
+        CellArray<double> wj_vector{mesh->connectivity(), 2};
+        parallel_for(
+          wj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            wj_vector[cell_id][0] = x[0] + x[1] * 2;
+            wj_vector[cell_id][1] = x[0] * x[1];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, wj_vector);
+      }();
+
+      SECTION("sum")
+      {
+        SECTION("Vh + Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, +, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1_u, +, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2_u, +, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3_u, +, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, +, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, +, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, +, p_R3x3_v);
+
+          CHECK_VECTOR_VH2_TO_VH(p_Vector3_u, +, p_Vector3_v);
+
+          REQUIRE_THROWS_WITH(p_R_u + p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u + p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u + p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u + p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_R_u + p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_R_v, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_Vector2_w, "error: Vh(P0Vector:R) spaces have different sizes");
+
+          REQUIRE_THROWS_WITH(p_R_u + p_other_mesh_R_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1_u + p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2_u + p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3_u + p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u + p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u + p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u + p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_Vector3_u + p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh + X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, +, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1_u, +, (TinyVector<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2_u, +, (TinyVector<2>{1.2, 2.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3_u, +, (TinyVector<3>{3.2, 7.1, 5.2}));
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, +, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, +, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, +,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R) and R^1");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R) and R^2");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyVector<3>{2, 3, 2}), "error: incompatible operand types Vh(P0:R) and R^3");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R_u + (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u + (double{1}), "error: incompatible operand types Vh(P0Vector:R) and R");
+          REQUIRE_THROWS_WITH(p_Vector3_u + (TinyVector<1>{1}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^1");
+          REQUIRE_THROWS_WITH(p_Vector3_u + (TinyVector<2>{1, 2}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^2");
+        }
+
+        SECTION("X + Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, +, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, +, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<1>{1.3}), +, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<2>{1.2, 2.3}), +, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<3>{3.2, 7.1, 5.2}), +, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), +, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), +, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}),
+                                  +, p_R3x3_u);
+
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) + p_R_u, "error: incompatible operand types R^1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) + p_R_u, "error: incompatible operand types R^2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<3>{2, 3, 2}) + p_R_u, "error: incompatible operand types R^3 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) + p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) + p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) + p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((double{1}) + p_Vector3_u, "error: incompatible operand types R and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) + p_Vector3_u,
+                              "error: incompatible operand types R^1 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) + p_Vector3_u,
+                              "error: incompatible operand types R^2 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("difference")
+      {
+        SECTION("Vh - Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, -, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1_u, -, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2_u, -, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3_u, -, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, -, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, -, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, -, p_R3x3_v);
+
+          CHECK_VECTOR_VH2_TO_VH(p_Vector3_u, -, p_Vector3_v);
+
+          REQUIRE_THROWS_WITH(p_R_u - p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u - p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u - p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u - p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_R_v, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_Vector2_w, "error: Vh(P0Vector:R) spaces have different sizes");
+
+          REQUIRE_THROWS_WITH(p_R_u - p_other_mesh_R_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1_u - p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2_u - p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3_u - p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u - p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u - p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u - p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_Vector3_u - p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh - X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, -, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1_u, -, (TinyVector<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2_u, -, (TinyVector<2>{1.2, 2.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3_u, -, (TinyVector<3>{3.2, 7.1, 5.2}));
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, -, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, -, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, -,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R) and R^1");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R) and R^2");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyVector<3>{2, 3, 2}), "error: incompatible operand types Vh(P0:R) and R^3");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R_u - (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u - (double{1}), "error: incompatible operand types Vh(P0Vector:R) and R");
+          REQUIRE_THROWS_WITH(p_Vector3_u - (TinyVector<1>{1}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^1");
+          REQUIRE_THROWS_WITH(p_Vector3_u - (TinyVector<2>{1, 2}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^2");
+        }
+
+        SECTION("X - Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, -, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, -, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<1>{1.3}), -, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<2>{1.2, 2.3}), -, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyVector<3>{3.2, 7.1, 5.2}), -, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), -, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), -, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}),
+                                  -, p_R3x3_u);
+
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) - p_R_u, "error: incompatible operand types R^1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) - p_R_u, "error: incompatible operand types R^2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyVector<3>{2, 3, 2}) - p_R_u, "error: incompatible operand types R^3 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) - p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) - p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) - p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((double{1}) - p_Vector3_u, "error: incompatible operand types R and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<1>{1}) - p_Vector3_u,
+                              "error: incompatible operand types R^1 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyVector<2>{1, 2}) - p_Vector3_u,
+                              "error: incompatible operand types R^2 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("product")
+      {
+        SECTION("Vh * Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, *, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, *, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, *, p_R3x3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R1x1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R2x2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, *, p_R3x3_v);
+
+          CHECK_SCALAR_VH2_TO_VH(p_R1x1_u, *, p_R1_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R2x2_u, *, p_R2_v);
+          CHECK_SCALAR_VH2_TO_VH(p_R3x3_u, *, p_R3_v);
+
+          {
+            std::shared_ptr p_fuv = p_R_u * p_Vector3_v;
+
+            REQUIRE(p_fuv.use_count() > 0);
+            REQUIRE_NOTHROW(dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*p_fuv));
+
+            const auto& fuv = dynamic_cast<const DiscreteFunctionP0Vector<Dimension, double>&>(*p_fuv);
+
+            auto lhs_values = p_R_u->cellValues();
+            auto rhs_arrays = p_Vector3_v->cellArrays();
+            bool is_same    = true;
+            for (CellId cell_id = 0; cell_id < lhs_values.numberOfItems(); ++cell_id) {
+              for (size_t i = 0; i < fuv.size(); ++i) {
+                if (fuv[cell_id][i] != (lhs_values[cell_id] * rhs_arrays[cell_id][i])) {
+                  is_same = false;
+                  break;
+                }
+              }
+            }
+
+            REQUIRE(is_same);
+          }
+
+          REQUIRE_THROWS_WITH(p_R1_u * p_R1_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u * p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u * p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R1_u * p_R2x2_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0:R^2x2)");
+
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_R2x2_v, "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_R3x3_v, "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0:R^3x3)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_R1x1_v, "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0:R^1x1)");
+
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_R2_v, "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_R3_v, "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_R1_v, "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0:R^1)");
+
+          REQUIRE_THROWS_WITH(p_R1_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^1) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R2_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R3_u * p_Vector3_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^1x1) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^2x2) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0:R^3x3) and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_u * p_Vector3_v,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0Vector:R)");
+
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R1_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R2_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R3_u, "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R1x1_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R2x2_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH(p_Vector3_v * p_R3x3_u,
+                              "error: incompatible operand types Vh(P0Vector:R) and Vh(P0:R^3x3)");
+
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R1x1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R2x2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_R3x3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R1x1_u * p_other_mesh_R1_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R2x2_u * p_other_mesh_R2_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R3x3_u * p_other_mesh_R3_u, "error: operands are defined on different meshes");
+          REQUIRE_THROWS_WITH(p_R_u * p_other_mesh_Vector3_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("Vh * X -> Vh")
+        {
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, bool{true});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, uint64_t{1});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, int64_t{2});
+          CHECK_SCALAR_VHxX_TO_VH(p_R_u, *, double{1.3});
+
+          CHECK_SCALAR_VHxX_TO_VH(p_R1x1_u, *, (TinyMatrix<1>{1.3}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R2x2_u, *, (TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}));
+          CHECK_SCALAR_VHxX_TO_VH(p_R3x3_u, *,
+                                  (TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                 4.7, 2.3, 7.1,   //
+                                                 9.7, 3.2, 6.8}));
+
+          REQUIRE_THROWS_WITH(p_R1_u * (TinyVector<1>{1}), "error: incompatible operand types Vh(P0:R^1) and R^1");
+          REQUIRE_THROWS_WITH(p_R2_u * (TinyVector<2>{1, 2}), "error: incompatible operand types Vh(P0:R^2) and R^2");
+          REQUIRE_THROWS_WITH(p_R3_u * (TinyVector<3>{2, 3, 2}),
+                              "error: incompatible operand types Vh(P0:R^3) and R^3");
+          REQUIRE_THROWS_WITH(p_R1_u * (TinyMatrix<1>{2}), "error: incompatible operand types Vh(P0:R^1) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R2_u * (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R^2) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R3_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R^3) and R^3x3");
+          REQUIRE_THROWS_WITH(p_R2x2_u * (TinyMatrix<1>{2}),
+                              "error: incompatible operand types Vh(P0:R^2x2) and R^1x1");
+          REQUIRE_THROWS_WITH(p_R1x1_u * (TinyMatrix<2>{2, 3, 1, 4}),
+                              "error: incompatible operand types Vh(P0:R^1x1) and R^2x2");
+          REQUIRE_THROWS_WITH(p_R2x2_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0:R^2x2) and R^3x3");
+
+          REQUIRE_THROWS_WITH(p_Vector3_u * (TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}),
+                              "error: incompatible operand types Vh(P0Vector:R) and R^3x3");
+          REQUIRE_THROWS_WITH(p_Vector3_u * (double{2}), "error: incompatible operand types Vh(P0Vector:R) and R");
+        }
+
+        SECTION("X * Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R1x1_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R2x2_u);
+
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, *, p_R3x3_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, *, p_R3x3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), *, p_R1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), *, p_R2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                                     4.7, 2.3, 7.1,   //
+                                                                     9.7, 3.2, 6.8}),
+                                                      *, p_R3_u);
+
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<1>{1.3}), *, p_R1x1_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<2>{1.2, 2.3, 4.2, 5.1}), *, p_R2x2_u);
+          CHECK_SCALAR_XxVH_TO_VH((TinyMatrix<3>{3.2, 7.1, 5.2,   //
+                                                                     4.7, 2.3, 7.1,   //
+                                                                     9.7, 3.2, 6.8}),
+                                                      *, p_R3x3_u);
+
+          CHECK_VECTOR_XxVH_TO_VH(bool{true}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(uint64_t{1}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(int64_t{2}, *, p_Vector3_u);
+          CHECK_VECTOR_XxVH_TO_VH(double{1.3}, *, p_Vector3_u);
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R_u, "error: incompatible operand types R^1x1 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R2_u, "error: incompatible operand types R^1x1 and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R3_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^3)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R2_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R1_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^1)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_R2x2_u,
+                              "error: incompatible operand types R^1x1 and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R3x3_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^3x3)");
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_R2x2_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0:R^2x2)");
+          REQUIRE_THROWS_WITH((TinyMatrix<2>{2, 3, 1, 4}) * p_R1x1_u,
+                              "error: incompatible operand types R^2x2 and Vh(P0:R^1x1)");
+
+          REQUIRE_THROWS_WITH((TinyMatrix<3>{1, 3, 6, 4, 7, 2, 5, 9, 8}) * p_Vector3_u,
+                              "error: incompatible operand types R^3x3 and Vh(P0Vector:R)");
+          REQUIRE_THROWS_WITH((TinyMatrix<1>{2}) * p_Vector3_u,
+                              "error: incompatible operand types R^1x1 and Vh(P0Vector:R)");
+        }
+      }
+
+      SECTION("ratio")
+      {
+        SECTION("Vh / Vh -> Vh")
+        {
+          CHECK_SCALAR_VH2_TO_VH(p_R_u, /, p_R_v);
+
+          REQUIRE_THROWS_WITH(p_R_u / p_R1_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R2_u / p_R1_v, "error: incompatible operand types Vh(P0:R^2) and Vh(P0:R^1)");
+          REQUIRE_THROWS_WITH(p_R3_u / p_R1x1_v, "error: incompatible operand types Vh(P0:R^3) and Vh(P0:R^1x1)");
+          REQUIRE_THROWS_WITH(p_R_u / p_R2x2_v, "error: incompatible operand types Vh(P0:R) and Vh(P0:R^2x2)");
+
+          REQUIRE_THROWS_WITH(p_R_u / p_other_mesh_R_u, "error: operands are defined on different meshes");
+        }
+
+        SECTION("X / Vh -> Vh")
+        {
+          CHECK_SCALAR_XxVH_TO_VH(bool{true}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(uint64_t{1}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(int64_t{2}, /, p_R_u);
+          CHECK_SCALAR_XxVH_TO_VH(double{1.3}, /, p_R_u);
+        }
+      }
+    }
+  }
+
+  SECTION("unary operators")
+  {
+    SECTION("1D")
+    {
+      constexpr size_t Dimension = 1;
+
+      using Rd = TinyVector<Dimension>;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      CellValue<double> u_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      std::shared_ptr p_R_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, u_R_values);
+
+      std::shared_ptr p_R1_u = [=] {
+        CellValue<TinyVector<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+      }();
+
+      constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1] + x[2]};
+        }
+      };
+
+      std::shared_ptr p_R2_u = [=] {
+        CellValue<TinyVector<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+      }();
+
+      constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1], x[0] + x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1], x[2]};
+        }
+      };
+
+      std::shared_ptr p_R3_u = [=] {
+        CellValue<TinyVector<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R1x1_u = [=] {
+        CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R2x2_u = [=] {
+        CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                           2 * x[1], -x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R3x3_u = [=] {
+        CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                           2 * x[1],        -x[0],           x[0] - x[1],   //
+                           3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_Vector3_u = [=] {
+        CellArray<double> uj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj_vector[cell_id][0] = 2 * x[0] + 1;
+            uj_vector[cell_id][1] = 1 - x[1] * x[2];
+            uj_vector[cell_id][2] = x[0] + x[2];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+      }();
+
+      SECTION("unary minus")
+      {
+        SECTION("- Vh -> Vh")
+        {
+          CHECK_SCALAR_VH_TO_VH(-, p_R_u);
+
+          CHECK_SCALAR_VH_TO_VH(-, p_R1_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R2_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R3_u);
+
+          CHECK_SCALAR_VH_TO_VH(-, p_R1x1_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R2x2_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R3x3_u);
+
+          CHECK_VECTOR_VH_TO_VH(-, p_Vector3_u);
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      constexpr size_t Dimension = 2;
+
+      using Rd = TinyVector<Dimension>;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh2D();
+
+      CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      CellValue<double> u_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      std::shared_ptr p_R_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, u_R_values);
+
+      std::shared_ptr p_R1_u = [=] {
+        CellValue<TinyVector<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+      }();
+
+      constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1] + x[2]};
+        }
+      };
+
+      std::shared_ptr p_R2_u = [=] {
+        CellValue<TinyVector<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+      }();
+
+      constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1], x[0] + x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1], x[2]};
+        }
+      };
+
+      std::shared_ptr p_R3_u = [=] {
+        CellValue<TinyVector<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R1x1_u = [=] {
+        CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R2x2_u = [=] {
+        CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                           2 * x[1], -x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R3x3_u = [=] {
+        CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                           2 * x[1],        -x[0],           x[0] - x[1],   //
+                           3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_Vector3_u = [=] {
+        CellArray<double> uj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj_vector[cell_id][0] = 2 * x[0] + 1;
+            uj_vector[cell_id][1] = 1 - x[1] * x[2];
+            uj_vector[cell_id][2] = x[0] + x[2];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+      }();
+
+      SECTION("unary minus")
+      {
+        SECTION("- Vh -> Vh")
+        {
+          CHECK_SCALAR_VH_TO_VH(-, p_R_u);
+
+          CHECK_SCALAR_VH_TO_VH(-, p_R1_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R2_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R3_u);
+
+          CHECK_SCALAR_VH_TO_VH(-, p_R1x1_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R2x2_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R3x3_u);
+
+          CHECK_VECTOR_VH_TO_VH(-, p_Vector3_u);
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      constexpr size_t Dimension = 3;
+
+      using Rd = TinyVector<Dimension>;
+
+      std::shared_ptr mesh = MeshDataBaseForTests::get().cartesianMesh3D();
+
+      CellValue<const Rd> xj = MeshDataManager::instance().getMeshData(*mesh).xj();
+
+      CellValue<double> u_R_values = [=] {
+        CellValue<double> build_values{mesh->connectivity()};
+        parallel_for(
+          build_values.numberOfItems(),
+          PUGS_LAMBDA(const CellId cell_id) { build_values[cell_id] = 0.2 + std::cos(l2Norm(xj[cell_id])); });
+        return build_values;
+      }();
+
+      std::shared_ptr p_R_u = std::make_shared<const DiscreteFunctionP0<Dimension, double>>(mesh, u_R_values);
+
+      std::shared_ptr p_R1_u = [=] {
+        CellValue<TinyVector<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id][0] = 2 * xj[cell_id][0] + 1; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<1>>>(mesh, uj);
+      }();
+
+      constexpr auto to_2d = [&](const TinyVector<Dimension>& x) -> TinyVector<2> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1] + x[2]};
+        }
+      };
+
+      std::shared_ptr p_R2_u = [=] {
+        CellValue<TinyVector<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<2>>>(mesh, uj);
+      }();
+
+      constexpr auto to_3d = [&](const TinyVector<Dimension>& x) -> TinyVector<3> {
+        if constexpr (Dimension == 1) {
+          return {x[0], 1 + x[0] * x[0], 2 - x[0]};
+        } else if constexpr (Dimension == 2) {
+          return {x[0], x[1], x[0] + x[1]};
+        } else if constexpr (Dimension == 3) {
+          return {x[0], x[1], x[2]};
+        }
+      };
+
+      std::shared_ptr p_R3_u = [=] {
+        CellValue<TinyVector<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj[cell_id]           = {2 * x[0] + 1, 1 - x[1] * x[2], x[0] + x[2]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyVector<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R1x1_u = [=] {
+        CellValue<TinyMatrix<1>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) { uj[cell_id] = {2 * xj[cell_id][0] + 1}; });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<1>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R2x2_u = [=] {
+        CellValue<TinyMatrix<2>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<2> x = to_2d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1, 1 - x[1],   //
+                           2 * x[1], -x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<2>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_R3x3_u = [=] {
+        CellValue<TinyMatrix<3>> uj{mesh->connectivity()};
+        parallel_for(
+          uj.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+
+            uj[cell_id] = {2 * x[0] + 1,    1 - x[1],        3,             //
+                           2 * x[1],        -x[0],           x[0] - x[1],   //
+                           3 * x[2] - x[1], x[1] - 2 * x[2], x[2] - x[0]};
+          });
+
+        return std::make_shared<const DiscreteFunctionP0<Dimension, TinyMatrix<3>>>(mesh, uj);
+      }();
+
+      std::shared_ptr p_Vector3_u = [=] {
+        CellArray<double> uj_vector{mesh->connectivity(), 3};
+        parallel_for(
+          uj_vector.numberOfItems(), PUGS_LAMBDA(const CellId cell_id) {
+            const TinyVector<3> x = to_3d(xj[cell_id]);
+            uj_vector[cell_id][0] = 2 * x[0] + 1;
+            uj_vector[cell_id][1] = 1 - x[1] * x[2];
+            uj_vector[cell_id][2] = x[0] + x[2];
+          });
+
+        return std::make_shared<const DiscreteFunctionP0Vector<Dimension, double>>(mesh, uj_vector);
+      }();
+
+      SECTION("unary minus")
+      {
+        SECTION("- Vh -> Vh")
+        {
+          CHECK_SCALAR_VH_TO_VH(-, p_R_u);
+
+          CHECK_SCALAR_VH_TO_VH(-, p_R1_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R2_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R3_u);
+
+          CHECK_SCALAR_VH_TO_VH(-, p_R1x1_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R2x2_u);
+          CHECK_SCALAR_VH_TO_VH(-, p_R3x3_u);
+
+          CHECK_VECTOR_VH_TO_VH(-, p_Vector3_u);
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_EmbeddedIDiscreteFunctionUtils.cpp b/tests/test_EmbeddedIDiscreteFunctionUtils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0ded74fd6b5764bfb67917a48a1a24ab8b1db873
--- /dev/null
+++ b/tests/test_EmbeddedIDiscreteFunctionUtils.cpp
@@ -0,0 +1,125 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <language/utils/EmbeddedIDiscreteFunctionUtils.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
+#include <scheme/DiscreteFunctionP0Vector.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("EmbeddedIDiscreteFunctionUtils", "[language]")
+{
+  using R1 = TinyVector<1, double>;
+  using R2 = TinyVector<2, double>;
+  using R3 = TinyVector<3, double>;
+
+  using R1x1 = TinyMatrix<1, 1, double>;
+  using R2x2 = TinyMatrix<2, 2, double>;
+  using R3x3 = TinyMatrix<3, 3, double>;
+
+  SECTION("operand type name")
+  {
+    SECTION("basic types")
+    {
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(double{1}) == "R");
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(std::make_shared<double>(1)) == "R");
+    }
+
+    SECTION("discrete P0 function")
+    {
+      std::shared_ptr mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, double>{mesh_1d}) == "Vh(P0:R)");
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, R1>{mesh_1d}) == "Vh(P0:R^1)");
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, R2>{mesh_1d}) == "Vh(P0:R^2)");
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, R3>{mesh_1d}) == "Vh(P0:R^3)");
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, R1x1>{mesh_1d}) ==
+              "Vh(P0:R^1x1)");
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, R2x2>{mesh_1d}) ==
+              "Vh(P0:R^2x2)");
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0<1, R3x3>{mesh_1d}) ==
+              "Vh(P0:R^3x3)");
+    }
+
+    SECTION("discrete P0Vector function")
+    {
+      std::shared_ptr mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(DiscreteFunctionP0Vector<1, double>{mesh_1d, 2}) ==
+              "Vh(P0Vector:R)");
+    }
+  }
+
+  SECTION("check if is same discretization")
+  {
+    SECTION("from shared_ptr")
+    {
+      std::shared_ptr mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      REQUIRE(
+        EmbeddedIDiscreteFunctionUtils::isSameDiscretization(std::make_shared<DiscreteFunctionP0<1, double>>(mesh_1d),
+                                                             std::make_shared<DiscreteFunctionP0<1, double>>(mesh_1d)));
+
+      REQUIRE(not EmbeddedIDiscreteFunctionUtils::
+                isSameDiscretization(std::make_shared<DiscreteFunctionP0<1, double>>(mesh_1d),
+                                     std::make_shared<DiscreteFunctionP0Vector<1, double>>(mesh_1d, 1)));
+    }
+
+    SECTION("from value")
+    {
+      std::shared_ptr mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, double>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, double>{mesh_1d}));
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R1>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, R1>{mesh_1d}));
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R2>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, R2>{mesh_1d}));
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R3>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, R3>{mesh_1d}));
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R1x1>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, R1x1>{mesh_1d}));
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R2x2>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, R2x2>{mesh_1d}));
+
+      REQUIRE(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R3x3>{mesh_1d},
+                                                                   DiscreteFunctionP0<1, R3x3>{mesh_1d}));
+
+      REQUIRE(not EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, double>{mesh_1d},
+                                                                       DiscreteFunctionP0<1, R1>{mesh_1d}));
+
+      REQUIRE(not EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R2>{mesh_1d},
+                                                                       DiscreteFunctionP0<1, R2x2>{mesh_1d}));
+
+      REQUIRE(not EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, R3x3>{mesh_1d},
+                                                                       DiscreteFunctionP0<1, R2x2>{mesh_1d}));
+    }
+
+    SECTION("invalid data type")
+    {
+      std::shared_ptr mesh_1d = MeshDataBaseForTests::get().cartesianMesh1D();
+
+      REQUIRE_THROWS_WITH(EmbeddedIDiscreteFunctionUtils::isSameDiscretization(DiscreteFunctionP0<1, int64_t>{mesh_1d},
+                                                                               DiscreteFunctionP0<1, int64_t>{mesh_1d}),
+                          "unexpected error: invalid data type Vh(P0:Z)");
+    }
+  }
+
+#ifndef NDEBUG
+  SECTION("errors")
+  {
+    REQUIRE_THROWS_WITH(EmbeddedIDiscreteFunctionUtils::getOperandTypeName(std::shared_ptr<double>()),
+                        "dangling shared_ptr");
+  }
+
+#endif   // NDEBUG
+}
diff --git a/tests/test_FunctionArgumentConverter.cpp b/tests/test_FunctionArgumentConverter.cpp
index 7befebe52d93fc7ba0e17b940dba88acaecfb306..643fbba0b092d337743ea5aeaedb25eb86a5b445 100644
--- a/tests/test_FunctionArgumentConverter.cpp
+++ b/tests/test_FunctionArgumentConverter.cpp
@@ -172,6 +172,18 @@ TEST_CASE("FunctionArgumentConverter", "[language]")
             std::vector<TinyVector<2>>{TinyVector<2>{1, 3.2}, TinyVector<2>{-1, 0.2}});
     REQUIRE(std::get<std::vector<TinyVector<2>>>(execution_policy.currentContext()[2]) ==
             std::vector<TinyVector<2>>{TinyVector<2>{-3, 12.2}, TinyVector<2>{2, 1.2}});
+
+    std::shared_ptr symbol_table = std::make_shared<SymbolTable>();
+    AggregateDataVariant v_fid{std::vector<DataVariant>{uint64_t{3}, uint64_t{2}, uint64_t{7}}};
+
+    FunctionListArgumentConverter<FunctionSymbolId, FunctionSymbolId> converterFid{0, symbol_table};
+    converterFid.convert(execution_policy, v_fid);
+
+    auto&& fid_tuple = std::get<std::vector<FunctionSymbolId>>(execution_policy.currentContext()[0]);
+
+    REQUIRE(fid_tuple[0].id() == 3);
+    REQUIRE(fid_tuple[1].id() == 2);
+    REQUIRE(fid_tuple[2].id() == 7);
   }
 
   SECTION("FunctionArgumentToFunctionSymbolIdConverter")
@@ -184,4 +196,16 @@ TEST_CASE("FunctionArgumentConverter", "[language]")
 
     REQUIRE(std::get<FunctionSymbolId>(execution_policy.currentContext()[0]).id() == f_id);
   }
+
+  SECTION("FunctionArgumentToTupleFunctionSymbolIdConverter")
+  {
+    std::shared_ptr symbol_table = std::make_shared<SymbolTable>();
+
+    const uint64_t f_id = 3;
+    FunctionArgumentToTupleFunctionSymbolIdConverter converter0{0, symbol_table};
+    converter0.convert(execution_policy, f_id);
+    auto&& tuple = std::get<std::vector<FunctionSymbolId>>(execution_policy.currentContext()[0]);
+    REQUIRE(tuple.size() == 1);
+    REQUIRE(tuple[0].id() == f_id);
+  }
 }
diff --git a/tests/test_MathModule.cpp b/tests/test_MathModule.cpp
index 7c2a9bc5438367b7f3cf70d14d3b8acc58a29ae7..4d71927f13d3229d450c941250bb4202d071b109 100644
--- a/tests/test_MathModule.cpp
+++ b/tests/test_MathModule.cpp
@@ -15,7 +15,7 @@ TEST_CASE("MathModule", "[language]")
   MathModule math_module;
   const auto& name_builtin_function = math_module.getNameBuiltinFunctionMap();
 
-  REQUIRE(name_builtin_function.size() == 25);
+  REQUIRE(name_builtin_function.size() == 29);
 
   SECTION("double -> double")
   {
@@ -323,6 +323,63 @@ TEST_CASE("MathModule", "[language]")
       auto result = std::pow(arg0, arg1);
       REQUIRE(std::get<decltype(result)>(result_variant) == Catch::Approx(result));
     }
+
+    SECTION("min")
+    {
+      auto i_function = name_builtin_function.find("min:R*R");
+      REQUIRE(i_function != name_builtin_function.end());
+
+      IBuiltinFunctionEmbedder& function_embedder = *i_function->second;
+      DataVariant result_variant                  = function_embedder.apply({arg0_variant, arg1_variant});
+
+      auto result = std::min(arg0, arg1);
+      REQUIRE(std::get<decltype(result)>(result_variant) == Catch::Approx(result));
+    }
+
+    SECTION("max")
+    {
+      auto i_function = name_builtin_function.find("max:R*R");
+      REQUIRE(i_function != name_builtin_function.end());
+
+      IBuiltinFunctionEmbedder& function_embedder = *i_function->second;
+      DataVariant result_variant                  = function_embedder.apply({arg0_variant, arg1_variant});
+
+      auto result = std::max(arg0, arg1);
+      REQUIRE(std::get<decltype(result)>(result_variant) == Catch::Approx(result));
+    }
+  }
+
+  SECTION("(uint64_t, uint64_t) -> uint64_t")
+  {
+    int64_t arg0 = 3;
+    int64_t arg1 = -2;
+
+    DataVariant arg0_variant = arg0;
+    DataVariant arg1_variant = arg1;
+
+    SECTION("min")
+    {
+      auto i_function = name_builtin_function.find("min:Z*Z");
+      REQUIRE(i_function != name_builtin_function.end());
+
+      IBuiltinFunctionEmbedder& function_embedder = *i_function->second;
+      DataVariant result_variant                  = function_embedder.apply({arg0_variant, arg1_variant});
+
+      auto result = std::min(arg0, arg1);
+      REQUIRE(std::get<decltype(result)>(result_variant) == Catch::Approx(result));
+    }
+
+    SECTION("max")
+    {
+      auto i_function = name_builtin_function.find("max:Z*Z");
+      REQUIRE(i_function != name_builtin_function.end());
+
+      IBuiltinFunctionEmbedder& function_embedder = *i_function->second;
+      DataVariant result_variant                  = function_embedder.apply({arg0_variant, arg1_variant});
+
+      auto result = std::max(arg0, arg1);
+      REQUIRE(std::get<decltype(result)>(result_variant) == Catch::Approx(result));
+    }
   }
 
   SECTION("(R^d, R^d) -> double")
diff --git a/tests/test_OFStream.cpp b/tests/test_OFStream.cpp
index 32b4ff21b8e725dcaff822b586f9810d617d3a4d..5ca9c38fd4b8f01f6ed28a263d9199905481edf5 100644
--- a/tests/test_OFStream.cpp
+++ b/tests/test_OFStream.cpp
@@ -12,7 +12,7 @@ TEST_CASE("OFStream", "[language]")
 {
   SECTION("ofstream")
   {
-    const std::string basename = "ofstream_";
+    const std::string basename = std::filesystem::temp_directory_path().append("ofstream_");
     const std::string filename = basename + std::to_string(parallel::rank());
 
     // Ensures that the file is closed after this line
diff --git a/tests/test_SmallVector.cpp b/tests/test_SmallVector.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3b499c3ab3e984247b7f20f96b626f8ee413763a
--- /dev/null
+++ b/tests/test_SmallVector.cpp
@@ -0,0 +1,472 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <utils/PugsAssert.hpp>
+
+#include <algebra/SmallVector.hpp>
+#include <algebra/TinyVector.hpp>
+
+// Instantiate to ensure full coverage is performed
+template class SmallVector<int>;
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("SmallVector", "[algebra]")
+{
+  SECTION("size")
+  {
+    SmallVector<int> x{5};
+    REQUIRE(x.size() == 5);
+  }
+
+  SECTION("write access")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    REQUIRE(x[0] == 0);
+    REQUIRE(x[1] == 1);
+    REQUIRE(x[2] == 2);
+    REQUIRE(x[3] == 3);
+    REQUIRE(x[4] == 4);
+  }
+
+  SECTION("fill")
+  {
+    SmallVector<int> x{5};
+    x.fill(2);
+
+    REQUIRE(x[0] == 2);
+    REQUIRE(x[1] == 2);
+    REQUIRE(x[2] == 2);
+    REQUIRE(x[3] == 2);
+    REQUIRE(x[4] == 2);
+  }
+
+  SECTION("copy constructor (shallow)")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    const SmallVector<int> y = x;
+    REQUIRE(y[0] == 0);
+    REQUIRE(y[1] == 1);
+    REQUIRE(y[2] == 2);
+    REQUIRE(y[3] == 3);
+    REQUIRE(y[4] == 4);
+  }
+
+  SECTION("copy constructor (move)")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    const SmallVector<int> y = std::move(x);
+    REQUIRE(y[0] == 0);
+    REQUIRE(y[1] == 1);
+    REQUIRE(y[2] == 2);
+    REQUIRE(y[3] == 3);
+    REQUIRE(y[4] == 4);
+  }
+
+  SECTION("copy (shallow)")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    SmallVector<int> y{5};
+    y = x;
+    REQUIRE(y[0] == 0);
+    REQUIRE(y[1] == 1);
+    REQUIRE(y[2] == 2);
+    REQUIRE(y[3] == 3);
+    REQUIRE(y[4] == 4);
+
+    x[0] = 14;
+    x[1] = 13;
+    x[2] = 12;
+    x[3] = 11;
+    x[4] = 10;
+
+    REQUIRE(x[0] == 14);
+    REQUIRE(x[1] == 13);
+    REQUIRE(x[2] == 12);
+    REQUIRE(x[3] == 11);
+    REQUIRE(x[4] == 10);
+
+    REQUIRE(y[0] == 14);
+    REQUIRE(y[1] == 13);
+    REQUIRE(y[2] == 12);
+    REQUIRE(y[3] == 11);
+    REQUIRE(y[4] == 10);
+  }
+
+  SECTION("copy (deep)")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    SmallVector<int> y{5};
+    y = copy(x);
+
+    x[0] = 14;
+    x[1] = 13;
+    x[2] = 12;
+    x[3] = 11;
+    x[4] = 10;
+
+    REQUIRE(y[0] == 0);
+    REQUIRE(y[1] == 1);
+    REQUIRE(y[2] == 2);
+    REQUIRE(y[3] == 3);
+    REQUIRE(y[4] == 4);
+
+    REQUIRE(x[0] == 14);
+    REQUIRE(x[1] == 13);
+    REQUIRE(x[2] == 12);
+    REQUIRE(x[3] == 11);
+    REQUIRE(x[4] == 10);
+  }
+
+  SECTION("copy to const (shallow)")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    SmallVector<const int> y;
+    y = x;
+    REQUIRE(y[0] == 0);
+    REQUIRE(y[1] == 1);
+    REQUIRE(y[2] == 2);
+    REQUIRE(y[3] == 3);
+    REQUIRE(y[4] == 4);
+
+    SmallVector<int> z{5};
+    z[0] = 14;
+    z[1] = 13;
+    z[2] = 12;
+    z[3] = 11;
+    z[4] = 10;
+
+    y = z;
+    REQUIRE(z[0] == 14);
+    REQUIRE(z[1] == 13);
+    REQUIRE(z[2] == 12);
+    REQUIRE(z[3] == 11);
+    REQUIRE(z[4] == 10);
+
+    REQUIRE(y[0] == 14);
+    REQUIRE(y[1] == 13);
+    REQUIRE(y[2] == 12);
+    REQUIRE(y[3] == 11);
+    REQUIRE(y[4] == 10);
+  }
+
+  SECTION("self scalar multiplication")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    x *= 2;
+
+    REQUIRE(x[0] == 0);
+    REQUIRE(x[1] == 2);
+    REQUIRE(x[2] == 4);
+    REQUIRE(x[3] == 6);
+    REQUIRE(x[4] == 8);
+  }
+
+  SECTION("left scalar multiplication")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    const SmallVector<int> y = 2 * x;
+
+    REQUIRE(y[0] == 0);
+    REQUIRE(y[1] == 2);
+    REQUIRE(y[2] == 4);
+    REQUIRE(y[3] == 6);
+    REQUIRE(y[4] == 8);
+  }
+
+  SECTION("dot product")
+  {
+    SmallVector<int> x{5};
+    x[0] = 0;
+    x[1] = 1;
+    x[2] = 2;
+    x[3] = 3;
+    x[4] = 4;
+
+    SmallVector<int> y{5};
+    y[0] = 7;
+    y[1] = 3;
+    y[2] = 4;
+    y[3] = 2;
+    y[4] = 8;
+
+    const int s = dot(x, y);
+    REQUIRE(s == 49);
+  }
+
+  SECTION("self scalar division")
+  {
+    SmallVector<int> x{5};
+    x[0] = 2;
+    x[1] = 3;
+    x[2] = 5;
+    x[3] = 7;
+    x[4] = 8;
+
+    x /= 2;
+
+    REQUIRE(x[0] == 1);
+    REQUIRE(x[1] == 1);
+    REQUIRE(x[2] == 2);
+    REQUIRE(x[3] == 3);
+    REQUIRE(x[4] == 4);
+  }
+
+  SECTION("self minus")
+  {
+    SmallVector<int> x{5};
+    x[0] = 2;
+    x[1] = 3;
+    x[2] = 5;
+    x[3] = 7;
+    x[4] = 8;
+
+    SmallVector<int> y{5};
+    y[0] = 3;
+    y[1] = 8;
+    y[2] = 6;
+    y[3] = 2;
+    y[4] = 4;
+
+    x -= y;
+
+    REQUIRE(x[0] == -1);
+    REQUIRE(x[1] == -5);
+    REQUIRE(x[2] == -1);
+    REQUIRE(x[3] == 5);
+    REQUIRE(x[4] == 4);
+  }
+
+  SECTION("self sum")
+  {
+    SmallVector<int> x{5};
+    x[0] = 2;
+    x[1] = 3;
+    x[2] = 5;
+    x[3] = 7;
+    x[4] = 8;
+
+    SmallVector<int> y{5};
+    y[0] = 3;
+    y[1] = 8;
+    y[2] = 6;
+    y[3] = 2;
+    y[4] = 4;
+
+    x += y;
+
+    REQUIRE(x[0] == 5);
+    REQUIRE(x[1] == 11);
+    REQUIRE(x[2] == 11);
+    REQUIRE(x[3] == 9);
+    REQUIRE(x[4] == 12);
+  }
+
+  SECTION("sum")
+  {
+    SmallVector<int> x{5};
+    x[0] = 2;
+    x[1] = 3;
+    x[2] = 5;
+    x[3] = 7;
+    x[4] = 8;
+
+    SmallVector<int> y{5};
+    y[0] = 3;
+    y[1] = 8;
+    y[2] = 6;
+    y[3] = 2;
+    y[4] = 4;
+
+    SmallVector z = x + y;
+
+    REQUIRE(z[0] == 5);
+    REQUIRE(z[1] == 11);
+    REQUIRE(z[2] == 11);
+    REQUIRE(z[3] == 9);
+    REQUIRE(z[4] == 12);
+  }
+
+  SECTION("difference")
+  {
+    SmallVector<int> x{5};
+    x[0] = 2;
+    x[1] = 3;
+    x[2] = 5;
+    x[3] = 7;
+    x[4] = 8;
+
+    SmallVector<int> y{5};
+    y[0] = 3;
+    y[1] = 8;
+    y[2] = 6;
+    y[3] = 2;
+    y[4] = 4;
+
+    SmallVector z = x - y;
+
+    REQUIRE(z[0] == -1);
+    REQUIRE(z[1] == -5);
+    REQUIRE(z[2] == -1);
+    REQUIRE(z[3] == 5);
+    REQUIRE(z[4] == 4);
+  }
+
+  SECTION("output")
+  {
+    SmallVector<int> x{5};
+    x[0] = 3;
+    x[1] = 7;
+    x[2] = 2;
+    x[3] = 1;
+    x[4] = -4;
+
+    std::ostringstream vector_ost;
+    vector_ost << x;
+    std::ostringstream ref_ost;
+    ref_ost << 0 << ':' << x[0];
+    for (size_t i = 1; i < x.size(); ++i) {
+      ref_ost << ' ' << i << ':' << x[i];
+    }
+    REQUIRE(vector_ost.str() == ref_ost.str());
+  }
+
+  SECTION("SmallVector from TinyVector")
+  {
+    TinyVector<5> tiny_vector{1, 3, 5, 7, 9};
+
+    SmallVector vector{tiny_vector};
+
+    REQUIRE(vector[0] == 1);
+    REQUIRE(vector[1] == 3);
+    REQUIRE(vector[2] == 5);
+    REQUIRE(vector[3] == 7);
+    REQUIRE(vector[4] == 9);
+
+    SECTION("ensures deep copy")
+    {
+      tiny_vector = zero;
+
+      REQUIRE(tiny_vector[0] == 0);
+      REQUIRE(tiny_vector[1] == 0);
+      REQUIRE(tiny_vector[2] == 0);
+      REQUIRE(tiny_vector[3] == 0);
+      REQUIRE(tiny_vector[4] == 0);
+
+      REQUIRE(vector[0] == 1);
+      REQUIRE(vector[1] == 3);
+      REQUIRE(vector[2] == 5);
+      REQUIRE(vector[3] == 7);
+      REQUIRE(vector[4] == 9);
+    }
+  }
+
+#ifndef NDEBUG
+
+  SECTION("output with signaling NaN")
+  {
+    SmallVector<double> x{5};
+    x[0] = 3;
+    x[2] = 2;
+
+    std::ostringstream vector_ost;
+    vector_ost << x;
+    std::ostringstream ref_ost;
+    ref_ost << 0 << ':' << 3 << ' ';
+    ref_ost << 1 << ":nan ";
+    ref_ost << 2 << ':' << 2 << ' ';
+    ref_ost << 3 << ":nan ";
+    ref_ost << 4 << ":nan";
+    REQUIRE(vector_ost.str() == ref_ost.str());
+  }
+
+  SECTION("invalid dot product")
+  {
+    SmallVector<int> x{5};
+    SmallVector<int> y{4};
+
+    REQUIRE_THROWS_WITH(dot(x, y), "cannot compute dot product: incompatible vector sizes");
+  }
+
+  SECTION("invalid substract")
+  {
+    SmallVector<int> x{5};
+    SmallVector<int> y{4};
+
+    REQUIRE_THROWS_WITH(x -= y, "cannot substract vector: incompatible sizes");
+  }
+
+  SECTION("invalid add")
+  {
+    SmallVector<int> x{5};
+    SmallVector<int> y{4};
+
+    REQUIRE_THROWS_WITH(x += y, "cannot add vector: incompatible sizes");
+  }
+
+  SECTION("invalid difference")
+  {
+    SmallVector<int> x{5};
+    SmallVector<int> y{4};
+
+    REQUIRE_THROWS_WITH(x - y, "cannot compute vector difference: incompatible sizes");
+  }
+
+  SECTION("invalid sum")
+  {
+    SmallVector<int> x{5};
+    SmallVector<int> y{4};
+
+    REQUIRE_THROWS_WITH(x + y, "cannot compute vector sum: incompatible sizes");
+  }
+
+#endif   // NDEBUG
+}
diff --git a/tests/test_TinyMatrix.cpp b/tests/test_TinyMatrix.cpp
index c9b72d5de9ecb6b047ae8c16ad832f8b29b38157..81c187dc2f5c7663fb7491cfa5c68fc2fc2d7eb6 100644
--- a/tests/test_TinyMatrix.cpp
+++ b/tests/test_TinyMatrix.cpp
@@ -21,6 +21,16 @@ template class TinyMatrix<4, 4, double>;
 
 TEST_CASE("TinyMatrix", "[algebra]")
 {
+  REQUIRE(TinyMatrix<1, 1, int>::Dimension == 1);
+  REQUIRE(TinyMatrix<1, 1, int>::NumberOfRows == 1);
+  REQUIRE(TinyMatrix<1, 1, int>::NumberOfColumns == 1);
+  REQUIRE(TinyMatrix<2, 3, int>::Dimension == 6);
+  REQUIRE(TinyMatrix<2, 3, int>::NumberOfRows == 2);
+  REQUIRE(TinyMatrix<2, 3, int>::NumberOfColumns == 3);
+  REQUIRE(TinyMatrix<5, 4, int>::Dimension == 20);
+  REQUIRE(TinyMatrix<5, 4, int>::NumberOfRows == 5);
+  REQUIRE(TinyMatrix<5, 4, int>::NumberOfColumns == 4);
+
   TinyMatrix<3, 4, int> A(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
   REQUIRE(((A(0, 0) == 1) and (A(0, 1) == 2) and (A(0, 2) == 3) and (A(0, 3) == 4) and   //
            (A(1, 0) == 5) and (A(1, 1) == 6) and (A(1, 2) == 7) and (A(1, 3) == 8) and   //
diff --git a/tests/test_TinyVector.cpp b/tests/test_TinyVector.cpp
index cf77a22325ee19137367ab68eff4f2f5a817ef6a..0613e721cbce46386e6b1c86c3bc3e1123fdd1aa 100644
--- a/tests/test_TinyVector.cpp
+++ b/tests/test_TinyVector.cpp
@@ -15,6 +15,11 @@ template class TinyVector<3, int>;
 
 TEST_CASE("TinyVector", "[algebra]")
 {
+  REQUIRE(TinyVector<1, int>::Dimension == 1);
+  REQUIRE(TinyVector<2, int>::Dimension == 2);
+  REQUIRE(TinyVector<3, int>::Dimension == 3);
+  REQUIRE(TinyVector<4, int>::Dimension == 4);
+
   TinyVector<3, int> v(1, 2, 3);
   REQUIRE(((v[0] == 1) and (v[1] == 2) and (v[2] == 3)));
   REQUIRE(-v == TinyVector<3, int>(-1, -2, -3));
diff --git a/tests/test_UnaryExpressionProcessor.cpp b/tests/test_UnaryExpressionProcessor.cpp
index f63e45d1fa9912f1d007f83a2759bfc5b7316b42..2c3f6acb8eb7f8280e8717cdf495abd341dec5d0 100644
--- a/tests/test_UnaryExpressionProcessor.cpp
+++ b/tests/test_UnaryExpressionProcessor.cpp
@@ -1,6 +1,8 @@
 #include <catch2/catch_test_macros.hpp>
 #include <catch2/matchers/catch_matchers_all.hpp>
 
+#include <FixturesForBuiltinT.hpp>
+
 #include <language/ast/ASTBuilder.hpp>
 #include <language/ast/ASTModulesImporter.hpp>
 #include <language/ast/ASTNodeDataTypeBuilder.hpp>
@@ -8,6 +10,14 @@
 #include <language/ast/ASTNodeExpressionBuilder.hpp>
 #include <language/ast/ASTNodeTypeCleaner.hpp>
 #include <language/ast/ASTSymbolTableBuilder.hpp>
+#include <language/node_processor/UnaryExpressionProcessor.hpp>
+#include <language/utils/ASTNodeDataTypeTraits.hpp>
+#include <language/utils/AffectationProcessorBuilder.hpp>
+#include <language/utils/BasicAffectationRegistrerFor.hpp>
+#include <language/utils/DataHandler.hpp>
+#include <language/utils/OperatorRepository.hpp>
+#include <language/utils/TypeDescriptor.hpp>
+#include <language/utils/UnaryOperatorProcessorBuilder.hpp>
 #include <utils/Demangle.hpp>
 
 #include <pegtl/string_input.hpp>
@@ -66,37 +76,98 @@ TEST_CASE("UnaryExpressionProcessor", "[language]")
     CHECK_UNARY_EXPRESSION_RESULT(R"(let r:R, r = 2; r = -r;)", "r", -2.);
   }
 
+  SECTION("unary minus [builtin]")
+  {
+    std::string_view data = R"(let r:builtin_t, r = -bt;)";
+
+    TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"};
+    auto ast = ASTBuilder::build(input);
+
+    ASTModulesImporter{*ast};
+
+    BasicAffectationRegisterFor<EmbeddedData>{ASTNodeDataType::build<ASTNodeDataType::type_id_t>("builtin_t")};
+
+    OperatorRepository& repository = OperatorRepository::instance();
+
+    repository.addUnaryOperator<language::unary_minus>(
+      std::make_shared<UnaryOperatorProcessorBuilder<language::unary_minus, std::shared_ptr<const double>,
+                                                     std::shared_ptr<const double>>>());
+
+    SymbolTable& symbol_table = *ast->m_symbol_table;
+    auto [i_symbol, success]  = symbol_table.add(builtin_data_type.nameOfTypeId(), ast->begin());
+    if (not success) {
+      throw UnexpectedError("cannot add '" + builtin_data_type.nameOfTypeId() + "' type for testing");
+    }
+
+    i_symbol->attributes().setDataType(ASTNodeDataType::build<ASTNodeDataType::type_name_id_t>());
+    i_symbol->attributes().setIsInitialized();
+    i_symbol->attributes().value() = symbol_table.typeEmbedderTable().size();
+    symbol_table.typeEmbedderTable().add(std::make_shared<TypeDescriptor>(builtin_data_type.nameOfTypeId()));
+
+    auto [i_symbol_bt, success_bt] = symbol_table.add("bt", ast->begin());
+    if (not success_bt) {
+      throw UnexpectedError("cannot add 'bt' of type builtin_t for testing");
+    }
+    i_symbol_bt->attributes().setDataType(ast_node_data_type_from<std::shared_ptr<const double>>);
+    i_symbol_bt->attributes().setIsInitialized();
+    i_symbol_bt->attributes().value() =
+      EmbeddedData(std::make_shared<DataHandler<const double>>(std::make_shared<double>(3.2)));
+
+    ASTNodeTypeCleaner<language::import_instruction>{*ast};
+
+    ASTSymbolTableBuilder{*ast};
+    ASTNodeDataTypeBuilder{*ast};
+
+    ASTNodeDeclarationToAffectationConverter{*ast};
+    ASTNodeTypeCleaner<language::var_declaration>{*ast};
+
+    ASTNodeExpressionBuilder{*ast};
+    ExecutionPolicy exec_policy;
+    ast->execute(exec_policy);
+
+    using namespace TAO_PEGTL_NAMESPACE;
+    position use_position{internal::iterator{"fixture"}, "fixture"};
+    use_position.byte    = 10000;
+    auto [symbol, found] = symbol_table.find("r", use_position);
+
+    auto attributes     = symbol->attributes();
+    auto embedded_value = std::get<EmbeddedData>(attributes.value());
+
+    double value = *dynamic_cast<const DataHandler<const double>&>(embedded_value.get()).data_ptr();
+    REQUIRE(value == double{-3.2});
+  }
+
   SECTION("unary not")
   {
     CHECK_UNARY_EXPRESSION_RESULT(R"(let b:B, b = false; b = not b;)", "b", true);
     CHECK_UNARY_EXPRESSION_RESULT(R"(let b:B, b = true; b = not b;)", "b", false);
+  }
 
-    SECTION("errors")
+  SECTION("errors")
+  {
+    SECTION("undefined not operator")
     {
-      SECTION("undefined not operator")
-      {
-        auto error_message = [](std::string type_name) {
-          return std::string{R"(undefined unary operator
+      auto error_message = [](std::string type_name) {
+        return std::string{R"(undefined unary operator
 note: unexpected operand type )"} +
-                 type_name;
-        };
-
-        CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(let n:N, n = 0; not n;)", error_message("N"));
-        CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(not 1;)", error_message("Z"));
-        CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(not 1.3;)", error_message("R"));
-        CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(not "foo";)", error_message("string"));
-      }
-
-      SECTION("undefined unary minus operator")
-      {
-        auto error_message = [](std::string type_name) {
-          return std::string{R"(undefined unary operator
+               type_name;
+      };
+
+      CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(let n:N, n = 0; not n;)", error_message("N"));
+      CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(not 1;)", error_message("Z"));
+      CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(not 1.3;)", error_message("R"));
+      CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(not "foo";)", error_message("string"));
+    }
+
+    SECTION("undefined unary minus operator")
+    {
+      auto error_message = [](std::string type_name) {
+        return std::string{R"(undefined unary operator
 note: unexpected operand type )"} +
-                 type_name;
-        };
+               type_name;
+      };
 
-        CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(-"foo";)", error_message("string"));
-      }
+      CHECK_UNARY_EXPRESSION_THROWS_WITH(R"(-"foo";)", error_message("string"));
     }
   }
 }
diff --git a/tests/test_UnaryOperatorMangler.cpp b/tests/test_UnaryOperatorMangler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c533e882a445818923e0b7bd3719051eb5db8867
--- /dev/null
+++ b/tests/test_UnaryOperatorMangler.cpp
@@ -0,0 +1,17 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <language/utils/UnaryOperatorMangler.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("UnaryOperatorMangler", "[language]")
+{
+  SECTION("unary operators")
+  {
+    const ASTNodeDataType Z = ASTNodeDataType::build<ASTNodeDataType::int_t>();
+
+    REQUIRE(unaryOperatorMangler<language::unary_minus>(Z) == "- Z");
+    REQUIRE(unaryOperatorMangler<language::unary_not>(Z) == "not Z");
+  }
+}