diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8707cbcd382044632773d355cde24d26b61dea47..ada084f53d15d441b3deac7a722522f444e28d18 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,7 +22,7 @@ message(STATUS "Found Nektar++: version ${NEKTAR++_VERSION}")
 
 set(CMAKE_INSTALL_RPATH "${NEKTAR++_LIBRARY_DIRS}")
 
-set(SRC Field.cpp Operators/Operator.cpp Operators/OperatorBwdTrans.cpp Operators/BwdTrans/BwdTransImpl.cpp)
+set(SRC Field.cpp Operators/Operator.cpp Operators/OperatorBwdTrans.cpp Operators/BwdTrans/BwdTransImpl.cpp Operators/OperatorIProductWRTBase.cpp Operators/IProductWRTBase/IProductWRTBaseImpl.cpp)
 
 if (NEKTAR_USE_CUDA)
     enable_language(CUDA)
diff --git a/Field.cpp b/Field.cpp
index d5d8d47e5a0824c1aa45c313996ce665cea339ff..0a161abd59e2a9842793ae3a3479691423b0080d 100644
--- a/Field.cpp
+++ b/Field.cpp
@@ -12,31 +12,35 @@ std::vector<BlockAttributes> GetBlockAttributes(
     // initialize the basisKeys
     std::vector<BasisKey> prevbasisKeys(3, NullBasisKey);
     std::vector<BasisKey> thisbasisKeys(3, NullBasisKey);
+    int prevIsDeformed = -1, thisIsDeformed = -1;
 
     // loop over elements
     for (int i = 0; i < explist->GetNumElmts(); i++)
     {
-        auto exp = explist->GetExp(i);
+        auto expPtr = explist->GetExp(i);
 
         // fetch basiskeys of current element
-        for (int d = 0; d < exp->GetNumBases(); d++)
+        for (int d = 0; d < expPtr->GetNumBases(); d++)
         {
-            thisbasisKeys[d] = exp->GetBasis(d)->GetBasisKey();
+            thisbasisKeys[d] = expPtr->GetBasis(d)->GetBasisKey();
         }
 
+        thisIsDeformed = expPtr->GetMetricInfo()->GetGtype();
+
         // if the basis is the same as the previous one,
         // increment the number of elements
-        if (thisbasisKeys == prevbasisKeys)
+        if (thisbasisKeys == prevbasisKeys && thisIsDeformed == prevIsDeformed)
         {
             blockAttr.back().num_elements++;
             blockAttr.back().block_size += blockAttr.back().num_pts;
         }
         else // if not, create a new block with the number of elements = 1
         {
-            size_t num_pts = state == FieldState::Phys ? exp->GetTotPoints()
-                                                   : exp->GetNcoeffs();
+            size_t num_pts = state == FieldState::Phys ? expPtr->GetTotPoints()
+                                                       : expPtr->GetNcoeffs();
             blockAttr.push_back({1, num_pts});
-            prevbasisKeys = thisbasisKeys;
+            prevbasisKeys  = thisbasisKeys;
+            prevIsDeformed = thisIsDeformed;
         }
     }
 
diff --git a/Field.hpp b/Field.hpp
index 843175d32dd43a93e7a2418107eb6f09437e6c6c..5e97e51bf20f02fb6c858133c0879cf4ed29f8ba 100644
--- a/Field.hpp
+++ b/Field.hpp
@@ -17,11 +17,10 @@ namespace Nektar
 {
 namespace MultiRegions
 {
-    class ExpList;
-    typedef std::shared_ptr<ExpList> ExpListSharedPtr;
-}
-}
-
+class ExpList;
+typedef std::shared_ptr<ExpList> ExpListSharedPtr;
+} // namespace MultiRegions
+} // namespace Nektar
 
 /**
  * @brief Captures the structure of a block of elements of identical shape
@@ -116,9 +115,8 @@ public:
 
         size_t storage_size = std::accumulate(
             field.block_attributes.begin(), field.block_attributes.end(), 0,
-            [](size_t acc, const BlockAttributes &block) {
-                return acc + block.block_size;
-            });
+            [](size_t acc, const BlockAttributes &block)
+            { return acc + block.block_size; });
 
         // Create new TMemoryRegion and polymorphically store as MemoryRegionCPU
         field.m_storage = std::make_unique<TMemoryRegion<TType>>(
diff --git a/MemoryRegionCPU.hpp b/MemoryRegionCPU.hpp
index 877f27cf47d1a9afa7a70332954e374a089f66af..1c1e169c8c337a42a896054b7f6b2ee424a44b79 100644
--- a/MemoryRegionCPU.hpp
+++ b/MemoryRegionCPU.hpp
@@ -13,8 +13,8 @@
 template <typename TData> class MemoryRegionCPU
 {
 public:
-    MemoryRegionCPU()                                      = delete;
-    MemoryRegionCPU(const MemoryRegionCPU &rhs)            = delete;
+    MemoryRegionCPU()                           = delete;
+    MemoryRegionCPU(const MemoryRegionCPU &rhs) = delete;
     MemoryRegionCPU &operator=(const MemoryRegionCPU &rhs) = delete;
 
     MemoryRegionCPU(MemoryRegionCPU &&rhs)
diff --git a/MemoryRegionCUDA.hpp b/MemoryRegionCUDA.hpp
index 2187341bee8d1e5033656e0746b0c576cd9fd40f..ff61995b92af25660b9e3533555dccf0c0fc3d76 100644
--- a/MemoryRegionCUDA.hpp
+++ b/MemoryRegionCUDA.hpp
@@ -42,10 +42,10 @@ public:
     void operator=(MemoryRegionCUDA &&rhs)
     {
         MemoryRegionCPU<TData>::operator=(std::move(rhs));
-        m_device     = rhs.m_device;
-        m_size       = rhs.m_size;
-        rhs.m_device = nullptr;
-        rhs.m_size   = 0;
+        m_device                        = rhs.m_device;
+        m_size                          = rhs.m_size;
+        rhs.m_device                    = nullptr;
+        rhs.m_size                      = 0;
     }
 
     virtual TData *GetCPUPtr() override
diff --git a/Operators/BwdTrans/BwdTransImpl.cpp b/Operators/BwdTrans/BwdTransImpl.cpp
index d2290d59c497bbf205b527775b964f8bb516c4eb..9a92e3cde029bae54d33af694277a276f9e3a758 100644
--- a/Operators/BwdTrans/BwdTransImpl.cpp
+++ b/Operators/BwdTrans/BwdTransImpl.cpp
@@ -1,5 +1,5 @@
-#include "BwdTransStdMat.hpp"
 #include "BwdTransMatFree.hpp"
+#include "BwdTransStdMat.hpp"
 #include "BwdTransSumFac.hpp"
 
 namespace Nektar::Operators::detail
diff --git a/Operators/IProductWRTBase/IProductWRTBaseImpl.cpp b/Operators/IProductWRTBase/IProductWRTBaseImpl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..44364c06ddf91312ad7ae04d9fb192cd7c561b84
--- /dev/null
+++ b/Operators/IProductWRTBase/IProductWRTBaseImpl.cpp
@@ -0,0 +1,13 @@
+#include "IProductWRTBaseStdMat.hpp"
+
+namespace Nektar::Operators::detail
+{
+
+// Add different IProductWRTBase implementations to the factory.
+template <>
+std::string OperatorIProductWRTBaseImpl<double, ImplStdMat>::className =
+    GetOperatorFactory<double>().RegisterCreatorFunction(
+        "IProductWRTBaseStdMat",
+        OperatorIProductWRTBaseImpl<double, ImplStdMat>::instantiate, "...");
+
+} // namespace Nektar::Operators::detail
diff --git a/Operators/IProductWRTBase/IProductWRTBaseStdMat.hpp b/Operators/IProductWRTBase/IProductWRTBaseStdMat.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d8264455995dfb0067caf3267cfe3e86da291aa
--- /dev/null
+++ b/Operators/IProductWRTBase/IProductWRTBaseStdMat.hpp
@@ -0,0 +1,129 @@
+#include "Operators/OperatorIProductWRTBase.hpp"
+#include <StdRegions/StdExpansion.h>
+
+namespace Nektar::Operators::detail
+{
+
+// standard matrix implementation
+template <typename TData>
+class OperatorIProductWRTBaseImpl<TData, ImplStdMat>
+    : public OperatorIProductWRTBase<TData>
+{
+public:
+    OperatorIProductWRTBaseImpl(
+        const MultiRegions::ExpListSharedPtr &expansionList)
+        : OperatorIProductWRTBase<TData>(std::move(expansionList))
+    {
+        size_t jacSize   = 0;
+        size_t nTotElmts = this->m_expansionList->GetNumElmts();
+        // Calculate the jacobian array size
+        for (size_t e = 0; e < nTotElmts; ++e)
+        {
+            // Determine shape and type of the element
+            auto const expPtr = this->m_expansionList->GetExp(e);
+            if (expPtr->GetMetricInfo()->GetGtype() ==
+                SpatialDomains::eDeformed)
+            {
+                jacSize += expPtr->GetTotPoints();
+            }
+            else
+            {
+                jacSize++;
+            }
+        }
+
+        // Allocate memory for the jacobian
+        m_jac = {jacSize, 0.0};
+
+        // Initialise jacobian.
+        size_t index = 0;
+        for (size_t e = 0; e < nTotElmts; ++e)
+        {
+            auto expPtr = this->m_expansionList->GetExp(e);
+            auto &auxJac =
+                expPtr->GetMetricInfo()->GetJac(expPtr->GetPointsKeys());
+            if (expPtr->GetMetricInfo()->GetGtype() ==
+                SpatialDomains::eDeformed)
+            {
+                size_t nqe = expPtr->GetTotPoints();
+                for (size_t i = 0; i < nqe; ++i)
+                {
+                    m_jac[index++] = auxJac[i];
+                }
+            }
+            else
+            {
+                m_jac[index++] = auxJac[0];
+            }
+        }
+    }
+
+    void apply(Field<TData, FieldState::Phys> &in,
+               Field<TData, FieldState::Coeff> &out) override
+    {
+        auto const *inptr = in.GetStorage().GetCPUPtr();
+        auto *outptr      = out.GetStorage().GetCPUPtr();
+
+        size_t expIdx = 0;
+        size_t jacIdx = 0;
+        for (size_t block_idx = 0; block_idx < in.GetBlocks().size();
+             ++block_idx)
+        {
+            auto const expPtr = this->m_expansionList->GetExp(expIdx);
+            auto nElmts       = in.GetBlocks()[block_idx].num_elements;
+            auto nqTot        = expPtr->GetTotPoints();
+            auto nmTot        = expPtr->GetNcoeffs();
+
+            Nektar::StdRegions::StdMatrixKey key(
+                StdRegions::eIProductWRTBase, expPtr->DetShapeType(), *expPtr);
+
+            // This is the B^{T} matrix
+            auto const matPtr = expPtr->GetStdMatrix(key);
+
+            Array<OneD, NekDouble> wsp(nqTot * nElmts, 0.0);
+            if (expPtr->GetMetricInfo()->GetGtype() ==
+                SpatialDomains::eDeformed)
+            {
+                for (size_t i = 0; i < nElmts * nqTot; ++i)
+                {
+                    wsp[i] = m_jac[jacIdx++] * inptr[i];
+                }
+            }
+            else
+            {
+                for (size_t e = 0; e < nElmts; ++e)
+                {
+                    for (size_t i = 0; i < nqTot; ++i)
+                    {
+                        wsp[e * nqTot + i] =
+                            m_jac[jacIdx] * inptr[e * nqTot + i];
+                    }
+                    jacIdx++;
+                }
+            }
+
+            Blas::Dgemm('N', 'N', matPtr->GetRows(), nElmts,
+                        matPtr->GetColumns(), 1.0, matPtr->GetRawPtr(),
+                        matPtr->GetRows(), wsp.get(), nqTot, 0.0, outptr,
+                        nmTot);
+
+            inptr += nqTot * nElmts;
+            outptr += nmTot * nElmts;
+            expIdx += nElmts;
+        }
+    }
+
+    static std::unique_ptr<Operator<TData>> instantiate(
+        const MultiRegions::ExpListSharedPtr &expansionList)
+    {
+        return std::make_unique<OperatorIProductWRTBaseImpl<TData, ImplStdMat>>(
+            std::move(expansionList));
+    }
+
+    static std::string className;
+
+private:
+    Array<OneD, NekDouble> m_jac;
+};
+
+} // namespace Nektar::Operators::detail
diff --git a/Operators/OperatorIProductWRTBase.cpp b/Operators/OperatorIProductWRTBase.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4dcdbf1411b1911788743bc3355eed8f7d74c5aa
--- /dev/null
+++ b/Operators/OperatorIProductWRTBase.cpp
@@ -0,0 +1,9 @@
+#include "OperatorIProductWRTBase.hpp"
+
+namespace Nektar::Operators
+{
+
+template <> const std::string IProductWRTBase<>::key = "IProductWRTBase";
+template <> const std::string IProductWRTBase<>::default_impl = "StdMat";
+
+} // namespace Nektar::Operators
\ No newline at end of file
diff --git a/Operators/OperatorIProductWRTBase.hpp b/Operators/OperatorIProductWRTBase.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fe6adfdd22a9d4c76dec75b2e9b6a085b7ef7abe
--- /dev/null
+++ b/Operators/OperatorIProductWRTBase.hpp
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "Field.hpp"
+#include "Operator.hpp"
+
+namespace Nektar::Operators
+{
+
+// IProductWRTBase base class
+// Defines the apply operator to enforce apply parameter types
+template <typename TData> class OperatorIProductWRTBase : public Operator<TData>
+{
+public:
+    virtual ~OperatorIProductWRTBase() = default;
+
+    OperatorIProductWRTBase(const MultiRegions::ExpListSharedPtr &expansionList)
+        : Operator<TData>(std::move(expansionList))
+    {
+    }
+
+    virtual void apply(Field<TData, FieldState::Phys> &in,
+                       Field<TData, FieldState::Coeff> &out) = 0;
+    virtual void operator()(Field<TData, FieldState::Phys> &in,
+                            Field<TData, FieldState::Coeff> &out)
+    {
+        apply(in, out);
+    }
+};
+
+// Descriptor / traits class for IProductWRTBase
+template <typename TData = default_fp_type> struct IProductWRTBase
+{
+    using class_name = OperatorIProductWRTBase<TData>;
+    using FieldIn    = Field<TData, FieldState::Phys>;
+    using FieldOut   = Field<TData, FieldState::Coeff>;
+    static const std::string key;
+    static const std::string default_impl;
+
+    static std::shared_ptr<class_name> create(
+        const MultiRegions::ExpListSharedPtr &expansionList,
+        std::string pKey = "")
+    {
+        return Operator<TData>::template create<IProductWRTBase>(
+            std::move(expansionList), pKey);
+    }
+};
+
+namespace detail
+{
+// Template for IProductWRTBase implementations
+template <typename TData, typename Op> class OperatorIProductWRTBaseImpl;
+} // namespace detail
+
+} // namespace Nektar::Operators
diff --git a/line.xml b/line.xml
new file mode 100644
index 0000000000000000000000000000000000000000..45b9d23682019010d27a7fe34b6db30275419824
--- /dev/null
+++ b/line.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<NEKTAR>
+    <GEOMETRY DIM="1" SPACE="1">
+        <VERTEX COMPRESSED="B64Z-LittleEndian" BITSIZE="64">eJxjYMAPGFF4H+zR5QEVbwEx</VERTEX>
+        <ELEMENT>
+            <S COMPRESSED="B64Z-LittleEndian" BITSIZE="64">eJxjYEAFjFAaAAAgAAIA</S>
+        </ELEMENT>
+        <COMPOSITE>
+            <C ID="0"> S[0] </C>
+        </COMPOSITE>
+        <DOMAIN>
+            <D ID="0"> C[0] </D>
+        </DOMAIN>
+    </GEOMETRY>
+    <Metadata>
+        <Provenance>
+            <GitBranch>refs/heads/master</GitBranch>
+            <GitSHA1>b9041a54043f57d230bcc1eb5be5c9a048b69a92</GitSHA1>
+            <Hostname>dyn3237-113.wlan.ic.ac.uk</Hostname>
+            <NektarVersion>5.3.0</NektarVersion>
+            <Timestamp>07-Jun-2023 11:25:53</Timestamp>
+        </Provenance>
+        <NekMeshCommandLine>-v line.msh line.xml </NekMeshCommandLine>
+    </Metadata>
+    <EXPANSIONS>
+        <E COMPOSITE="C[0]" NUMMODES="6" TYPE="MODIFIED" FIELDS="u" />
+    </EXPANSIONS>
+</NEKTAR>
diff --git a/main.cpp b/main.cpp
index 00ce6f150fc262bfb91dc0bdb750df8d5a055baf..94521fba49a392368c2ca4c15eb1a9225626fb11 100644
--- a/main.cpp
+++ b/main.cpp
@@ -14,6 +14,7 @@
 
 #include "Field.hpp"
 #include "Operators/OperatorBwdTrans.hpp"
+#include "Operators/OperatorIProductWRTBase.hpp"
 
 #include <LibUtilities/BasicUtils/SessionReader.h>
 //#include <LibUtilities/SimdLib/tinysimd.hpp>
@@ -91,11 +92,11 @@ int main(int argc, char *argv[])
 
     std::cout << "Initial shape:\n" << in << std::endl << std::endl;
 
+#ifdef NEKTAR_ENABLE_SIMD_AVX2
     // Test out field reshaping
     in.ReshapeStorage<4>();
     std::cout << "Reshaped to 4:\n" << in << std::endl << std::endl;
 
-#ifdef NEKTAR_ENABLE_SIMD_AVX2
     // Test out SIMD instructions
     using vec_t = tinysimd::simd<double>;
 
@@ -206,6 +207,40 @@ int main(int argc, char *argv[])
     BwdTrans<>::create(explist, "CUDA")->apply(in, out);
 #endif
 
+    std::cout << "Inner Product of a function WRT the Basis" << std::endl;
+
+    // Create two Field objects with a MemoryRegionCPU backend by default
+    // for the inner product with respect to base
+    auto inPhys   = Field<double, FieldState::Phys>::create(blocks_phys);
+    auto outCoeff = Field<double, FieldState::Coeff>::create(blocks_coeff);
+
+    double *y = inPhys.GetStorage().GetCPUPtr();
+    for (auto const &block : blocks_phys)
+    {
+        for (size_t el = 0; el < block.num_elements; ++el)
+        {
+            for (size_t phys = 0; phys < block.num_pts; ++phys)
+            {
+                // Each element is the index of the quadrature points
+                // this is useful for testing reshapes
+                *(y++) = phys;
+            }
+        }
+    }
+
+    memset(outCoeff.GetStorage().GetCPUPtr(), 0,
+           outCoeff.GetStorage().size() * sizeof(double));
+
+    std::cout << "Initial shape:\n" << inPhys << std::endl << std::endl;
+
+    // IProductWRTBase
+    auto ipwrtb = IProductWRTBase<>::create(explist, "StdMat");
+    // ... and then apply it
+    ipwrtb->apply(inPhys, outCoeff); // out and in is inverted for the IP
+
+    // Let's display the result
+    std::cout << "Out:\n" << outCoeff << std::endl;
+
     std::cout << "END" << std::endl;
     return 0;
 }
diff --git a/squareDeformed.xml b/squareDeformed.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a5a0f6cad71ad4fc0bbc48261dca9a2d64ba5556
--- /dev/null
+++ b/squareDeformed.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<NEKTAR>
+    <GEOMETRY DIM="2" SPACE="2">
+        <VERTEX COMPRESSED="B64Z-LittleEndian" BITSIZE="64">eJxjYICAx7ZT/D4/OW/vfW1rRBvvBXuoMAMjAyoo9QCRCHkmBvyAGUr/XPwfCM7bo8uzoPAeYMizosmjm8OGVT/CHHas9iPkObC6GiHPiSL+AcN9XGjy6O7jhtJ6YPKFfcsjVHkeFP0vMMznxSqPcB8fVvch5PlR5GHiCHsEoPQntRlz3sx8aS+W9+p69IyXcHlBKK0QYgwEr+195s0Ego9weSGs5iPCSRjmsikQfZ1CEHNg8iJQ2hgMPtvDaJi8KMzn4IRywd4Dzf1iDNgAQl4cSqOmW4T7JLDqR8gDAHwmZuEA</VERTEX>
+        <EDGE COMPRESSED="B64Z-LittleEndian" BITSIZE="64">eJx1kkcSwlAMQ0MLoQZC7z3c/4Zs/Ba8GXuj0ZflyJ4UxX91Euwm2EuQ6usdPkh4KU4Nk75KnL6RODXWO/6JOHtP5WPuTD70uXzotfzoC3HusQxkX+Y0yfdXgaX0tXKRc6M+9K3ykm8nTr69OPkO4sw7Ki/zTuLkOisfvovmsvc1kL3JdZMf/R7I/8BdHuLc5SnO//IK5E7c5S2O76P5+NrAWvo3sFH/D7Z4BrgA</EDGE>
+        <ELEMENT>
+            <Q COMPRESSED="B64Z-LittleEndian" BITSIZE="64">eJx10ccSwjAMRdEAofeS0CH0//9DNvcueDNkc8a2LEtKUfx+LWxjJ/Zdl9jFXtxz3ccBDiPPCMdRh3G+M8EpznAedSzifPmnTvdXuI56dYNVxFuHfdW4xR3u0X4OeIy8J3Qe5jtHfd5zXpd41/xXdG7mbfCGd7SfBz4j7oXO1b6t7x35/S+fuG+cc/kCmoQFPwAA</Q>
+        </ELEMENT>
+        <COMPOSITE>
+            <C ID="13"> E[36,34,11,1] </C>
+            <C ID="14"> E[2,4,16,15] </C>
+            <C ID="15"> E[12,20,28,31] </C>
+            <C ID="16"> E[30,24,39,35] </C>
+            <C ID="17"> Q[0-15] </C>
+        </COMPOSITE>
+        <DOMAIN>
+            <D ID="0"> C[17] </D>
+        </DOMAIN>
+    </GEOMETRY>
+    <EXPANSIONS>
+        <E COMPOSITE="C[17]" NUMMODES="4" TYPE="MODIFIED" FIELDS="u" />
+    </EXPANSIONS>
+</NEKTAR>
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index b86f61ac7c1251219954dc086a262b38f7bd1f20..1a4f15028a80f74ef5c49515f64334c88b5d3599 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,23 +1,37 @@
-set(test_src test_bwdtrans.cpp)
-
 find_package(
   Boost
   REQUIRED
   COMPONENTS unit_test_framework
 )
 
-add_executable(test_bwdtrans ${test_src})
+add_executable(test_bwdtrans test_bwdtrans.cpp)
 target_link_libraries(test_bwdtrans PRIVATE Operators)
 target_link_libraries(test_bwdtrans PRIVATE ${NEKTAR++_LIBRARIES})
 target_link_libraries(test_bwdtrans PRIVATE Boost::unit_test_framework)
-
 target_include_directories(test_bwdtrans PRIVATE "${CMAKE_SOURCE_DIR}")
 target_include_directories(test_bwdtrans PRIVATE ${NEKTAR++_INCLUDE_DIRS})
+target_compile_definitions(test_bwdtrans PRIVATE
+    -DTEST_PATH="${CMAKE_SOURCE_DIR}")
+
+add_executable(test_ipwrtbase test_ipwrtbase.cpp)
+target_link_libraries(test_ipwrtbase PRIVATE Operators)
+target_link_libraries(test_ipwrtbase PRIVATE ${NEKTAR++_LIBRARIES})
+target_link_libraries(test_ipwrtbase PRIVATE Boost::unit_test_framework)
+target_include_directories(test_ipwrtbase PRIVATE "${CMAKE_SOURCE_DIR}")
+target_include_directories(test_ipwrtbase PRIVATE ${NEKTAR++_INCLUDE_DIRS})
+target_compile_definitions(test_ipwrtbase PRIVATE
+    -DTEST_PATH="${CMAKE_SOURCE_DIR}")
 
 add_test(
   NAME BwdTrans
   COMMAND test_bwdtrans
   # Test executable is looking for input session file in the working
   # directory
-  WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/input"
+  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+)
+
+add_test(
+  NAME IProductWRTBase
+  COMMAND test_ipwrtbase
+  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
 )
diff --git a/tests/init_fields.hpp b/tests/init_fields.hpp
index 0d44eb0c78b9d0a5abae54d507277c03c77bcee5..9aeb7f82f39bc72bb5219feff10ef56e3139e456 100644
--- a/tests/init_fields.hpp
+++ b/tests/init_fields.hpp
@@ -1,10 +1,12 @@
+#pragma once
 #include <boost/test/unit_test_log.hpp>
-#include <vector>
 #include <string>
+#include <vector>
 
 #include "Field.hpp"
 #include <MultiRegions/ExpList.h>
 #include <Operators/OperatorBwdTrans.hpp>
+#include <Operators/OperatorIProductWRTBase.hpp>
 
 using namespace Nektar::Operators;
 using namespace Nektar::LibUtilities;
@@ -18,42 +20,57 @@ using namespace Nektar;
  * before (after) each call to BOOST_FIXTURE_TEST_CASE(<test name>,
  * InitFields) macro.
  *
- * See "Single test case fixture" on the Boost.Test documentation for more details:
+ * See "Single test case fixture" on the Boost.Test documentation for more
+ * details:
  * https://www.boost.org/doc/libs/1_82_0/libs/test/doc/html/boost_test/tests_organization/fixtures/case.html
  */
-struct InitFields {
-  Field<double, FieldState::Coeff> *fixt_in;
-  Field<double, FieldState::Phys> *fixt_out;
-  MultiRegions::ExpListSharedPtr fixt_explist {nullptr};
-  ~InitFields() {BOOST_TEST_MESSAGE("teardown fixture");}
-
-  InitFields() {
-    BOOST_TEST_MESSAGE("Creating input and output fields");
-    // Initialise a session, graph and create an expansion list
-    LibUtilities::SessionReaderSharedPtr session;
-    SpatialDomains::MeshGraphSharedPtr   graph;
-
-    // Construct a fake command-line argument array to be fed to
-    // Session::Reader::CreateInstance. The first element stands for
-    // the name of the executable which, in our case, doesn't matter.
-    int argc = 2;
-    char *argv[] = {
-      (char*)"exe_name", (char*)"square.xml"
-    };
-
-    session = LibUtilities::SessionReader::CreateInstance(argc, argv);
-    graph   = SpatialDomains::MeshGraph::Read(session);
-    fixt_explist = MemoryManager<MultiRegions::ExpList>::AllocateSharedPtr
-                    (session, graph);
-
-    // Generate a blocks definition from the expansion list for each state
-    auto blocks_phys  = GetBlockAttributes(FieldState::Phys,  fixt_explist);
-    auto blocks_coeff = GetBlockAttributes(FieldState::Coeff, fixt_explist);
-
-    // Create two Field objects with a MemoryRegionCPU backend by default
-    auto f_in  = Field<double, FieldState::Coeff>::create(blocks_coeff);
-    auto f_out = Field<double, FieldState::Phys >::create(blocks_phys);
-    fixt_in  = new Field<double, FieldState::Coeff>(std::move(f_in));
-    fixt_out = new Field<double, FieldState::Phys>(std::move(f_out));
-  }
+
+template <FieldState stateIn  = FieldState::Coeff,
+          FieldState stateOut = FieldState::Phys>
+class InitFields
+{
+public:
+    Field<double, stateIn> *fixt_in;
+    Field<double, stateOut> *fixt_out;
+    MultiRegions::ExpListSharedPtr fixt_explist{nullptr};
+    ~InitFields()
+    {
+        BOOST_TEST_MESSAGE("teardown fixture");
+    }
+
+    InitFields()
+    {
+    }
+
+    void Configure()
+    {
+        BOOST_TEST_MESSAGE("Creating input and output fields");
+        // Initialise a session, graph and create an expansion list
+        LibUtilities::SessionReaderSharedPtr session;
+        SpatialDomains::MeshGraphSharedPtr graph;
+
+        // Construct a fake command-line argument array to be fed to
+        // Session::Reader::CreateInstance. The first element stands for
+        // the name of the executable which, in our case, doesn't matter.
+        int argc     = 2;
+        char *argv[] = {(char *)"exe_name", GetMeshName().data()};
+
+        session      = LibUtilities::SessionReader::CreateInstance(argc, argv);
+        graph        = SpatialDomains::MeshGraph::Read(session);
+        fixt_explist = MemoryManager<MultiRegions::ExpList>::AllocateSharedPtr(
+            session, graph);
+
+        // Generate a blocks definition from the expansion list for each state
+        auto blocks_in  = GetBlockAttributes(stateIn, fixt_explist);
+        auto blocks_out = GetBlockAttributes(stateOut, fixt_explist);
+
+        // Create two Field objects with a MemoryRegionCPU backend by default
+        auto f_in  = Field<double, stateIn>::create(blocks_in);
+        auto f_out = Field<double, stateOut>::create(blocks_out);
+        fixt_in    = new Field<double, stateIn>(std::move(f_in));
+        fixt_out   = new Field<double, stateOut>(std::move(f_out));
+    }
+
+protected:
+    virtual std::string GetMeshName() = 0;
 };
diff --git a/tests/test_bwdtrans.cpp b/tests/test_bwdtrans.cpp
index 33f338d29f04b9c0225064903568dcfe20c3e665..5116addecf30f5e78ce15aa7b77a1f4725423c6e 100644
--- a/tests/test_bwdtrans.cpp
+++ b/tests/test_bwdtrans.cpp
@@ -8,8 +8,8 @@
 #include "Operators/OperatorBwdTrans.hpp"
 
 #include <LibUtilities/BasicUtils/SessionReader.h>
-#include <SpatialDomains/MeshGraph.h>
 #include <MultiRegions/ExpList.h>
+#include <SpatialDomains/MeshGraph.h>
 
 #include "init_fields.hpp"
 
@@ -18,8 +18,29 @@ using namespace Nektar::Operators;
 using namespace Nektar::LibUtilities;
 using namespace Nektar;
 
-BOOST_FIXTURE_TEST_CASE( bwdtrans, InitFields )
+class Line : public InitFields<FieldState::Coeff, FieldState::Phys>
+{
+protected:
+    virtual std::string GetMeshName() override
+    {
+        return "line.xml";
+    }
+};
+
+class Square : public InitFields<FieldState::Coeff, FieldState::Phys>
 {
+protected:
+    virtual std::string GetMeshName() override
+    {
+        return "square.xml";
+    }
+};
+
+using init = InitFields<FieldState::Coeff, FieldState::Phys>;
+BOOST_FIXTURE_TEST_CASE(bwdtrans, Line)
+{
+    Configure();
+
     double *x = fixt_in->GetStorage().GetCPUPtr();
 
     // For each element, initialise first coefficient to zero and rest
@@ -30,10 +51,12 @@ BOOST_FIXTURE_TEST_CASE( bwdtrans, InitFields )
         {
             for (size_t coeff = 0; coeff < block.num_pts; ++coeff)
             {
-                if (coeff == 0) {
+                if (coeff == 0)
+                {
                     *(x++) = 1.0;
                 }
-                else {
+                else
+                {
                     *(x++) = 0.0;
                 }
             }
@@ -43,5 +66,5 @@ BOOST_FIXTURE_TEST_CASE( bwdtrans, InitFields )
     BwdTrans<>::create(fixt_explist, "StdMat")->apply(*fixt_in, *fixt_out);
 
     double *y = fixt_out->GetStorage().GetCPUPtr();
-    BOOST_TEST( y[0] == 1.0 );
+    BOOST_TEST(y[0] == 1.0);
 }
diff --git a/tests/test_ipwrtbase.cpp b/tests/test_ipwrtbase.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..459f0e24a1a651ba4c016bf3d496b17725048967
--- /dev/null
+++ b/tests/test_ipwrtbase.cpp
@@ -0,0 +1,67 @@
+#define BOOST_TEST_MODULE example
+#include <boost/test/unit_test.hpp>
+
+#include <iostream>
+#include <memory>
+
+#include "Field.hpp"
+#include "Operators/OperatorIProductWRTBase.hpp"
+
+#include <LibUtilities/BasicUtils/SessionReader.h>
+#include <MultiRegions/ExpList.h>
+#include <SpatialDomains/MeshGraph.h>
+
+#include "init_fields.hpp"
+
+using namespace std;
+using namespace Nektar::Operators;
+using namespace Nektar::LibUtilities;
+using namespace Nektar;
+
+class Line : public InitFields<FieldState::Phys, FieldState::Coeff>
+{
+protected:
+    virtual std::string GetMeshName() override
+    {
+        return "line.xml";
+    }
+};
+
+class Square : public InitFields<FieldState::Phys, FieldState::Coeff>
+{
+protected:
+    virtual std::string GetMeshName() override
+    {
+        return "square.xml";
+    }
+};
+
+BOOST_FIXTURE_TEST_CASE(ipwrtbase, Line)
+{
+    Configure();
+
+    static double *x = fixt_in->GetStorage().GetCPUPtr();
+
+    for (auto const &block : fixt_in->GetBlocks())
+    {
+        for (size_t el = 0; el < block.num_elements; ++el)
+        {
+            for (size_t phys = 0; phys < block.num_pts; ++phys)
+            {
+                *(x++) = phys;
+            }
+        }
+    }
+
+    IProductWRTBase<>::create(fixt_explist, "StdMat")
+        ->apply(*fixt_in, *fixt_out);
+
+    static double *y = fixt_out->GetStorage().GetCPUPtr();
+    double TOL       = 1e-12;
+    BOOST_CHECK_CLOSE(y[0], 1.09753217840612, TOL);
+    BOOST_CHECK_CLOSE(y[1], 1.90246782159389, TOL);
+    BOOST_CHECK_CLOSE(y[2], 0.5, TOL);
+    BOOST_CHECK_CLOSE(y[3], 0.150377322580158, TOL);
+    BOOST_TEST(std::abs(y[4] - 9.36750677027476e-17) < TOL);
+    BOOST_CHECK_CLOSE(y[5], 0.00746847423694819, TOL);
+}