diff --git a/generate_all_hyteg_forms.py b/generate_all_hyteg_forms.py
index d96ca5defea2c8af6a16ac3616d77dc6eb526d6f..b37446c88d8cea049feaafdbb64ba29ce217c5ed 100644
--- a/generate_all_hyteg_forms.py
+++ b/generate_all_hyteg_forms.py
@@ -34,6 +34,7 @@ from hog.element_geometry import (
 from hog.function_space import (
     LagrangianFunctionSpace,
     N1E1Space,
+    P2PlusBubbleSpace,
     TrialSpace,
     TestSpace,
 )
@@ -133,6 +134,8 @@ class FormInfo:
         """A compact representation of the function spaces."""
         if self.trial_family == "N1E1":
             return "n1e1"
+        elif self.trial_family == "P2 enhanced with Bubble":
+            return "p2_plus_bubble"
         elif self.trial_degree == self.test_degree:
             return f"p{self.trial_degree}"
         else:
@@ -163,6 +166,8 @@ class FormInfo:
         sub_dir = "p1"
         if self.trial_family == "N1E1":
             sub_dir = "n1e1"
+        elif self.trial_family == "P2 enhanced with Bubble":
+            return "p2_plus_bubble"
         elif self.trial_degree == self.test_degree:
             sub_dir = f"p{self.trial_degree}"
         else:
@@ -242,6 +247,15 @@ form_infos = [
         quad_schemes={2: 3, 3: 3},
         blending=ExternalMap(),
     ),
+    FormInfo(
+        "diffusion",
+        trial_degree=2,
+        test_degree=2,
+        trial_family="P2 enhanced with Bubble",
+        test_family="P2 enhanced with Bubble",
+        quad_schemes={2: "exact"},
+        integrate_rows=[],
+    ),
     FormInfo(
         "mass",
         trial_degree=1,
@@ -296,6 +310,15 @@ form_infos = [
         blending=ExternalMap(),
         integrate_rows=[],
     ),
+    FormInfo(
+        "mass",
+        trial_degree=2,
+        test_degree=2,
+        trial_family="P2 enhanced with Bubble",
+        test_family="P2 enhanced with Bubble",
+        quad_schemes={2: "exact"},
+        integrate_rows=[],
+    ),
     FormInfo(
         "curl_curl",
         trial_degree=1,
@@ -1077,6 +1100,8 @@ def main():
         trial: TrialSpace
         if form_info.trial_family == "N1E1":
             trial = TrialSpace(N1E1Space(symbolizer))
+        elif form_info.trial_family == "P2 enhanced with Bubble":
+            trial = TrialSpace(P2PlusBubbleSpace(symbolizer))
         else:
             trial = TrialSpace(
                 LagrangianFunctionSpace(form_info.trial_degree, symbolizer)
@@ -1085,6 +1110,8 @@ def main():
         test: TestSpace
         if form_info.test_family == "N1E1":
             test = TestSpace(N1E1Space(symbolizer))
+        elif form_info.test_family == "P2 enhanced with Bubble":
+            test = TestSpace(P2PlusBubbleSpace(symbolizer))
         else:
             test = TestSpace(LagrangianFunctionSpace(form_info.test_degree, symbolizer))
 
diff --git a/generate_all_operators.py b/generate_all_operators.py
index 6ce4a12dcab8ddddd60fb2e05107591d8415a84a..a2cae70e1450478d1f91aa65be0e0f3c5d513ca2 100644
--- a/generate_all_operators.py
+++ b/generate_all_operators.py
@@ -29,7 +29,14 @@ import sympy as sp
 from sympy.core.cache import clear_cache
 from tabulate import tabulate
 
-from hog.blending import GeometryMap, IdentityMap, AnnulusMap, IcosahedralShellMap, ParametricMap
+from hog.blending import (
+    GeometryMap,
+    IdentityMap,
+    AnnulusMap,
+    IcosahedralShellMap,
+    ParametricMap,
+    AffineMap2D,
+)
 from hog.cse import CseImplementation
 from hog.element_geometry import (
     ElementGeometry,
@@ -46,6 +53,7 @@ from hog.forms import (
     shear_heating,
     epsilon,
     full_stokes,
+    mass,
     nonlinear_diffusion,
     nonlinear_diffusion_newton_galerkin,
     supg_diffusion,
@@ -64,6 +72,7 @@ from hog.function_space import (
     LagrangianFunctionSpace,
     N1E1Space,
     TensorialVectorFunctionSpace,
+    P2PlusBubbleSpace,
     TrialSpace,
     TestSpace,
 )
@@ -105,6 +114,7 @@ ALL_GEOMETRY_MAPS = [
     IcosahedralShellMap(),
     ParametricMap(1),
     ParametricMap(2),
+    AffineMap2D(),
 ]
 
 
@@ -209,7 +219,7 @@ def parse_arguments():
         "--blending",
         default=str(IdentityMap()),
         help=f"Enter a blending type for the operator. Note some geometry maps only work in 2D or 3D respectively. "
-        f"Available geometry maps: {[str(m) for m in ALL_GEOMETRY_MAPS]}",
+        f"Available geometry maps: {sorted([str(m) for m in ALL_GEOMETRY_MAPS])}",
         # TODO add choices list and type=string.lower
     )
 
@@ -584,6 +594,7 @@ def all_operators(
     P1Vector = TensorialVectorFunctionSpace(P1)
     P2 = LagrangianFunctionSpace(2, symbolizer)
     P2Vector = TensorialVectorFunctionSpace(P2)
+    P2PlusBubble = P2PlusBubbleSpace(symbolizer)
     N1E1 = N1E1Space(symbolizer)
 
     two_d = list(geometries & {TriangleElement()})
@@ -593,6 +604,7 @@ def all_operators(
 
     # fmt: off
     # TODO switch to manual specification of opts for now/developement, later use default factory
+    
     ops.append(OperatorInfo("N1E1", "CurlCurl", TrialSpace(N1E1), TestSpace(N1E1), form=curl_curl,
                             type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
     ops.append(OperatorInfo("N1E1", "Mass", TrialSpace(N1E1), TestSpace(N1E1), form=mass_n1e1,
@@ -647,6 +659,12 @@ def all_operators(
                             form_boundary=mass_boundary, type_descriptor=type_descriptor, geometries=list(geometries),
                             opts=opts, blending=blending))
 
+    ops.append(OperatorInfo("P2PlusBubble", "Mass", TrialSpace(P2PlusBubble), TestSpace(P2PlusBubble), form=mass,
+                            type_descriptor=type_descriptor, geometries=two_d, opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P2PlusBubble", "Diffusion", TrialSpace(P2PlusBubble), TestSpace(P2PlusBubble), form=diffusion,
+                            type_descriptor=type_descriptor, geometries=two_d, opts=opts, blending=blending))
+
     # fmt: on
 
     p2vec_epsilon = partial(
diff --git a/hog/blending.py b/hog/blending.py
index 63478a11fe9b88e99f8bb5be6d44564b0aed03fd..e908feb1eed5558fdc2dd7b092775aee4e405ade 100644
--- a/hog/blending.py
+++ b/hog/blending.py
@@ -1,5 +1,5 @@
 # HyTeG Operator Generator
-# Copyright (C) 2024  HyTeG Team
+# Copyright (C) 2024-2025  HyTeG Team
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -115,7 +115,6 @@ class ParametricMap(GeometryMap):
     """
 
     def __init__(self, degree: int):
-
         if degree not in [1, 2]:
             raise HOGException(
                 "Only first and second order parametric maps are supported."
@@ -229,7 +228,7 @@ class AnnulusMap(GeometryMap):
         return jac
 
     def hessian(self, x: sp.Matrix) -> List[sp.Matrix]:
-        """Evaluates the Jacobian of the geometry map at the passed point."""
+        """Evaluates the derivatives of the inverse Jacobian matrix of the geometry map at the passed point."""
 
         if sp.shape(x) != (2, 1):
             raise HOGException("Invalid input shape for AnnulusMap.")
@@ -299,6 +298,7 @@ class AnnulusMap(GeometryMap):
                 f"real_t {self.rayVertex[i]} = std::dynamic_pointer_cast< AnnulusMap >( face.getGeometryMap() )->rayVertex()[{i}];",
                 f"real_t {self.thrVertex[i]} = std::dynamic_pointer_cast< AnnulusMap >( face.getGeometryMap() )->thrVertex()[{i}];",
             ]
+
         return "\n".join(code)
 
 
@@ -469,7 +469,7 @@ class IcosahedralShellMap(GeometryMap):
         return jac
 
     def hessian(self, x_: sp.Matrix) -> List[sp.Matrix]:
-        """Evaluates the Jacobian of the geometry map at the passed point."""
+        """Evaluates the derivatives of the inverse Jacobian matrix of the geometry map at the passed point."""
 
         if sp.shape(x_) != (3, 1):
             raise HOGException("Invalid input shape for IcosahedralShellMap.")
@@ -595,3 +595,62 @@ class IcosahedralShellMap(GeometryMap):
             ]
 
         return "\n".join(code)
+
+
+class AffineMap2D(GeometryMap):
+    """
+    This blending map uses a 2x2 matrix M and a shift vector v to
+    construct an affine mapping x -> M * x + v.
+
+    This is mostly intended for testing purposes, as we normally
+    would not want to have the overhead of blending in such a situation.
+    """
+
+    def __init__(self):
+        self.mat = sp.Matrix([["bMat_00", "bMat_01"], ["bMat_10", "bMat_11"]])
+        self.vec = sp.Matrix([["bVec_0"], ["bVec_1"]])
+
+    def supported_geometries(self) -> List[ElementGeometry]:
+        return [TriangleElement()]
+
+    def is_affine(self) -> bool:
+        # is_affine is supposed to indicate that the map does not have to be re-computed
+        # for each quadrature point - however, there seems to currently be a problem with
+        # the implementation of that optimization. Thus, we return False for the moment.
+        return False
+
+    def evaluate(self, x: sp.Matrix) -> sp.Matrix:
+        return self.mat * x + self.vec
+
+    def jacobian(self, x: sp.Matrix) -> sp.Matrix:
+        jac = self.mat.copy()
+        return jac
+
+    def hessian(self, x: sp.Matrix) -> List[sp.Matrix]:
+        hess = [sp.zeros(2, 2), sp.zeros(2, 2)]
+        return hess
+
+    def coupling_includes(self) -> List[str]:
+        return ["hyteg/geometry/AffineMap2D.hpp"]
+
+    def parameter_coupling_code(self) -> str:
+        code = []
+
+        code += [
+            f"WALBERLA_CHECK_NOT_NULLPTR( std::dynamic_pointer_cast< AffineMap2D >( face.getGeometryMap() ),"
+            f'"This operator requires the AffineMap2D to be registered as GeometryMap on every macro-face." )'
+        ]
+
+        code += [
+            f"real_t {self.mat[0,0]} = std::dynamic_pointer_cast< AffineMap2D >( face.getGeometryMap() )->getMatrix()(0,0);",
+            f"real_t {self.mat[0,1]} = std::dynamic_pointer_cast< AffineMap2D >( face.getGeometryMap() )->getMatrix()(0,1);",
+            f"real_t {self.mat[1,0]} = std::dynamic_pointer_cast< AffineMap2D >( face.getGeometryMap() )->getMatrix()(1,0);",
+            f"real_t {self.mat[1,1]} = std::dynamic_pointer_cast< AffineMap2D >( face.getGeometryMap() )->getMatrix()(1,1);",
+        ]
+
+        for i in range(2):
+            code += [
+                f"real_t {self.vec[i]} = std::dynamic_pointer_cast< AffineMap2D >( face.getGeometryMap() )->getVector()[{i}];",
+            ]
+
+        return "\n".join(code)
diff --git a/hog/forms.py b/hog/forms.py
index f67f0781d698489310f163fcd996231aecee66e0..ec630ce41999ca80a8396f1d0de0838f231fe435 100644
--- a/hog/forms.py
+++ b/hog/forms.py
@@ -36,6 +36,7 @@ from hog.fem_helpers import (
 from hog.function_space import (
     FunctionSpace,
     N1E1Space,
+    P2PlusBubbleSpace,
     TrialSpace,
     TestSpace,
     LagrangianFunctionSpace,
@@ -1097,7 +1098,7 @@ Weak formulation
 
     ∫ ((∇ρ / ρ) · u) v
 """
-    
+
     from hog.recipes.integrands.volume.frozen_velocity import integrand
 
     return process_integrand(
@@ -1114,6 +1115,7 @@ Weak formulation
         },
     )
 
+
 def zero_form(
     trial: TrialSpace,
     test: TestSpace,
diff --git a/hog/function_space.py b/hog/function_space.py
index 2d27469ee14c707c782a33f433f4cd7798eb9ca8..6384e94ad383416da02a28235b194cc4a8de28c4 100644
--- a/hog/function_space.py
+++ b/hog/function_space.py
@@ -80,14 +80,14 @@ class FunctionSpace(ABC):
     ) -> List[sp.MatrixBase]:
         """Returns a list containing the gradients of the shape functions on the element.
 
-        Particularly, for each (vector- or scalar-valued) shape function N = (N_1, ..., N_n) returns the _transposed_
+        Particularly, for each (vector- or scalar-valued) shape function N = (N_1, ..., N_n) returns the *transposed*
         Jacobian
 
                           ⎡∂N₁/∂x₁  ···  ∂Nₙ/∂x₁⎤
                           ⎢   ·             ·   ⎥
         grad(N) = J(N)ᵀ = ⎢   ·             ·   ⎥
                           ⎢   ·             ·   ⎥
-                          ⎣∂N₁/∂xₖ  ···  ∂Nₙ/∂xₖ ⎦
+                          ⎣∂N₁/∂xₖ  ···  ∂Nₙ/∂xₖ⎦
 
         i.e., the returned gradient is a column vector for scalar shape functions.
 
@@ -326,7 +326,7 @@ class TensorialVectorFunctionSpace(FunctionSpace):
     shape functions of the given scalar function space.
 
     For instance, a tensorial function space of dim d = 3 that is based on a P1 (Lagrangian) scalar function space with
-    n shape functions N_1, ..., N_n has the shape functions
+    n shape functions N_1, ..., N_n has the shape functions::
 
       (N_1, 0, 0),
       ...
@@ -338,8 +338,9 @@ class TensorialVectorFunctionSpace(FunctionSpace):
       ...
       (0, 0, N_n).
 
+
     This class also enables specifying only a single component to get the shape functions, e.g. only for component 1
-    (starting to count components at 0)
+    (starting to count components at 0)::
 
       (0, N_1, 0),
       ...
@@ -592,3 +593,106 @@ class N1E1Space(FunctionSpace):
 
     def __repr__(self):
         return str(self)
+
+
+class P2PlusBubbleSpace(FunctionSpace):
+    """
+    Space of continuous piecewise quadratic functions enhanced with local bubbles vanishing on element boundaries.
+
+    In the 2D case the triplet :math:`(K,P,\Sigma)` describing the Finite Element is given by the following:
+
+    #. K represents the standard 2D-simplex, i.e. the unit triangle.
+    #. The local function space :math:`P` on :math:`K` is given by
+       :math:`P = \mathbb{P}_2 \oplus \ell_1 \ell_2 \ell_3 \mathbb{R}`.
+       Here the :math:`\ell_i` represent barycentric coordinates. Thus, any function :math:`v\in P` can uniquely be
+       composed into :math:`v = p + b`, where p is a quadratic polynomial and b a bubble function.
+    #. The set :math:`\Sigma` of linear functionals :math:`L_k : P \rightarrow \mathbb{R}, k = 1,\ldots,7` is given
+       by
+
+    .. math::
+            L_m( v ) = v(x_m), m = 1,\ldots 6 \\
+            L_7( v ) = b(x_7) 
+
+    Here :math:`x_1, \ldots, x_3` represents the vertices of :math:`K`, :math:`x_4,\ldots,x_6` the midpoints
+    of its edges and :math:`x_7` its barycenter.
+    """
+
+    def __init__(self, symbolizer: Symbolizer):
+        self._symbolizer = symbolizer
+
+    @property
+    def is_vectorial(self) -> bool:
+        return False
+
+    @property
+    def is_continuous(self) -> bool:
+        return True
+
+    @property
+    def degree(self) -> int:
+        return 2
+
+    @property
+    def symbolizer(self) -> Symbolizer:
+        return self._symbolizer
+
+    @property
+    def family(self) -> str:
+        return "P2 enhanced with Bubble"
+
+    def shape(
+        self,
+        geometry: ElementGeometry,
+        domain: str = "reference",
+        dof_map: Optional[List[int]] = None,
+    ) -> List[sp.MatrixBase]:
+        """Returns a list containing the shape functions on the element.
+
+        :param dof_map: this list can be used to specify (remap) the DoF ordering of the element
+        """
+        if domain in ["ref", "reference"]:
+            symbols = self.symbolizer.ref_coords_as_list(geometry.dimensions)
+
+            basis_functions = []
+
+            if isinstance(geometry, TriangleElement):
+                L_1 = 1 - symbols[0] - symbols[1]
+                L_2 = symbols[0]
+                L_3 = symbols[1]
+                basis_functions = [
+                    L_1 * (2 * L_1 - 1),
+                    L_2 * (2 * L_2 - 1),
+                    L_3 * (2 * L_3 - 1),
+                    4 * L_2 * L_3,
+                    4 * L_1 * L_3,
+                    4 * L_1 * L_2,
+                    27 * L_1 * L_2 * L_3,
+                ]
+
+            elif isinstance(geometry, TetrahedronElement):
+                raise HOGException(
+                    f"P2PlusBubbleSpace is currently only implemented for triangle elements"
+                )
+            else:
+                raise HOGException(
+                    "Basis functions not implemented for the specified element type and geometry."
+                )
+
+            if dof_map:
+                raise HOGException("DoF reordering not implemented.")
+
+            basis_functions = [sp.Matrix([b]) for b in basis_functions]
+            return basis_functions
+
+        raise HOGException(
+            "Basis functions not implemented for the specified element type and geometry."
+        )
+
+    def __eq__(self, other: Any) -> bool:
+        return type(self) == type(other)
+
+    def __str__(self):
+        return f"P2PlusBubble"
+
+    def __repr__(self):
+        return str(self)
diff --git a/hog/hyteg_form_template.py b/hog/hyteg_form_template.py
index f0d43087871aab1c97978286f5a0a4776f6eafbb..b1c7c34124dd97cb339213ac35a8b6fc11cc2b32 100644
--- a/hog/hyteg_form_template.py
+++ b/hog/hyteg_form_template.py
@@ -21,7 +21,7 @@ from hog.ast import Assignment, CodeBlock
 from hog.exception import HOGException
 from hog.quadrature import Quadrature
 from hog.symbolizer import Symbolizer
-from hog.function_space import N1E1Space, TrialSpace, TestSpace
+from hog.function_space import N1E1Space, P2PlusBubbleSpace, TrialSpace, TestSpace
 from hog.element_geometry import ElementGeometry
 from hog.code_generation import code_block_from_element_matrix
 from hog.multi_assignment import Member
@@ -292,6 +292,8 @@ class HyTeGFormClass:
 
         if isinstance(self.trial, N1E1Space):
             super_class = f"n1e1::N1E1Form"
+        elif isinstance(self.trial, P2PlusBubbleSpace):
+            super_class = f"P2PlusBubbleForm"
         elif self.trial.degree == self.test.degree:
             super_class = f"P{self.trial.degree}FormHyTeG"
         else:
@@ -358,6 +360,8 @@ class HyTeGForm:
 
         if isinstance(self.trial, N1E1Space):
             super_class = f"N1E1Form"
+        elif isinstance(self.trial, P2PlusBubbleSpace):
+            super_class = f"P2PlusBubbleForm"
         elif self.trial.degree == self.test.degree:
             super_class = f"form_hyteg_base/P{self.trial.degree}FormHyTeG"
         else:
diff --git a/hog/operator_generation/function_space_implementations/__init__.py b/hog/operator_generation/function_space_implementations/__init__.py
index b133d9c3e2a9fb91d0a0adb2c637c1069dc457c8..f18bade22e37884ba7fa6b49a68da21bb257b993 100644
--- a/hog/operator_generation/function_space_implementations/__init__.py
+++ b/hog/operator_generation/function_space_implementations/__init__.py
@@ -20,5 +20,6 @@ __all__ = [
     "p0_space_impl",
     "p1_space_impl",
     "p2_space_impl",
+    "p2_plus_bubble_space_impl",
     "n1e1_space_impl",
 ]
diff --git a/hog/operator_generation/function_space_implementations/function_space_impl_factory.py b/hog/operator_generation/function_space_implementations/function_space_impl_factory.py
index c3762ee3c596c3279b34c93a2366528934cb81d5..4a6eb73c35d71080cefd1b06b0392e7d588abe36 100644
--- a/hog/operator_generation/function_space_implementations/function_space_impl_factory.py
+++ b/hog/operator_generation/function_space_implementations/function_space_impl_factory.py
@@ -19,6 +19,7 @@ from hog.exception import HOGException
 from hog.function_space import (
     FunctionSpace,
     LagrangianFunctionSpace,
+    P2PlusBubbleSpace,
     TensorialVectorFunctionSpace,
     N1E1Space,
 )
@@ -38,6 +39,9 @@ from hog.operator_generation.function_space_implementations.p2_space_impl import
     P2FunctionSpaceImpl,
     P2VectorFunctionSpaceImpl,
 )
+from hog.operator_generation.function_space_implementations.p2_plus_bubble_space_impl import (
+    P2PlusBubbleFunctionSpaceImpl,
+)
 from hog.operator_generation.function_space_implementations.n1e1_space_impl import (
     N1E1FunctionSpaceImpl,
 )
@@ -92,6 +96,8 @@ def create_impl(
             )
     elif isinstance(func_space, N1E1Space):
         impl_class = N1E1FunctionSpaceImpl
+    elif isinstance(func_space, P2PlusBubbleSpace):
+        impl_class = P2PlusBubbleFunctionSpaceImpl
     else:
         raise HOGException("Unexpected function space")
 
diff --git a/hog/operator_generation/function_space_implementations/p2_plus_bubble_space_impl.py b/hog/operator_generation/function_space_implementations/p2_plus_bubble_space_impl.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f7e33049f068cee8d63073a142485e09086f866
--- /dev/null
+++ b/hog/operator_generation/function_space_implementations/p2_plus_bubble_space_impl.py
@@ -0,0 +1,136 @@
+# HyTeG Operator Generator
+# Copyright (C) 2024  HyTeG Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+import itertools
+import numpy as np
+import sympy as sp
+from typing import List, Tuple, Set, Union, Dict
+
+import pystencils as ps
+from pystencils import Field, FieldType
+from pystencils.backends.cbackend import CustomCodeNode
+
+from hog.element_geometry import ElementGeometry
+from hog.exception import HOGException
+from hog.function_space import (
+    FunctionSpace,
+    LagrangianFunctionSpace,
+    TensorialVectorFunctionSpace,
+    N1E1Space,
+)
+from hog.operator_generation.indexing import (
+    CellType,
+    FaceType,
+    VolumeDoFMemoryLayout,
+    micro_element_to_vertex_indices,
+    micro_vertex_to_edge_indices,
+    micro_element_to_volume_indices,
+    IndexingInfo,
+)
+from hog.operator_generation.types import HOGType
+from hog.operator_generation.function_space_implementations.function_space_impl_base import (
+    FunctionSpaceImpl,
+)
+
+from hog.operator_generation.function_space_implementations.p2_space_impl import (
+    P2FunctionSpaceImpl,
+)
+
+
+class P2PlusBubbleFunctionSpaceImpl(FunctionSpaceImpl):
+    def __init__(
+        self,
+        fe_space: FunctionSpace,
+        name: str,
+        type_descriptor: HOGType,
+        is_pointer: bool = False,
+    ):
+        super().__init__(fe_space, name, type_descriptor, is_pointer)
+        self.field = self._create_field(name)
+        self.p2Impl = P2FunctionSpaceImpl(fe_space, name, type_descriptor, is_pointer)
+
+    def pre_communication(self, dim: int) -> str:
+        return self.p2Impl.pre_communication(dim)
+
+    def zero_halos(self, dim: int) -> str:
+        return self.p2Impl.zero_halos(dim)
+
+    def post_communication(
+        self, dim: int, params: str, transform_basis: bool = True
+    ) -> str:
+        return self.p2Impl.post_communication(dim, params, transform_basis)
+
+    def pointer_retrieval(self, dim: int) -> str:
+        """C++ code for retrieving pointers to the numerical data stored in the macro primitives `face` (2d) or `cell` (3d)."""
+        Macro = {2: "Face", 3: "Cell"}[dim]
+        macro = {2: "face", 3: "cell"}[dim]
+
+        result_str = self.p2Impl.pointer_retrieval(dim)
+        result_str += f"{self.type_descriptor.pystencils_type}* _data_{self.name} = {self._deref()}.getVolumeDoFFunction().dofMemory( it.first, level );"
+
+        return result_str
+
+    def invert_elementwise(self, dim: int) -> str:
+        return f"{self._deref()}.invertElementwise( level );"
+
+    def local_dofs(
+        self,
+        geometry: ElementGeometry,
+        element_index: Tuple[int, int, int],
+        element_type: Union[FaceType, CellType],
+        indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
+    ) -> List[Field.Access]:
+        complete_list_of_indices = self.p2Impl.local_dofs(
+            geometry,
+            element_index,
+            element_type,
+            indexing_info,
+            element_vertex_ordering,
+        )
+
+        volume_dof_indices = micro_element_to_volume_indices(
+            element_type, element_index, 1, VolumeDoFMemoryLayout.SoA
+        )
+
+        volume_array_indices = [
+            dof_idx.array_index(geometry, indexing_info)
+            for dof_idx in volume_dof_indices
+        ]
+
+        complete_list_of_indices += [
+            self.field.absolute_access((idx,), (0,)) for idx in volume_array_indices
+        ]
+
+        return complete_list_of_indices
+
+    def func_type_string(self) -> str:
+        return f"P2PlusBubbleFunction< {self.type_descriptor.pystencils_type} >"
+
+    def includes(self) -> Set[str]:
+        return {f"hyteg/experimental/P2PlusBubbleFunction.hpp"}
+
+    def dof_transformation(
+        self,
+        geometry: ElementGeometry,
+        element_index: Tuple[int, int, int],
+        element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
+    ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
+        return (
+            CustomCodeNode("", [], []),
+            sp.Identity(self.fe_space.num_dofs(geometry)),
+        )
diff --git a/hog/operator_generation/indexing.py b/hog/operator_generation/indexing.py
index 896bcae8e6c4ff5ea2d04c69af5b447a2fd9b70d..218a8fef66eb9cf5dcec11e9494d2bcd92e130ba 100644
--- a/hog/operator_generation/indexing.py
+++ b/hog/operator_generation/indexing.py
@@ -27,7 +27,7 @@ from hog.symbolizer import Symbolizer
 from pystencils.integer_functions import int_div
 from pystencils import TypedSymbol
 from enum import Enum
-from typing import Tuple, List, Union
+from typing import Tuple, List, Union, cast
 
 import operator
 import sympy as sp
@@ -259,13 +259,21 @@ def linear_macro_cell_index(width: int, x: int, y: int, z: int) -> int:
 
 def facedof_index(
     level: int,
-    index: Tuple[int, int, int],
+    # Hack: see issue #46 for details
+    index: Union[Tuple[int, int], Tuple[int, int, int]],
     faceType: Union[None, EdgeType, FaceType, CellType],
     num_microfaces_per_face: sp.Symbol,
     num_microedges_per_edge: sp.Symbol,
 ) -> int:
     """Indexes triangles/faces. Used to compute offsets in volume dof indexing in 2D and AoS layout."""
-    x, y, _ = index
+
+    # second part of hack for #46
+    # also see https://github.com/python/mypy/issues/1178
+    if len(index) == 3:
+        x, y, _ = cast(Tuple[int, int, int], index)
+    else:
+        x, y = cast(Tuple[int, int], index)
+
     # width = num_faces_per_row_by_type(level, faceType)
     if faceType == FaceType.GRAY:
         return linear_macro_face_index(num_microedges_per_edge, x, y)