diff --git a/generate_all_operators.py b/generate_all_operators.py
index 6ce4a12dcab8ddddd60fb2e05107591d8415a84a..af0df0d527b10e42fa51c1b0e0ed256b134066b5 100644
--- a/generate_all_operators.py
+++ b/generate_all_operators.py
@@ -1,910 +1,915 @@
-# 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 argparse
-import sys
-from argparse import ArgumentParser
-from dataclasses import dataclass, field
-from functools import partial
-import logging
-import os
-import re
-from typing import Callable, List, Optional, Sequence, Set, Tuple, Union
-
-import numpy as np
-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.cse import CseImplementation
-from hog.element_geometry import (
-    ElementGeometry,
-    TriangleElement,
-    TetrahedronElement,
-    LineElement,
-)
-from hog.exception import HOGException
-from hog.forms import (
-    diffusion,
-    divergence,
-    gradient,
-    div_k_grad,
-    shear_heating,
-    epsilon,
-    full_stokes,
-    nonlinear_diffusion,
-    nonlinear_diffusion_newton_galerkin,
-    supg_diffusion,
-    advection,
-    supg_advection,
-    grad_rho_by_rho_dot_u,
-)
-from hog.forms_boundary import (
-    mass_boundary,
-    freeslip_gradient_weak_boundary,
-    freeslip_divergence_weak_boundary,
-    freeslip_momentum_weak_boundary,
-)
-from hog.forms_vectorial import curl_curl, curl_curl_plus_mass, mass_n1e1
-from hog.function_space import (
-    LagrangianFunctionSpace,
-    N1E1Space,
-    TensorialVectorFunctionSpace,
-    TrialSpace,
-    TestSpace,
-)
-from hog.logger import get_logger, TimedLogger
-from hog.operator_generation.kernel_types import (
-    ApplyWrapper,
-    AssembleWrapper,
-    AssembleDiagonalWrapper,
-    KernelWrapperType,
-)
-from hog.operator_generation.loop_strategies import (
-    LoopStrategy,
-    SAWTOOTH,
-    CUBES,
-    FUSEDROWS,
-)
-from hog.operator_generation.operators import (
-    HyTeGElementwiseOperator,
-    MacroIntegrationDomain,
-)
-
-from operator import itemgetter
-from hog.operator_generation.optimizer import (
-    Opts,
-    opts_arg_mapping,
-    ordered_opts_suffix,
-)
-from hog.quadrature import Quadrature
-from hog.symbolizer import Symbolizer
-from generate_all_hyteg_forms import valid_base_dir
-from hog.operator_generation.types import parse_argument_type, HOGType, HOGPrecision
-import quadpy
-from hog.quadrature.quadrature import select_quadrule
-from hog.integrand import Form
-
-ALL_GEOMETRY_MAPS = [
-    IdentityMap(),
-    AnnulusMap(),
-    IcosahedralShellMap(),
-    ParametricMap(1),
-    ParametricMap(2),
-]
-
-
-def parse_arguments():
-    parser = ArgumentParser(
-        description="Generates a collection of elementwise operators."
-    )
-
-    list_or_gen = parser.add_mutually_exclusive_group(required=True)
-    list_or_gen.add_argument(
-        "-l",
-        "--list-operators",
-        action="store_true",
-        help="List all available operators.",
-    )
-    list_or_gen.add_argument(
-        "--list-quadratures",
-        nargs=1,
-        help="List all available quadrature rules for a given geometry (t1: Line, t2: Triangle, t3: Tetrahedron)",
-    )
-
-    list_or_gen.add_argument(
-        "--hyteg",
-        help=(
-            "Path to the HyTeG repository. The generated code is written to the correct directory in the source tree."
-        ),
-    )
-    list_or_gen.add_argument(
-        "--output",
-        help=("Path to output directory."),
-    )
-
-    parser.add_argument(
-        "-s",
-        "--space-mapping",
-        default=".*?",
-        help="Filter by function space, e.g. 'P1' or 'P1toP2' (regex).",
-        # TODO add choices list and type=string.lower
-    )
-
-    parser.add_argument(
-        "-f",
-        "--form",
-        default=".*?",
-        help="Filter by form, e.g. 'CurlCurl' or 'Diffusion' (regex).",
-        # TODO add choices list and type=string.lower
-    )
-
-    parser.add_argument(
-        "-o",
-        "--opts",
-        default="",
-        nargs="+",
-        help=f"""
-    Enter a space separated list of optimizations. 
-    Not all combinations are currently supported. Available optimizations are: {Opts.__members__.keys()}""",
-        # TODO add choices list and type=string.lower
-    )
-    # TODO explanations for optimizations e.g. CUTLOOPS is not straight-forward
-
-    parser.add_argument(
-        "--loop-strategy",
-        default="SAWTOOTH",
-        help="Enter a loop strategy for the operator. Available strategies are SAWTOOTH, CUBES.",
-        # TODO add choices list and type=string.lower
-    )
-
-    quad_rule_or_degree = parser.add_mutually_exclusive_group(required=False)
-    quad_rule_or_degree.add_argument(
-        "--quad-rule",
-        type=str,
-        nargs=2,
-        default=None,
-        help="""Supply two rules by their identifier (leftmost column when calling with --list-quadratures),
-                the first will be used for triangles and the second for tetrahedra.""",
-    )
-    quad_rule_or_degree.add_argument(
-        "--quad-degree",
-        type=int,
-        nargs=2,
-        default=[-1, -1],
-        help="""Supply two polynomial degrees (as integers) up to which the quadrature should be exact. 
-                The generator will chose the rule with minimal points that integrates the specified 
-                degree exactly. The first integer will be used for triangles and the second for tetrahedra.""",
-    )
-
-    prec_or_mpfr = parser.add_mutually_exclusive_group(required=False)
-    prec_or_mpfr.add_argument(
-        "-p",
-        "--precision",
-        type=str.lower,
-        help=(
-            "Defines the precision in which the kernels are calculated; "
-            "If not specified we choose fp64(double) precision"
-        ),
-        choices=list(choice.value for choice in HOGPrecision),
-        default="fp64",
-    )
-
-    parser.add_argument(
-        "-b",
-        "--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]}",
-        # TODO add choices list and type=string.lower
-    )
-
-    parser.add_argument(
-        "--name",
-        help=f"Specify the name of the generated C++ class.",
-    )
-
-    parser.add_argument(
-        "--dimensions",
-        type=int,
-        nargs="+",
-        default=[2, 3],
-        help=f"Domain dimensions for which operators shall be generated.",
-    )
-
-    logging_group = parser.add_mutually_exclusive_group(required=False)
-    logging_group.add_argument(
-        "-v",
-        "--verbose",
-        type=int,
-        help="specify verbosity level (0 = silent, 1 = info, 2 = debug)",
-        choices=[0, 1, 2],
-    )
-    logging_group.add_argument(
-        "--loglevel",
-        type=str.lower,
-        help=(
-            "Defines which messages are shown and which not; "
-            "If not specified 'warning' is chosen."
-        ),
-        choices=["debug", "info", "warning", "error", "critical"],
-    )
-
-    clang_format_group = parser.add_mutually_exclusive_group(required=False)
-    clang_format_group.add_argument(
-        "--no-clang-format",
-        dest="clang_format",
-        action="store_false",
-        help="Do not apply clang-format on generated files.",
-    )
-
-    clang_format_group.add_argument(
-        "--clang-format-binary",
-        default="clang-format",
-        help=f"Allows to specify the name of the clang-format binary and/or optionally its full path."
-        f" By default 'clang-format' will be used.",
-    )
-
-    args = parser.parse_args()
-    return parser, args
-
-
-strategy_args_mapping = {
-    "CUBES": CUBES(),
-    "SAWTOOTH": SAWTOOTH(),
-    "FUSEDROWS": FUSEDROWS(),
-}
-
-
-def main():
-    clear_cache()
-
-    parser, args = parse_arguments()
-
-    # Adapt clang_format_binary according to the value of clang_format
-    if "clang_format" in vars(args):
-        if not args.clang_format:
-            args.clang_format_binary = None
-
-    if args.verbose:
-        if args.verbose == 0:
-            log_level = logging.CRITICAL
-        elif args.verbose == 1:
-            log_level = logging.INFO
-        elif args.verbose == 2:
-            log_level = logging.DEBUG
-        else:
-            raise HOGException(f"Invalid verbosity level {args.verbose}.")
-    elif args.loglevel:
-        if args.loglevel == "debug":
-            log_level = logging.DEBUG
-        elif args.loglevel == "info":
-            log_level = logging.INFO
-        elif args.loglevel == "warning":
-            log_level = logging.WARNING
-        elif args.loglevel == "error":
-            log_level = logging.ERROR
-        elif args.loglevel == "critical":
-            log_level = logging.CRITICAL
-        else:
-            raise HOGException(f"Invalid log level {args.loglevel}.")
-    else:
-        # The default log level is chosen here
-        log_level = logging.DEBUG
-
-    logger = get_logger(log_level)
-    TimedLogger.set_log_level(log_level)
-
-    logger.info(f"Running {__file__} with arguments: {' '.join(sys.argv[1:])}")
-
-    symbolizer = Symbolizer()
-
-    if args.list_quadratures:
-        geometry = args.list_quadratures[0]
-        logger.debug(f"Listing quadrature rules for geometry: {geometry}.")
-        if geometry == "t3":
-            schemes = quadpy.t3.schemes
-        elif geometry == "t2":
-            schemes = quadpy.t2.schemes
-        else:
-            raise HOGException(f"Unexpected geometry: {geometry}")
-
-        all_schemes = []
-        for key, s in schemes.items():
-            try:
-                scheme = s()
-                entry = {
-                    "key": key,
-                    "name": scheme.name,
-                    "points": scheme.points.shape[1],
-                    "degree": scheme.degree,
-                    "test_tolerance": scheme.test_tolerance,
-                }
-                all_schemes.append(entry)
-            except TypeError as e:
-                # Some schemes seem to require parameters, we simply exclude these with this litte try-except hack
-                pass
-
-        all_schemes = sorted(all_schemes, key=itemgetter("degree", "points"))
-        table = tabulate(all_schemes, headers="keys", tablefmt="grid")
-        print(table)
-        exit()
-
-    blending = None
-    for m in ALL_GEOMETRY_MAPS:
-        if str(m) == args.blending:
-            blending = m
-    if blending is None:
-        raise HOGException("Invalid geometry map.")
-    logger.debug(f"Blending: {blending}")
-
-    type_descriptor = parse_argument_type(args)
-
-    logger.debug(f"Data type name is {type_descriptor.pystencils_type}.")
-
-    opts = {opts_arg_mapping[opt] for opt in args.opts}
-    loop_strategy = strategy_args_mapping[args.loop_strategy]
-    re_form = re.compile(args.form)
-
-    if (
-        not args.list_operators
-        and args.quad_degree == [-1, -1]
-        and args.quad_rule is None
-    ):
-        parser.error("Either --quad-degree or --quad-rule are required.")
-    quad = {
-        geometry: info
-        for geometry, info in zip(
-            [TriangleElement(), TetrahedronElement()],
-            args.quad_degree if args.quad_rule is None else args.quad_rule,
-        )
-    }
-
-    enabled_geometries: Set[TriangleElement | TetrahedronElement] = set()
-    if 2 in args.dimensions:
-        enabled_geometries.add(TriangleElement())
-    if 3 in args.dimensions:
-        enabled_geometries.add(TetrahedronElement())
-
-    operators = all_operators(
-        symbolizer,
-        [(opts, loop_strategy, ordered_opts_suffix(loop_strategy, opts))],
-        type_descriptor,
-        blending,
-        geometries=enabled_geometries,
-    )
-
-    filtered_operators = list(
-        filter(
-            lambda o: re.fullmatch(args.space_mapping, o.mapping, re.IGNORECASE)
-            and re.fullmatch(args.form, o.name, re.IGNORECASE),
-            operators,
-        )
-    )
-
-    filtered_operators = [f for f in filtered_operators if f.geometries]
-
-    if len(filtered_operators) == 0:
-        raise HOGException(
-            f"Form {args.form} does not match any available forms. Run --list for a list."
-        )
-
-    if args.list_operators:
-        print(
-            tabulate(
-                sorted(
-                    [
-                        (
-                            o.mapping,
-                            o.name,
-                            ", ".join(
-                                str(geo.dimensions) + "D" for geo in o.geometries
-                            ),
-                            len(o.opts),
-                        )
-                        for o in filtered_operators
-                    ]
-                ),
-                headers=(
-                    "Space mapping",
-                    "Form",
-                    "Dimensions",
-                    "Optimizations",
-                ),
-            )
-        )
-        exit()
-
-    if args.hyteg:
-        args.output = os.path.join(args.hyteg, "src/hyteg/operatorgeneration/generated")
-        if not valid_base_dir(args.hyteg):
-            raise HOGException(
-                "The specified directory does not seem to be the HyTeG directory."
-            )
-
-    for operator in filtered_operators:
-        for opt, loop, opt_name in operator.opts:
-            blending_str = (
-                "_" + str(blending) if not isinstance(blending, IdentityMap) else ""
-            )
-            if args.name:
-                name = args.name
-            else:
-                name = (
-                    f"{operator.mapping}Elementwise{operator.name}{blending_str}{opt_name}"
-                    f"{'_' + str(type_descriptor) if type_descriptor.add_file_suffix else ''}"
-                )
-            with TimedLogger(f"Generating {name}", level=logging.INFO):
-                generate_elementwise_op(
-                    args,
-                    symbolizer,
-                    operator,
-                    name,
-                    opt,
-                    loop,
-                    blending,
-                    quad,
-                    quad,
-                    type_descriptor=type_descriptor,
-                )
-
-
-def all_opts_sympy_cse() -> List[Tuple[Set[Opts], LoopStrategy, str]]:
-    return [
-        (
-            set(
-                {
-                    Opts.MOVECONSTANTS,
-                    Opts.REORDER,
-                    Opts.ELIMTMPS,
-                    Opts.QUADLOOPS,
-                    Opts.VECTORIZE,
-                }
-            ),
-            SAWTOOTH(),
-            "_const_vect_fused_quadloops_reordered_elimtmps",
-        ),
-    ]
-
-
-def all_opts_poly_cse() -> List[Tuple[Set[Opts], LoopStrategy, str]]:
-    return [
-        (o | {Opts.POLYCSE}, l, n + "_polycse") for (o, l, n) in all_opts_sympy_cse()
-    ]
-
-
-def all_opts_both_cses() -> List[Tuple[Set[Opts], LoopStrategy, str]]:
-    return all_opts_sympy_cse() + all_opts_poly_cse()
-
-
-@dataclass
-class OperatorInfo:
-    mapping: str
-    name: str
-    trial_space: TrialSpace
-    test_space: TestSpace
-    form: (
-        Callable[
-            [
-                TrialSpace,
-                TestSpace,
-                ElementGeometry,
-                Symbolizer,
-                GeometryMap,
-            ],
-            Form,
-        ]
-        | None
-    )
-    type_descriptor: HOGType
-    form_boundary: (
-        Callable[
-            [
-                TrialSpace,
-                TestSpace,
-                ElementGeometry,
-                ElementGeometry,
-                Symbolizer,
-                GeometryMap,
-            ],
-            Form,
-        ]
-        | None
-    ) = None
-    kernel_types: List[KernelWrapperType] = None  # type: ignore[assignment] # will definitely be initialized in __post_init__
-    geometries: Sequence[ElementGeometry] = field(
-        default_factory=lambda: [TriangleElement(), TetrahedronElement()]
-    )
-    opts: List[Tuple[Set[Opts], LoopStrategy, str]] = field(
-        default_factory=all_opts_sympy_cse
-    )
-    blending: GeometryMap = field(default_factory=lambda: IdentityMap())
-
-    def __post_init__(self):
-        # Removing geometries that are not supported by blending.
-        # I don't like this, but it looks like the collection of operators has to be refactored anyway.
-        self.geometries = list(
-            set(self.geometries) & set(self.blending.supported_geometries())
-        )
-
-        if self.kernel_types is None:
-            dims = [g.dimensions for g in self.geometries]
-            self.kernel_types = [
-                ApplyWrapper(
-                    self.trial_space,
-                    self.test_space,
-                    type_descriptor=self.type_descriptor,
-                    dims=dims,
-                )
-            ]
-
-            all_opts = set().union(*[o for (o, _, _) in self.opts])
-            if not ({Opts.VECTORIZE, Opts.VECTORIZE512}.intersection(all_opts)):
-                self.kernel_types.append(
-                    AssembleWrapper(
-                        self.trial_space,
-                        self.test_space,
-                        type_descriptor=self.type_descriptor,
-                        dims=dims,
-                    )
-                )
-
-            if self.test_space == self.trial_space:
-                self.kernel_types.append(
-                    AssembleDiagonalWrapper(
-                        self.trial_space,
-                        type_descriptor=self.type_descriptor,
-                        dims=dims,
-                    )
-                )
-
-
-def all_operators(
-    symbolizer: Symbolizer,
-    opts: List[Tuple[Set[Opts], LoopStrategy, str]],
-    type_descriptor: HOGType,
-    blending: GeometryMap,
-    geometries: Set[Union[TriangleElement, TetrahedronElement]],
-) -> List[OperatorInfo]:
-    P1 = LagrangianFunctionSpace(1, symbolizer)
-    P1Vector = TensorialVectorFunctionSpace(P1)
-    P2 = LagrangianFunctionSpace(2, symbolizer)
-    P2Vector = TensorialVectorFunctionSpace(P2)
-    N1E1 = N1E1Space(symbolizer)
-
-    two_d = list(geometries & {TriangleElement()})
-    three_d = list(geometries & {TetrahedronElement()})
-
-    ops: List[OperatorInfo] = []
-
-    # 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,
-                            type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
-    ops.append(OperatorInfo("N1E1", "CurlCurlPlusMass", TrialSpace(N1E1), TestSpace(N1E1),
-                            form=partial(curl_curl_plus_mass, alpha_fem_space=P1, beta_fem_space=P1),
-                            type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
-    ops.append(OperatorInfo("P1", "Diffusion", TrialSpace(P1), TestSpace(P1), form=diffusion,
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-    ops.append(OperatorInfo("P1", "DivKGrad", TrialSpace(P1), TestSpace(P1),
-                            form=partial(div_k_grad, coefficient_function_space=P1),
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P2", "Diffusion", TrialSpace(P2), TestSpace(P2), form=diffusion,
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-    ops.append(OperatorInfo("P2", "DivKGrad", TrialSpace(P2), TestSpace(P2),
-                            form=partial(div_k_grad, coefficient_function_space=P2),
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P2", "ShearHeating", TrialSpace(P2), TestSpace(P2),
-                            form=partial(shear_heating, viscosity_function_space=P2, velocity_function_space=P2),
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P1", "NonlinearDiffusion", TrialSpace(P1), TestSpace(P1),
-                            form=partial(nonlinear_diffusion, coefficient_function_space=P1),
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-    ops.append(OperatorInfo("P1", "NonlinearDiffusionNewtonGalerkin", TrialSpace(P1),
-                            TestSpace(P1), form=partial(nonlinear_diffusion_newton_galerkin,
-                            coefficient_function_space=P1, only_newton_galerkin_part_of_form=False),
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P1Vector", "Diffusion", TrialSpace(P1Vector), TestSpace(P1Vector),
-                            form=diffusion, type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P2", "SUPGDiffusion", TrialSpace(P2), TestSpace(P2), 
-                            form=partial(supg_diffusion, velocity_function_space=P2, diffusivityXdelta_function_space=P2), 
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-    
-    ops.append(OperatorInfo("P2", "AdvectionConstCp", TrialSpace(P2), TestSpace(P2), 
-                            form=partial(advection, velocity_function_space=P2, coefficient_function_space=P2, constant_cp = True), 
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P2", "AdvectionVarCp", TrialSpace(P2), TestSpace(P2), 
-                            form=partial(advection, velocity_function_space=P2, coefficient_function_space=P2), 
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-    
-    ops.append(OperatorInfo("P2", "SUPGAdvection", TrialSpace(P2), TestSpace(P2), 
-                            form=partial(supg_advection, velocity_function_space=P2, coefficient_function_space=P2), 
-                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
-
-    ops.append(OperatorInfo("P2", "BoundaryMass", TrialSpace(P2), TestSpace(P2), form=None,
-                            form_boundary=mass_boundary, type_descriptor=type_descriptor, geometries=list(geometries),
-                            opts=opts, blending=blending))
-
-    # fmt: on
-
-    p2vec_epsilon = partial(
-        epsilon,
-        variable_viscosity=True,
-        coefficient_function_space=P2,
-    )
-    ops.append(
-        OperatorInfo(
-            f"P2Vector",
-            f"Epsilon",
-            TrialSpace(P2Vector),
-            TestSpace(P2Vector),
-            form=p2vec_epsilon,
-            type_descriptor=type_descriptor,
-            geometries=list(geometries),
-            opts=opts,
-            blending=blending,
-        )
-    )
-
-    p2vec_freeslip_momentum_weak_boundary = partial(
-        freeslip_momentum_weak_boundary, function_space_mu=P2
-    )
-
-    ops.append(
-        OperatorInfo(
-            f"P2Vector",
-            f"EpsilonFreeslip",
-            TrialSpace(P2Vector),
-            TestSpace(P2Vector),
-            form=p2vec_epsilon,
-            form_boundary=p2vec_freeslip_momentum_weak_boundary,
-            type_descriptor=type_descriptor,
-            geometries=list(geometries),
-            opts=opts,
-            blending=blending,
-        )
-    )
-
-    ops.append(
-        OperatorInfo(
-            f"P2VectorToP1",
-            f"DivergenceFreeslip",
-            TrialSpace(P2Vector),
-            TestSpace(P1),
-            form=divergence,
-            form_boundary=freeslip_divergence_weak_boundary,
-            type_descriptor=type_descriptor,
-            geometries=list(geometries),
-            opts=opts,
-            blending=blending,
-        )
-    )
-
-    ops.append(
-        OperatorInfo(
-            f"P1ToP2Vector",
-            f"GradientFreeslip",
-            TrialSpace(P1),
-            TestSpace(P2Vector),
-            form=gradient,
-            form_boundary=freeslip_gradient_weak_boundary,
-            type_descriptor=type_descriptor,
-            geometries=list(geometries),
-            opts=opts,
-            blending=blending,
-        )
-    )
-
-    p2vec_grad_rho = partial(
-        grad_rho_by_rho_dot_u,
-        density_function_space=P2,
-    )
-    ops.append(
-        OperatorInfo(
-            f"P2VectorToP1",
-            f"GradRhoByRhoDotU",
-            TrialSpace(P1),
-            TestSpace(P2Vector),
-            form=p2vec_grad_rho,
-            type_descriptor=type_descriptor,
-            geometries=list(geometries),
-            opts=opts,
-            blending=blending,
-        )
-    )
-
-    for c in [0, 1, 2]:
-        # fmt: off
-        if c == 2:
-            div_geometries = three_d
-        else:
-            div_geometries = list(geometries)
-        ops.append(OperatorInfo(f"P2ToP1", f"Div_{c}",
-                                TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), TestSpace(P1),
-                                form=partial(divergence, component_index=c),
-                                type_descriptor=type_descriptor, opts=opts, geometries=div_geometries,
-                                blending=blending))
-        ops.append(OperatorInfo(f"P1ToP2", f"DivT_{c}", TrialSpace(P1),
-                                TestSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
-                                form=partial(gradient, component_index=c),
-                                type_descriptor=type_descriptor, opts=opts, geometries=div_geometries,
-                                blending=blending))
-        # fmt: on
-
-    for c in [0, 1]:
-        for r in [0, 1]:
-            p2_epsilon = partial(
-                epsilon,
-                variable_viscosity=True,
-                coefficient_function_space=P2,
-                component_trial=c,
-                component_test=r,
-            )
-
-            p2_full_stokes = partial(
-                full_stokes,
-                variable_viscosity=True,
-                coefficient_function_space=P2,
-                component_trial=c,
-                component_test=r,
-            )
-            # fmt: off
-            ops.append(
-                OperatorInfo(f"P2", f"Epsilon_{r}_{c}",
-                             TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
-                             TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_epsilon,
-                             type_descriptor=type_descriptor, geometries=list(geometries), opts=opts,
-                             blending=blending))
-            ops.append(OperatorInfo(f"P2", f"FullStokes_{r}_{c}",
-                                    TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
-                                    TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)),
-                                    form=p2_full_stokes, type_descriptor=type_descriptor, geometries=list(geometries),
-                                    opts=opts, blending=blending))
-            # fmt: on
-    for c, r in [(0, 2), (1, 2), (2, 2), (2, 1), (2, 0)]:
-        p2_epsilon = partial(
-            epsilon,
-            variable_viscosity=True,
-            coefficient_function_space=P2,
-            component_trial=c,
-            component_test=r,
-        )
-
-        p2_full_stokes = partial(
-            full_stokes,
-            variable_viscosity=True,
-            coefficient_function_space=P2,
-            component_trial=c,
-            component_test=r,
-        )
-        # fmt: off
-        ops.append(
-            OperatorInfo(f"P2", f"Epsilon_{r}_{c}", TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
-                         TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_epsilon,
-                         type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
-        ops.append(
-            OperatorInfo(f"P2", f"FullStokes_{r}_{c}", TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
-                         TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_full_stokes,
-                         type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
-        # fmt: on
-
-    # Removing all operators without viable element types (e.g.: some ops only support 2D, but a blending map maybe only
-    # supports 3D).
-    ops = [o for o in ops if o.geometries]
-
-    return ops
-
-
-def generate_elementwise_op(
-    args: argparse.Namespace,
-    symbolizer: Symbolizer,
-    op_info: OperatorInfo,
-    name: str,
-    optimizations: Set[Opts],
-    loop_strategy: LoopStrategy,
-    blending: GeometryMap,
-    quad_info: dict[ElementGeometry, int | str],
-    quad_info_boundary: dict[ElementGeometry, int | str],
-    type_descriptor: HOGType,
-) -> None:
-    """Generates a single operator and writes it to the HyTeG directory."""
-
-    operator = HyTeGElementwiseOperator(
-        name,
-        symbolizer,
-        kernel_wrapper_types=op_info.kernel_types,
-        type_descriptor=type_descriptor,
-    )
-
-    for geometry in op_info.geometries:
-        # Is this necessary? Currently, the decision of the geometry is all over the place.
-        if geometry not in blending.supported_geometries():
-            continue
-
-        if op_info.form is not None:
-            quad = Quadrature(
-                select_quadrule(quad_info[geometry], geometry),
-                geometry,
-            )
-
-            form = op_info.form(
-                op_info.trial_space,
-                op_info.test_space,
-                geometry,
-                symbolizer,
-                blending=blending,  # type: ignore[call-arg] # kw-args are not supported by Callable
-            )
-
-            operator.add_volume_integral(
-                name="".join(name.split()),
-                volume_geometry=geometry,
-                quad=quad,
-                blending=blending,
-                form=form,
-                loop_strategy=loop_strategy,
-                optimizations=optimizations,
-            )
-
-        if op_info.form_boundary is not None:
-            boundary_geometry: ElementGeometry
-            if geometry == TriangleElement():
-                boundary_geometry = LineElement(space_dimension=2)
-            elif geometry == TetrahedronElement():
-                boundary_geometry = TriangleElement(space_dimension=3)
-            else:
-                raise HOGException("Invalid volume geometry for boundary integral.")
-
-            quad = Quadrature(
-                select_quadrule(quad_info_boundary[geometry], boundary_geometry),
-                boundary_geometry,
-            )
-
-            form_boundary = op_info.form_boundary(
-                op_info.trial_space,
-                op_info.test_space,
-                geometry,
-                boundary_geometry,
-                symbolizer,
-                blending=blending,  # type: ignore[call-arg] # kw-args are not supported by Callable
-            )
-
-            operator.add_boundary_integral(
-                name="".join(name.split()),
-                volume_geometry=geometry,
-                quad=quad,
-                blending=blending,
-                form=form_boundary,
-                optimizations=set(),
-            )
-
-    dir_path = os.path.join(args.output, op_info.name.split("_")[0])
-    operator.generate_class_code(
-        dir_path,
-        clang_format_binary=args.clang_format_binary,
-    )
-
-
-if __name__ == "__main__":
-    main()
+# 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 argparse
+import sys
+from argparse import ArgumentParser
+from dataclasses import dataclass, field
+from functools import partial
+import logging
+import os
+import re
+from typing import Callable, List, Optional, Sequence, Set, Tuple, Union
+
+import numpy as np
+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.cse import CseImplementation
+from hog.element_geometry import (
+    ElementGeometry,
+    TriangleElement,
+    TetrahedronElement,
+    LineElement,
+)
+from hog.exception import HOGException
+from hog.forms import (
+    diffusion,
+    divergence,
+    gradient,
+    div_k_grad,
+    shear_heating,
+    epsilon,
+    full_stokes,
+    nonlinear_diffusion,
+    nonlinear_diffusion_newton_galerkin,
+    supg_diffusion,
+    advection,
+    supg_advection,
+    grad_rho_by_rho_dot_u,
+    linear_elasticity,
+)
+from hog.forms_boundary import (
+    mass_boundary,
+    freeslip_gradient_weak_boundary,
+    freeslip_divergence_weak_boundary,
+    freeslip_momentum_weak_boundary,
+)
+from hog.forms_vectorial import curl_curl, curl_curl_plus_mass, mass_n1e1
+from hog.function_space import (
+    LagrangianFunctionSpace,
+    N1E1Space,
+    TensorialVectorFunctionSpace,
+    TrialSpace,
+    TestSpace,
+)
+from hog.logger import get_logger, TimedLogger
+from hog.operator_generation.kernel_types import (
+    ApplyWrapper,
+    AssembleWrapper,
+    AssembleDiagonalWrapper,
+    KernelWrapperType,
+)
+from hog.operator_generation.loop_strategies import (
+    LoopStrategy,
+    SAWTOOTH,
+    CUBES,
+    FUSEDROWS,
+)
+from hog.operator_generation.operators import (
+    HyTeGElementwiseOperator,
+    MacroIntegrationDomain,
+)
+
+from operator import itemgetter
+from hog.operator_generation.optimizer import (
+    Opts,
+    opts_arg_mapping,
+    ordered_opts_suffix,
+)
+from hog.quadrature import Quadrature
+from hog.symbolizer import Symbolizer
+from generate_all_hyteg_forms import valid_base_dir
+from hog.operator_generation.types import parse_argument_type, HOGType, HOGPrecision
+import quadpy
+from hog.quadrature.quadrature import select_quadrule
+from hog.integrand import Form
+
+ALL_GEOMETRY_MAPS = [
+    IdentityMap(),
+    AnnulusMap(),
+    IcosahedralShellMap(),
+    ParametricMap(1),
+    ParametricMap(2),
+]
+
+
+def parse_arguments():
+    parser = ArgumentParser(
+        description="Generates a collection of elementwise operators."
+    )
+
+    list_or_gen = parser.add_mutually_exclusive_group(required=True)
+    list_or_gen.add_argument(
+        "-l",
+        "--list-operators",
+        action="store_true",
+        help="List all available operators.",
+    )
+    list_or_gen.add_argument(
+        "--list-quadratures",
+        nargs=1,
+        help="List all available quadrature rules for a given geometry (t1: Line, t2: Triangle, t3: Tetrahedron)",
+    )
+
+    list_or_gen.add_argument(
+        "--hyteg",
+        help=(
+            "Path to the HyTeG repository. The generated code is written to the correct directory in the source tree."
+        ),
+    )
+    list_or_gen.add_argument(
+        "--output",
+        help=("Path to output directory."),
+    )
+
+    parser.add_argument(
+        "-s",
+        "--space-mapping",
+        default=".*?",
+        help="Filter by function space, e.g. 'P1' or 'P1toP2' (regex).",
+        # TODO add choices list and type=string.lower
+    )
+
+    parser.add_argument(
+        "-f",
+        "--form",
+        default=".*?",
+        help="Filter by form, e.g. 'CurlCurl' or 'Diffusion' (regex).",
+        # TODO add choices list and type=string.lower
+    )
+
+    parser.add_argument(
+        "-o",
+        "--opts",
+        default="",
+        nargs="+",
+        help=f"""
+    Enter a space separated list of optimizations. 
+    Not all combinations are currently supported. Available optimizations are: {Opts.__members__.keys()}""",
+        # TODO add choices list and type=string.lower
+    )
+    # TODO explanations for optimizations e.g. CUTLOOPS is not straight-forward
+
+    parser.add_argument(
+        "--loop-strategy",
+        default="SAWTOOTH",
+        help="Enter a loop strategy for the operator. Available strategies are SAWTOOTH, CUBES.",
+        # TODO add choices list and type=string.lower
+    )
+
+    quad_rule_or_degree = parser.add_mutually_exclusive_group(required=False)
+    quad_rule_or_degree.add_argument(
+        "--quad-rule",
+        type=str,
+        nargs=2,
+        default=None,
+        help="""Supply two rules by their identifier (leftmost column when calling with --list-quadratures),
+                the first will be used for triangles and the second for tetrahedra.""",
+    )
+    quad_rule_or_degree.add_argument(
+        "--quad-degree",
+        type=int,
+        nargs=2,
+        default=[-1, -1],
+        help="""Supply two polynomial degrees (as integers) up to which the quadrature should be exact. 
+                The generator will chose the rule with minimal points that integrates the specified 
+                degree exactly. The first integer will be used for triangles and the second for tetrahedra.""",
+    )
+
+    prec_or_mpfr = parser.add_mutually_exclusive_group(required=False)
+    prec_or_mpfr.add_argument(
+        "-p",
+        "--precision",
+        type=str.lower,
+        help=(
+            "Defines the precision in which the kernels are calculated; "
+            "If not specified we choose fp64(double) precision"
+        ),
+        choices=list(choice.value for choice in HOGPrecision),
+        default="fp64",
+    )
+
+    parser.add_argument(
+        "-b",
+        "--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]}",
+        # TODO add choices list and type=string.lower
+    )
+
+    parser.add_argument(
+        "--name",
+        help=f"Specify the name of the generated C++ class.",
+    )
+
+    parser.add_argument(
+        "--dimensions",
+        type=int,
+        nargs="+",
+        default=[2, 3],
+        help=f"Domain dimensions for which operators shall be generated.",
+    )
+
+    logging_group = parser.add_mutually_exclusive_group(required=False)
+    logging_group.add_argument(
+        "-v",
+        "--verbose",
+        type=int,
+        help="specify verbosity level (0 = silent, 1 = info, 2 = debug)",
+        choices=[0, 1, 2],
+    )
+    logging_group.add_argument(
+        "--loglevel",
+        type=str.lower,
+        help=(
+            "Defines which messages are shown and which not; "
+            "If not specified 'warning' is chosen."
+        ),
+        choices=["debug", "info", "warning", "error", "critical"],
+    )
+
+    clang_format_group = parser.add_mutually_exclusive_group(required=False)
+    clang_format_group.add_argument(
+        "--no-clang-format",
+        dest="clang_format",
+        action="store_false",
+        help="Do not apply clang-format on generated files.",
+    )
+
+    clang_format_group.add_argument(
+        "--clang-format-binary",
+        default="clang-format",
+        help=f"Allows to specify the name of the clang-format binary and/or optionally its full path."
+        f" By default 'clang-format' will be used.",
+    )
+
+    args = parser.parse_args()
+    return parser, args
+
+
+strategy_args_mapping = {
+    "CUBES": CUBES(),
+    "SAWTOOTH": SAWTOOTH(),
+    "FUSEDROWS": FUSEDROWS(),
+}
+
+
+def main():
+    clear_cache()
+
+    parser, args = parse_arguments()
+
+    # Adapt clang_format_binary according to the value of clang_format
+    if "clang_format" in vars(args):
+        if not args.clang_format:
+            args.clang_format_binary = None
+
+    if args.verbose:
+        if args.verbose == 0:
+            log_level = logging.CRITICAL
+        elif args.verbose == 1:
+            log_level = logging.INFO
+        elif args.verbose == 2:
+            log_level = logging.DEBUG
+        else:
+            raise HOGException(f"Invalid verbosity level {args.verbose}.")
+    elif args.loglevel:
+        if args.loglevel == "debug":
+            log_level = logging.DEBUG
+        elif args.loglevel == "info":
+            log_level = logging.INFO
+        elif args.loglevel == "warning":
+            log_level = logging.WARNING
+        elif args.loglevel == "error":
+            log_level = logging.ERROR
+        elif args.loglevel == "critical":
+            log_level = logging.CRITICAL
+        else:
+            raise HOGException(f"Invalid log level {args.loglevel}.")
+    else:
+        # The default log level is chosen here
+        log_level = logging.DEBUG
+
+    logger = get_logger(log_level)
+    TimedLogger.set_log_level(log_level)
+
+    logger.info(f"Running {__file__} with arguments: {' '.join(sys.argv[1:])}")
+
+    symbolizer = Symbolizer()
+
+    if args.list_quadratures:
+        geometry = args.list_quadratures[0]
+        logger.debug(f"Listing quadrature rules for geometry: {geometry}.")
+        if geometry == "t3":
+            schemes = quadpy.t3.schemes
+        elif geometry == "t2":
+            schemes = quadpy.t2.schemes
+        else:
+            raise HOGException(f"Unexpected geometry: {geometry}")
+
+        all_schemes = []
+        for key, s in schemes.items():
+            try:
+                scheme = s()
+                entry = {
+                    "key": key,
+                    "name": scheme.name,
+                    "points": scheme.points.shape[1],
+                    "degree": scheme.degree,
+                    "test_tolerance": scheme.test_tolerance,
+                }
+                all_schemes.append(entry)
+            except TypeError as e:
+                # Some schemes seem to require parameters, we simply exclude these with this litte try-except hack
+                pass
+
+        all_schemes = sorted(all_schemes, key=itemgetter("degree", "points"))
+        table = tabulate(all_schemes, headers="keys", tablefmt="grid")
+        print(table)
+        exit()
+
+    blending = None
+    for m in ALL_GEOMETRY_MAPS:
+        if str(m) == args.blending:
+            blending = m
+    if blending is None:
+        raise HOGException("Invalid geometry map.")
+    logger.debug(f"Blending: {blending}")
+
+    type_descriptor = parse_argument_type(args)
+
+    logger.debug(f"Data type name is {type_descriptor.pystencils_type}.")
+
+    opts = {opts_arg_mapping[opt] for opt in args.opts}
+    loop_strategy = strategy_args_mapping[args.loop_strategy]
+    re_form = re.compile(args.form)
+
+    if (
+        not args.list_operators
+        and args.quad_degree == [-1, -1]
+        and args.quad_rule is None
+    ):
+        parser.error("Either --quad-degree or --quad-rule are required.")
+    quad = {
+        geometry: info
+        for geometry, info in zip(
+            [TriangleElement(), TetrahedronElement()],
+            args.quad_degree if args.quad_rule is None else args.quad_rule,
+        )
+    }
+
+    enabled_geometries: Set[TriangleElement | TetrahedronElement] = set()
+    if 2 in args.dimensions:
+        enabled_geometries.add(TriangleElement())
+    if 3 in args.dimensions:
+        enabled_geometries.add(TetrahedronElement())
+
+    operators = all_operators(
+        symbolizer,
+        [(opts, loop_strategy, ordered_opts_suffix(loop_strategy, opts))],
+        type_descriptor,
+        blending,
+        geometries=enabled_geometries,
+    )
+
+    filtered_operators = list(
+        filter(
+            lambda o: re.fullmatch(args.space_mapping, o.mapping, re.IGNORECASE)
+            and re.fullmatch(args.form, o.name, re.IGNORECASE),
+            operators,
+        )
+    )
+
+    filtered_operators = [f for f in filtered_operators if f.geometries]
+
+    if len(filtered_operators) == 0:
+        raise HOGException(
+            f"Form {args.form} does not match any available forms. Run --list for a list."
+        )
+
+    if args.list_operators:
+        print(
+            tabulate(
+                sorted(
+                    [
+                        (
+                            o.mapping,
+                            o.name,
+                            ", ".join(
+                                str(geo.dimensions) + "D" for geo in o.geometries
+                            ),
+                            len(o.opts),
+                        )
+                        for o in filtered_operators
+                    ]
+                ),
+                headers=(
+                    "Space mapping",
+                    "Form",
+                    "Dimensions",
+                    "Optimizations",
+                ),
+            )
+        )
+        exit()
+
+    if args.hyteg:
+        args.output = os.path.join(args.hyteg, "src/hyteg/operatorgeneration/generated")
+        if not valid_base_dir(args.hyteg):
+            raise HOGException(
+                "The specified directory does not seem to be the HyTeG directory."
+            )
+
+    for operator in filtered_operators:
+        for opt, loop, opt_name in operator.opts:
+            blending_str = (
+                "_" + str(blending) if not isinstance(blending, IdentityMap) else ""
+            )
+            if args.name:
+                name = args.name
+            else:
+                name = (
+                    f"{operator.mapping}Elementwise{operator.name}{blending_str}{opt_name}"
+                    f"{'_' + str(type_descriptor) if type_descriptor.add_file_suffix else ''}"
+                )
+            with TimedLogger(f"Generating {name}", level=logging.INFO):
+                generate_elementwise_op(
+                    args,
+                    symbolizer,
+                    operator,
+                    name,
+                    opt,
+                    loop,
+                    blending,
+                    quad,
+                    quad,
+                    type_descriptor=type_descriptor,
+                )
+
+
+def all_opts_sympy_cse() -> List[Tuple[Set[Opts], LoopStrategy, str]]:
+    return [
+        (
+            set(
+                {
+                    Opts.MOVECONSTANTS,
+                    Opts.REORDER,
+                    Opts.ELIMTMPS,
+                    Opts.QUADLOOPS,
+                    Opts.VECTORIZE,
+                }
+            ),
+            SAWTOOTH(),
+            "_const_vect_fused_quadloops_reordered_elimtmps",
+        ),
+    ]
+
+
+def all_opts_poly_cse() -> List[Tuple[Set[Opts], LoopStrategy, str]]:
+    return [
+        (o | {Opts.POLYCSE}, l, n + "_polycse") for (o, l, n) in all_opts_sympy_cse()
+    ]
+
+
+def all_opts_both_cses() -> List[Tuple[Set[Opts], LoopStrategy, str]]:
+    return all_opts_sympy_cse() + all_opts_poly_cse()
+
+
+@dataclass
+class OperatorInfo:
+    mapping: str
+    name: str
+    trial_space: TrialSpace
+    test_space: TestSpace
+    form: (
+        Callable[
+            [
+                TrialSpace,
+                TestSpace,
+                ElementGeometry,
+                Symbolizer,
+                GeometryMap,
+            ],
+            Form,
+        ]
+        | None
+    )
+    type_descriptor: HOGType
+    form_boundary: (
+        Callable[
+            [
+                TrialSpace,
+                TestSpace,
+                ElementGeometry,
+                ElementGeometry,
+                Symbolizer,
+                GeometryMap,
+            ],
+            Form,
+        ]
+        | None
+    ) = None
+    kernel_types: List[KernelWrapperType] = None  # type: ignore[assignment] # will definitely be initialized in __post_init__
+    geometries: Sequence[ElementGeometry] = field(
+        default_factory=lambda: [TriangleElement(), TetrahedronElement()]
+    )
+    opts: List[Tuple[Set[Opts], LoopStrategy, str]] = field(
+        default_factory=all_opts_sympy_cse
+    )
+    blending: GeometryMap = field(default_factory=lambda: IdentityMap())
+
+    def __post_init__(self):
+        # Removing geometries that are not supported by blending.
+        # I don't like this, but it looks like the collection of operators has to be refactored anyway.
+        self.geometries = list(
+            set(self.geometries) & set(self.blending.supported_geometries())
+        )
+
+        if self.kernel_types is None:
+            dims = [g.dimensions for g in self.geometries]
+            self.kernel_types = [
+                ApplyWrapper(
+                    self.trial_space,
+                    self.test_space,
+                    type_descriptor=self.type_descriptor,
+                    dims=dims,
+                )
+            ]
+
+            all_opts = set().union(*[o for (o, _, _) in self.opts])
+            if not ({Opts.VECTORIZE, Opts.VECTORIZE512}.intersection(all_opts)):
+                self.kernel_types.append(
+                    AssembleWrapper(
+                        self.trial_space,
+                        self.test_space,
+                        type_descriptor=self.type_descriptor,
+                        dims=dims,
+                    )
+                )
+
+            if self.test_space == self.trial_space:
+                self.kernel_types.append(
+                    AssembleDiagonalWrapper(
+                        self.trial_space,
+                        type_descriptor=self.type_descriptor,
+                        dims=dims,
+                    )
+                )
+
+
+def all_operators(
+    symbolizer: Symbolizer,
+    opts: List[Tuple[Set[Opts], LoopStrategy, str]],
+    type_descriptor: HOGType,
+    blending: GeometryMap,
+    geometries: Set[Union[TriangleElement, TetrahedronElement]],
+) -> List[OperatorInfo]:
+    P1 = LagrangianFunctionSpace(1, symbolizer)
+    P1Vector = TensorialVectorFunctionSpace(P1)
+    P2 = LagrangianFunctionSpace(2, symbolizer)
+    P2Vector = TensorialVectorFunctionSpace(P2)
+    N1E1 = N1E1Space(symbolizer)
+
+    two_d = list(geometries & {TriangleElement()})
+    three_d = list(geometries & {TetrahedronElement()})
+
+    ops: List[OperatorInfo] = []
+
+    # 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,
+                            type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
+    ops.append(OperatorInfo("N1E1", "CurlCurlPlusMass", TrialSpace(N1E1), TestSpace(N1E1),
+                            form=partial(curl_curl_plus_mass, alpha_fem_space=P1, beta_fem_space=P1),
+                            type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
+    ops.append(OperatorInfo("P1", "Diffusion", TrialSpace(P1), TestSpace(P1), form=diffusion,
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+    ops.append(OperatorInfo("P1", "DivKGrad", TrialSpace(P1), TestSpace(P1),
+                            form=partial(div_k_grad, coefficient_function_space=P1),
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P2", "Diffusion", TrialSpace(P2), TestSpace(P2), form=diffusion,
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+    ops.append(OperatorInfo("P2", "DivKGrad", TrialSpace(P2), TestSpace(P2),
+                            form=partial(div_k_grad, coefficient_function_space=P2),
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P2", "ShearHeating", TrialSpace(P2), TestSpace(P2),
+                            form=partial(shear_heating, viscosity_function_space=P2, velocity_function_space=P2),
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P1", "NonlinearDiffusion", TrialSpace(P1), TestSpace(P1),
+                            form=partial(nonlinear_diffusion, coefficient_function_space=P1),
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+    ops.append(OperatorInfo("P1", "NonlinearDiffusionNewtonGalerkin", TrialSpace(P1),
+                            TestSpace(P1), form=partial(nonlinear_diffusion_newton_galerkin,
+                            coefficient_function_space=P1, only_newton_galerkin_part_of_form=False),
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P1Vector", "Diffusion", TrialSpace(P1Vector), TestSpace(P1Vector),
+                            form=diffusion, type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P2", "SUPGDiffusion", TrialSpace(P2), TestSpace(P2), 
+                            form=partial(supg_diffusion, velocity_function_space=P2, diffusivityXdelta_function_space=P2), 
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+    
+    ops.append(OperatorInfo("P2", "AdvectionConstCp", TrialSpace(P2), TestSpace(P2), 
+                            form=partial(advection, velocity_function_space=P2, coefficient_function_space=P2, constant_cp = True), 
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P2", "AdvectionVarCp", TrialSpace(P2), TestSpace(P2), 
+                            form=partial(advection, velocity_function_space=P2, coefficient_function_space=P2), 
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+    
+    ops.append(OperatorInfo("P2", "SUPGAdvection", TrialSpace(P2), TestSpace(P2), 
+                            form=partial(supg_advection, velocity_function_space=P2, coefficient_function_space=P2), 
+                            type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P2", "BoundaryMass", TrialSpace(P2), TestSpace(P2), form=None,
+                            form_boundary=mass_boundary, type_descriptor=type_descriptor, geometries=list(geometries),
+                            opts=opts, blending=blending))
+
+    ops.append(OperatorInfo("P1Vector", "LinearElasticity", TrialSpace(P1Vector), TestSpace(P1Vector), form=linear_elasticity
+                            , form_boundary=mass_boundary, type_descriptor=type_descriptor,
+                            geometries=list(geometries), opts=opts, blending=blending))
+
+    # fmt: on
+
+    p2vec_epsilon = partial(
+        epsilon,
+        variable_viscosity=True,
+        coefficient_function_space=P2,
+    )
+    ops.append(
+        OperatorInfo(
+            f"P2Vector",
+            f"Epsilon",
+            TrialSpace(P2Vector),
+            TestSpace(P2Vector),
+            form=p2vec_epsilon,
+            type_descriptor=type_descriptor,
+            geometries=list(geometries),
+            opts=opts,
+            blending=blending,
+        )
+    )
+
+    p2vec_freeslip_momentum_weak_boundary = partial(
+        freeslip_momentum_weak_boundary, function_space_mu=P2
+    )
+
+    ops.append(
+        OperatorInfo(
+            f"P2Vector",
+            f"EpsilonFreeslip",
+            TrialSpace(P2Vector),
+            TestSpace(P2Vector),
+            form=p2vec_epsilon,
+            form_boundary=p2vec_freeslip_momentum_weak_boundary,
+            type_descriptor=type_descriptor,
+            geometries=list(geometries),
+            opts=opts,
+            blending=blending,
+        )
+    )
+
+    ops.append(
+        OperatorInfo(
+            f"P2VectorToP1",
+            f"DivergenceFreeslip",
+            TrialSpace(P2Vector),
+            TestSpace(P1),
+            form=divergence,
+            form_boundary=freeslip_divergence_weak_boundary,
+            type_descriptor=type_descriptor,
+            geometries=list(geometries),
+            opts=opts,
+            blending=blending,
+        )
+    )
+
+    ops.append(
+        OperatorInfo(
+            f"P1ToP2Vector",
+            f"GradientFreeslip",
+            TrialSpace(P1),
+            TestSpace(P2Vector),
+            form=gradient,
+            form_boundary=freeslip_gradient_weak_boundary,
+            type_descriptor=type_descriptor,
+            geometries=list(geometries),
+            opts=opts,
+            blending=blending,
+        )
+    )
+
+    p2vec_grad_rho = partial(
+        grad_rho_by_rho_dot_u,
+        density_function_space=P2,
+    )
+    ops.append(
+        OperatorInfo(
+            f"P2VectorToP1",
+            f"GradRhoByRhoDotU",
+            TrialSpace(P1),
+            TestSpace(P2Vector),
+            form=p2vec_grad_rho,
+            type_descriptor=type_descriptor,
+            geometries=list(geometries),
+            opts=opts,
+            blending=blending,
+        )
+    )
+
+    for c in [0, 1, 2]:
+        # fmt: off
+        if c == 2:
+            div_geometries = three_d
+        else:
+            div_geometries = list(geometries)
+        ops.append(OperatorInfo(f"P2ToP1", f"Div_{c}",
+                                TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), TestSpace(P1),
+                                form=partial(divergence, component_index=c),
+                                type_descriptor=type_descriptor, opts=opts, geometries=div_geometries,
+                                blending=blending))
+        ops.append(OperatorInfo(f"P1ToP2", f"DivT_{c}", TrialSpace(P1),
+                                TestSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
+                                form=partial(gradient, component_index=c),
+                                type_descriptor=type_descriptor, opts=opts, geometries=div_geometries,
+                                blending=blending))
+        # fmt: on
+
+    for c in [0, 1]:
+        for r in [0, 1]:
+            p2_epsilon = partial(
+                epsilon,
+                variable_viscosity=True,
+                coefficient_function_space=P2,
+                component_trial=c,
+                component_test=r,
+            )
+
+            p2_full_stokes = partial(
+                full_stokes,
+                variable_viscosity=True,
+                coefficient_function_space=P2,
+                component_trial=c,
+                component_test=r,
+            )
+            # fmt: off
+            ops.append(
+                OperatorInfo(f"P2", f"Epsilon_{r}_{c}",
+                             TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
+                             TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_epsilon,
+                             type_descriptor=type_descriptor, geometries=list(geometries), opts=opts,
+                             blending=blending))
+            ops.append(OperatorInfo(f"P2", f"FullStokes_{r}_{c}",
+                                    TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
+                                    TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)),
+                                    form=p2_full_stokes, type_descriptor=type_descriptor, geometries=list(geometries),
+                                    opts=opts, blending=blending))
+            # fmt: on
+    for c, r in [(0, 2), (1, 2), (2, 2), (2, 1), (2, 0)]:
+        p2_epsilon = partial(
+            epsilon,
+            variable_viscosity=True,
+            coefficient_function_space=P2,
+            component_trial=c,
+            component_test=r,
+        )
+
+        p2_full_stokes = partial(
+            full_stokes,
+            variable_viscosity=True,
+            coefficient_function_space=P2,
+            component_trial=c,
+            component_test=r,
+        )
+        # fmt: off
+        ops.append(
+            OperatorInfo(f"P2", f"Epsilon_{r}_{c}", TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
+                         TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_epsilon,
+                         type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
+        ops.append(
+            OperatorInfo(f"P2", f"FullStokes_{r}_{c}", TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)),
+                         TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_full_stokes,
+                         type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending))
+        # fmt: on
+
+    # Removing all operators without viable element types (e.g.: some ops only support 2D, but a blending map maybe only
+    # supports 3D).
+    ops = [o for o in ops if o.geometries]
+
+    return ops
+
+
+def generate_elementwise_op(
+    args: argparse.Namespace,
+    symbolizer: Symbolizer,
+    op_info: OperatorInfo,
+    name: str,
+    optimizations: Set[Opts],
+    loop_strategy: LoopStrategy,
+    blending: GeometryMap,
+    quad_info: dict[ElementGeometry, int | str],
+    quad_info_boundary: dict[ElementGeometry, int | str],
+    type_descriptor: HOGType,
+) -> None:
+    """Generates a single operator and writes it to the HyTeG directory."""
+
+    operator = HyTeGElementwiseOperator(
+        name,
+        symbolizer,
+        kernel_wrapper_types=op_info.kernel_types,
+        type_descriptor=type_descriptor,
+    )
+
+    for geometry in op_info.geometries:
+        # Is this necessary? Currently, the decision of the geometry is all over the place.
+        if geometry not in blending.supported_geometries():
+            continue
+
+        if op_info.form is not None:
+            quad = Quadrature(
+                select_quadrule(quad_info[geometry], geometry),
+                geometry,
+            )
+
+            form = op_info.form(
+                op_info.trial_space,
+                op_info.test_space,
+                geometry,
+                symbolizer,
+                blending=blending,  # type: ignore[call-arg] # kw-args are not supported by Callable
+            )
+
+            operator.add_volume_integral(
+                name="".join(name.split()),
+                volume_geometry=geometry,
+                quad=quad,
+                blending=blending,
+                form=form,
+                loop_strategy=loop_strategy,
+                optimizations=optimizations,
+            )
+
+        if op_info.form_boundary is not None:
+            boundary_geometry: ElementGeometry
+            if geometry == TriangleElement():
+                boundary_geometry = LineElement(space_dimension=2)
+            elif geometry == TetrahedronElement():
+                boundary_geometry = TriangleElement(space_dimension=3)
+            else:
+                raise HOGException("Invalid volume geometry for boundary integral.")
+
+            quad = Quadrature(
+                select_quadrule(quad_info_boundary[geometry], boundary_geometry),
+                boundary_geometry,
+            )
+
+            form_boundary = op_info.form_boundary(
+                op_info.trial_space,
+                op_info.test_space,
+                geometry,
+                boundary_geometry,
+                symbolizer,
+                blending=blending,  # type: ignore[call-arg] # kw-args are not supported by Callable
+            )
+
+            operator.add_boundary_integral(
+                name="".join(name.split()),
+                volume_geometry=geometry,
+                quad=quad,
+                blending=blending,
+                form=form_boundary,
+                optimizations=set(),
+            )
+
+    dir_path = os.path.join(args.output, op_info.name.split("_")[0])
+    operator.generate_class_code(
+        dir_path,
+        clang_format_binary=args.clang_format_binary,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hog/forms.py b/hog/forms.py
index 73a03f83620437b35ed6bcf9ca2ef285ed41f91e..58e7ec47f8cb8e84464bc32afb670c641493122b 100644
--- a/hog/forms.py
+++ b/hog/forms.py
@@ -1,1180 +1,1180 @@
-# 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 logging
-import sympy as sp
-from typing import Optional, Callable, Any
-
-from hog.element_geometry import ElementGeometry
-from hog.exception import HOGException
-from hog.fem_helpers import (
-    trafo_ref_to_affine,
-    jac_ref_to_affine,
-    jac_affine_to_physical,
-    hessian_shape_affine_ref_pullback,
-    hessian_shape_blending_ref_pullback,
-    create_empty_element_matrix,
-    element_matrix_iterator,
-    scalar_space_dependent_coefficient,
-    vector_space_dependent_coefficient,
-    fem_function_on_element,
-    fem_function_gradient_on_element,
-)
-from hog.function_space import FunctionSpace, N1E1Space, TrialSpace, TestSpace
-from hog.math_helpers import dot, inv, abs, det, double_contraction
-from hog.quadrature import Quadrature, Tabulation
-from hog.symbolizer import Symbolizer
-from hog.logger import TimedLogger
-from hog.blending import GeometryMap, ExternalMap, IdentityMap
-from hog.integrand import process_integrand, Form
-
-
-def diffusion(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-Diffusion operator without coefficients.
-
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-    
-    ∫ ∇u : ∇v
-    
-    Note that the double contraction (:) reduces to the dot product for scalar function spaces, i.e. the form becomes
-    
-    ∫ ∇u · ∇v 
-"""
-
-    from hog.recipes.integrands.volume.diffusion import integrand
-    from hog.recipes.integrands.volume.diffusion_affine import (
-        integrand as integrand_affine,
-    )
-
-    # mypy type checking supposedly cannot figure out ternaries.
-    # https://stackoverflow.com/a/70832391
-    integr: Callable[..., Any] = integrand
-    if blending == IdentityMap():
-        integr = integrand_affine
-
-    return process_integrand(
-        integr,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def mass(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-Mass operator.
-
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-
-    ∫ uv
-"""
-
-    from hog.recipes.integrands.volume.mass import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def div_k_grad(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    coefficient_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-Diffusion operator with a scalar coefficient.
-
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-    k: coefficient    (space: {coefficient_function_space})
-
-    ∫ k ∇u · ∇v
-"""
-
-    from hog.recipes.integrands.volume.div_k_grad import integrand
-    from hog.recipes.integrands.volume.div_k_grad_affine import (
-        integrand as integrand_affine,
-    )
-
-    # mypy type checking supposedly cannot figure out ternaries.
-    # https://stackoverflow.com/a/70832391
-    integr: Callable[..., Any] = integrand
-    if blending == IdentityMap():
-        integr = integrand_affine
-
-    return process_integrand(
-        integr,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        fe_coefficients={"k": coefficient_function_space},
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def nonlinear_diffusion(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    coefficient_function_space: FunctionSpace,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-Diffusion operator with coefficient function.
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-    c: FE coefficient function (space: {coefficient_function_space})
-    
-    ∫ a(c) ∇u · ∇v
-
-Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended for :math:`c = u`,
-      hence, the naming, but will work for other coefficients, too.
-"""
-
-    if blending != IdentityMap():
-        raise HOGException(
-            "The nonlinear-diffusion form does currently not support blending."
-        )
-
-    if trial != test:
-        raise HOGException(
-            "Trial space must be equal to test space to assemble non-linear diffusion matrix."
-        )
-
-    def integrand(
-        *,
-        jac_a_inv,
-        jac_a_abs_det,
-        grad_u,
-        grad_v,
-        k,
-        tabulate,
-        **_,
-    ):
-        a = sp.Rational(1, 8) + k["u"] * k["u"]
-        return a * tabulate(
-            double_contraction(
-                jac_a_inv.T * grad_u,
-                jac_a_inv.T * grad_v,
-            )
-            * jac_a_abs_det
-        )
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-        fe_coefficients={"u": coefficient_function_space},
-    )
-
-
-def nonlinear_diffusion_newton_galerkin(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    coefficient_function_space: FunctionSpace,
-    blending: GeometryMap = IdentityMap(),
-    only_newton_galerkin_part_of_form: Optional[bool] = True,
-) -> Form:
-    docstring = f"""
-
-Bi-linear form for the solution of the non-linear diffusion equation by a Newton-Galerkin approach
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-    k: FE coefficient function (space: {coefficient_function_space})
-    
-    ∫ a(k) ∇u · ∇v + ∫ a'(k) u ∇k · ∇v
-
-Note: :math:`a(k) = 1/8 + k^2` is currently hard-coded and the form is intended for :math:`k = u`.
-"""
-    if trial != test:
-        raise HOGException(
-            "Trial space must be equal to test space to assemble diffusion matrix."
-        )
-
-    if blending != IdentityMap():
-        raise HOGException(
-            "The nonlinear_diffusion_newton_galerkin form does currently not support blending."
-        )
-
-    def integrand(
-        *,
-        jac_a_inv,
-        jac_a_abs_det,
-        u,
-        grad_u,
-        grad_v,
-        k,
-        grad_k,
-        tabulate,
-        **_,
-    ):
-        a = sp.Rational(1, 8) + k["k"] * k["k"]
-        a_prime = 2 * k["k"]
-
-        diffusion_term = a * tabulate(
-            dot(jac_a_inv.T * grad_u, jac_a_inv.T * grad_v) * jac_a_abs_det
-        )
-
-        newton_galerkin_term = (
-            a_prime
-            * u
-            * dot(jac_a_inv.T * grad_k["k"], tabulate(jac_a_inv.T * grad_v))
-            * tabulate(jac_a_abs_det)
-        )
-
-        if only_newton_galerkin_part_of_form:
-            return newton_galerkin_term
-        else:
-            return diffusion_term + newton_galerkin_term
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=False,
-        docstring=docstring,
-        fe_coefficients={"k": coefficient_function_space},
-    )
-
-
-def epsilon(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    component_trial: int = 0,
-    component_test: int = 0,
-    variable_viscosity: bool = True,
-    coefficient_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-"Epsilon" operator.
-
-Geometry map:    {blending}
-
-Weak formulation
-
-    u: trial function (vectorial space: {trial})
-    v: test function  (vectorial space: {test})
-    μ: coefficient    (scalar space:    {coefficient_function_space})
-
-    ∫ 2 μ ε(u) : ε(v)
-    
-where
-    
-    ε(w) := (1/2) (∇w + (∇w)ᵀ)
-"""
-    if not variable_viscosity:
-        raise HOGException("Constant viscosity currently not supported.")
-
-    from hog.recipes.integrands.volume.epsilon import integrand
-    from hog.recipes.integrands.volume.epsilon_affine import (
-        integrand as integrand_affine,
-    )
-
-    # mypy type checking supposedly cannot figure out ternaries.
-    # https://stackoverflow.com/a/70832391
-    integr: Callable[..., Any] = integrand
-    if blending == IdentityMap():
-        integr = integrand_affine
-
-    return process_integrand(
-        integr,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-        fe_coefficients={"mu": coefficient_function_space},
-    )
-
-
-def k_mass(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    coefficient_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-Diffusion operator with a scalar coefficient.
-
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-    k: coefficient    (space: {coefficient_function_space})
-
-    ∫ k uv
-"""
-
-    from hog.recipes.integrands.volume.k_mass import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def pspg(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    quad: Quadrature,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-PSPG stabilisation.
-
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (space: {trial})
-    v: test function  (space: {test})
-
-    ∫ τ ∇u · ∇v
-
-    where tau is an element-wise factor given by
-
-    2D: 
-        τ = -CellVolume(triangle) / 5               
-    3D:
-        τ = -(CellVolume(tetrahedron))**(2/3) / 12
-
-See e.g. 
-
-    Brezzi, F., & Douglas, J. (1988). 
-    Stabilized mixed methods for the Stokes problem. 
-    Numerische Mathematik, 53, 225-235.
-
-or 
-
-    Hughes, T. J., Franca, L. P., & Balestra, M. (1986). 
-    A new finite element formulation for computational fluid dynamics: V. 
-    Circumventing the Babuška-Brezzi condition: A stable Petrov-Galerkin formulation of the Stokes problem accommodating
-    equal-order interpolations. 
-    Computer Methods in Applied Mechanics and Engineering, 59(1), 85-99.
-
-for details.
-"""
-
-    from hog.recipes.integrands.volume.pspg import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def linear_form(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    quad: Quadrature,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-) -> sp.Matrix:
-    """
-    Implements a linear form of type:
-
-        (k(x), psi)
-
-    where psi a test function and k = k(x) a scalar, external function.
-    """
-
-    if trial != test:
-        raise HOGException(
-            "Trial space must be equal to test space to assemble linear form (jep this is weird, but linear forms are implemented as diagonal matrices)."
-        )
-
-    if quad.is_exact() and isinstance(blending, ExternalMap):
-        raise HOGException(
-            "Exact integration is not supported for externally defined blending functions."
-        )
-
-    with TimedLogger("assembling linear form", level=logging.DEBUG):
-        jac_affine = jac_ref_to_affine(geometry, symbolizer)
-        jac_affine_det = abs(det(jac_affine))
-
-        if isinstance(blending, ExternalMap):
-            jac_blending = jac_affine_to_physical(geometry, symbolizer)
-        else:
-            affine_coords = trafo_ref_to_affine(geometry, symbolizer)
-            jac_blending = blending.jacobian(affine_coords)
-
-        jac_blending_det = abs(det(jac_blending))
-
-        mat = create_empty_element_matrix(trial, test, geometry)
-
-        it = element_matrix_iterator(trial, test, geometry)
-
-        if isinstance(trial, N1E1Space):
-            jac_affine_inv_T = inv(jac_affine).T
-            jac_blending_inv_T = inv(jac_blending).T
-            space_dependent_coefficient = vector_space_dependent_coefficient
-        else:
-            space_dependent_coefficient = scalar_space_dependent_coefficient
-        k = space_dependent_coefficient("k", geometry, symbolizer, blending=blending)
-
-        with TimedLogger(
-            f"integrating {mat.shape[0]} expressions",
-            level=logging.DEBUG,
-        ):
-            for data in it:
-                if data.row == data.col:
-                    psi = data.test_shape
-
-                    if isinstance(trial, N1E1Space):
-                        form = (
-                            dot(k, jac_blending_inv_T * jac_affine_inv_T * psi)
-                            * jac_affine_det
-                            * jac_blending_det
-                        )
-                    else:
-                        form = k * psi * jac_affine_det * jac_blending_det
-
-                    mat[data.row, data.col] = quad.integrate(form, symbolizer)
-
-    return mat
-
-
-def divergence(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    component_index: int = 0,
-) -> Form:
-    docstring = f"""
-Divergence.
-
-Component:    {component_index}
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (vectorial space: {trial})
-    v: test function  (scalar space:    {test})
-
-    ∫ - ( ∇ · u ) v
-"""
-
-    from hog.recipes.integrands.volume.divergence import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=False,
-        docstring=docstring,
-    )
-
-
-def gradient(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    component_index: int = 0,
-) -> Form:
-    docstring = f"""
-    Gradient.
-
-    Component:    {component_index}
-    Geometry map: {blending}
-
-    Weak formulation
-
-        u: trial function (scalar space:    {trial})
-        v: test function  (vectorial space: {test})
-
-        ∫ - ( ∇ · v ) u
-    """
-
-    from hog.recipes.integrands.volume.gradient import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=False,
-        docstring=docstring,
-    )
-
-
-def full_stokes(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    component_trial: int = 0,
-    component_test: int = 0,
-    variable_viscosity: bool = True,
-    coefficient_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-Implements the fully coupled viscous operator of the Stokes problem.
-The latter is the extension of the Epsilon operator to the case where
-the velocity field need not be divergence-free. This is e.g. the case
-in the (truncated) anelastic liquid approximation of mantle convection.
-
-The strong representation of the operator is given by:
-
-   - div[ μ (grad(u)+grad(u)ᵀ) ] + 2/3 grad[ μ div(u) ]
-
-Note that the factor 2/3 means that for 2D this is the pseudo-3D form
-of the operator.
-
-Component trial: {component_trial}
-Component test:  {component_test}
-Geometry map:    {blending}
-
-Weak formulation
-
-    u: trial function (vectorial space: {trial})
-    v: test function  (vectorial space: {test})
-    μ: coefficient    (scalar space:    {coefficient_function_space})
-
-    ∫ μ {{ ( 2 ε(u) : ε(v) ) - (2/3) [ ( ∇ · u ) · ( ∇ · v ) ] }}
-    
-where
-    
-    ε(w) := (1/2) (∇w + (∇w)ᵀ)
-"""
-
-    from hog.recipes.integrands.volume.full_stokes import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-        fe_coefficients={"mu": coefficient_function_space},
-    )
-
-
-def shear_heating(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    component_trial: int = 0,
-    component_test: int = 0,
-    variable_viscosity: bool = True,
-    viscosity_function_space: Optional[FunctionSpace] = None,
-    velocity_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-Implements the fully coupled viscous operator for the shear heating term.
-The latter is the extension of the Epsilon operator to the case where
-the velocity field need not be divergence-free. This is e.g. the case
-in the (truncated) anelastic liquid approximation of mantle convection.
-
-https://doi.org/10.1111/j.1365-246X.2009.04413.x
-(3) and (5)
-
-https://doi.org/10.5194/gmd-15-5127-2022
-Listing 2
-
-The strong representation of the operator is given by:
-
-    𝜏(w) : grad(w)
-    2 {{[ μ (grad(w)+grad(w)ᵀ) / 2 ] - 1/dim [ μ div(w) ]I}} : grad(w)
-
-Note that the factor 1/dim means that for 2D this is the pseudo-3D form
-of the operator.
-
-Component trial: {component_trial}
-Component test:  {component_test}
-Geometry map:    {blending}
-
-Weak formulation
-
-    T: trial function (scalar space:    {trial})
-    s: test function  (scalar space:    {test})
-    μ: coefficient    (scalar space:    {viscosity_function_space})
-    w: velocity       (vectorial space: {velocity_function_space})
-
-    ∫ {{ 2 {{[ μ (grad(w)+grad(w)ᵀ) / 2 ] - 1/dim [ μ div(w) ]I}} : grad(w) }} T_h s_h
-    
-The resulting matrix must be multiplied with a vector of ones to be used as the shear heating term in the RHS
-"""
-
-    def integrand(
-        *,
-        jac_a_inv,
-        jac_b_inv,
-        jac_a_abs_det,
-        jac_b_abs_det,
-        u,
-        v,
-        k,
-        grad_k,
-        volume_geometry,
-        tabulate,
-        **_,
-    ):
-        """First function: mu, other functions: ux, uy, uz."""
-
-        mu = k["mu"]
-
-        # grad_k[0] is grad_mu_ref
-        grad_wx = jac_b_inv.T * jac_a_inv.T * grad_k["wx"]
-        grad_wy = jac_b_inv.T * jac_a_inv.T * grad_k["wy"]
-        grad_wz = jac_b_inv.T * jac_a_inv.T * grad_k["wz"]
-
-        grad_w = grad_wx.row_join(grad_wy)
-        dim = volume_geometry.dimensions
-        if dim == 3:
-            grad_w = grad_w.row_join(grad_wz)
-
-        sym_grad_w = 0.5 * (grad_w + grad_w.T)
-
-        divdiv = grad_w.trace() * sp.eye(dim)
-
-        tau = 2 * (sym_grad_w - sp.Rational(1, dim) * divdiv)
-
-        return (
-            mu
-            * (double_contraction(tau, grad_w)[0])
-            * jac_b_abs_det
-            * tabulate(jac_a_abs_det * u * v)
-        )
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        fe_coefficients={
-            "mu": viscosity_function_space,
-            "wx": velocity_function_space,
-            "wy": velocity_function_space,
-            "wz": velocity_function_space,
-        },
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def divdiv(
-    trial: TrialSpace,
-    test: TestSpace,
-    component_trial: int,
-    component_test: int,
-    geometry: ElementGeometry,
-    quad: Quadrature,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-divdiv operator which is used as a stabilization term that is taken from
-
-    Blank, L. (2014).
-    On Divergence-Free Finite Element Methods for the Stokes Equations (Freie Universität Berlin).
-    p. 84, eq. (6.2)
-
-for the P1-P0 stabilized Stokes discretization.
-
-Component trial: {component_trial}
-Component test:  {component_test}
-Geometry map:    {blending}
-
-Weak formulation
-
-    u: trial function (vectorial space: {trial})
-    v: test function  (vectorial space: {test})
-
-    ∫ ( ∇ · u ) · ( ∇ · v )
-"""
-
-    from hog.recipes.integrands.volume.divdiv import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-    )
-
-
-def advection(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    velocity_function_space: FunctionSpace,
-    coefficient_function_space: FunctionSpace,
-    constant_cp: bool = False,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-advection operator which needs to be used in combination with SUPG
-
-Geometry map:    {blending}
-
-Weak formulation
-
-    T: trial function (scalar space: {trial})
-    s: test function  (scalar space: {test})
-    u: velocity function (vectorial space: {velocity_function_space})
-
-    ∫ cp ( u · ∇T ) s
-"""
-
-    from hog.recipes.integrands.volume.advection import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=False,
-        docstring=docstring,
-        fe_coefficients={
-            "ux": velocity_function_space,
-            "uy": velocity_function_space,
-            "uz": velocity_function_space,
-        } if constant_cp else {
-            "ux": velocity_function_space,
-            "uy": velocity_function_space,
-            "uz": velocity_function_space,
-            "cp": coefficient_function_space,
-        },
-    )
-
-
-def supg_advection(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    velocity_function_space: FunctionSpace,
-    coefficient_function_space: FunctionSpace,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-advection operator which needs to be used in combination with SUPG
-
-Geometry map:    {blending}
-
-Weak formulation
-
-    T: trial function (scalar space: {trial})
-    s: test function  (scalar space: {test})
-    u: velocity function (vectorial space: {velocity_function_space})
-
-    ∫ cp ( u · ∇T ) 𝛿(u · ∇s)
-"""
-
-    from hog.recipes.integrands.volume.supg_advection import integrand
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=False,
-        docstring=docstring,
-        fe_coefficients={
-            "ux": velocity_function_space,
-            "uy": velocity_function_space,
-            "uz": velocity_function_space,
-            "cp_times_delta": coefficient_function_space,
-        },
-    )
-
-
-def supg_diffusion(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    velocity_function_space: FunctionSpace,
-    diffusivityXdelta_function_space: FunctionSpace,
-    blending: GeometryMap = IdentityMap(),
-) -> Form:
-    docstring = f"""
-Form for SUPGDiffusion operator used for SUPG stabilisation
-
-Geometry map: {blending}
-
-Weak formulation
-
-    T: trial function (space: {trial})
-    s: test function  (space: {test})
-    w: velocity function (space: {velocity_function_space})
-   k𝛿: FE function representing k·𝛿 (space: {diffusivityXdelta_function_space})
-    
-    For OpGen,
-
-    ∫ k(ΔT) · 𝛿(w · ∇s)
-
-    -------------------
-
-    For ExternalMap (only for testing, currently not supported),
-
-    ∫ (ΔT) s
-"""
-
-    def integrand(
-        *,
-        jac_a_inv,
-        jac_b_inv,
-        hessian_b,
-        jac_a_abs_det,
-        jac_b_abs_det,
-        grad_u,
-        grad_v,
-        hessian_u,
-        k,
-        volume_geometry,
-        tabulate,
-        **_,
-    ):
-        """First function: k𝛿, other functions: ux, uy, uz."""
-
-        k_times_delta = k["diffusivity_times_delta"]
-        wx = k["wx"]
-        wy = k["wy"]
-        wz = k["wz"]
-
-        dim = volume_geometry.dimensions
-        if dim == 2:
-            w = sp.Matrix([[wx], [wy]])
-        elif dim == 3:
-            w = sp.Matrix([[wx], [wy], [wz]])
-
-        if isinstance(blending, IdentityMap):
-            hessian_affine = hessian_shape_affine_ref_pullback(hessian_u, jac_a_inv)
-
-            laplacian = sum([hessian_affine[i, i] for i in range(geometry.dimensions)])
-
-            form = (
-                k_times_delta
-                * tabulate(laplacian)
-                * dot(w, tabulate(jac_a_inv.T * grad_v))
-                * jac_a_abs_det
-            )
-
-        else:
-            hessian_blending = hessian_shape_blending_ref_pullback(
-                geometry,
-                grad_u,
-                hessian_u,
-                jac_a_inv,
-                hessian_b,
-                jac_b_inv,
-            )
-
-            laplacian = sum(
-                [hessian_blending[i, i] for i in range(geometry.dimensions)]
-            )
-
-            form = (
-                laplacian
-                * dot(w, jac_b_inv.T * tabulate(jac_a_inv.T * grad_v))
-                * k_times_delta
-                * jac_a_abs_det
-                * jac_b_abs_det
-            )
-
-        return form
-
-    return process_integrand(
-        integrand,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        fe_coefficients={
-            "diffusivity_times_delta": diffusivityXdelta_function_space,
-            "wx": velocity_function_space,
-            "wy": velocity_function_space,
-            "wz": velocity_function_space,
-        },
-    )
-
-
-def grad_rho_by_rho_dot_u(
-    trial: TrialSpace,
-    test: TestSpace,
-    geometry: ElementGeometry,
-    symbolizer: Symbolizer,
-    blending: GeometryMap = IdentityMap(),
-    density_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-RHS operator for the frozen velocity approach.
-
-Geometry map: {blending}
-
-Weak formulation
-
-    u: trial function (vectorial space: {trial})
-    v: test function  (space: {test})
-    rho: coefficient    (space: {density_function_space})
-
-    ∫ ((∇ρ / ρ) · u) v
-"""
-
-    with TimedLogger(
-        "assembling grad_rho_by_rho_dot_u-mass matrix", level=logging.DEBUG
-    ):
-        tabulation = Tabulation(symbolizer)
-
-        jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
-
-        if isinstance(blending, ExternalMap):
-            jac_blending = jac_affine_to_physical(geometry, symbolizer)
-        else:
-            affine_coords = trafo_ref_to_affine(geometry, symbolizer)
-            # jac_blending = blending.jacobian(affine_coords)
-            jac_blending_inv = symbolizer.jac_affine_to_blending_inv(
-                geometry.dimensions
-            )
-            jac_blending_det = symbolizer.abs_det_jac_affine_to_blending()
-
-        # jac_blending_det = abs(det(jac_blending))
-
-        mat = create_empty_element_matrix(trial, test, geometry)
-
-        it = element_matrix_iterator(trial, test, geometry)
-
-        if density_function_space:
-            rho, dof_symbols = fem_function_on_element(
-                density_function_space,
-                geometry,
-                symbolizer,
-                domain="reference",
-                function_id="rho",
-            )
-
-            grad_rho, _ = fem_function_gradient_on_element(
-                density_function_space,
-                geometry,
-                symbolizer,
-                domain="reference",
-                function_id="grad_rho",
-                dof_symbols=dof_symbols,
-            )
-
-        with TimedLogger(
-            f"integrating {mat.shape[0] * mat.shape[1]} expressions",
-            level=logging.DEBUG,
-        ):
-            for data in it:
-                phi = data.trial_shape
-                psi = data.test_shape
-                # print("phi = ", psi.shape)
-                # phi_psi_jac_affine_det = tabulation.register_factor(
-                #     "phi_psi_jac_affine_det",
-                #     sp.Matrix([phi * psi * jac_affine_det]),
-                # )[0]
-                if blending == IdentityMap():
-                    form = (
-                        dot(((jac_affine_inv.T * grad_rho) / rho[0]), phi)
-                        * psi
-                        * jac_affine_det
-                    )
-                else:
-                    form = (
-                        dot(
-                            (
-                                (jac_blending_inv.T * jac_affine_inv.T * grad_rho)
-                                / rho[0]
-                            ),
-                            phi,
-                        )
-                        * psi
-                        * jac_affine_det
-                        * jac_blending_det
-                    )
-                mat[data.row, data.col] = form
-
-    return Form(mat, tabulation, symmetric=trial == test, docstring=docstring)
-
-
-def zero_form(
-    trial: FunctionSpace, test: FunctionSpace, geometry: ElementGeometry
-) -> sp.Matrix:
-    rows = test.num_dofs(geometry)
-    cols = trial.num_dofs(geometry) if trial is not None else 1
-    return sp.zeros(rows, cols)
-
-
-def linear_elasticity(
-        trial: FunctionSpace,
-        test: FunctionSpace,
-        geometry: ElementGeometry,
-        symbolizer: Symbolizer,
-        blending: GeometryMap = IdentityMap(),
-        component_trial: int = 0,
-        component_test: int = 0,
-        variable_viscosity: bool = True,
-        coefficient_function_space: Optional[FunctionSpace] = None,
-) -> Form:
-    docstring = f"""
-"Linear elasticity" operator.
-
-Geometry map:    {blending}
-
-Weak formulation
-
-    u: trial function (vectorial space: {trial})
-    v: test function  (vectorial space: {test})
-    λ: coefficient    (scalar space:    {coefficient_function_space})
-    μ: coefficient    (scalar space:    {coefficient_function_space})
-
-    ∫ σ(u) : ε(v)
-
-where
-
-    σ(w) := λ(∇.w)I + μ(∇w + (∇w)ᵀ)
-    ε(w) := (1/2) (∇w + (∇w)ᵀ)
-"""
-    if not variable_viscosity:
-        raise HOGException("Constant viscosity currently not supported.")
-
-    from hog.recipes.integrands.volume.elasticity import integrand
-    from hog.recipes.integrands.volume.elasticity_affine import (
-        integrand as integrand_affine,
-    )
-
-    # mypy type checking supposedly cannot figure out ternaries.
-    # https://stackoverflow.com/a/70832391
-    integr: Callable[..., Any] = integrand
-    if blending == IdentityMap():
-        integr = integrand_affine
-
-    return process_integrand(
-        integr,
-        trial,
-        test,
-        geometry,
-        symbolizer,
-        blending=blending,
-        is_symmetric=trial == test,
-        docstring=docstring,
-        fe_coefficients={"mu": coefficient_function_space, "lambda": coefficient_function_space,},
-    )
+# 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 logging
+import sympy as sp
+from typing import Optional, Callable, Any
+
+from hog.element_geometry import ElementGeometry
+from hog.exception import HOGException
+from hog.fem_helpers import (
+    trafo_ref_to_affine,
+    jac_ref_to_affine,
+    jac_affine_to_physical,
+    hessian_shape_affine_ref_pullback,
+    hessian_shape_blending_ref_pullback,
+    create_empty_element_matrix,
+    element_matrix_iterator,
+    scalar_space_dependent_coefficient,
+    vector_space_dependent_coefficient,
+    fem_function_on_element,
+    fem_function_gradient_on_element,
+)
+from hog.function_space import FunctionSpace, N1E1Space, TrialSpace, TestSpace
+from hog.math_helpers import dot, inv, abs, det, double_contraction
+from hog.quadrature import Quadrature, Tabulation
+from hog.symbolizer import Symbolizer
+from hog.logger import TimedLogger
+from hog.blending import GeometryMap, ExternalMap, IdentityMap
+from hog.integrand import process_integrand, Form
+
+
+def diffusion(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Diffusion operator without coefficients.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    
+    ∫ ∇u : ∇v
+    
+    Note that the double contraction (:) reduces to the dot product for scalar function spaces, i.e. the form becomes
+    
+    ∫ ∇u · ∇v 
+"""
+
+    from hog.recipes.integrands.volume.diffusion import integrand
+    from hog.recipes.integrands.volume.diffusion_affine import (
+        integrand as integrand_affine,
+    )
+
+    # mypy type checking supposedly cannot figure out ternaries.
+    # https://stackoverflow.com/a/70832391
+    integr: Callable[..., Any] = integrand
+    if blending == IdentityMap():
+        integr = integrand_affine
+
+    return process_integrand(
+        integr,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def mass(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Mass operator.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+
+    ∫ uv
+"""
+
+    from hog.recipes.integrands.volume.mass import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def div_k_grad(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    coefficient_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+Diffusion operator with a scalar coefficient.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    k: coefficient    (space: {coefficient_function_space})
+
+    ∫ k ∇u · ∇v
+"""
+
+    from hog.recipes.integrands.volume.div_k_grad import integrand
+    from hog.recipes.integrands.volume.div_k_grad_affine import (
+        integrand as integrand_affine,
+    )
+
+    # mypy type checking supposedly cannot figure out ternaries.
+    # https://stackoverflow.com/a/70832391
+    integr: Callable[..., Any] = integrand
+    if blending == IdentityMap():
+        integr = integrand_affine
+
+    return process_integrand(
+        integr,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        fe_coefficients={"k": coefficient_function_space},
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def nonlinear_diffusion(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    coefficient_function_space: FunctionSpace,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Diffusion operator with coefficient function.
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    c: FE coefficient function (space: {coefficient_function_space})
+    
+    ∫ a(c) ∇u · ∇v
+
+Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended for :math:`c = u`,
+      hence, the naming, but will work for other coefficients, too.
+"""
+
+    if blending != IdentityMap():
+        raise HOGException(
+            "The nonlinear-diffusion form does currently not support blending."
+        )
+
+    if trial != test:
+        raise HOGException(
+            "Trial space must be equal to test space to assemble non-linear diffusion matrix."
+        )
+
+    def integrand(
+        *,
+        jac_a_inv,
+        jac_a_abs_det,
+        grad_u,
+        grad_v,
+        k,
+        tabulate,
+        **_,
+    ):
+        a = sp.Rational(1, 8) + k["u"] * k["u"]
+        return a * tabulate(
+            double_contraction(
+                jac_a_inv.T * grad_u,
+                jac_a_inv.T * grad_v,
+            )
+            * jac_a_abs_det
+        )
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+        fe_coefficients={"u": coefficient_function_space},
+    )
+
+
+def nonlinear_diffusion_newton_galerkin(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    coefficient_function_space: FunctionSpace,
+    blending: GeometryMap = IdentityMap(),
+    only_newton_galerkin_part_of_form: Optional[bool] = True,
+) -> Form:
+    docstring = f"""
+
+Bi-linear form for the solution of the non-linear diffusion equation by a Newton-Galerkin approach
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    k: FE coefficient function (space: {coefficient_function_space})
+    
+    ∫ a(k) ∇u · ∇v + ∫ a'(k) u ∇k · ∇v
+
+Note: :math:`a(k) = 1/8 + k^2` is currently hard-coded and the form is intended for :math:`k = u`.
+"""
+    if trial != test:
+        raise HOGException(
+            "Trial space must be equal to test space to assemble diffusion matrix."
+        )
+
+    if blending != IdentityMap():
+        raise HOGException(
+            "The nonlinear_diffusion_newton_galerkin form does currently not support blending."
+        )
+
+    def integrand(
+        *,
+        jac_a_inv,
+        jac_a_abs_det,
+        u,
+        grad_u,
+        grad_v,
+        k,
+        grad_k,
+        tabulate,
+        **_,
+    ):
+        a = sp.Rational(1, 8) + k["k"] * k["k"]
+        a_prime = 2 * k["k"]
+
+        diffusion_term = a * tabulate(
+            dot(jac_a_inv.T * grad_u, jac_a_inv.T * grad_v) * jac_a_abs_det
+        )
+
+        newton_galerkin_term = (
+            a_prime
+            * u
+            * dot(jac_a_inv.T * grad_k["k"], tabulate(jac_a_inv.T * grad_v))
+            * tabulate(jac_a_abs_det)
+        )
+
+        if only_newton_galerkin_part_of_form:
+            return newton_galerkin_term
+        else:
+            return diffusion_term + newton_galerkin_term
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=False,
+        docstring=docstring,
+        fe_coefficients={"k": coefficient_function_space},
+    )
+
+
+def epsilon(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+    variable_viscosity: bool = True,
+    coefficient_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+"Epsilon" operator.
+
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    μ: coefficient    (scalar space:    {coefficient_function_space})
+
+    ∫ 2 μ ε(u) : ε(v)
+    
+where
+    
+    ε(w) := (1/2) (∇w + (∇w)ᵀ)
+"""
+    if not variable_viscosity:
+        raise HOGException("Constant viscosity currently not supported.")
+
+    from hog.recipes.integrands.volume.epsilon import integrand
+    from hog.recipes.integrands.volume.epsilon_affine import (
+        integrand as integrand_affine,
+    )
+
+    # mypy type checking supposedly cannot figure out ternaries.
+    # https://stackoverflow.com/a/70832391
+    integr: Callable[..., Any] = integrand
+    if blending == IdentityMap():
+        integr = integrand_affine
+
+    return process_integrand(
+        integr,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+        fe_coefficients={"mu": coefficient_function_space},
+    )
+
+
+def k_mass(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    coefficient_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+Diffusion operator with a scalar coefficient.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    k: coefficient    (space: {coefficient_function_space})
+
+    ∫ k uv
+"""
+
+    from hog.recipes.integrands.volume.k_mass import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def pspg(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    quad: Quadrature,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+PSPG stabilisation.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+
+    ∫ τ ∇u · ∇v
+
+    where tau is an element-wise factor given by
+
+    2D: 
+        τ = -CellVolume(triangle) / 5               
+    3D:
+        τ = -(CellVolume(tetrahedron))**(2/3) / 12
+
+See e.g. 
+
+    Brezzi, F., & Douglas, J. (1988). 
+    Stabilized mixed methods for the Stokes problem. 
+    Numerische Mathematik, 53, 225-235.
+
+or 
+
+    Hughes, T. J., Franca, L. P., & Balestra, M. (1986). 
+    A new finite element formulation for computational fluid dynamics: V. 
+    Circumventing the Babuška-Brezzi condition: A stable Petrov-Galerkin formulation of the Stokes problem accommodating
+    equal-order interpolations. 
+    Computer Methods in Applied Mechanics and Engineering, 59(1), 85-99.
+
+for details.
+"""
+
+    from hog.recipes.integrands.volume.pspg import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def linear_form(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    quad: Quadrature,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> sp.Matrix:
+    """
+    Implements a linear form of type:
+
+        (k(x), psi)
+
+    where psi a test function and k = k(x) a scalar, external function.
+    """
+
+    if trial != test:
+        raise HOGException(
+            "Trial space must be equal to test space to assemble linear form (jep this is weird, but linear forms are implemented as diagonal matrices)."
+        )
+
+    if quad.is_exact() and isinstance(blending, ExternalMap):
+        raise HOGException(
+            "Exact integration is not supported for externally defined blending functions."
+        )
+
+    with TimedLogger("assembling linear form", level=logging.DEBUG):
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+        jac_affine_det = abs(det(jac_affine))
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            affine_coords = trafo_ref_to_affine(geometry, symbolizer)
+            jac_blending = blending.jacobian(affine_coords)
+
+        jac_blending_det = abs(det(jac_blending))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+
+        it = element_matrix_iterator(trial, test, geometry)
+
+        if isinstance(trial, N1E1Space):
+            jac_affine_inv_T = inv(jac_affine).T
+            jac_blending_inv_T = inv(jac_blending).T
+            space_dependent_coefficient = vector_space_dependent_coefficient
+        else:
+            space_dependent_coefficient = scalar_space_dependent_coefficient
+        k = space_dependent_coefficient("k", geometry, symbolizer, blending=blending)
+
+        with TimedLogger(
+            f"integrating {mat.shape[0]} expressions",
+            level=logging.DEBUG,
+        ):
+            for data in it:
+                if data.row == data.col:
+                    psi = data.test_shape
+
+                    if isinstance(trial, N1E1Space):
+                        form = (
+                            dot(k, jac_blending_inv_T * jac_affine_inv_T * psi)
+                            * jac_affine_det
+                            * jac_blending_det
+                        )
+                    else:
+                        form = k * psi * jac_affine_det * jac_blending_det
+
+                    mat[data.row, data.col] = quad.integrate(form, symbolizer)
+
+    return mat
+
+
+def divergence(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_index: int = 0,
+) -> Form:
+    docstring = f"""
+Divergence.
+
+Component:    {component_index}
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (scalar space:    {test})
+
+    ∫ - ( ∇ · u ) v
+"""
+
+    from hog.recipes.integrands.volume.divergence import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=False,
+        docstring=docstring,
+    )
+
+
+def gradient(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_index: int = 0,
+) -> Form:
+    docstring = f"""
+    Gradient.
+
+    Component:    {component_index}
+    Geometry map: {blending}
+
+    Weak formulation
+
+        u: trial function (scalar space:    {trial})
+        v: test function  (vectorial space: {test})
+
+        ∫ - ( ∇ · v ) u
+    """
+
+    from hog.recipes.integrands.volume.gradient import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=False,
+        docstring=docstring,
+    )
+
+
+def full_stokes(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+    variable_viscosity: bool = True,
+    coefficient_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+Implements the fully coupled viscous operator of the Stokes problem.
+The latter is the extension of the Epsilon operator to the case where
+the velocity field need not be divergence-free. This is e.g. the case
+in the (truncated) anelastic liquid approximation of mantle convection.
+
+The strong representation of the operator is given by:
+
+   - div[ μ (grad(u)+grad(u)ᵀ) ] + 2/3 grad[ μ div(u) ]
+
+Note that the factor 2/3 means that for 2D this is the pseudo-3D form
+of the operator.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    μ: coefficient    (scalar space:    {coefficient_function_space})
+
+    ∫ μ {{ ( 2 ε(u) : ε(v) ) - (2/3) [ ( ∇ · u ) · ( ∇ · v ) ] }}
+    
+where
+    
+    ε(w) := (1/2) (∇w + (∇w)ᵀ)
+"""
+
+    from hog.recipes.integrands.volume.full_stokes import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+        fe_coefficients={"mu": coefficient_function_space},
+    )
+
+
+def shear_heating(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+    variable_viscosity: bool = True,
+    viscosity_function_space: Optional[FunctionSpace] = None,
+    velocity_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+Implements the fully coupled viscous operator for the shear heating term.
+The latter is the extension of the Epsilon operator to the case where
+the velocity field need not be divergence-free. This is e.g. the case
+in the (truncated) anelastic liquid approximation of mantle convection.
+
+https://doi.org/10.1111/j.1365-246X.2009.04413.x
+(3) and (5)
+
+https://doi.org/10.5194/gmd-15-5127-2022
+Listing 2
+
+The strong representation of the operator is given by:
+
+    𝜏(w) : grad(w)
+    2 {{[ μ (grad(w)+grad(w)ᵀ) / 2 ] - 1/dim [ μ div(w) ]I}} : grad(w)
+
+Note that the factor 1/dim means that for 2D this is the pseudo-3D form
+of the operator.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    T: trial function (scalar space:    {trial})
+    s: test function  (scalar space:    {test})
+    μ: coefficient    (scalar space:    {viscosity_function_space})
+    w: velocity       (vectorial space: {velocity_function_space})
+
+    ∫ {{ 2 {{[ μ (grad(w)+grad(w)ᵀ) / 2 ] - 1/dim [ μ div(w) ]I}} : grad(w) }} T_h s_h
+    
+The resulting matrix must be multiplied with a vector of ones to be used as the shear heating term in the RHS
+"""
+
+    def integrand(
+        *,
+        jac_a_inv,
+        jac_b_inv,
+        jac_a_abs_det,
+        jac_b_abs_det,
+        u,
+        v,
+        k,
+        grad_k,
+        volume_geometry,
+        tabulate,
+        **_,
+    ):
+        """First function: mu, other functions: ux, uy, uz."""
+
+        mu = k["mu"]
+
+        # grad_k[0] is grad_mu_ref
+        grad_wx = jac_b_inv.T * jac_a_inv.T * grad_k["wx"]
+        grad_wy = jac_b_inv.T * jac_a_inv.T * grad_k["wy"]
+        grad_wz = jac_b_inv.T * jac_a_inv.T * grad_k["wz"]
+
+        grad_w = grad_wx.row_join(grad_wy)
+        dim = volume_geometry.dimensions
+        if dim == 3:
+            grad_w = grad_w.row_join(grad_wz)
+
+        sym_grad_w = 0.5 * (grad_w + grad_w.T)
+
+        divdiv = grad_w.trace() * sp.eye(dim)
+
+        tau = 2 * (sym_grad_w - sp.Rational(1, dim) * divdiv)
+
+        return (
+            mu
+            * (double_contraction(tau, grad_w)[0])
+            * jac_b_abs_det
+            * tabulate(jac_a_abs_det * u * v)
+        )
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        fe_coefficients={
+            "mu": viscosity_function_space,
+            "wx": velocity_function_space,
+            "wy": velocity_function_space,
+            "wz": velocity_function_space,
+        },
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def divdiv(
+    trial: TrialSpace,
+    test: TestSpace,
+    component_trial: int,
+    component_test: int,
+    geometry: ElementGeometry,
+    quad: Quadrature,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+divdiv operator which is used as a stabilization term that is taken from
+
+    Blank, L. (2014).
+    On Divergence-Free Finite Element Methods for the Stokes Equations (Freie Universität Berlin).
+    p. 84, eq. (6.2)
+
+for the P1-P0 stabilized Stokes discretization.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+
+    ∫ ( ∇ · u ) · ( ∇ · v )
+"""
+
+    from hog.recipes.integrands.volume.divdiv import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+    )
+
+
+def advection(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    velocity_function_space: FunctionSpace,
+    coefficient_function_space: FunctionSpace,
+    constant_cp: bool = False,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+advection operator which needs to be used in combination with SUPG
+
+Geometry map:    {blending}
+
+Weak formulation
+
+    T: trial function (scalar space: {trial})
+    s: test function  (scalar space: {test})
+    u: velocity function (vectorial space: {velocity_function_space})
+
+    ∫ cp ( u · ∇T ) s
+"""
+
+    from hog.recipes.integrands.volume.advection import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=False,
+        docstring=docstring,
+        fe_coefficients={
+            "ux": velocity_function_space,
+            "uy": velocity_function_space,
+            "uz": velocity_function_space,
+        } if constant_cp else {
+            "ux": velocity_function_space,
+            "uy": velocity_function_space,
+            "uz": velocity_function_space,
+            "cp": coefficient_function_space,
+        },
+    )
+
+
+def supg_advection(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    velocity_function_space: FunctionSpace,
+    coefficient_function_space: FunctionSpace,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+advection operator which needs to be used in combination with SUPG
+
+Geometry map:    {blending}
+
+Weak formulation
+
+    T: trial function (scalar space: {trial})
+    s: test function  (scalar space: {test})
+    u: velocity function (vectorial space: {velocity_function_space})
+
+    ∫ cp ( u · ∇T ) 𝛿(u · ∇s)
+"""
+
+    from hog.recipes.integrands.volume.supg_advection import integrand
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=False,
+        docstring=docstring,
+        fe_coefficients={
+            "ux": velocity_function_space,
+            "uy": velocity_function_space,
+            "uz": velocity_function_space,
+            "cp_times_delta": coefficient_function_space,
+        },
+    )
+
+
+def supg_diffusion(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    velocity_function_space: FunctionSpace,
+    diffusivityXdelta_function_space: FunctionSpace,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Form for SUPGDiffusion operator used for SUPG stabilisation
+
+Geometry map: {blending}
+
+Weak formulation
+
+    T: trial function (space: {trial})
+    s: test function  (space: {test})
+    w: velocity function (space: {velocity_function_space})
+   k𝛿: FE function representing k·𝛿 (space: {diffusivityXdelta_function_space})
+    
+    For OpGen,
+
+    ∫ k(ΔT) · 𝛿(w · ∇s)
+
+    -------------------
+
+    For ExternalMap (only for testing, currently not supported),
+
+    ∫ (ΔT) s
+"""
+
+    def integrand(
+        *,
+        jac_a_inv,
+        jac_b_inv,
+        hessian_b,
+        jac_a_abs_det,
+        jac_b_abs_det,
+        grad_u,
+        grad_v,
+        hessian_u,
+        k,
+        volume_geometry,
+        tabulate,
+        **_,
+    ):
+        """First function: k𝛿, other functions: ux, uy, uz."""
+
+        k_times_delta = k["diffusivity_times_delta"]
+        wx = k["wx"]
+        wy = k["wy"]
+        wz = k["wz"]
+
+        dim = volume_geometry.dimensions
+        if dim == 2:
+            w = sp.Matrix([[wx], [wy]])
+        elif dim == 3:
+            w = sp.Matrix([[wx], [wy], [wz]])
+
+        if isinstance(blending, IdentityMap):
+            hessian_affine = hessian_shape_affine_ref_pullback(hessian_u, jac_a_inv)
+
+            laplacian = sum([hessian_affine[i, i] for i in range(geometry.dimensions)])
+
+            form = (
+                k_times_delta
+                * tabulate(laplacian)
+                * dot(w, tabulate(jac_a_inv.T * grad_v))
+                * jac_a_abs_det
+            )
+
+        else:
+            hessian_blending = hessian_shape_blending_ref_pullback(
+                geometry,
+                grad_u,
+                hessian_u,
+                jac_a_inv,
+                hessian_b,
+                jac_b_inv,
+            )
+
+            laplacian = sum(
+                [hessian_blending[i, i] for i in range(geometry.dimensions)]
+            )
+
+            form = (
+                laplacian
+                * dot(w, jac_b_inv.T * tabulate(jac_a_inv.T * grad_v))
+                * k_times_delta
+                * jac_a_abs_det
+                * jac_b_abs_det
+            )
+
+        return form
+
+    return process_integrand(
+        integrand,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        fe_coefficients={
+            "diffusivity_times_delta": diffusivityXdelta_function_space,
+            "wx": velocity_function_space,
+            "wy": velocity_function_space,
+            "wz": velocity_function_space,
+        },
+    )
+
+
+def grad_rho_by_rho_dot_u(
+    trial: TrialSpace,
+    test: TestSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    density_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+RHS operator for the frozen velocity approach.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (space: {test})
+    rho: coefficient    (space: {density_function_space})
+
+    ∫ ((∇ρ / ρ) · u) v
+"""
+
+    with TimedLogger(
+        "assembling grad_rho_by_rho_dot_u-mass matrix", level=logging.DEBUG
+    ):
+        tabulation = Tabulation(symbolizer)
+
+        jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            affine_coords = trafo_ref_to_affine(geometry, symbolizer)
+            # jac_blending = blending.jacobian(affine_coords)
+            jac_blending_inv = symbolizer.jac_affine_to_blending_inv(
+                geometry.dimensions
+            )
+            jac_blending_det = symbolizer.abs_det_jac_affine_to_blending()
+
+        # jac_blending_det = abs(det(jac_blending))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+
+        it = element_matrix_iterator(trial, test, geometry)
+
+        if density_function_space:
+            rho, dof_symbols = fem_function_on_element(
+                density_function_space,
+                geometry,
+                symbolizer,
+                domain="reference",
+                function_id="rho",
+            )
+
+            grad_rho, _ = fem_function_gradient_on_element(
+                density_function_space,
+                geometry,
+                symbolizer,
+                domain="reference",
+                function_id="grad_rho",
+                dof_symbols=dof_symbols,
+            )
+
+        with TimedLogger(
+            f"integrating {mat.shape[0] * mat.shape[1]} expressions",
+            level=logging.DEBUG,
+        ):
+            for data in it:
+                phi = data.trial_shape
+                psi = data.test_shape
+                # print("phi = ", psi.shape)
+                # phi_psi_jac_affine_det = tabulation.register_factor(
+                #     "phi_psi_jac_affine_det",
+                #     sp.Matrix([phi * psi * jac_affine_det]),
+                # )[0]
+                if blending == IdentityMap():
+                    form = (
+                        dot(((jac_affine_inv.T * grad_rho) / rho[0]), phi)
+                        * psi
+                        * jac_affine_det
+                    )
+                else:
+                    form = (
+                        dot(
+                            (
+                                (jac_blending_inv.T * jac_affine_inv.T * grad_rho)
+                                / rho[0]
+                            ),
+                            phi,
+                        )
+                        * psi
+                        * jac_affine_det
+                        * jac_blending_det
+                    )
+                mat[data.row, data.col] = form
+
+    return Form(mat, tabulation, symmetric=trial == test, docstring=docstring)
+
+
+def zero_form(
+    trial: FunctionSpace, test: FunctionSpace, geometry: ElementGeometry
+) -> sp.Matrix:
+    rows = test.num_dofs(geometry)
+    cols = trial.num_dofs(geometry) if trial is not None else 1
+    return sp.zeros(rows, cols)
+
+
+def linear_elasticity(
+        trial: FunctionSpace,
+        test: FunctionSpace,
+        geometry: ElementGeometry,
+        symbolizer: Symbolizer,
+        blending: GeometryMap = IdentityMap(),
+        component_trial: int = 0,
+        component_test: int = 0,
+        variable_viscosity: bool = True,
+        coefficient_function_space: Optional[FunctionSpace] = None,
+) -> Form:
+    docstring = f"""
+"Linear elasticity" operator.
+
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    λ: coefficient    (scalar space:    {coefficient_function_space})
+    μ: coefficient    (scalar space:    {coefficient_function_space})
+
+    ∫ σ(u) : ε(v)
+
+where
+
+    σ(w) := λ(∇.w)I + μ(∇w + (∇w)ᵀ)
+    ε(w) := (1/2) (∇w + (∇w)ᵀ)
+"""
+    if not variable_viscosity:
+        raise HOGException("Constant viscosity currently not supported.")
+
+    from hog.recipes.integrands.volume.elasticity import integrand
+    from hog.recipes.integrands.volume.elasticity_affine import (
+        integrand as integrand_affine,
+    )
+
+    # mypy type checking supposedly cannot figure out ternaries.
+    # https://stackoverflow.com/a/70832391
+    integr: Callable[..., Any] = integrand
+    if blending == IdentityMap():
+        integr = integrand_affine
+
+    return process_integrand(
+        integr,
+        trial,
+        test,
+        geometry,
+        symbolizer,
+        blending=blending,
+        is_symmetric=trial == test,
+        docstring=docstring,
+        fe_coefficients={"mu": coefficient_function_space, "lambda": coefficient_function_space,},
+    )
diff --git a/hog/recipes/integrands/volume/elasticity.py b/hog/recipes/integrands/volume/elasticity.py
index 35ea06b5c93174e5b023c62e8334c3858dce6af7..5125580da62fbc5ef1a6a95a1b2a9dbcfff20d8f 100644
--- a/hog/recipes/integrands/volume/elasticity.py
+++ b/hog/recipes/integrands/volume/elasticity.py
@@ -1,47 +1,47 @@
-# 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/>.
-
-from hog.recipes.common import *
-import sympy as sp
-
-def integrand(
-    *,
-    jac_a_inv,
-    jac_a_abs_det,
-    jac_b_inv,
-    jac_b_abs_det,
-    grad_u,
-    grad_v,
-    k,
-    tabulate,
-    **_,
-):
-
-    grad_u_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_u)
-    grad_v_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_v)
-
-    def symm_grad(w):
-        return 0.5 * (w + w.T)
-
-    symm_grad_u = symm_grad(grad_u_chain)
-    sigma_u = k["lambda"] * grad_u_chain.trace() * sp.eye(grad_u_chain.shape[0]) + k["mu"] * 2 * symm_grad_u
-    symm_grad_v = symm_grad(grad_v_chain)
-
-    return (
-        double_contraction(sigma_u, symm_grad_v)
-        * tabulate(jac_a_abs_det)
-        * jac_b_abs_det
-    )
+# 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/>.
+
+from hog.recipes.common import *
+import sympy as sp
+
+def integrand(
+    *,
+    jac_a_inv,
+    jac_a_abs_det,
+    jac_b_inv,
+    jac_b_abs_det,
+    grad_u,
+    grad_v,
+    k,
+    tabulate,
+    **_,
+):
+
+    grad_u_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_u)
+    grad_v_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_v)
+
+    def symm_grad(w):
+        return 0.5 * (w + w.T)
+
+    symm_grad_u = symm_grad(grad_u_chain)
+    sigma_u = k["lambda"] * grad_u_chain.trace() * sp.eye(grad_u_chain.shape[0]) + k["mu"] * 2 * symm_grad_u
+    symm_grad_v = symm_grad(grad_v_chain)
+
+    return (
+        double_contraction(sigma_u, symm_grad_v)
+        * tabulate(jac_a_abs_det)
+        * jac_b_abs_det
+    )
diff --git a/hog/recipes/integrands/volume/elasticity_affine.py b/hog/recipes/integrands/volume/elasticity_affine.py
index b51bd8a78603f90cc37ae5d991ed982c58355132..8d9c4edf9e1082b9b9dc448c2281b4f0009e14d1 100644
--- a/hog/recipes/integrands/volume/elasticity_affine.py
+++ b/hog/recipes/integrands/volume/elasticity_affine.py
@@ -1,44 +1,44 @@
-# 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/>.
-
-from hog.recipes.common import *
-import sympy as sp
-
-
-def integrand(
-    *,
-    jac_a_inv,
-    jac_a_abs_det,
-    grad_u,
-    grad_v,
-    k,
-    tabulate,
-    **_,
-):
-
-    grad_u_chain = jac_a_inv.T * grad_u
-    grad_v_chain = jac_a_inv.T * grad_v
-
-    def symm_grad(w):
-        return 0.5 * (w + w.T)
-
-    symm_grad_u = symm_grad(grad_u_chain)
-    sigma_u = k["lambda"] * grad_u_chain.trace() * sp.eye(grad_u_chain.shape[0]) + k["mu"] * 2 * symm_grad_u
-    symm_grad_v = symm_grad(grad_v_chain)
-
-    return tabulate(
-        double_contraction(sigma_u, symm_grad_v) * jac_a_abs_det
-    )
+# 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/>.
+
+from hog.recipes.common import *
+import sympy as sp
+
+
+def integrand(
+    *,
+    jac_a_inv,
+    jac_a_abs_det,
+    grad_u,
+    grad_v,
+    k,
+    tabulate,
+    **_,
+):
+
+    grad_u_chain = jac_a_inv.T * grad_u
+    grad_v_chain = jac_a_inv.T * grad_v
+
+    def symm_grad(w):
+        return 0.5 * (w + w.T)
+
+    symm_grad_u = symm_grad(grad_u_chain)
+    sigma_u = k["lambda"] * grad_u_chain.trace() * sp.eye(grad_u_chain.shape[0]) + k["mu"] * 2 * symm_grad_u
+    symm_grad_v = symm_grad(grad_v_chain)
+
+    return tabulate(
+        double_contraction(sigma_u, symm_grad_v) * jac_a_abs_det
+    )