diff --git a/boundaries/boundaryhandling.py b/boundaries/boundaryhandling.py
index 54c8e5bbb9ae58b260c82e874ba2548ebd983d1b..d43c00c944d84aaab02c1522011f600d7f69f12f 100644
--- a/boundaries/boundaryhandling.py
+++ b/boundaries/boundaryhandling.py
@@ -3,13 +3,31 @@ import numpy as np
 from pystencils import TypedSymbol, Field
 from pystencils.backends.cbackend import CustomCppCode
 from lbmpy.boundaries.createindexlist import createBoundaryIndexList
+from pystencils.slicing import normalizeSlice
 
 INV_DIR_SYMBOL = TypedSymbol("invDir", "int")
 WEIGHTS_SYMBOL = TypedSymbol("weights", "double")
 
 
 class BoundaryHandling(object):
-    def __init__(self, symbolicPdfField, domainShape, lbMethod, ghostLayers=1, target='cpu'):
+    def __init__(self, pdfField, domainShape, lbMethod, ghostLayers=1, target='cpu'):
+        """
+        Class for managing boundary kernels
+
+        :param pdfField: either pdf numpy array (including ghost layers), or pystencils.Field
+        :param domainShape: domain size without ghost layers
+        :param lbMethod: lattice Boltzmann method
+        :param ghostLayers: number of ghost layers
+        :param target: either 'cpu' or 'gpu'
+        """
+        if isinstance(pdfField, np.ndarray):
+            symbolicPdfField = Field.createFromNumpyArray('pdfs', pdfField, indexDimensions=1)
+            assert pdfField.shape[:-1] == tuple(d + 2*ghostLayers for d in domainShape)
+        elif isinstance(pdfField, Field):
+            symbolicPdfField = pdfField
+        else:
+            raise ValueError("pdfField has to be either a numpy array or a pystencils.Field")
+
         self._symbolicPdfField = symbolicPdfField
         self._shapeWithGhostLayers = [d + 2 * ghostLayers for d in domainShape]
         self._fluidFlag = 2 ** 30
@@ -23,7 +41,48 @@ class BoundaryHandling(object):
         if target not in ('cpu', 'gpu'):
             raise ValueError("Invalid target '%s' . Allowed values: 'cpu' or 'gpu'" % (target,))
 
+    def setBoundary(self, function, indexExpr, maskArr=None, name=None):
+        """
+        Sets boundary in a rectangular region (slice)
+
+        :param function: boundary
+        :param indexExpr: slice expression, where boundary should be set, see :mod:`pystencils.slicing`
+        :param maskArr: optional boolean (masked) array specifying where the boundary should be set
+        :param name: name of the boundary
+        """
+        if name is None:
+            if hasattr(function, '__name__'):
+                name = function.__name__
+            elif hasattr(function, 'name'):
+                name = function.name
+            else:
+                raise ValueError("Boundary function has to have a '__name__' or 'name' attribute "
+                                 "if name is not specified")
+
+        if function not in self._boundaryFunctions:
+            self.addBoundary(function, name)
+
+        flag = self.getFlag(name)
+
+        indexExpr = normalizeSlice(indexExpr, self._shapeWithGhostLayers)
+        if maskArr is None:
+            self.flagField[indexExpr] = flag
+        else:
+            flagFieldView = self.flagField[indexExpr]
+            flagFieldView[maskArr] = flag
+
+        self.invalidateIndexCache()
+
     def addBoundary(self, boundaryFunction, name=None):
+        """
+        Adds a boundary condition, i.e. reserves a flog in the flag field and returns that flag
+        If a boundary with that name already exists, the existing flag is returned.
+        This flag can be logicalled or'ed to the boundaryHandling.flagField
+
+        :param boundaryFunction: boundary condition function, see :mod:`lbmpy.boundaries.boundaryconditions`
+        :param name: boundaries with different name are considered different. If not given
+                     ```boundaryFunction.__name__`` is used
+        """
         if name is None:
             name = boundaryFunction.__name__
 
@@ -45,27 +104,6 @@ class BoundaryHandling(object):
     def getFlag(self, name):
         return 2 ** self._nameToIndex[name]
 
-    def setBoundary(self, function, indexExpr, maskArr=None):
-        if hasattr(function, '__name__'):
-            name = function.__name__
-        elif hasattr(function, 'name'):
-            name = function.name
-        else:
-            raise ValueError("Boundary function has to have a '__name__' or 'name' attribute")
-
-        if function not in self._boundaryFunctions:
-            self.addBoundary(function, name)
-
-        flag = self.getFlag(name)
-
-        if maskArr is None:
-            self.flagField[indexExpr] = flag
-        else:
-            flagFieldView = self.flagField[indexExpr]
-            flagFieldView[maskArr] = flag
-
-        self.invalidateIndexCache()
-
     def prepare(self):
         self.invalidateIndexCache()
         for boundaryIdx, boundaryFunc in enumerate(self._boundaryFunctions):
@@ -86,6 +124,8 @@ class BoundaryHandling(object):
                 assert False
 
     def __call__(self, **kwargs):
+        if len(self._boundaryFunctions) == 0:
+            return
         if len(self._boundarySweeps) == 0:
             self.prepare()
         for boundarySweep in self._boundarySweeps:
diff --git a/creationfunctions.py b/creationfunctions.py
index 3a28cedfc9cf7c9888bc30b76346e246bfeeb8eb..846c872cd99adb7de82f4beb1b0e7fa2ce90bfde 100644
--- a/creationfunctions.py
+++ b/creationfunctions.py
@@ -1,14 +1,144 @@
-"""
-Factory functions for standard LBM methods
+r"""
+Creating LBM kernels
+====================
+
+
+Terminology and creation pipeline
+---------------------------------
+
+Kernel functions are created in three steps:
+
+1. *Method*:
+         the method defines the collision process. Currently there are two big categories:
+         moment and cumulant based methods. A method defines how each moment or cumulant is relaxed by
+         storing the equilibrium value and the relaxation rate for each moment/cumulant.
+2. *Collision/Update Rule*:
+         Methods can generate a "collision rule" which is an equation collection that define the
+         post collision values as a function of the pre-collision values. On these equation collection
+         simplifications are applied to reduce the number of floating point operations.
+         At this stage an entropic optimization step can also be added to determine one relaxation rate by an
+         entropy condition.
+         Then a streaming rule is added which transforms the collision rule into an update rule.
+         The streaming step depends on the pdf storage (source/destination, AABB pattern, EsoTwist).
+         Currently only the simple source/destination  pattern is supported.
+3. *AST*:
+        The abstract syntax tree describes the structure of the kernel, including loops and conditionals.
+        The ast can be modified e.g. to add OpenMP pragmas, reorder loops or apply other optimizations.
+4. *Function*:
+        This step compiles the AST into an executable function, either for CPU or GPUs. This function
+        behaves like a normal Python function and runs one LBM time step.
+
+The function :func:`createLatticeBoltzmannFunction` runs the whole pipeline, the other functions in this module
+execute this pipeline only up to a certain step. Each function optionally also takes the result of the previous step.
+
+For example, to modify the AST one can run::
+
+    ast = createLatticeBoltzmannAst(...)
+    # modify ast here
+    func = createLatticeBoltzmannFunction(ast=ast, ...)
+
+
+Parameters
+----------
+
+The following list describes common parameters for the creation functions. They have to be passed as keyword parameters.
+
+
+Method parameters
+^^^^^^^^^^^^^^^^^
+
+General:
+
+- ``stencil='D2Q9'``: stencil name e.g. 'D2Q9', 'D3Q19'. See :func:`pystencils.stencils.getStencil` for details
+- ``method='srt'``: name of lattice Boltzmann method. This determines the selection and relaxation pattern of
+  moments/cumulants, i.e. which moment/cumulant basis is chosen, and which of the basis vectors are relaxed together
+    - ``srt``: single relaxation time (:func:`lbmpy.methods.createSRT`)
+    - ``trt``: two relaxation time, first relaxation rate is for even moments and determines the viscosity (as in SRT),
+      the second relaxation rate is used for relaxing odd moments, and controls the bulk viscosity.
+      (:func:`lbmpy.methods.createTRT`)
+    - ``mrt``: orthogonal multi relaxation time model, number of relaxation rates depends on the stencil
+      (:func:`lbmpy.methods.createOrthogonalMRT`)
+    - ``mrt3``: three relaxation time method, where shear moments are relaxed with first relaxation rate (and therefore
+      determine viscosity, second rate relaxes the shear tensor trace (determines bulk viscosity) and last rate relaxes
+      all other, higher order moments. If two relaxation rates are chosen the same this is equivalent to a KBC type
+      relaxation (:func:`lbmpy.methods.createThreeRelaxationRateMRT`)
+    - ``mrt_raw``: non-orthogonal MRT where all relaxation rates can be specified independently i.e. there are as many
+      relaxation rates as stencil entries. Look at the generated method in Jupyter to see which moment<->relaxation rate
+      mapping (:func:`lbmpy.methods.createRawMRT`)
+    - ``trt-kbc-n<N>`` where <N> is 1,2,3 or 4. Special two-relaxation method. This is not the entropic method
+      yet, only the relaxation pattern. To get the entropic method, see parameters below!
+      (:func:`lbmpy.methods.createKBCTypeTRT`)
+- ``relaxationRates``: sequence of relaxation rates, number depends on selected method. If you specify more rates than
+  method needs, the additional rates are ignored.
+- ``compressible=False``: affects the selection of equilibrium moments. Both options approximate the *incompressible*
+  Navier Stokes Equations. However when chosen as False, the approximation is better, the standard LBM derivation is
+  compressible.
+- ``equilibriumAccuracyOrder=2``: order in velocity, at which the equilibrium moment/cumulant approximation is
+  truncated. Order 2 is sufficient to approximate Navier-Stokes
+- ``forceModel=None``: possible values: ``None``, ``'simple'``, ``'luo'``, ``'guo'``. For details see
+  :mod:`lbmpy.forcemodels`
+- ``useContinuousMaxwellianEquilibrium=True``: way to compute equilibrium moments/cumulants, if False the standard
+  discretized LBM equilibrium is used, otherwise the equilibrium moments are computed from the continuous Maxwellian.
+  This makes only a difference if sparse stencils are used e.g. D2Q9 and D3Q27 are not affected, D319 and DQ15 are
+- ``cumulant=False``: use cumulants instead of moments
+- ``initialVelocity=None``: initial velocity in domain, can either be a tuple (x,y,z) velocity to set a constant
+  velocity everywhere, or a numpy array with the same size of the domain, with a last coordinate of shape dim to set
+  velocities on cell level
+
+Entropic methods:
+
+- ``entropic=False``: In case there are two distinct relaxation rate in a method, one of them (usually the one, not
+  determining the viscosity) can be automatically chosen w.r.t an entropy condition. For details see
+  :mod:`lbmpy.methods.entropic`
+- ``entropicNewtonIterations=None``: For moment methods the entropy optimum can be calculated in closed form.
+  For cumulant methods this is not possible, in that case it is computed using Newton iterations. This parameter can be
+  used to force Newton iterations and specify how many should be done
+- ``omegaOutputField=None``: you can pass a pystencils Field here, where the calculated free relaxation
+  rate is written to
+
+
+Optimization Parameters
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Simplifications / Transformations:
+
+- ``doCseInOpposingDirections=False``: run common subexpression elimination for opposing stencil directions
+- ``doOverallCse=False``: run common subexpression elimination after all other simplifications have been executed
+- ``split=False``: split innermost loop, to handle only 2 directions per loop. This reduces the number of parallel
+  load/store streams and thus speeds up the kernel on most architectures
+
+
+Field size information:
+
+- ``pdfArr``: pass a numpy array here to create kernels with fixed size and create the loop nest according to layout
+  of this array
+- ``fieldSize``: create kernel for fixed field size
+- ``fieldLayout='c'``:   ``'c'`` or ``'numpy'`` for standard numpy layout, ``'reverseNumpy'`` or ``'f'`` for fortran
+  layout, this does not apply when pdfArr was given, then the same layout as pdfArr is used
+
+GPU:
+
+- ``target='cpu'``: ``'cpu'`` or ``'gpu'``, last option requires a CUDA enabled graphics card
+  and installed *pycuda* package
+- ``gpuIndexing='block'``: determines mapping of CUDA threads to cells. Can be either ``'block'`` or ``'line'``
+- ``gpuIndexingParams='block'``: parameters passed to init function of gpu indexing.
+  For ``'block'`` indexing one can e.g. specify the block size ``{'blockSize' : (128, 4, 1)}``
+
+Other:
+
+- ``openMP=True``: only applicable for cpu simulations. Can be a boolean to turn multi threading on/off, or an integer
+  specifying the number of threads. If True is specified OpenMP chooses the number of threads
+- ``doublePrecision=True``:  by default simulations run with double precision floating point numbers, by setting this
+  parameter to False, single precision is used, which is much faster, especially on GPUs
 """
 import sympy as sp
 from copy import copy
 from functools import partial
 
-from lbmpy.methods.creationfunctions import createKBCTypeTRT, createRawMRT, createThreeRelaxationRateMRT
+from lbmpy.methods import createSRT, createTRT, createOrthogonalMRT, createKBCTypeTRT, \
+    createRawMRT, createThreeRelaxationRateMRT
 from lbmpy.methods.entropic import addIterativeEntropyCondition, addEntropyCondition
 from lbmpy.stencils import getStencil
-from lbmpy.methods import createSRT, createTRT, createOrthogonalMRT
 import lbmpy.forcemodels as forceModels
 from lbmpy.simplificationfactory import createSimplificationStrategy
 from lbmpy.updatekernels import createStreamPullKernel, createPdfArray
@@ -22,15 +152,15 @@ def updateWithDefaultParameters(params, optParams):
         'compressible': False,
         'equilibriumAccuracyOrder': 2,
 
-        'entropic': False,
-        'entropicNewtonIterations': None,
-        'omegaOutputField': None,
-
-        'useContinuousMaxwellianEquilibrium': False,
-        'cumulant': False,
         'forceModel': 'none',  # can be 'simple', 'luo' or 'guo'
         'force': (0, 0, 0),
+        'useContinuousMaxwellianEquilibrium': True,
+        'cumulant': False,
         'initialVelocity': None,
+
+        'entropic': False,
+        'entropicNewtonIterations': None,
+        'omegaOutputField': None,
     }
 
     defaultOptimizationDescription = {
@@ -39,7 +169,7 @@ def updateWithDefaultParameters(params, optParams):
         'split': False,
 
         'fieldSize': None,
-        'fieldLayout': 'reverseNumpy',  # can be 'numpy' (='c'), 'reverseNumpy' (='f'), 'fzyx', 'zyxf'
+        'fieldLayout': 'c',  # can be 'numpy' (='c'), 'reverseNumpy' (='f'), 'fzyx', 'zyxf'
 
         'target': 'cpu',
         'openMP': True,
@@ -140,7 +270,7 @@ def createLatticeBoltzmannUpdateRule(lbMethod=None, optimizationParams={}, **kwa
 
     if params['entropic']:
         if params['entropicNewtonIterations']:
-            if isinstance(params['entropicNewtonIterations'], bool):
+            if isinstance(params['entropicNewtonIterations'], bool) or params['cumulant']:
                 iterations = 3
             else:
                 iterations = params['entropicNewtonIterations']
diff --git a/methods/__init__.py b/methods/__init__.py
index cf454ae62242ca87ce713d80d8089e760114dfbe..d842bfcd3bd68f73f38ae54caacb5fea33ec4a2e 100644
--- a/methods/__init__.py
+++ b/methods/__init__.py
@@ -1,5 +1,6 @@
 from lbmpy.methods.abstractlbmethod import AbstractLbMethod
 from lbmpy.methods.momentbased import MomentBasedLbMethod, RelaxationInfo
 from lbmpy.methods.creationfunctions import createSRT, createTRT, createTRTWithMagicNumber, createOrthogonalMRT, \
-    createWithContinuousMaxwellianEqMoments, createWithDiscreteMaxwellianEqMoments
+    createWithContinuousMaxwellianEqMoments, createWithDiscreteMaxwellianEqMoments, createKBCTypeTRT, createRawMRT, \
+    createThreeRelaxationRateMRT
 from lbmpy.methods.conservedquantitycomputation import AbstractConservedQuantityComputation, DensityVelocityComputation
diff --git a/plot2d.py b/plot2d.py
index 0023377f7eb609bd16f59d3fc6bec5940d615227..c303162ff339bf7acfbb2d5987f8c2b4a2ea4398 100644
--- a/plot2d.py
+++ b/plot2d.py
@@ -8,8 +8,10 @@ def vectorField(field, step=2, **kwargs):
 
 def vectorFieldMagnitude(field, **kwargs):
     from numpy.linalg import norm
-    field = norm(field, axis=2, ord=2)
-    return scalarField(field, **kwargs)
+    norm = norm(field, axis=2, ord=2)
+    if hasattr(field, 'mask'):
+        norm = np.ma.masked_array(norm, mask=field.mask[:,:,0])
+    return scalarField(norm, **kwargs)
 
 
 def scalarField(field, **kwargs):
@@ -32,9 +34,11 @@ def vectorFieldMagnitudeAnimation(runFunction, plotSetupFunction=lambda: None,
 
     def updatefig(*args):
         f = runFunction()
-        f = norm(f, axis=2, ord=2)
-        f = np.swapaxes(f, 0, 1)
-        im.set_array(f)
+        normed = norm(f, axis=2, ord=2)
+        if hasattr(f, 'mask'):
+            normed = np.ma.masked_array(normed, mask=f.mask[:, :, 0])
+        normed = np.swapaxes(normed, 0, 1)
+        im.set_array(normed)
         plotUpdateFunction()
         return im,
 
diff --git a/scenarios.py b/scenarios.py
new file mode 100644
index 0000000000000000000000000000000000000000..2cbd3cf325753ffa6710241041d9a45224d91541
--- /dev/null
+++ b/scenarios.py
@@ -0,0 +1,341 @@
+import numpy as np
+from functools import partial
+from pystencils.field import getLayoutOfArray, createNumpyArrayWithLayout
+from pystencils.slicing import sliceFromDirection, addGhostLayers, getPeriodicBoundaryFunctor, removeGhostLayers, \
+    normalizeSlice, makeSlice
+from lbmpy.creationfunctions import createLatticeBoltzmannFunction, updateWithDefaultParameters
+from lbmpy.macroscopic_value_kernels import compileMacroscopicValuesGetter, compileMacroscopicValuesSetter
+from lbmpy.boundaries import BoundaryHandling, noSlip, ubb, fixedDensity
+from lbmpy.stencils import getStencil
+from lbmpy.updatekernels import createPdfArray
+
+
+class Scenario(object):
+    def __init__(self, domainSize, methodParameters, optimizationParams, lbmKernel=None,
+                 initialVelocity=None, preUpdateFunctions=[], kernelParams={}):
+        """
+        Constructor for generic scenarios. You probably want to use one of the simpler scenario factory functions
+        of this file.
+
+        :param domainSize: tuple, defining the domain size without ghost layers
+        :param methodParameters: dict with method parameters, as documented in :mod:`lbmpy.creationfunctions`,
+                                 passed to :func:`lbmpy.creationfunction.createLatticeBoltzmannFunction`
+        :param optimizationParams: dict with optimization parameters, as documented in :mod:`lbmpy.creationfunctions`,
+                                   passed to :func:`lbmpy.creationfunction.createLatticeBoltzmannFunction`
+        :param lbmKernel: a lattice boltzmann function can be passed here, if None it is created with the parameters
+                          specified above
+        :param initialVelocity: tuple with initial velocity of the domain, can either be a constant or a numpy array
+                                with first axes shaped like the domain, and the last dimension of size #dimensions
+        :param preUpdateFunctions: list of functions that are called before the LBM kernel. They get the pdf array as
+                                   only argument. Can be used for custom boundary conditions, periodicity, etc.
+        :param kernelParams: dict which is passed to the kernel as additional parameters
+        """
+
+        ghostLayers = 1
+        domainSizeWithGhostLayer = tuple([s + 2 * ghostLayers for s in domainSize])
+        D = len(domainSize)
+        if 'stencil' not in methodParameters:
+            methodParameters['stencil'] = 'D2Q9' if D == 2 else 'D3Q27'
+
+        if isinstance(initialVelocity, np.ndarray):
+            initialVelocity = addGhostLayers(initialVelocity, indexDimensions=1, ghostLayers=1)
+
+        methodParameters, optimizationParams = updateWithDefaultParameters(methodParameters, optimizationParams)
+
+        Q = len(getStencil(methodParameters['stencil']))
+        self._pdfArrays = [createPdfArray(domainSize, Q, layout=optimizationParams['fieldLayout']),
+                           createPdfArray(domainSize, Q, layout=optimizationParams['fieldLayout'])]
+
+        # Create kernel
+        if lbmKernel is None:
+            optimizationParams['pdfArr'] = self._pdfArrays[0]
+            methodParameters['optimizationParams'] = optimizationParams
+            self._lbmKernel = createLatticeBoltzmannFunction(**methodParameters)
+        else:
+            self._lbmKernel = lbmKernel
+        assert D == self._lbmKernel.method.dim, "Domain size and stencil do not match"
+
+        # Macroscopic value input/output
+        pdfArrLayout = getLayoutOfArray(self._pdfArrays[0])
+        pdfArrLayoutNoIdx = getLayoutOfArray(self._pdfArrays[0], indexDimensionIds=[D])
+        self._density = createNumpyArrayWithLayout(domainSizeWithGhostLayer, layout=pdfArrLayoutNoIdx)
+        self._velocity = createNumpyArrayWithLayout(list(domainSizeWithGhostLayer) + [D], layout=pdfArrLayout)
+        self._getMacroscopic = compileMacroscopicValuesGetter(self._lbmKernel.method, ['density', 'velocity'],
+                                                              pdfArr=self._pdfArrays[0], target='cpu')
+
+        self._boundaryHandling = BoundaryHandling(self._pdfArrays[0], domainSize, self.method,
+                                                  target=optimizationParams['target'])
+
+        self._preUpdateFunctions = preUpdateFunctions
+        self._kernelParams = kernelParams
+        self._pdfGpuArrays = []
+
+        if initialVelocity is None:
+            initialVelocity = [0] * D
+
+        setMacroscopic = compileMacroscopicValuesSetter(self.method, {'density': 1.0, 'velocity': initialVelocity},
+                                                        pdfArr=self._pdfArrays[0], target='cpu')
+        setMacroscopic(pdfs=self._pdfArrays[0])
+
+        if optimizationParams['target'] == 'gpu':
+            import pycuda.gpuarray as gpuarray
+            self._pdfGpuArrays = [gpuarray.to_gpu(a) for a in self._pdfArrays]
+        else:
+            self._pdfGpuArrays = []
+
+    def run(self, timeSteps=1):
+        """Run the scenario for the given amount of time steps"""
+        if len(self._pdfGpuArrays) > 0:
+            self._gpuTimeloop(timeSteps)
+        else:
+            self._cpuTimeloop(timeSteps)
+
+    @property
+    def velocity(self):
+        """Velocity as numpy array"""
+        mask = np.logical_not(np.bitwise_and(self.boundaryHandling.flagField, self.boundaryHandling._fluidFlag))
+        mask = np.repeat(mask[..., np.newaxis], self.dim, axis=2)
+        return removeGhostLayers(np.ma.masked_array(self._velocity, mask), indexDimensions=1)
+
+    @property
+    def density(self):
+        """Density as numpy array"""
+        mask = np.logical_not(np.bitwise_and(self.boundaryHandling.flagField, self.boundaryHandling._fluidFlag))
+        return removeGhostLayers(np.ma.masked_array(self._density, mask))
+
+    @property
+    def pdfs(self):
+        """Particle distribution functions as numpy array"""
+        return removeGhostLayers(self._pdfArrays[0], indexDimensions=1)
+
+    @property
+    def boundaryHandling(self):
+        """Boundary handling instance of the scenario. Use this to change the boundary setup"""
+        return self._boundaryHandling
+
+    @property
+    def method(self):
+        """Lattice boltzmann method description"""
+        return self._lbmKernel.method
+
+    @property
+    def dim(self):
+        """Dimension of the domain"""
+        return self.method.dim
+
+    @property
+    def ast(self):
+        """Returns abstract syntax tree of the kernel"""
+        return self._lbmKernel.ast
+
+    @property
+    def updateRule(self):
+        """Equation collection defining the LBM update rule (already in simplified form)"""
+        return self._lbmKernel.updateRule
+
+    def animateVelocity(self, steps=10, **kwargs):
+        import lbmpy.plot2d as plt
+
+        def runFunction():
+            self.run(steps)
+            return self.velocity
+        return plt.vectorFieldMagnitudeAnimation(runFunction, **kwargs)
+
+    def plotVelocity(self, **kwargs):
+        import lbmpy.plot2d as plt
+        if self.dim == 2:
+            plt.vectorFieldMagnitude(self.velocity, **kwargs)
+            plt.title("Velocity Magnitude")
+            plt.colorbar()
+            plt.axis('equal')
+        elif self.dim == 3:
+            idxSlice = normalizeSlice(makeSlice[:, :, 0.5], self.velocity.shape[:3])
+            plt.vectorFieldMagnitude(self.velocity[idxSlice], **kwargs)
+            plt.title("Velocity Magnitude in x-y (z at half of domain)")
+            plt.colorbar()
+            plt.axis('equal')
+        else:
+            raise NotImplementedError("Can only plot 2D and 3D scenarios")
+
+    def plotDensity(self, **kwargs):
+        import lbmpy.plot2d as plt
+        if self.dim == 2:
+            plt.scalarField(self.density, **kwargs)
+            plt.title("Density")
+            plt.colorbar()
+            plt.axis('equal')
+        elif self.dim == 3:
+            idxSlice = normalizeSlice(makeSlice[:, :, 0.5], self.density.shape)
+            plt.scalarField(self.density[idxSlice], **kwargs)
+            plt.title("Density in x-y (z at half of domain)")
+            plt.colorbar()
+            plt.axis('equal')
+        else:
+            raise NotImplementedError("Can only plot 2D and 3D scenarios")
+
+    def _timeloop(self, pdfArrays, timeSteps):
+        for t in range(timeSteps):
+            for f in self._preUpdateFunctions:
+                f(pdfArrays[0])
+            if self._boundaryHandling is not None:
+                self._boundaryHandling(pdfs=pdfArrays[0])
+            self._lbmKernel(src=pdfArrays[0], dst=pdfArrays[1], **self._kernelParams)
+
+            pdfArrays[0], pdfArrays[1] = pdfArrays[1], pdfArrays[0]  # swap
+
+    def _cpuTimeloop(self, timeSteps):
+        self._timeloop(self._pdfArrays, timeSteps)
+        self._getMacroscopic(pdfs=self._pdfArrays[0], density=self._density, velocity=self._velocity)
+
+    def _gpuTimeloop(self, timeSteps):
+        # Transfer data to gpu
+        for cpuArr, gpuArr in zip(self._pdfArrays, self._pdfGpuArrays):
+            gpuArr.set(cpuArr)
+
+        self._timeloop(self._pdfGpuArrays, timeSteps)
+
+        # Transfer data from gpu to cpu
+        for cpuArr, gpuArr in zip(self._pdfArrays, self._pdfGpuArrays):
+            gpuArr.get(cpuArr)
+
+        self._getMacroscopic(pdfs=self._pdfArrays[0], density=self._density, velocity=self._velocity)
+
+
+# ---------------------------------------- Example Scenarios -----------------------------------------------------------
+
+
+def createFullyPeriodicFlow(initialVelocity, optimizationParams={}, lbmKernel=None, kernelParams={}, **kwargs):
+    """
+    Creates a fully periodic setup with prescribed velocity field
+    """
+    domainSize = initialVelocity.shape
+
+    stencil = getStencil(kwargs['stencil'])
+    periodicity = getPeriodicBoundaryFunctor(stencil)
+
+    return Scenario(domainSize, kwargs, optimizationParams, lbmKernel, initialVelocity, kernelParams, [periodicity])
+
+
+def createLidDrivenCavity(domainSize, lidVelocity=0.005, optimizationParams={}, lbmKernel=None,
+                          kernelParams={}, **kwargs):
+    """
+    Creates a lid driven cavity scenario with prescribed lid velocity in lattice coordinates
+    """
+    scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, kernelParams=kernelParams)
+
+    myUbb = partial(ubb, velocity=[lidVelocity, 0, 0][:scenario.method.dim])
+    myUbb.name = 'ubb'
+    dim = scenario.method.dim
+    scenario.boundaryHandling.setBoundary(myUbb, sliceFromDirection('N', dim))
+    for direction in ('W', 'E', 'S') if scenario.method.dim == 2 else ('W', 'E', 'S', 'T', 'B'):
+        scenario.boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim))
+
+    return scenario
+
+
+def createPressureGradientDrivenChannel(dim, pressureDifference, domainSize=None, radius=None, length=None,
+                                        lbmKernel=None, optimizationParams={}, initialVelocity=None,
+                                        boundarySetupFunctions=[], kernelParams={}, **kwargs):
+    """
+    Creates a channel flow in x direction, which is driven by two pressure boundaries.
+    """
+    assert dim in (2, 3)
+
+    if radius is not None:
+        assert length is not None
+        if dim == 3:
+            domainSize = (length, 2 * radius + 1, 2 * radius + 1)
+            roundChannel = True
+        else:
+            if domainSize is None:
+                domainSize = (length, 2 * radius)
+    else:
+        roundChannel = False
+
+    if domainSize is None:
+        raise ValueError("Missing domainSize or radius and length parameters!")
+
+    scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, kernelParams=kernelParams,
+                        initialVelocity=initialVelocity)
+    boundaryHandling = scenario.boundaryHandling
+    pressureBoundaryInflow = partial(fixedDensity, density=1.0 + pressureDifference)
+    pressureBoundaryInflow.__name__ = "Inflow"
+
+    pressureBoundaryOutflow = partial(fixedDensity, density=1.0)
+    pressureBoundaryOutflow.__name__ = "Outflow"
+    boundaryHandling.setBoundary(pressureBoundaryInflow, sliceFromDirection('W', dim))
+    boundaryHandling.setBoundary(pressureBoundaryOutflow, sliceFromDirection('E', dim))
+
+    if dim == 2:
+        for direction in ('N', 'S'):
+            boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim))
+    elif dim == 3:
+        if roundChannel:
+            noSlipIdx = boundaryHandling.addBoundary(noSlip)
+            ff = boundaryHandling.flagField
+            yMid = ff.shape[1] // 2
+            zMid = ff.shape[2] // 2
+            y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2]))
+            ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx
+        else:
+            for direction in ('N', 'S', 'T', 'B'):
+                boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim))
+
+    for userFunction in boundarySetupFunctions:
+        userFunction(boundaryHandling, scenario.method, domainSize)
+
+    return scenario
+
+
+def createForceDrivenChannel(dim, force, domainSize=None, radius=None, length=None, lbmKernel=None,
+                             optimizationParams={}, initialVelocity=None, boundarySetupFunctions=[],
+                             kernelParams={}, **kwargs):
+    """
+    Creates a channel flow in x direction, which is driven by a constant force along the x axis
+    """
+    assert dim in (2, 3)
+    kwargs['force'] = tuple([force, 0, 0][:dim])
+
+    if radius is not None:
+        assert length is not None
+        if dim == 3:
+            domainSize = (length, 2 * radius + 1, 2 * radius + 1)
+            roundChannel = True
+        else:
+            if domainSize is None:
+                domainSize = (length, 2 * radius)
+    else:
+        roundChannel = False
+
+    def periodicity(pdfArr):
+        pdfArr[0, :, :] = pdfArr[-2, :, :]
+        pdfArr[-1, :, :] = pdfArr[1, :, :]
+
+    scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel,
+                        initialVelocity=initialVelocity, preUpdateFunctions=[periodicity],
+                        kernelParams=kernelParams)
+
+    boundaryHandling = scenario.boundaryHandling
+
+    if dim == 2:
+        for direction in ('N', 'S'):
+            boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim))
+    elif dim == 3:
+        if roundChannel:
+            noSlipIdx = boundaryHandling.addBoundary(noSlip)
+            ff = boundaryHandling.flagField
+            yMid = ff.shape[1] // 2
+            zMid = ff.shape[2] // 2
+            y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2]))
+            ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx
+        else:
+            for direction in ('N', 'S', 'T', 'B'):
+                boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim))
+    for userFunction in boundarySetupFunctions:
+        userFunction(boundaryHandling, scenario.method, domainSize)
+
+    assert domainSize is not None
+    if 'forceModel' not in kwargs:
+        kwargs['forceModel'] = 'guo'
+
+    return scenario
diff --git a/serialscenario.py b/serialscenario.py
deleted file mode 100644
index aa4af97ad6e03c5dbe8aad53d85105d02ebad403..0000000000000000000000000000000000000000
--- a/serialscenario.py
+++ /dev/null
@@ -1,223 +0,0 @@
-from functools import partial
-import numpy as np
-from pystencils import Field
-from pystencils.field import getLayoutOfArray, createNumpyArrayWithLayout
-from pystencils.slicing import sliceFromDirection, addGhostLayers, getPeriodicBoundaryFunctor
-from lbmpy.creationfunctions import createLatticeBoltzmannFunction, updateWithDefaultParameters
-from lbmpy.macroscopic_value_kernels import compileMacroscopicValuesGetter, compileMacroscopicValuesSetter
-from lbmpy.boundaries import BoundaryHandling, noSlip, ubb, fixedDensity
-from lbmpy.stencils import getStencil
-from lbmpy.updatekernels import createPdfArray
-
-
-def createScenario(domainSize, boundarySetupFunction, methodParameters, optimizationParams, lbmKernel=None,
-                   initialVelocity=None, preUpdateFunctions=[], kernelParams={}):
-    ghostLayers = 1
-    domainSizeWithGhostLayer = tuple([s + 2 * ghostLayers for s in domainSize])
-    D = len(domainSize)
-    if 'stencil' not in methodParameters:
-        methodParameters['stencil'] = 'D2Q9' if D == 2 else 'D3Q27'
-
-    methodParameters, optimizationParams = updateWithDefaultParameters(methodParameters, optimizationParams)
-
-    Q = len(getStencil(methodParameters['stencil']))
-    pdfArrays = [createPdfArray(domainSize, Q, layout=optimizationParams['fieldLayout']),
-                 createPdfArray(domainSize, Q, layout=optimizationParams['fieldLayout'])]
-
-    # Create kernel
-    if lbmKernel is None:
-        methodParameters['optimizationParams'] = optimizationParams
-        lbmKernel = createLatticeBoltzmannFunction(**methodParameters)
-    method = lbmKernel.method
-
-    assert D == method.dim, "Domain size and stencil do not match"
-
-    # Boundary setup
-    if boundarySetupFunction is not None:
-        symPdfField = Field.createFromNumpyArray('pdfs', pdfArrays[0], indexDimensions=1)
-        boundaryHandling = BoundaryHandling(symPdfField, domainSize, lbmKernel.method,
-                                            target=optimizationParams['target'])
-        boundarySetupFunction(boundaryHandling=boundaryHandling, method=method)
-        boundaryHandling.prepare()
-    else:
-        boundaryHandling = None
-
-    # Macroscopic value input/output
-    pdfArrLayout = getLayoutOfArray(pdfArrays[0])
-    pdfArrLayoutNoIdx = getLayoutOfArray(pdfArrays[0], indexDimensionIds=[D])
-    densityArr = [createNumpyArrayWithLayout(domainSizeWithGhostLayer, layout=pdfArrLayoutNoIdx)]
-    velocityArr = [createNumpyArrayWithLayout(list(domainSizeWithGhostLayer) + [D], layout=pdfArrLayout)]
-    getMacroscopic = compileMacroscopicValuesGetter(method, ['density', 'velocity'], pdfArr=pdfArrays[0], target='cpu')
-
-    if initialVelocity is None:
-        initialVelocity = [0] * D
-    setMacroscopic = compileMacroscopicValuesSetter(method, {'density': 1.0, 'velocity': initialVelocity},
-                                                    pdfArr=pdfArrays[0], target='cpu')
-    setMacroscopic(pdfs=pdfArrays[0])
-
-    if optimizationParams['target'] == 'gpu':
-        import pycuda.gpuarray as gpuarray
-        pdfGpuArrays = [gpuarray.to_gpu(a) for a in pdfArrays]
-    else:
-        pdfGpuArrays = []
-
-    def cpuTimeLoop(timeSteps):
-        for t in range(timeSteps):
-            for f in preUpdateFunctions:
-                f(pdfArrays[0])
-            if boundaryHandling is not None:
-                boundaryHandling(pdfs=pdfArrays[0])
-            lbmKernel(src=pdfArrays[0], dst=pdfArrays[1], **kernelParams)
-
-            pdfArrays[0], pdfArrays[1] = pdfArrays[1], pdfArrays[0]
-        getMacroscopic(pdfs=pdfArrays[0], density=densityArr[0], velocity=velocityArr[0])
-        return pdfArrays[0], densityArr[0], velocityArr[0]
-
-    def gpuTimeLoop(timeSteps):
-        # Transfer data to gpu
-        for cpuArr, gpuArr in zip(pdfArrays, pdfGpuArrays):
-            gpuArr.set(cpuArr)
-
-        for t in range(timeSteps):
-            for f in preUpdateFunctions:
-                f(pdfGpuArrays[0])
-            if boundaryHandling is not None:
-                boundaryHandling(pdfs=pdfGpuArrays[0])
-            lbmKernel(src=pdfGpuArrays[0], dst=pdfGpuArrays[1], **kernelParams)
-
-            pdfGpuArrays[0], pdfGpuArrays[1] = pdfGpuArrays[1], pdfGpuArrays[0]
-
-        # Transfer data from gpu to cpu
-        for cpuArr, gpuArr in zip(pdfArrays, pdfGpuArrays):
-            gpuArr.get(cpuArr)
-
-        getMacroscopic(pdfs=pdfArrays[0], density=densityArr[0], velocity=velocityArr[0])
-        return pdfArrays[0], densityArr[0], velocityArr[0]
-
-    cpuTimeLoop.kernel = lbmKernel
-    gpuTimeLoop.kernel = lbmKernel
-
-    return gpuTimeLoop if optimizationParams['target'] == 'gpu' else cpuTimeLoop
-
-
-def createFullyPeriodicFlow(initialVelocity, optimizationParams={}, lbmKernel=None, kernelParams={}, **kwargs):
-    domainSize = initialVelocity.shape
-    # extend velocity with ghost layer
-    initialVelocityWithGl = addGhostLayers(initialVelocity, indexDimensions=1, ghostLayers=1)
-
-    stencil = getStencil(kwargs['stencil'])
-    periodicity = getPeriodicBoundaryFunctor(stencil)
-
-    return createScenario(domainSize, None, kwargs, optimizationParams, lbmKernel=lbmKernel,
-                          initialVelocity=initialVelocityWithGl, kernelParams=kernelParams,
-                          preUpdateFunctions=[periodicity])
-
-
-def createLidDrivenCavity(domainSize, lidVelocity=0.005, optimizationParams={}, lbmKernel=None,
-                          kernelParams={}, **kwargs):
-    def boundarySetupFunction(boundaryHandling, method):
-        myUbb = partial(ubb, velocity=[lidVelocity, 0, 0][:method.dim])
-        myUbb.name = 'ubb'
-        boundaryHandling.setBoundary(myUbb, sliceFromDirection('N', method.dim))
-        for direction in ('W', 'E', 'S') if method.dim == 2 else ('W', 'E', 'S', 'T', 'B'):
-            boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, method.dim))
-
-    return createScenario(domainSize, boundarySetupFunction, kwargs, optimizationParams, lbmKernel=lbmKernel,
-                          kernelParams=kernelParams)
-
-
-def createPressureGradientDrivenChannel(dim, pressureDifference, domainSize=None, radius=None, length=None,
-                                        lbmKernel=None, optimizationParams={}, boundarySetupFunctions=[],
-                                        kernelParams={}, **kwargs):
-    assert dim in (2, 3)
-
-    if radius is not None:
-        assert length is not None
-        if dim == 3:
-            domainSize = (length, 2 * radius + 1, 2 * radius + 1)
-            roundChannel = True
-        else:
-            if domainSize is None:
-                domainSize = (length, 2 * radius)
-    else:
-        roundChannel = False
-
-    def boundarySetupFunction(boundaryHandling, method):
-        pressureBoundaryInflow = partial(fixedDensity, density=1.0 + pressureDifference)
-        pressureBoundaryInflow.__name__ = "Inflow"
-
-        pressureBoundaryOutflow = partial(fixedDensity, density=1.0)
-        pressureBoundaryOutflow.__name__ = "Outflow"
-        boundaryHandling.setBoundary(pressureBoundaryInflow, sliceFromDirection('W', method.dim))
-        boundaryHandling.setBoundary(pressureBoundaryOutflow, sliceFromDirection('E', method.dim))
-
-        if method.dim == 2:
-            for direction in ('N', 'S'):
-                boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, method.dim))
-        elif method.dim == 3:
-            if roundChannel:
-                noSlipIdx = boundaryHandling.addBoundary(noSlip)
-                ff = boundaryHandling.flagField
-                yMid = ff.shape[1] // 2
-                zMid = ff.shape[2] // 2
-                y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2]))
-                ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx
-            else:
-                for direction in ('N', 'S', 'T', 'B'):
-                    boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, method.dim))
-
-        for userFunction in boundarySetupFunctions:
-            userFunction(boundaryHandling, method, domainSize)
-
-    assert domainSize is not None
-    return createScenario(domainSize, boundarySetupFunction, kwargs, optimizationParams, lbmKernel=lbmKernel,
-                          kernelParams=kernelParams)
-
-
-def createForceDrivenChannel(dim, force, domainSize=None, radius=None, length=None, lbmKernel=None,
-                             optimizationParams={}, initialVelocity=None, boundarySetupFunctions=[],
-                             kernelParams={}, **kwargs):
-    assert dim in (2, 3)
-    kwargs['force'] = tuple([force, 0, 0][:dim])
-
-    if radius is not None:
-        assert length is not None
-        if dim == 3:
-            domainSize = (length, 2 * radius + 1, 2 * radius + 1)
-            roundChannel = True
-        else:
-            if domainSize is None:
-                domainSize = (length, 2 * radius)
-    else:
-        roundChannel = False
-
-    def boundarySetupFunction(boundaryHandling, method):
-        if method.dim == 2:
-            for direction in ('N', 'S'):
-                boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, method.dim))
-        elif method.dim == 3:
-            if roundChannel:
-                noSlipIdx = boundaryHandling.addBoundary(noSlip)
-                ff = boundaryHandling.flagField
-                yMid = ff.shape[1] // 2
-                zMid = ff.shape[2] // 2
-                y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2]))
-                ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx
-            else:
-                for direction in ('N', 'S', 'T', 'B'):
-                    boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, method.dim))
-        for userFunction in boundarySetupFunctions:
-            userFunction(boundaryHandling, method, domainSize)
-
-    def periodicity(pdfArr):
-        pdfArr[0, :, :] = pdfArr[-2, :, :]
-        pdfArr[-1, :, :] = pdfArr[1, :, :]
-
-    assert domainSize is not None
-    if 'forceModel' not in kwargs:
-        kwargs['forceModel'] = 'guo'
-
-    return createScenario(domainSize, boundarySetupFunction, kwargs, optimizationParams, lbmKernel=lbmKernel,
-                          initialVelocity=initialVelocity, preUpdateFunctions=[periodicity],
-                          kernelParams=kernelParams)
-