diff --git a/apps/showcases/FreeSurface/CMakeLists.txt b/apps/showcases/FreeSurface/CMakeLists.txt
index 34bed9ecd86323ffd5f914a757ca1d4453d4fbe7..19b28ec98daca1812d3259c5fa581afce2e713ad 100644
--- a/apps/showcases/FreeSurface/CMakeLists.txt
+++ b/apps/showcases/FreeSurface/CMakeLists.txt
@@ -29,14 +29,17 @@ waLBerla_add_executable(NAME    GravityWave
                         DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
 
 if( WALBERLA_BUILD_WITH_CODEGEN )
-   walberla_generate_target_from_python( NAME      GravityWaveLatticeModelGeneration
-                                         FILE      GravityWaveLatticeModelGeneration.py
-                                         OUT_FILES GravityWaveLatticeModel.cpp GravityWaveLatticeModel.h )
+   walberla_generate_target_from_python( NAME      GravityWaveGeneration
+                                         FILE      GravityWave.py
+                                         OUT_FILES GravityWaveStorageSpecification.h GravityWaveStorageSpecification.${CODEGEN_FILE_SUFFIX}
+                                                   GravityWaveSweepCollection.h GravityWaveSweepCollection.${CODEGEN_FILE_SUFFIX}
+                                                   NoSlip.h NoSlip.${CODEGEN_FILE_SUFFIX}
+                                                   GravityWaveBoundaryCollection.h
+                                                   GravityWaveInfoHeader.h)
 
    waLBerla_add_executable(NAME    GravityWaveCodegen
                            FILES   GravityWaveCodegen.cpp
-                           DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk
-                                   GravityWaveLatticeModelGeneration)
+                           DEPENDS blockforest boundary core domain_decomposition field geometry lbm_generated postprocessing timeloop vtk GravityWaveGeneration)
 endif()
 
 waLBerla_add_executable(NAME    MovingDrop
diff --git a/apps/showcases/FreeSurface/GravityWave.prm b/apps/showcases/FreeSurface/GravityWave.prm
index ffc5a4c49401890cc77f02ced1d2daedb4af9b64..31d3a3d4d3364c09c77734630242c2a11cd750e1 100644
--- a/apps/showcases/FreeSurface/GravityWave.prm
+++ b/apps/showcases/FreeSurface/GravityWave.prm
@@ -43,6 +43,11 @@ EvaluationParameters
    filename                gravity-wave.txt;
 }
 
+Logging
+{
+    logLevel info;  // info progress detail tracing
+}
+
 BoundaryParameters
 {
    // X
diff --git a/apps/showcases/FreeSurface/GravityWave.py b/apps/showcases/FreeSurface/GravityWave.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc2cd9449f6d5e28af8f6834bb096f77b3f38530
--- /dev/null
+++ b/apps/showcases/FreeSurface/GravityWave.py
@@ -0,0 +1,70 @@
+import sympy as sp
+import pystencils as ps
+from lbmpy.creationfunctions import LBMConfig, LBMOptimisation, create_lb_collision_rule
+from lbmpy.boundaries import NoSlip
+from lbmpy.enums import ForceModel, Method, Stencil
+from lbmpy.stencils import LBStencil
+
+from pystencils_walberla import CodeGeneration, generate_info_header, generate_sweep
+from lbmpy_walberla import generate_lbm_package, lbm_boundary_generator
+
+
+info_header = """
+const bool infoCseGlobal = {cse_global};
+const bool infoCsePdfs = {cse_pdfs};
+"""
+
+
+with CodeGeneration() as ctx:
+    # general parameters
+    layout = 'fzyx'
+    data_type = "float64" if ctx.double_accuracy else "float32"
+
+    stencil = LBStencil(Stencil.D3Q19)
+    omega = sp.Symbol('omega')
+
+    assert stencil.D == 3, "This application supports only three-dimensional stencils"
+    pdfs, pdfs_tmp = ps.fields(f"pdfs({stencil.Q}), pdfs_tmp({stencil.Q}): {data_type}[3D]", layout='fzyx')
+    density_field, velocity_field = ps.fields(f"density, velocity(3) : {data_type}[3D]", layout='fzyx')
+    macroscopic_fields = {'density': density_field, 'velocity': velocity_field}
+    force_field = ps.fields(f"force(3): {data_type}[3D]", layout='fzyx')
+
+    # method definition
+    lbm_config = LBMConfig(stencil=stencil,
+                           method=Method.SRT,
+                           relaxation_rate=omega,
+                           compressible=True,
+                           force=force_field,
+                           force_model=ForceModel.GUO,
+                           zero_centered=False,
+                           streaming_pattern='pull')  # free surface implementation only works with pull pattern
+
+    # optimizations to be used by the code generator
+    lbm_opt = LBMOptimisation(cse_global=True,
+                              field_layout=layout, symbolic_field=pdfs, symbolic_temporary_field=pdfs_tmp)
+
+    collision_rule = create_lb_collision_rule(lbm_config=lbm_config,
+                                              lbm_optimisation=lbm_opt)
+
+    no_slip = lbm_boundary_generator(class_name='NoSlip', flag_uid='NoSlip',
+                                     boundary_object=NoSlip())
+
+    generate_lbm_package(ctx, name="GravityWave",
+                         collision_rule=collision_rule,
+                         lbm_config=lbm_config, lbm_optimisation=lbm_opt,
+                         nonuniform=False, boundaries=[no_slip, ],
+                         macroscopic_fields=macroscopic_fields,
+                         target=ps.Target.CPU)
+
+    infoHeaderParams = {
+        'cse_global': int(lbm_opt.cse_global),
+        'cse_pdfs': int(lbm_opt.cse_pdfs),
+    }
+
+    field_typedefs = {'VectorField_T': velocity_field,
+                      'ScalarField_T': density_field}
+
+    # Info header containing correct template definitions for stencil and field
+    generate_info_header(ctx, 'GravityWaveInfoHeader',
+                         field_typedefs=field_typedefs,
+                         additional_code=info_header.format(**infoHeaderParams))
diff --git a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
index 65d18594b5b2c9f024ef5347b871ff2b9b79501a..a248b5d6d9ff431b9d23303a72c58fa45d2ec077 100644
--- a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
+++ b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
@@ -23,47 +23,49 @@
 #include "blockforest/Initialization.h"
 
 #include "core/Environment.h"
+#include "core/logging/Initialization.h"
 
 #include "field/Gather.h"
 
-#include "lbm/PerformanceLogger.h"
-#include "lbm/field/AddToStorage.h"
-#include "lbm/free_surface/LoadBalancing.h"
-#include "lbm/free_surface/SurfaceMeshWriter.h"
-#include "lbm/free_surface/TotalMassComputer.h"
-#include "lbm/free_surface/VtkWriter.h"
-#include "lbm/free_surface/bubble_model/Geometry.h"
-#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h"
-#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h"
-#include "lbm/free_surface/surface_geometry/Utility.h"
-#include "lbm/lattice_model/D3Q19.h"
+#include "geometry/InitBoundaryHandling.h"
 
-#include "GravityWaveLatticeModel.h"
+#include "lbm_generated/blockforest/SimpleCommunication.h"
+#include "lbm_generated/evaluation/PerformanceEvaluation.h"
+#include "lbm_generated/communication/UniformGeneratedPdfPackInfo.h"
+#include "lbm_generated/field/AddToStorage.h"
+#include "lbm_generated/free_surface/InitFunctions.h"
+#include "lbm_generated/free_surface/LoadBalancing.h"
+#include "lbm_generated/free_surface/SurfaceMeshWriter.h"
+#include "lbm_generated/free_surface/TotalMassComputer.h"
+#include "lbm_generated/free_surface/VtkWriter.h"
+#include "lbm_generated/free_surface/dynamics/SurfaceDynamicsHandler.h"
+#include "lbm_generated/free_surface/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm_generated/free_surface/surface_geometry/Utility.h"
+
+#include "GravityWaveInfoHeader.h"
 
 namespace walberla
 {
-namespace free_surface
+namespace free_surface_generated
 {
 namespace GravityWaveCodegen
 {
-using ScalarField_T          = GhostLayerField< real_t, 1 >;
-using VectorField_T          = GhostLayerField< Vector3< real_t >, 1 >;
-using VectorFieldFlattened_T = GhostLayerField< real_t, 3 >;
+using StorageSpecification_T = lbm::GravityWaveStorageSpecification;
+using Stencil_T              = StorageSpecification_T::Stencil;
+using PdfField_T             = lbm_generated::PdfField< StorageSpecification_T >;
+using SweepCollection_T      = lbm::GravityWaveSweepCollection;
 
-using LatticeModel_T        = lbm::GravityWaveLatticeModel;
-using LatticeModelStencil_T = LatticeModel_T::Stencil;
-using PdfField_T            = lbm::PdfField< LatticeModel_T >;
-using PdfCommunication_T    = blockforest::SimpleCommunication< LatticeModelStencil_T >;
 
 // the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner
 // directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil
-using CommunicationStencil_T =
-   typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
-using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >;
+using CommunicationStencil_T = typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+using Communication_T        = lbm_generated::SimpleCommunication< CommunicationStencil_T >;
 
 using flag_t                        = uint32_t;
 using FlagField_T                   = FlagField< flag_t >;
-using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+using BoundaryCollection_T          = lbm::GravityWaveBoundaryCollection< FlagField_T >;
+
+using blockforest::communication::UniformBufferedScheme;
 
 // write each entry in "vector" to line in a file; columns are separated by tabs
 template< typename T >
@@ -112,6 +114,7 @@ class SymmetryXEvaluator
          fillFieldGathered =
             std::make_shared< ScalarField_T >(domainSize_[0], domainSize_[1], domainSize_[2], uint_c(0));
       }
+      WALBERLA_ASSERT_NOT_NULLPTR(*fillFieldGathered)
       field::gather< ScalarField_T, ScalarField_T >(*fillFieldGathered, blockForest, fillFieldID_);
 
       WALBERLA_ROOT_SECTION()
@@ -164,16 +167,15 @@ class SymmetryXEvaluator
 }; // class SymmetryXEvaluator
 
 // get interface position in y-direction at the specified (global) x-coordinate
-template< typename FreeSurfaceBoundaryHandling_T >
 class SurfaceYPositionEvaluator
 {
  public:
    SurfaceYPositionEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest,
-                             const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                             const ConstBlockDataID& flagFieldID, const FlagInfo<FlagField_T>& flagInfo,
                              const ConstBlockDataID& fillFieldID, const Vector3< uint_t >& domainSize,
                              cell_idx_t globalXCoordinate, uint_t frequency,
                              const std::shared_ptr< real_t >& surfaceYPosition)
-      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), fillFieldID_(fillFieldID),
+      : blockForest_(blockForest), flagFieldID_(flagFieldID), flagInfo_(flagInfo), fillFieldID_(fillFieldID),
         domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition),
         frequency_(frequency), executionCounter_(uint_c(0))
    {}
@@ -183,17 +185,10 @@ class SurfaceYPositionEvaluator
       auto blockForest = blockForest_.lock();
       WALBERLA_CHECK_NOT_NULLPTR(blockForest);
 
-      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
-      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
-
       ++executionCounter_;
 
       // only evaluate in given frequencies
       if (executionCounter_ % frequency_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; }
-
-      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
-      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
-
       *surfaceYPosition_ = real_c(0);
 
       for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
@@ -209,13 +204,13 @@ class SurfaceYPositionEvaluator
             Cell localEvalCell = Cell(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0));
             blockForest->transformGlobalToBlockLocalCell(localEvalCell, *blockIt);
 
-            const FlagField_T* const flagField   = blockIt->template getData< const FlagField_T >(flagFieldID);
+            const FlagField_T* const flagField   = blockIt->template getData< const FlagField_T >(flagFieldID_);
             const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_);
 
             // searching from top ensures that the interface cell with the greatest y-coordinate is found first
             for (cell_idx_t y = cell_idx_c((flagField)->ySize() - uint_c(1)); y >= cell_idx_t(0); --y)
             {
-               if (flagInfo.isInterface(flagField->get(localEvalCell[0], y, cell_idx_c(0))))
+               if (flagInfo_.isInterface(flagField->get(localEvalCell[0], y, cell_idx_c(0))))
                {
                   const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0));
 
@@ -238,7 +233,8 @@ class SurfaceYPositionEvaluator
 
  private:
    std::weak_ptr< const StructuredBlockForest > blockForest_;
-   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   ConstBlockDataID flagFieldID_;
+   FlagInfo<FlagField_T> flagInfo_;
    ConstBlockDataID fillFieldID_;
    Vector3< uint_t > domainSize_;
    cell_idx_t globalXCoordinate_;
@@ -248,6 +244,15 @@ class SurfaceYPositionEvaluator
    uint_t executionCounter_;
 }; // class SurfaceYPositionEvaluator
 
+template< typename FlagField_T >
+void flagFieldInitFunction(FlagField_T* flagField, IBlock* const, const Set< field::FlagUID >& obstacleIDs,
+                           const Set< field::FlagUID >& outflowIDs, const Set< field::FlagUID >& inflowIDs,
+                           const Set< field::FlagUID >& freeSlipIDs)
+{
+   // register flags in the flag field
+   FlagInfo< FlagField_T >::registerFlags(flagField, obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs);
+}
+
 int main(int argc, char** argv)
 {
    Environment walberlaEnv(argc, argv);
@@ -256,6 +261,7 @@ int main(int argc, char** argv)
 
    // print content of parameter file
    WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+   logging::configureLogging(walberlaEnv.config());
 
    // get block forest parameters from parameter file
    auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
@@ -358,28 +364,62 @@ int main(int argc, char** argv)
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename);
 
    // create non-uniform block forest (non-uniformity required for load balancing)
-   const std::shared_ptr< StructuredBlockForest > blockForest =
-      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+   const std::shared_ptr< StructuredBlockForest > blockForest = createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
 
    // add force field
    const BlockDataID forceDensityFieldID =
-      field::addToStorage< VectorFieldFlattened_T >(blockForest, "Force field", real_c(0), field::fzyx, uint_c(1));
+      field::addToStorage< VectorField_T >(blockForest, "Force field", real_c(0), field::fzyx, uint_c(1));
 
-   // create lattice model
-   LatticeModel_T latticeModel = LatticeModel_T(forceDensityFieldID, relaxationRate);
+   // create storage specification
+   StorageSpecification_T storageSpecification = StorageSpecification_T();
 
    // add pdf field
-   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   const BlockDataID pdfFieldID     = lbm_generated::addPdfFieldToStorage(blockForest, "PDF field", storageSpecification, uint_c(1), field::fzyx);
+   const BlockDataID velFieldId     = field::addToStorage< VectorField_T >(blockForest, "vel", real_c(0.0), field::fzyx);
+   const BlockDataID densityFieldId = field::addToStorage< ScalarField_T >(blockForest, "density", real_c(1.0), field::fzyx);
 
    // add fill level field (initialized with 0, i.e., gas everywhere)
-   const BlockDataID fillFieldID =
-      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2));
-
-   // add boundary handling
-   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
-      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
-   const BlockDataID flagFieldID                                      = freeSurfaceBoundaryHandling->getFlagFieldID();
-   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+   const BlockDataID fillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2));
+
+   // initialize obstacleIDs
+   Set< FlagUID > obstacleIDs;
+   obstacleIDs += field::FlagUID("NoSlip");
+   obstacleIDs += field::FlagUID("UBB");
+   obstacleIDs += field::FlagUID("UBB_Inflow");
+   obstacleIDs += field::FlagUID("Pressure");
+   obstacleIDs += field::FlagUID("PressureOutflow");
+   obstacleIDs += field::FlagUID("Outlet");
+   obstacleIDs += field::FlagUID("FreeSlip");
+
+   // initialize outflowIDs
+   Set< FlagUID > outflowIDs;
+   outflowIDs += field::FlagUID("PressureOutflow");
+   outflowIDs += field::FlagUID("Outlet");
+
+   // initialize outflowIDs
+   Set< FlagUID > inflowIDs;
+   inflowIDs += field::FlagUID("UBB_Inflow");
+
+   // initialize freeSlipIDs
+   Set< FlagUID > freeSlipIDs;
+   freeSlipIDs += field::FlagUID("FreeSlip");
+
+   auto ffInitFunc = std::bind(flagFieldInitFunction< FlagField_T >, std::placeholders::_1,
+                               std::placeholders::_2, obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs);
+
+   auto flagInfo = FlagInfo< FlagField_T >(obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs);
+   const BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >(blockForest, "Boundary Flag Field", uint_c(2), true, ffInitFunc);
+
+   // Boundaries
+   const FlagUID fluidFlagUID("Fluid");
+   auto boundariesConfig = walberlaEnv.config()->getBlock("Boundaries");
+   if (boundariesConfig)
+   {
+      WALBERLA_LOG_INFO_ON_ROOT("Setting boundary conditions")
+      geometry::initBoundaryHandling< FlagField_T >(*blockForest, flagFieldID, boundariesConfig);
+   }
+   geometry::setNonBoundaryCellsToDomain< FlagField_T >(*blockForest, flagFieldID, fluidFlagUID);
+   BoundaryCollection_T boundaryCollection(blockForest, flagFieldID, pdfFieldID, fluidFlagUID);
 
    // samples used in the Monte-Carlo like estimation of the fill level
    const uint_t fillLevelInitSamples = uint_c(100); // actually there will be 101 since 0 is also included
@@ -427,44 +467,33 @@ int main(int argc, char** argv)
       }) // WALBERLA_FOR_ALL_CELLS
    }
 
-   // initialize domain boundary conditions from config file
-   const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters");
-   freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters);
-
    // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling
    // might not detect solid flags correctly
-   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+   // TODO: Implement as free function; might only be needed with bubble model ....
+   // freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
 
    // communication after initialization
    Communication_T communication(blockForest, flagFieldID, fillFieldID, forceDensityFieldID);
    communication();
 
-   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
-   pdfCommunication();
+   auto packInfo = std::make_shared<lbm_generated::UniformGeneratedPdfPackInfo< PdfField_T >>(pdfFieldID);
+   UniformBufferedScheme< Stencil_T > pdfCommunication(blockForest);
+   pdfCommunication.addPackInfo(packInfo);
 
-   // add bubble model
-   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
-   if (enableBubbleModel)
-   {
-      const std::shared_ptr< bubble_model::BubbleModel< LatticeModelStencil_T > > bubbleModelDerived =
-         std::make_shared< bubble_model::BubbleModel< LatticeModelStencil_T > >(blockForest, enableBubbleSplits);
-      bubbleModelDerived->initFromFillLevelField(fillFieldID);
-      bubbleModelDerived->setAtmosphere(Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), uint_c(0)),
-                                        real_c(1));
-
-      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
-   }
-   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+   const Cell innerOuterSplit = Cell(Vector3<cell_idx_t>(1, 1, 1));
+   SweepCollection_T sweepCollection(blockForest, forceDensityFieldID, pdfFieldID, densityFieldId, velFieldId, relaxationRate, innerOuterSplit);
 
    // initialize hydrostatic pressure
-   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, acceleration, liquidDepth);
-
+   initHydrostaticPressure< ScalarField_T >(blockForest, densityFieldId, acceleration, liquidDepth);
    // initialize force density field
-   initForceDensityFieldCodegen< PdfField_T, FlagField_T, VectorFieldFlattened_T, ScalarField_T >(
-      blockForest, forceDensityFieldID, fillFieldID, pdfFieldID, flagFieldID, flagInfo, acceleration);
-
+   initForceDensityField< FlagField_T, VectorField_T, ScalarField_T >(blockForest, forceDensityFieldID, fillFieldID, densityFieldId, flagFieldID, flagInfo, acceleration);
    // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure)
-   setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID);
+   setDensityInNonFluidCellsToOne< FlagField_T, VectorField_T, ScalarField_T >(blockForest, flagInfo, flagFieldID, velFieldId, densityFieldId);
+
+   for (auto& block : *blockForest)
+   {
+      sweepCollection.initialise(&block);
+   }
 
    // create timeloop
    SweepTimeloop timeloop(blockForest, timesteps);
@@ -477,8 +506,8 @@ int main(int argc, char** argv)
    if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; }
 
    // add surface geometry handler
-   const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler(
-      blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting,
+   SurfaceGeometryHandler< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo< FlagField_T > > geometryHandler(
+      blockForest, flagInfo, flagFieldID, fillFieldID, curvatureModel, computeCurvature, enableWetting,
       contactAngle);
 
    geometryHandler.addSweeps(timeloop);
@@ -488,25 +517,24 @@ int main(int argc, char** argv)
    const ConstBlockDataID normalFieldID    = geometryHandler.getConstNormalFieldID();
 
    // add boundary handling for standard boundaries and free surface boundaries
-   const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T, true,
-                                 VectorFieldFlattened_T >
-      dynamicsHandler(blockForest, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID, normalFieldID,
-                      curvatureFieldID, freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel,
+   const SurfaceDynamicsHandler< StorageSpecification_T, SweepCollection_T, BoundaryCollection_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo< FlagField_T >>
+      dynamicsHandler(blockForest, sweepCollection, boundaryCollection, flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID, normalFieldID,
+                      curvatureFieldID, pdfReconstructionModel,
                       pdfRefillingModel, excessMassDistributionModel, relaxationRate, acceleration, surfaceTension,
                       useSimpleMassExchange, cellConversionThreshold, cellConversionForceThreshold);
 
    dynamicsHandler.addSweeps(timeloop);
 
    // add load balancing
-   const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer(
-      blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5),
+   const LoadBalancer< FlagField_T, CommunicationStencil_T, Stencil_T > loadBalancer(
+      blockForest, communication, pdfCommunication, uint_c(50), uint_c(10), uint_c(5),
       loadBalancingFrequency, printLoadBalancingStatistics);
    timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
 
    // add sweep for evaluating the surface position in y-direction
    const std::shared_ptr< real_t > surfaceYPosition = std::make_shared< real_t >(real_c(0));
-   const SurfaceYPositionEvaluator< FreeSurfaceBoundaryHandling_T > positionEvaluator(
-      blockForest, freeSurfaceBoundaryHandling, fillFieldID, domainSize, cell_idx_c(real_c(domainWidth) * real_c(0.5)),
+   const SurfaceYPositionEvaluator positionEvaluator(
+      blockForest, flagFieldID, flagInfo, fillFieldID, domainSize, cell_idx_c(real_c(domainWidth) * real_c(0.5)),
       evaluationFrequency, surfaceYPosition);
    timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position");
 
@@ -519,14 +547,13 @@ int main(int argc, char** argv)
    // add evaluator for total and excessive mass (mass that is currently undistributed)
    const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
    const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
-   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
-      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+   const TotalMassComputer< PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, flagFieldID, flagInfo, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
       evaluationFrequency, totalMass, excessMass);
    timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
 
    // add VTK output
-   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T,
-                 true, VectorFieldFlattened_T >(
+   addVTKOutput< FlagInfo<FlagField_T>, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
       geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
       geometryHandler.getObstNormalFieldID());
@@ -538,9 +565,9 @@ int main(int argc, char** argv)
    timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
 
    // add logging for computational performance
-   const lbm::PerformanceLogger< FlagField_T > performanceLogger(
-      blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, performanceLogFrequency);
-   timeloop.addFuncAfterTimeStep(performanceLogger, "Evaluator: performance logging");
+//   const lbm_generated::PerformanceLogger< FlagField_T > performanceLogger(
+//      blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, performanceLogFrequency);
+//   timeloop.addFuncAfterTimeStep(performanceLogger, "Evaluator: performance logging");
 
    WcTimingPool timingPool;
 
@@ -591,4 +618,4 @@ void writeVectorToFile(const std::vector< T >& vector, const std::string& filena
 } // namespace free_surface
 } // namespace walberla
 
-int main(int argc, char** argv) { return walberla::free_surface::GravityWaveCodegen::main(argc, argv); }
\ No newline at end of file
+int main(int argc, char** argv) { return walberla::free_surface_generated::GravityWaveCodegen::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py b/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py
deleted file mode 100644
index aa61ee7979a125e427e7c5c2d588c141fd11b81d..0000000000000000000000000000000000000000
--- a/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import sympy as sp
-import pystencils as ps
-from lbmpy.creationfunctions import LBMConfig, LBMOptimisation, create_lb_collision_rule
-from lbmpy.enums import ForceModel, Method, Stencil
-from lbmpy.stencils import LBStencil
-
-from pystencils_walberla import CodeGeneration
-from lbmpy_walberla import generate_lattice_model
-
-
-with CodeGeneration() as ctx:
-    # general parameters
-    layout = 'fzyx'
-    data_type = "float64" if ctx.double_accuracy else "float32"
-
-    stencil = LBStencil(Stencil.D3Q19)
-    omega = sp.Symbol('omega')
-    force_field = ps.fields(f"force(3): {data_type}[3D]", layout='fzyx')
-
-    # method definition
-    lbm_config = LBMConfig(stencil=stencil,
-                           method=Method.SRT,
-                           relaxation_rate=omega,
-                           compressible=True,
-                           force=force_field,
-                           force_model=ForceModel.GUO,
-                           zero_centered=False,
-                           streaming_pattern='pull')  # free surface implementation only works with pull pattern
-
-    # optimizations to be used by the code generator
-    lbm_opt = LBMOptimisation(cse_global=True,
-                              field_layout=layout)
-
-    collision_rule = create_lb_collision_rule(lbm_config=lbm_config,
-                                              lbm_optimisation=lbm_opt)
-
-    generate_lattice_model(ctx, "GravityWaveLatticeModel", collision_rule, field_layout=layout)
diff --git a/src/lbm_generated/CMakeLists.txt b/src/lbm_generated/CMakeLists.txt
index 2513a58f2e646025fa86107409058a7576a3f62f..431df203e05125b5f06f79c5545c12167af56fae 100644
--- a/src/lbm_generated/CMakeLists.txt
+++ b/src/lbm_generated/CMakeLists.txt
@@ -15,11 +15,14 @@ target_link_libraries( lbm_generated
         vtk
         )
 
+add_subdirectory( blockforest )
 add_subdirectory( boundary )
 add_subdirectory( communication )
 add_subdirectory( gpu )
+add_subdirectory( macroscopics )
 add_subdirectory( evaluation )
 add_subdirectory( field )
+add_subdirectory( free_surface )
 add_subdirectory( refinement )
 add_subdirectory( storage_specification )
 add_subdirectory( sweep_collection )
\ No newline at end of file
diff --git a/src/lbm_generated/blockforest/CMakeLists.txt b/src/lbm_generated/blockforest/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c9eb5be64761ed0adec532b651e6a6ea97ae78d4
--- /dev/null
+++ b/src/lbm_generated/blockforest/CMakeLists.txt
@@ -0,0 +1,5 @@
+target_sources( lbm_generated
+        PRIVATE
+        SimpleCommunication.h
+        UpdateSecondGhostLayer.h
+        )
diff --git a/src/lbm_generated/blockforest/SimpleCommunication.h b/src/lbm_generated/blockforest/SimpleCommunication.h
new file mode 100644
index 0000000000000000000000000000000000000000..0ce61f74cadf10792da33a78c0aadf57bf1d8ffc
--- /dev/null
+++ b/src/lbm_generated/blockforest/SimpleCommunication.h
@@ -0,0 +1,171 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SimpleCommunication.h
+//! \ingroup blockforest
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/Abort.h"
+#include "core/math/Vector3.h"
+#include "core/mpi/BufferDataTypeExtensions.h"
+
+#include "field/FlagField.h"
+#include "field/communication/PackInfo.h"
+
+namespace walberla::lbm_generated
+{
+using blockforest::communication::UniformBufferedScheme;
+using field::communication::PackInfo;
+
+template< typename Stencil_T >
+class SimpleCommunication : public blockforest::communication::UniformBufferedScheme< Stencil_T >
+{
+   using RealScalarField_T      = GhostLayerField< real_t, 1 >;
+   using VectorField_T          = GhostLayerField< Vector3< real_t >, 1 >;
+   using VectorFieldFlattened_T = GhostLayerField< real_t, 3 >;
+   using UintScalarField_T      = GhostLayerField< uint_t, 1 >;
+   using IDScalarField_T        = walberla::GhostLayerField< walberla::id_t, 1 >;
+
+   using FlagField16_T = FlagField< uint16_t >;
+   using FlagField32_T = FlagField< uint32_t >;
+   using FlagField64_T = FlagField< uint64_t >;
+
+ public:
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1;
+   }
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2;
+   }
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2,
+                       BlockDataID f3)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2 << f3;
+   }
+
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2,
+                       BlockDataID f3, BlockDataID f4)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2 << f3 << f4;
+   }
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2,
+                       BlockDataID f3, BlockDataID f4, BlockDataID f5)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2 << f3 << f4 << f5;
+   }
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2,
+                       BlockDataID f3, BlockDataID f4, BlockDataID f5, BlockDataID f6)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2 << f3 << f4 << f5 << f6;
+   }
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2,
+                       BlockDataID f3, BlockDataID f4, BlockDataID f5, BlockDataID f6, BlockDataID f7)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2 << f3 << f4 << f5 << f6 << f7;
+   }
+   SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2,
+                       BlockDataID f3, BlockDataID f4, BlockDataID f5, BlockDataID f6, BlockDataID f7, BlockDataID f8)
+      : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest)
+   {
+      (*this) << f1 << f2 << f3 << f4 << f5 << f6 << f7 << f8;
+   }
+
+   SimpleCommunication& operator<<(BlockDataID fieldId)
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      if (blockForest->begin() == blockForest->end()) { return *this; }
+      IBlock& firstBlock = *(blockForest->begin());
+
+      if (firstBlock.isDataClassOrSubclassOf< RealScalarField_T >(fieldId))
+      {
+         this->addPackInfo(make_shared< PackInfo< RealScalarField_T > >(fieldId));
+      }
+      else
+      {
+         if (firstBlock.isDataClassOrSubclassOf< VectorField_T >(fieldId))
+         {
+            this->addPackInfo(make_shared< PackInfo< VectorField_T > >(fieldId));
+         }
+         else
+         {
+            if (firstBlock.isDataClassOrSubclassOf< FlagField16_T >(fieldId))
+            {
+               this->addPackInfo(make_shared< PackInfo< FlagField16_T > >(fieldId));
+            }
+            else
+            {
+               if (firstBlock.isDataClassOrSubclassOf< FlagField32_T >(fieldId))
+               {
+                  this->addPackInfo(make_shared< PackInfo< FlagField32_T > >(fieldId));
+               }
+               else
+               {
+                  if (firstBlock.isDataClassOrSubclassOf< FlagField64_T >(fieldId))
+                  {
+                     this->addPackInfo(make_shared< PackInfo< FlagField64_T > >(fieldId));
+                  }
+                  else
+                  {
+                     if (firstBlock.isDataClassOrSubclassOf< IDScalarField_T >(fieldId))
+                     {
+                        this->addPackInfo(make_shared< PackInfo< IDScalarField_T > >(fieldId));
+                     }
+                     else
+                     {
+                        if (firstBlock.isDataClassOrSubclassOf< UintScalarField_T >(fieldId))
+                        {
+                           this->addPackInfo(make_shared< PackInfo< UintScalarField_T > >(fieldId));
+                        }
+                        else
+                        {
+                           if (firstBlock.isDataClassOrSubclassOf< VectorFieldFlattened_T >(fieldId))
+                           {
+                              this->addPackInfo(make_shared< PackInfo< VectorFieldFlattened_T > >(fieldId));
+                           }
+                           else { WALBERLA_ABORT("Problem with UID"); }
+                        }
+                     }
+                  }
+               }
+            }
+         }
+      }
+
+      return *this;
+   }
+
+ protected:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+}; // class SimpleCommunication
+
+} // namespace walberla
diff --git a/src/lbm_generated/blockforest/UpdateSecondGhostLayer.h b/src/lbm_generated/blockforest/UpdateSecondGhostLayer.h
new file mode 100644
index 0000000000000000000000000000000000000000..2a9bb43b9168094f74105973ee33f8351b6c441a
--- /dev/null
+++ b/src/lbm_generated/blockforest/UpdateSecondGhostLayer.h
@@ -0,0 +1,141 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file UpdateSecondGhostLayer.h
+//! \ingroup blockforest
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+
+namespace walberla::lbm_generated
+{
+/********************************************************************************************************************
+ * Manual update of a field's second and further ghost layers in setups with domain size = 1 and periodicity in at
+ * least one direction.
+ *
+ * If a field has two ghost layers and if the inner field has only a size of one (in one or more directions),
+ * regular communication does not update the second (and further) ghost layers correctly. Here, the content of the
+ * first ghost layers is manually copied to the second (and further) ghost layers when the dimension of the field is
+ * one in this direction.
+ *******************************************************************************************************************/
+template< typename Field_T >
+class UpdateSecondGhostLayer
+{
+ public:
+   UpdateSecondGhostLayer(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, BlockDataID fieldID)
+      : blockForest_(blockForestPtr), fieldID_(fieldID)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         Field_T* const field = blockIt->template getData< Field_T >(fieldID_);
+
+         // this function is only necessary if the flag field has at least two ghost layers
+         if (field->nrOfGhostLayers() < uint_c(2)) { isNecessary_ = false; }
+         else
+         {
+            // running this function is only necessary when the field is of size 1 in at least one direction
+            isNecessary_ = (field->xSize() == uint_c(1) && blockForest->isXPeriodic()) ||
+                           (field->ySize() == uint_c(1) && blockForest->isYPeriodic()) ||
+                           (field->zSize() == uint_c(1) && blockForest->isZPeriodic());
+         }
+      }
+   }
+
+   void operator()()
+   {
+      if (!isNecessary_) { return; }
+
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         Field_T* const field = blockIt->template getData< Field_T >(fieldID_);
+
+         if (field->xSize() == uint_c(1) && blockForest->isXPeriodic())
+         {
+            // iterate ghost layer at x == -1
+            for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::W, true); fieldIt != field->end();
+                 ++fieldIt)
+            {
+               // copy data from ghost layer at x == -1 to x == -2
+               fieldIt.neighbor(stencil::W) = *fieldIt;
+            }
+
+            // iterate ghost layer at x == xSize()+1
+            for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::E, true); fieldIt != field->end();
+                 ++fieldIt)
+            {
+               // copy data from ghost layer at x == xSize()+1 to x == xSize()+2
+               fieldIt.neighbor(stencil::E) = *fieldIt;
+            }
+         }
+
+         if (field->ySize() == uint_c(1) && blockForest->isYPeriodic())
+         {
+            // iterate ghost layer at y == -1
+            for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::S, true); fieldIt != field->end();
+                 ++fieldIt)
+            {
+               // copy data from ghost layer at y == -1 to y == -2
+               fieldIt.neighbor(stencil::S) = *fieldIt;
+            }
+
+            // iterate ghost layer at y == ySize()+1
+            for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::N, true); fieldIt != field->end();
+                 ++fieldIt)
+            {
+               // copy data from ghost layer at y == ySize()+1 to y == ySize()+2
+               fieldIt.neighbor(stencil::N) = *fieldIt;
+            }
+         }
+
+         if (field->zSize() == uint_c(1) && blockForest->isZPeriodic())
+         {
+            // iterate ghost layer at z == -1
+            for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::B, true); fieldIt != field->end();
+                 ++fieldIt)
+            {
+               // copy data from ghost layer at z == -1 to z == -2
+               fieldIt.neighbor(stencil::B) = *fieldIt;
+            }
+
+            // iterate ghost layer at y == zSize()+1
+            for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::T, true); fieldIt != field->end();
+                 ++fieldIt)
+            {
+               // copy data from ghost layer at z == zSize()+1 to z == zSize()+2
+               fieldIt.neighbor(stencil::T) = *fieldIt;
+            }
+         }
+      }
+   }
+
+ private:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+
+   BlockDataID fieldID_;
+
+   bool isNecessary_;
+}; // class UpdateSecondGhostLayer
+
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/BlockStateDetectorSweep.h b/src/lbm_generated/free_surface/BlockStateDetectorSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..e080b46844f3268cbde2bcbbd2c88974200dd943
--- /dev/null
+++ b/src/lbm_generated/free_surface/BlockStateDetectorSweep.h
@@ -0,0 +1,111 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file BlockStateDetectorSweep.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Set block states according to their content, i.e., free surface flags.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/uid/SUID.h"
+
+#include "FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Set block states according to free surface flag field:
+ *  - blocks that contain at least one interface cell are marked as "fullFreeSurface" (computationally most expensive
+ *    type of blocks)
+ *  - cells without interface cell and with at least one liquid cell are marked "onlyLBM"
+ *  - all other blocks are marked as "onlyGasAndBoundary"
+ **********************************************************************************************************************/
+template< typename FlagField_T >
+class BlockStateDetectorSweep
+{
+ public:
+   static const SUID fullFreeSurface;
+   static const SUID onlyGasAndBoundary;
+   static const SUID onlyLBM;
+
+   BlockStateDetectorSweep(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                           FlagInfo< FlagField_T > flagInfo, ConstBlockDataID flagFieldID)
+      : flagFieldID_(flagFieldID), flagInfo_(flagInfo)
+   {
+      const auto blockForest = blockForestPtr.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         (*this)(&(*blockIt)); // initialize block states
+      }
+   }
+
+   void operator()(IBlock* const block)
+   {
+      bool liquidFound    = false;
+      bool interfaceFound = false;
+
+      const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_);
+
+      // search the flag field for interface and liquid cells
+      WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(flagField, uint_c(1), {
+         const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+         // at inflow boundaries, interface cells are generated; therefore, the block must be fullFreeSurface even if it
+         // does not yet contain an interface cell
+         if (flagInfo_.isInterface(flagFieldPtr) || flagInfo_.isInflow(flagFieldPtr))
+         {
+            interfaceFound = true;
+            break; // stop search, as this block belongs already to the computationally most expensive block type
+         }
+
+         if (flagInfo_.isLiquid(flagFieldPtr)) { liquidFound = true; }
+      }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+
+      block->clearState();
+      if (interfaceFound)
+      {
+         block->addState(fullFreeSurface);
+         return;
+      }
+      if (liquidFound)
+      {
+         block->addState(onlyLBM);
+         return;
+      }
+      block->addState(onlyGasAndBoundary);
+   }
+
+ protected:
+   ConstBlockDataID flagFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+}; // class BlockStateDetectorSweep
+
+template< typename FlagField_T >
+const SUID BlockStateDetectorSweep< FlagField_T >::fullFreeSurface = SUID("fullFreeSurface");
+template< typename FlagField_T >
+const SUID BlockStateDetectorSweep< FlagField_T >::onlyGasAndBoundary = SUID("onlyGasAndBoundary");
+template< typename FlagField_T >
+const SUID BlockStateDetectorSweep< FlagField_T >::onlyLBM = SUID("onlyLBM");
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/CMakeLists.txt b/src/lbm_generated/free_surface/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..50a34dce935e820251e5bfc695de97c6e91fd4f0
--- /dev/null
+++ b/src/lbm_generated/free_surface/CMakeLists.txt
@@ -0,0 +1,17 @@
+target_sources( lbm_generated
+        PRIVATE
+        BlockStateDetectorSweep.h
+        FlagDefinitions.h
+        FlagInfo.h
+        FlagInfo.impl.h
+        InitFunctions.h
+        InterfaceFromFillLevel.h
+        LoadBalancing.h
+        MaxVelocityComputer.h
+        SurfaceMeshWriter.h
+        TotalMassComputer.h
+        VtkWriter.h
+        )
+
+add_subdirectory( dynamics )
+add_subdirectory( surface_geometry )
diff --git a/src/lbm_generated/free_surface/FlagDefinitions.h b/src/lbm_generated/free_surface/FlagDefinitions.h
new file mode 100644
index 0000000000000000000000000000000000000000..06393e23870253d458c4c0115f45c70f6f9ca233
--- /dev/null
+++ b/src/lbm_generated/free_surface/FlagDefinitions.h
@@ -0,0 +1,51 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file FlagInfo.h
+//! \ingroup free_surface
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Define free surface flags (e.g. conversion flags).
+//
+//======================================================================================================================
+
+#include "field/FlagUID.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+namespace flagIDs
+{
+/***********************************************************************************************************************
+ * Definition of free surface flag IDs.
+ **********************************************************************************************************************/
+const field::FlagUID interfaceFlagID                   = field::FlagUID("interface");
+const field::FlagUID liquidFlagID                      = field::FlagUID("liquid");
+const field::FlagUID gasFlagID                         = field::FlagUID("gas");
+const field::FlagUID convertedFlagID                   = field::FlagUID("converted");
+const field::FlagUID convertToGasFlagID                = field::FlagUID("convert to gas");
+const field::FlagUID convertToLiquidFlagID             = field::FlagUID("convert to liquid");
+const field::FlagUID convertedFromGasToInterfaceFlagID = field::FlagUID("convert from gas to interface");
+const field::FlagUID keepInterfaceForWettingFlagID     = field::FlagUID("convert to and keep interface for wetting");
+const field::FlagUID convertToInterfaceForInflowFlagID = field::FlagUID("convert to interface for inflow");
+
+const Set< field::FlagUID > liquidInterfaceFlagIDs = setUnion< field::FlagUID >(liquidFlagID, interfaceFlagID);
+
+const Set< field::FlagUID > liquidInterfaceGasFlagIDs =
+   setUnion(liquidInterfaceFlagIDs, Set< field::FlagUID >(gasFlagID));
+
+} // namespace flagIDs
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/FlagInfo.h b/src/lbm_generated/free_surface/FlagInfo.h
new file mode 100644
index 0000000000000000000000000000000000000000..bdad8fb03d24f6686f7abb059cdec21a792a5312
--- /dev/null
+++ b/src/lbm_generated/free_surface/FlagInfo.h
@@ -0,0 +1,215 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file FlagInfo.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Manage free surface flags (e.g. conversion flags).
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/mpi/Broadcast.h"
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/FlagField.h"
+
+#include "FlagDefinitions.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Manage free surface flags (e.g. conversion flags).
+ **********************************************************************************************************************/
+template< typename FlagField_T >
+class FlagInfo
+{
+ public:
+   using flag_t = typename FlagField_T::flag_t;
+
+   FlagInfo();
+   FlagInfo(const Set< field::FlagUID >& obstacleIDs,
+            const Set< field::FlagUID >& outflowIDs  = Set< field::FlagUID >::emptySet(),
+            const Set< field::FlagUID >& inflowIDs   = Set< field::FlagUID >::emptySet(),
+            const Set< field::FlagUID >& freeSlipIDs = Set< field::FlagUID >::emptySet());
+
+   bool operator==(const FlagInfo& o) const;
+   bool operator!=(const FlagInfo& o) const;
+
+   flag_t interfaceFlag;
+   flag_t liquidFlag;
+   flag_t gasFlag;
+
+   flag_t convertedFlag;                   // interface cell that is already converted
+   flag_t convertToGasFlag;                // interface cell with too low fill level, should be converted
+   flag_t convertToLiquidFlag;             // interface cell with too high fill level, should be converted
+   flag_t convertFromGasToInterfaceFlag;   // interface cell that was a gas cell and needs refilling of its pdfs
+   flag_t keepInterfaceForWettingFlag;     // gas/liquid cell that needs to be converted to interface cell for a smooth
+                                           // continuation of the wetting surface (see dissertation of S. Donath, 2011,
+                                           // section 6.3.5.3)
+   flag_t convertToInterfaceForInflowFlag; // gas cell that needs to be converted to interface cell to enable inflow
+
+   flag_t obstacleFlagMask;
+   flag_t outflowFlagMask;
+   flag_t inflowFlagMask;
+   flag_t freeSlipFlagMask; // free slip obstacle cell (needs to be treated separately since PDFs going from gas into
+                            // boundary are not available and must be reconstructed)
+
+   template< typename FieldItOrPtr_T >
+   inline bool isInterface(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, interfaceFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isLiquid(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, liquidFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isGas(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, gasFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isObstacle(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isPartOfMaskSet(flagItOrPtr, obstacleFlagMask);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isOutflow(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isPartOfMaskSet(flagItOrPtr, outflowFlagMask);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isInflow(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isPartOfMaskSet(flagItOrPtr, inflowFlagMask);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isFreeSlip(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isPartOfMaskSet(flagItOrPtr, freeSlipFlagMask);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool hasConverted(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, convertedFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool hasConvertedToGas(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, convertToGasFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool hasConvertedToLiquid(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, convertToLiquidFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool hasConvertedFromGasToInterface(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, convertFromGasToInterfaceFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isKeepInterfaceForWetting(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, keepInterfaceForWettingFlag);
+   }
+   template< typename FieldItOrPtr_T >
+   inline bool isConvertToInterfaceForInflow(const FieldItOrPtr_T& flagItOrPtr) const
+   {
+      return isFlagSet(flagItOrPtr, convertToInterfaceForInflowFlag);
+   }
+
+   inline bool isInterface(const flag_t val) const { return isFlagSet(val, interfaceFlag); }
+   inline bool isLiquid(const flag_t val) const { return isFlagSet(val, liquidFlag); }
+   inline bool isGas(const flag_t val) const { return isFlagSet(val, gasFlag); }
+   inline bool isObstacle(const flag_t val) const { return isPartOfMaskSet(val, obstacleFlagMask); }
+   inline bool isOutflow(const flag_t val) const { return isPartOfMaskSet(val, outflowFlagMask); }
+   inline bool isInflow(const flag_t val) const { return isPartOfMaskSet(val, inflowFlagMask); }
+   inline bool isFreeSlip(const flag_t val) const { return isPartOfMaskSet(val, freeSlipFlagMask); }
+   inline bool isKeepInterfaceForWetting(const flag_t val) const { return isFlagSet(val, keepInterfaceForWettingFlag); }
+   inline bool hasConvertedToGas(const flag_t val) const { return isFlagSet(val, convertToGasFlag); }
+   inline bool hasConvertedToLiquid(const flag_t val) const { return isFlagSet(val, convertToLiquidFlag); }
+   inline bool hasConvertedFromGasToInterface(const flag_t val) const
+   {
+      return isFlagSet(val, convertFromGasToInterfaceFlag);
+   }
+   inline bool isConvertToInterfaceForInflow(const flag_t val) const
+   {
+      return isFlagSet(val, convertToInterfaceForInflowFlag);
+   }
+
+   // check whether FlagInfo is identical on all blocks and all processes
+   bool isConsistentAcrossBlocksAndProcesses(const std::weak_ptr< StructuredBlockStorage >& blockStorage,
+                                             ConstBlockDataID flagFieldID) const;
+
+   // register flags in flag field
+   static void registerFlags(FlagField_T* field, const Set< field::FlagUID >& obstacleIDs,
+                             const Set< field::FlagUID >& outflowIDs  = Set< field::FlagUID >::emptySet(),
+                             const Set< field::FlagUID >& inflowIDs   = Set< field::FlagUID >::emptySet(),
+                             const Set< field::FlagUID >& freeSlipIDs = Set< field::FlagUID >::emptySet());
+
+   void registerFlags(FlagField_T* field) const;
+
+   Set< field::FlagUID > getObstacleIDSet() const { return obstacleIDs_; }
+   Set< field::FlagUID > getOutflowIDs() const { return outflowIDs_; }
+   Set< field::FlagUID > getInflowIDs() const { return inflowIDs_; }
+   Set< field::FlagUID > getFreeSlipIDs() const { return freeSlipIDs_; }
+
+ protected:
+   FlagInfo(const FlagField_T* field, const Set< field::FlagUID >& obstacleIDs, const Set< field::FlagUID >& outflowIDs,
+            const Set< field::FlagUID >& inflowIDs, const Set< field::FlagUID >& freeSlipIDs);
+
+   // create sets of flag IDs with flags from free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h
+   Set< field::FlagUID > obstacleIDs_;
+   Set< field::FlagUID > outflowIDs_;
+   Set< field::FlagUID > inflowIDs_;
+   Set< field::FlagUID > freeSlipIDs_;
+};
+
+template< typename FlagField_T >
+mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const FlagInfo< FlagField_T >& flagInfo)
+{
+   buf << flagInfo.interfaceFlag << flagInfo.liquidFlag << flagInfo.gasFlag << flagInfo.convertedFlag
+       << flagInfo.convertToGasFlag << flagInfo.convertToLiquidFlag << flagInfo.convertFromGasToInterfaceFlag
+       << flagInfo.keepInterfaceForWettingFlag << flagInfo.keepInterfaceForWettingFlag
+       << flagInfo.convertToInterfaceForInflowFlag << flagInfo.obstacleFlagMask << flagInfo.outflowFlagMask
+       << flagInfo.inflowFlagMask << flagInfo.freeSlipFlagMask;
+
+   return buf;
+}
+
+template< typename FlagField_T >
+mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, FlagInfo< FlagField_T >& flagInfo)
+{
+   buf >> flagInfo.interfaceFlag >> flagInfo.liquidFlag >> flagInfo.gasFlag >> flagInfo.convertedFlag >>
+      flagInfo.convertToGasFlag >> flagInfo.convertToLiquidFlag >> flagInfo.convertFromGasToInterfaceFlag >>
+      flagInfo.keepInterfaceForWettingFlag >> flagInfo.keepInterfaceForWettingFlag >>
+      flagInfo.convertToInterfaceForInflowFlag >> flagInfo.obstacleFlagMask >> flagInfo.outflowFlagMask >>
+      flagInfo.inflowFlagMask >> flagInfo.freeSlipFlagMask;
+
+   return buf;
+}
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "FlagInfo.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/FlagInfo.impl.h b/src/lbm_generated/free_surface/FlagInfo.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..728fc05c54de927ff5594ee8bb9b2564a5161cd4
--- /dev/null
+++ b/src/lbm_generated/free_surface/FlagInfo.impl.h
@@ -0,0 +1,199 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file FlagInfo.impl.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Define and manage free surface flags (e.g. conversion flags).
+//
+//======================================================================================================================
+
+#include "core/DataTypes.h"
+#include "core/mpi/Broadcast.h"
+
+#include "field/FlagField.h"
+
+#include "FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename FlagField_T >
+FlagInfo< FlagField_T >::FlagInfo()
+   : interfaceFlag(0), liquidFlag(0), gasFlag(0), convertedFlag(0), convertToGasFlag(0), convertToLiquidFlag(0),
+     convertFromGasToInterfaceFlag(0), keepInterfaceForWettingFlag(0), convertToInterfaceForInflowFlag(0),
+     obstacleFlagMask(0), outflowFlagMask(0), inflowFlagMask(0), freeSlipFlagMask(0)
+{}
+
+template< typename FlagField_T >
+FlagInfo< FlagField_T >::FlagInfo(const FlagField_T* field, const Set< field::FlagUID >& obstacleIDs,
+                                  const Set< field::FlagUID >& outflowIDs, const Set< field::FlagUID >& inflowIDs,
+                                  const Set< field::FlagUID >& freeSlipIDs)
+   : interfaceFlag(field->getFlag(flagIDs::interfaceFlagID)), liquidFlag(field->getFlag(flagIDs::liquidFlagID)),
+     gasFlag(field->getFlag(flagIDs::gasFlagID)), convertedFlag(field->getFlag(flagIDs::convertedFlagID)),
+     convertToGasFlag(field->getFlag(flagIDs::convertToGasFlagID)),
+     convertToLiquidFlag(field->getFlag(flagIDs::convertToLiquidFlagID)),
+     convertFromGasToInterfaceFlag(field->getFlag(flagIDs::convertedFromGasToInterfaceFlagID)),
+     keepInterfaceForWettingFlag(field->getFlag(flagIDs::keepInterfaceForWettingFlagID)),
+     convertToInterfaceForInflowFlag(field->getFlag(flagIDs::convertToInterfaceForInflowFlagID)),
+     obstacleIDs_(obstacleIDs), outflowIDs_(outflowIDs), inflowIDs_(inflowIDs), freeSlipIDs_(freeSlipIDs)
+{
+   // create obstacleFlagMask from obstacleIDs using bitwise OR
+   obstacleFlagMask = 0;
+   for (auto obstacleID = obstacleIDs.begin(); obstacleID != obstacleIDs.end(); ++obstacleID)
+   {
+      obstacleFlagMask = flag_t(obstacleFlagMask | field->getFlag(*obstacleID));
+   }
+
+   // create outflowFlagMask from outflowIDs using bitwise OR
+   outflowFlagMask = 0;
+   for (auto outflowID = outflowIDs.begin(); outflowID != outflowIDs.end(); ++outflowID)
+   {
+      outflowFlagMask = flag_t(outflowFlagMask | field->getFlag(*outflowID));
+   }
+
+   // create inflowFlagMask from inflowIDs using bitwise OR
+   inflowFlagMask = 0;
+   for (auto inflowID = inflowIDs.begin(); inflowID != inflowIDs.end(); ++inflowID)
+   {
+      inflowFlagMask = flag_t(inflowFlagMask | field->getFlag(*inflowID));
+   }
+
+   // create freeSlipFlagMask from freeSlipIDs using bitwise OR
+   freeSlipFlagMask = 0;
+   for (auto freeSlipID = freeSlipIDs.begin(); freeSlipID != freeSlipIDs.end(); ++freeSlipID)
+   {
+      freeSlipFlagMask = flag_t(freeSlipFlagMask | field->getFlag(*freeSlipID));
+   }
+}
+
+template< typename FlagField_T >
+FlagInfo< FlagField_T >::FlagInfo(const Set< field::FlagUID >& obstacleIDs, const Set< field::FlagUID >& outflowIDs,
+                                  const Set< field::FlagUID >& inflowIDs, const Set< field::FlagUID >& freeSlipIDs)
+   : obstacleIDs_(obstacleIDs), outflowIDs_(outflowIDs), inflowIDs_(inflowIDs), freeSlipIDs_(freeSlipIDs)
+{
+   // define flags
+   flag_t nextFreeBit = flag_t(0);
+   interfaceFlag = flag_t(flag_t(1) << nextFreeBit++); // outermost flag_t is necessary to avoid warning C4334 on MSVC
+   liquidFlag    = flag_t(flag_t(1) << nextFreeBit++);
+   gasFlag       = flag_t(flag_t(1) << nextFreeBit++);
+   convertedFlag = flag_t(flag_t(1) << nextFreeBit++);
+   convertToGasFlag                = flag_t(flag_t(1) << nextFreeBit++);
+   convertToLiquidFlag             = flag_t(flag_t(1) << nextFreeBit++);
+   convertFromGasToInterfaceFlag   = flag_t(flag_t(1) << nextFreeBit++);
+   keepInterfaceForWettingFlag     = flag_t(flag_t(1) << nextFreeBit++);
+   convertToInterfaceForInflowFlag = flag_t(flag_t(1) << nextFreeBit++);
+
+   obstacleFlagMask = flag_t(0);
+   outflowFlagMask  = flag_t(0);
+   inflowFlagMask   = flag_t(0);
+   freeSlipFlagMask = flag_t(0);
+
+   // define flags for obstacles, outflow, inflow and freeSlip
+   auto setUnion = obstacleIDs + outflowIDs + inflowIDs + freeSlipIDs;
+   for (auto id = setUnion.begin(); id != setUnion.end(); ++id)
+   {
+      if (obstacleIDs.contains(*id)) { obstacleFlagMask = obstacleFlagMask | flag_t((flag_t(1) << nextFreeBit)); }
+
+      if (outflowIDs.contains(*id)) { outflowFlagMask = outflowFlagMask | flag_t((flag_t(1) << nextFreeBit)); }
+
+      if (inflowIDs.contains(*id)) { inflowFlagMask = inflowFlagMask | flag_t((flag_t(1) << nextFreeBit)); }
+
+      if (freeSlipIDs.contains(*id)) { freeSlipFlagMask = freeSlipFlagMask | flag_t((flag_t(1) << nextFreeBit)); }
+
+      nextFreeBit++;
+   }
+}
+
+template< typename FlagField_T >
+void FlagInfo< FlagField_T >::registerFlags(FlagField_T* field, const Set< field::FlagUID >& obstacleIDs,
+                                            const Set< field::FlagUID >& outflowIDs,
+                                            const Set< field::FlagUID >& inflowIDs,
+                                            const Set< field::FlagUID >& freeSlipIDs)
+{
+   flag_t nextFreeBit = flag_t(0);
+   field->registerFlag(flagIDs::interfaceFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::liquidFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::gasFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::convertedFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::convertToGasFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::convertToLiquidFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::convertedFromGasToInterfaceFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::keepInterfaceForWettingFlagID, nextFreeBit++);
+   field->registerFlag(flagIDs::convertToInterfaceForInflowFlagID, nextFreeBit++);
+
+   // extract flags
+   auto setUnion = obstacleIDs + outflowIDs + inflowIDs + freeSlipIDs;
+   for (auto id = setUnion.begin(); id != setUnion.end(); ++id)
+   {
+      field->registerFlag(*id, nextFreeBit++);
+   }
+}
+
+template< typename FlagField_T >
+void FlagInfo< FlagField_T >::registerFlags(FlagField_T* field) const
+{
+   registerFlags(field, obstacleIDs_, outflowIDs_, inflowIDs_, freeSlipIDs_);
+}
+
+template< typename FlagField_T >
+bool FlagInfo< FlagField_T >::isConsistentAcrossBlocksAndProcesses(
+   const std::weak_ptr< StructuredBlockStorage >& blockStoragePtr, ConstBlockDataID flagFieldID) const
+{
+   // check consistency across processes
+   FlagInfo rootFlagInfo = *this;
+
+   // root broadcasts its FlagInfo to all other processes
+   mpi::broadcastObject(rootFlagInfo);
+
+   // this process' FlagInfo is not identical to the one of root
+   if (rootFlagInfo != *this) { return false; }
+
+   auto blockStorage = blockStoragePtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockStorage);
+
+   // check consistency across blocks
+   for (auto blockIt = blockStorage->begin(); blockIt != blockStorage->end(); ++blockIt)
+   {
+      const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID);
+      FlagInfo fi(flagField, obstacleIDs_, outflowIDs_, inflowIDs_, freeSlipIDs_);
+      if (fi != *this) { return false; }
+   }
+
+   return true;
+}
+
+template< typename FlagField_T >
+bool FlagInfo< FlagField_T >::operator==(const FlagInfo& o) const
+{
+   return interfaceFlag == o.interfaceFlag && gasFlag == o.gasFlag && liquidFlag == o.liquidFlag &&
+          convertToGasFlag == o.convertToGasFlag && convertToLiquidFlag == o.convertToLiquidFlag &&
+          convertFromGasToInterfaceFlag == o.convertFromGasToInterfaceFlag &&
+          keepInterfaceForWettingFlag == o.keepInterfaceForWettingFlag &&
+          convertToInterfaceForInflowFlag == o.convertToInterfaceForInflowFlag &&
+          obstacleFlagMask == o.obstacleFlagMask && outflowFlagMask == o.outflowFlagMask &&
+          inflowFlagMask == o.inflowFlagMask && freeSlipFlagMask == o.freeSlipFlagMask;
+}
+
+template< typename FlagField_T >
+bool FlagInfo< FlagField_T >::operator!=(const FlagInfo& o) const
+{
+   return !(*this == o);
+}
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/InitFunctions.h b/src/lbm_generated/free_surface/InitFunctions.h
new file mode 100644
index 0000000000000000000000000000000000000000..551a255b223de7275048686d6e10e8aab951cead
--- /dev/null
+++ b/src/lbm_generated/free_surface/InitFunctions.h
@@ -0,0 +1,279 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file InitFunctions.h
+//! \ingroup free_surface
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Initialization functions.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include <functional>
+
+#include "FlagInfo.h"
+#include "InterfaceFromFillLevel.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Initialize fill level with "value" in cells belonging to boundary and obstacles such that the bubble model does not
+ * detect obstacles as gas cells.
+ **********************************************************************************************************************/
+template< typename BoundaryHandling_T, typename Stencil_T, typename ScalarField_T >
+void initFillLevelsInBoundaries(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                                const ConstBlockDataID& handlingID, const BlockDataID& fillFieldID,
+                                real_t value = real_c(1))
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField           = blockIt->getData< ScalarField_T >(fillFieldID);
+      const BoundaryHandling_T* const handling = blockIt->getData< const BoundaryHandling_T >(handlingID);
+
+      // set fill level to "value" in every cell belonging to boundary
+      WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillField, {
+         if (handling->isBoundary(x, y, z)) { fillField->get(x, y, z) = value; }
+      }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+   }
+}
+
+/***********************************************************************************************************************
+ * Clear and initialize flags in every cell according to the fill level.
+ **********************************************************************************************************************/
+template< typename BoundaryHandling_T, typename Stencil_T, typename FlagField_T, typename ScalarField_T >
+void initFlagsFromFillLevels(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                             const FlagInfo< FlagField_T >& flagInfo, const BlockDataID& handlingID,
+                             const ConstBlockDataID& fillFieldID)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+      BoundaryHandling_T* const handling   = blockIt->getData< BoundaryHandling_T >(handlingID);
+
+      // clear all flags in the boundary handling
+      handling->removeFlag(flagInfo.gasFlag);
+      handling->removeFlag(flagInfo.liquidFlag);
+      handling->removeFlag(flagInfo.interfaceFlag);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // set flags only in non-boundary and non-obstacle cells
+         if (!handling->isBoundary(fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z()))
+         {
+            if (floatIsEqual(*fillFieldIt, real_c(0), real_c(1e-14)))
+            {
+               // set gas flag
+               handling->forceFlag(flagInfo.gasFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z());
+            }
+            else
+            {
+               if (*fillFieldIt < real_c(1.0))
+               {
+                  // set interface flag
+                  handling->forceFlag(flagInfo.interfaceFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z());
+               }
+               else
+               {
+                  // check if the cell is an interface cell due to direct neighboring gas cells
+                  if (isInterfaceFromFillLevel< Stencil_T >(fillFieldIt))
+                  {
+                     // set interface flag
+                     handling->forceFlag(flagInfo.interfaceFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z());
+                  }
+                  else
+                  {
+                     // set liquid flag
+                     handling->forceFlag(flagInfo.liquidFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z());
+                  }
+               }
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+}
+
+/***********************************************************************************************************************
+ * Initialize the hydrostatic pressure in the direction in which a force is acting in ALL cells (regardless of a cell's
+ * flag). The velocity remains unchanged.
+ *
+ * The force vector must have only one component, i.e., the direction of the force can only be in x-, y- or z-axis.
+ * The variable fluidHeight determines the height at which the density is equal to reference density (=1).
+ **********************************************************************************************************************/
+template< typename ScalarField_T >
+void initHydrostaticPressure(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                             const BlockDataID& densityFieldID, const Vector3< real_t >& force, real_t fluidHeight)
+{
+   // count number of non-zero components of the force vector
+   uint_t numForceComponents = uint_c(0);
+   if (!realIsEqual(force[0], real_c(0), real_c(1e-14))) { ++numForceComponents; }
+   if (!realIsEqual(force[1], real_c(0), real_c(1e-14))) { ++numForceComponents; }
+   if (!realIsEqual(force[2], real_c(0), real_c(1e-14))) { ++numForceComponents; }
+
+   WALBERLA_CHECK_EQUAL(numForceComponents, uint_c(1),
+                        "The current implementation of the hydrostatic pressure initialization does not support "
+                        "forces that have none or multiple components, i. e., a force that points in a direction other "
+                        "than the x-, y- or z-axis.");
+
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const densityField = blockIt->getData< ScalarField_T >(densityFieldID);
+      CellInterval local = densityField->xyzSizeWithGhostLayer(); // block-, i.e., process-local cell interval
+
+      for (auto cellIt = local.begin(); cellIt != local.end(); ++cellIt)
+      {
+         // transform the block-local coordinate to global coordinates
+         Cell global;
+         blockForest->transformBlockLocalToGlobalCell(global, *blockIt, *cellIt);
+
+         // get the current global coordinate, i.e., height of the fluid in the relevant direction
+         cell_idx_t coordinate = cell_idx_c(0);
+         real_t forceComponent = real_c(0);
+         if (!realIsEqual(force[0], real_c(0), real_c(1e-14)))
+         {
+            coordinate     = global.x();
+            forceComponent = force[0];
+         }
+         else
+         {
+            if (!realIsEqual(force[1], real_c(0), real_c(1e-14)))
+            {
+               coordinate     = global.y();
+               forceComponent = force[1];
+            }
+            else
+            {
+               if (!realIsEqual(force[2], real_c(0), real_c(1e-14)))
+               {
+                  coordinate     = global.z();
+                  forceComponent = force[2];
+               }
+               else
+               {
+                  WALBERLA_ABORT(
+                     "The current implementation of the hydrostatic pressure initialization does not support "
+                     "forces that have none or multiple components, i. e., a force that points in a direction other "
+                     "than the x-, y- or z-axis.")
+               }
+            }
+         }
+
+         // initialize the (hydrostatic) pressure, i.e., LBM density
+         // Bernoulli: p = p0 + density * gravity * height
+         // => LBM (density=1): rho = rho0 + gravity * height = 1 + 1/cs^2 * g * h = 1 + 3 * g * h
+         // shift global cell by 0.5 since density is set for cell center
+         densityField->get(*cellIt) = real_c(1) + real_c(3) * forceComponent * (real_c(coordinate) + real_c(0.5) - std::ceil(fluidHeight));
+      }
+   }
+}
+
+/***********************************************************************************************************************
+ * Initialize the force density field with a given acceleration and density of each cell.
+ * Differs from the version above by using a flattened vector field (GhostLayerField< real_t, 3 >). This is necessary
+ * because Pystencils does not support VectorField_T (GhostLayerField< Vector3<real_t>, 1 >).
+ **********************************************************************************************************************/
+template< typename FlagField_T, typename VectorField_T, typename ScalarField_T >
+void initForceDensityField(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                                  const BlockDataID& forceDensityFieldID, const ConstBlockDataID& fillFieldID,
+                                  const ConstBlockDataID& densityFieldID, const ConstBlockDataID& flagFieldID,
+                                  const FlagInfo< FlagField_T >& flagInfo, const Vector3< real_t >& acceleration)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      VectorField_T* const forceDensityField = blockIt->getData< VectorField_T >(forceDensityFieldID);
+      const ScalarField_T* const fillField            = blockIt->getData< const ScalarField_T >(fillFieldID);
+      const ScalarField_T* const densityField                = blockIt->getData< const ScalarField_T >(densityFieldID);
+      const FlagField_T* const flagField              = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(forceDensityFieldIt, forceDensityField, fillFieldIt, fillField, densityFieldIt, densityField,
+                             flagFieldIt, flagField, {
+                                // set force density in cells to acceleration * density * fillLevel (see equation 15
+                                // in Koerner et al., 2005);
+
+                                if (flagInfo.isInterface(*flagFieldIt))
+                                {
+                                   const real_t density   = densityField->get(densityFieldIt.cell());
+                                   forceDensityFieldIt[0] = acceleration[0] * *fillFieldIt * density;
+                                   forceDensityFieldIt[1] = acceleration[1] * *fillFieldIt * density;
+                                   forceDensityFieldIt[2] = acceleration[2] * *fillFieldIt * density;
+                                }
+
+                                else
+                                {
+                                   if (flagInfo.isLiquid(*flagFieldIt))
+                                   {
+                                      const real_t density   = densityField->get(densityFieldIt.cell());
+                                      forceDensityFieldIt[0] = acceleration[0] * density;
+                                      forceDensityFieldIt[1] = acceleration[1] * density;
+                                      forceDensityFieldIt[2] = acceleration[2] * density;
+                                   }
+                                }
+                             }) // WALBERLA_FOR_ALL_CELLS
+   }
+}
+
+/***********************************************************************************************************************
+ * Set density in non-liquid and non-interface cells to 1.
+ **********************************************************************************************************************/
+template< typename FlagField_T, typename VectorField_T, typename ScalarField_T>
+void setDensityInNonFluidCellsToOne(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                                    const FlagInfo< FlagField_T >& flagInfo, const ConstBlockDataID& flagFieldID,
+                                    const BlockDataID& velocityFieldID, const BlockDataID& densityFieldID)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      VectorField_T* const velocityField         = blockIt->getData< VectorField_T >(velocityFieldID);
+      ScalarField_T* const densityField         = blockIt->getData< ScalarField_T >(densityFieldID);
+
+      const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(velocityFieldIt, velocityField, densityFieldIt, densityField, flagFieldIt, flagField, {
+         if (!flagInfo.isLiquid(*flagFieldIt) && !flagInfo.isInterface(*flagFieldIt))
+         {
+            // set density in gas cells to 1 and velocity to zero
+            const cell_idx_t x = densityFieldIt.x();
+            const cell_idx_t y = densityFieldIt.y();
+            const cell_idx_t z = densityFieldIt.z();
+            densityField->get(x, y, z) = real_c(1.0);
+            velocityField->get(x, y, z, 0) = real_c(0.0);
+            velocityField->get(x, y, z, 1) = real_c(0.0);
+            velocityField->get(x, y, z, 2) = real_c(0.0);
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+}
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/InterfaceFromFillLevel.h b/src/lbm_generated/free_surface/InterfaceFromFillLevel.h
new file mode 100644
index 0000000000000000000000000000000000000000..cfe8ae6efc3f408575ced2d9f802cc2c6279674b
--- /dev/null
+++ b/src/lbm_generated/free_surface/InterfaceFromFillLevel.h
@@ -0,0 +1,78 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file InterfaceFromFillLevel.h
+//! \ingroup free_surface
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Check whether a cell should be an interface cell according to its properties.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/cell/Cell.h"
+
+#include <type_traits>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Check whether a cell is an interface cell with respect to its fill level and its direct neighborhood (liquid cells
+ * can not have gas cell in their direct neighborhood; therefore, the liquid cell is forced to be an interface cell).
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename ScalarField_T >
+inline bool isInterfaceFromFillLevel(const ScalarField_T& fillField, cell_idx_t x, cell_idx_t y, cell_idx_t z)
+{
+   WALBERLA_ASSERT(std::is_floating_point< typename ScalarField_T::value_type >::value,
+                   "Fill level field must contain floating point values.")
+
+   real_t fillLevel = fillField.get(x, y, z);
+
+   // this cell is regular gas cell
+   if (floatIsEqual(fillLevel, real_c(0.0), real_c(1e-14))) { return false; }
+
+   // this cell is regular interface cell
+   if (fillLevel < real_c(1.0)) { return true; }
+
+   // check this cell's direct neighborhood for gas cells (a liquid cell can not be direct neighbor to a gas cell)
+   for (auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d)
+   {
+      // this cell has a gas cell in its direct neighborhood; it therefore must not be a liquid cell but an interface
+      // cell although its fill level is 1.0
+      if (fillField.get(x + d.cx(), y + d.cy(), z + d.cz()) <= real_c(0.0)) { return true; }
+   }
+
+   // this cell is a regular fluid cell
+   return false;
+}
+
+template< typename Stencil_T, typename ScalarFieldIt_T >
+inline bool isInterfaceFromFillLevel(const ScalarFieldIt_T& fillFieldIt)
+{
+   return isInterfaceFromFillLevel< Stencil_T, typename ScalarFieldIt_T::FieldType >(
+      *(fillFieldIt.getField()), fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z());
+}
+
+template< typename Stencil_T, typename ScalarField >
+inline bool isInterfaceFromFillLevel(const ScalarField& fillField, const Cell& cell)
+{
+   return isInterfaceFromFillLevel< Stencil_T >(fillField, cell.x(), cell.y(), cell.z());
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/LoadBalancing.h b/src/lbm_generated/free_surface/LoadBalancing.h
new file mode 100644
index 0000000000000000000000000000000000000000..16cc9fe3f74b9fbb7b5027696e603f02d7ceb519
--- /dev/null
+++ b/src/lbm_generated/free_surface/LoadBalancing.h
@@ -0,0 +1,343 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file LoadBalancing.h
+//! \ingroup free_surface
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Free surface-specific functionality for load balancing.
+//
+//======================================================================================================================
+
+#include "blockforest/communication/UniformBufferedScheme.h"
+#include "blockforest/BlockForest.h"
+#include "blockforest/SetupBlockForest.h"
+#include "blockforest/StructuredBlockForest.h"
+#include "blockforest/loadbalancing/DynamicCurve.h"
+#include "blockforest/loadbalancing/StaticCurve.h"
+
+#include "core/logging/Logging.h"
+#include "core/math/AABB.h"
+#include "core/math/DistributedSample.h"
+#include "core/math/Vector3.h"
+#include "core/mpi/MPIManager.h"
+
+#include "lbm_generated/blockforest/SimpleCommunication.h"
+#include "lbm_generated/free_surface/BlockStateDetectorSweep.h"
+
+#include <algorithm>
+#include <numeric>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+
+template< typename FlagField_T >
+class ProcessLoadEvaluator;
+
+/***********************************************************************************************************************
+ * Create non-uniform block forest to be used for load balancing.
+ **********************************************************************************************************************/
+std::shared_ptr< StructuredBlockForest > createNonUniformBlockForest(const Vector3< uint_t >& domainSize,
+                                                                     const Vector3< uint_t >& cellsPerBlock,
+                                                                     const Vector3< uint_t >& numBlocks,
+                                                                     const Vector3< bool >& periodicity)
+{
+   WALBERLA_CHECK_EQUAL(domainSize[0], cellsPerBlock[0] * numBlocks[0],
+                        "The domain size is not divisible by the specified \"cellsPerBlock\" in x-direction.");
+   WALBERLA_CHECK_EQUAL(domainSize[1], cellsPerBlock[1] * numBlocks[1],
+                        "The domain size is not divisible by the specified \"cellsPerBlock\" in y-direction.");
+   WALBERLA_CHECK_EQUAL(domainSize[2], cellsPerBlock[2] * numBlocks[2],
+                        "The domain size is not divisible by the specified \"cellsPerBlock\" in z-direction.");
+
+   // create SetupBlockForest for allowing load balancing
+   SetupBlockForest setupBlockForest;
+
+   AABB domainAABB(real_c(0), real_c(0), real_c(0), real_c(domainSize[0]), real_c(domainSize[1]),
+                   real_c(domainSize[2]));
+
+   setupBlockForest.init(domainAABB, numBlocks[0], numBlocks[1], numBlocks[2], periodicity[0], periodicity[1],
+                         periodicity[2]);
+
+   // compute initial process distribution
+   setupBlockForest.balanceLoad(blockforest::StaticLevelwiseCurveBalance(true),
+                                uint_c(MPIManager::instance()->numProcesses()));
+
+   WALBERLA_LOG_INFO_ON_ROOT(setupBlockForest);
+
+   // define MPI communicator
+   if (!MPIManager::instance()->rankValid()) { MPIManager::instance()->useWorldComm(); }
+
+   // create BlockForest (will be encapsulated in StructuredBlockForest)
+   const std::shared_ptr< BlockForest > blockForest =
+      std::make_shared< BlockForest >(uint_c(MPIManager::instance()->rank()), setupBlockForest, false);
+
+   // create StructuredBlockForest
+   std::shared_ptr< StructuredBlockForest > structuredBlockForest =
+      std::make_shared< StructuredBlockForest >(blockForest, cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2]);
+   structuredBlockForest->createCellBoundingBoxes();
+
+   return structuredBlockForest;
+}
+
+/***********************************************************************************************************************
+ * Example for a load balancing implementation.
+ *
+ * IMPORTANT REMARK: The following implementation should be considered a demonstrator based on best-practices. That is,
+ * it was not thoroughly benchmarked and does not guarantee a performance-optimal load balancing.
+ **********************************************************************************************************************/
+template< typename FlagField_T, typename CommunicationStencil_T, typename LBMStencil_T >
+class LoadBalancer
+{
+ public:
+   LoadBalancer(const std::shared_ptr< StructuredBlockForest >& blockForestPtr,
+                const lbm_generated::SimpleCommunication< CommunicationStencil_T >& communication,
+                const blockforest::communication::UniformBufferedScheme< LBMStencil_T >& pdfCommunication,
+                uint_t blockWeightFullFreeSurface,
+                uint_t blockWeightOnlyLBM, uint_t blockWeightOnlyGasAndBoundary, uint_t frequency,
+                bool printStatistics = false)
+      : blockForest_(blockForestPtr), communication_(communication), pdfCommunication_(pdfCommunication),
+        blockWeightFullFreeSurface_(blockWeightFullFreeSurface),
+        blockWeightOnlyLBM_(blockWeightOnlyLBM), blockWeightOnlyGasAndBoundary_(blockWeightOnlyGasAndBoundary),
+        frequency_(frequency), printStatistics_(printStatistics), executionCounter_(uint_c(0)),
+        evaluator_(ProcessLoadEvaluator< FlagField_T >(blockForest_, blockWeightFullFreeSurface_, blockWeightOnlyLBM_,
+                                                       blockWeightOnlyGasAndBoundary_, uint_c(1)))
+   {
+      BlockForest& blockForest = blockForest_->getBlockForest();
+
+      // refinement is not implemented in FSLBM such that this can be set to false
+      blockForest.recalculateBlockLevelsInRefresh(false);
+
+      // rebalancing of blocks must be forced here, as it would normally be done when refinement levels change
+      blockForest.alwaysRebalanceInRefresh(true);
+
+      // depth of levels is not changing and must therefore not be communicated
+      blockForest.allowRefreshChangingDepth(false);
+
+      // refinement is not implemented in FSLBM such that this can be set to false
+      blockForest.allowMultipleRefreshCycles(false);
+
+      // leave algorithm when load balancing has been performed
+      blockForest.checkForEarlyOutAfterLoadBalancing(true);
+
+      // use PhantomWeight as defined below
+      blockForest.setRefreshPhantomBlockDataAssignmentFunction(blockWeightAssignment);
+      blockForest.setRefreshPhantomBlockDataPackFunction(phantomWeightsPack);
+      blockForest.setRefreshPhantomBlockDataUnpackFunction(phantomWeightsUnpack);
+
+      // assign load balancing function
+      blockForest.setRefreshPhantomBlockMigrationPreparationFunction(
+         blockforest::DynamicCurveBalance< PhantomWeight >(true, true));
+   }
+
+   void operator()()
+   {
+      if (frequency_ == uint_c(0)) { return; }
+
+      // only balance load in given frequencies
+      if (executionCounter_ % frequency_ == uint_c(0))
+      {
+         BlockForest& blockForest = blockForest_->getBlockForest();
+
+         // balance load by updating the blockForest
+         const uint_t modificationStamp = blockForest.getModificationStamp();
+         blockForest.refresh();
+
+         const uint_t newModificationStamp = blockForest.getModificationStamp();
+
+         if (newModificationStamp != modificationStamp)
+         {
+            // communicate all fields
+            communication_();
+            pdfCommunication_();
+
+            if (printStatistics_) { evaluator_(); }
+
+            if (blockForest.getNumberOfBlocks() == uint_c(0))
+            {
+               WALBERLA_ABORT(
+                  "Load balancing lead to a situation where there is a process with no blocks. This is "
+                  "not supported yet. This can be avoided by either using smaller blocks or, equivalently, more blocks "
+                  "per process.");
+            }
+         }
+      }
+      ++executionCounter_;
+   }
+
+ private:
+   std::shared_ptr< StructuredBlockForest > blockForest_;
+   lbm_generated::SimpleCommunication< CommunicationStencil_T > communication_;
+   blockforest::communication::UniformBufferedScheme< LBMStencil_T > pdfCommunication_;
+
+   uint_t blockWeightFullFreeSurface_;    // empirical choice, not thoroughly benchmarked: 50
+   uint_t blockWeightOnlyLBM_;            // empirical choice, not thoroughly benchmarked: 10
+   uint_t blockWeightOnlyGasAndBoundary_; // empirical choice, not thoroughly benchmarked: 5
+
+   uint_t frequency_;
+   bool printStatistics_;
+
+   uint_t executionCounter_;
+
+   ProcessLoadEvaluator< FlagField_T > evaluator_;
+
+   class PhantomWeight // used as a 'PhantomBlockForest::PhantomBlockDataAssignmentFunction'
+   {
+    public:
+      using weight_t = uint_t;
+      PhantomWeight(const weight_t _weight) : weight_(_weight) {}
+      weight_t weight() const { return weight_; }
+
+    private:
+      weight_t weight_;
+   }; // class PhantomWeight
+
+   std::function< void(mpi::SendBuffer& buffer, const PhantomBlock& block) > phantomWeightsPack =
+      [](mpi::SendBuffer& buffer, const PhantomBlock& block) { buffer << block.getData< PhantomWeight >().weight(); };
+
+   std::function< void(mpi::RecvBuffer& buffer, const PhantomBlock&, walberla::any& data) > phantomWeightsUnpack =
+      [](mpi::RecvBuffer& buffer, const PhantomBlock&, walberla::any& data) {
+         typename PhantomWeight::weight_t w;
+         buffer >> w;
+         data = PhantomWeight(w);
+      };
+
+   std::function< void(std::vector< std::pair< const PhantomBlock*, walberla::any > >& blockData,
+                       const PhantomBlockForest&) >
+      blockWeightAssignment =
+         [this](std::vector< std::pair< const PhantomBlock*, walberla::any > >& blockData, const PhantomBlockForest&) {
+            for (auto it = blockData.begin(); it != blockData.end(); ++it)
+            {
+               if (it->first->getState().contains(BlockStateDetectorSweep< FlagField_T >::fullFreeSurface))
+               {
+                  it->second = PhantomWeight(blockWeightFullFreeSurface_);
+               }
+               else
+               {
+                  if (it->first->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyLBM))
+                  {
+                     it->second = PhantomWeight(blockWeightOnlyLBM_);
+                  }
+                  else
+                  {
+                     if (it->first->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyGasAndBoundary))
+                     {
+                        it->second = PhantomWeight(blockWeightOnlyGasAndBoundary_);
+                     }
+                     else { WALBERLA_ABORT("Unknown block state"); }
+                  }
+               }
+            }
+         };
+
+}; // class LoadBalancer
+
+/***********************************************************************************************************************
+ * Evaluates and prints statistics about the current load distribution situation:
+ * - Average weight per process
+ * - Maximum weight per process
+ * - Minimum weight per process
+ **********************************************************************************************************************/
+template< typename FlagField_T >
+class ProcessLoadEvaluator
+{
+ public:
+   ProcessLoadEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                        uint_t blockWeightFullFreeSurface, uint_t blockWeightOnlyLBM,
+                        uint_t blockWeightOnlyGasAndBoundary, uint_t frequency)
+      : blockForest_(blockForest), blockWeightFullFreeSurface_(blockWeightFullFreeSurface),
+        blockWeightOnlyLBM_(blockWeightOnlyLBM), blockWeightOnlyGasAndBoundary_(blockWeightOnlyGasAndBoundary),
+        frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      if (frequency_ == uint_c(0)) { return; }
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % frequency_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; }
+
+      std::vector< real_t > weightSum = computeWeightSumPerProcess();
+
+      print(weightSum);
+   }
+
+   std::vector< real_t > computeWeightSumPerProcess()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      std::vector< real_t > weightSum(uint_c(MPIManager::instance()->numProcesses()), real_c(0));
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         if (blockForest->blockExistsLocally(blockIt->getId()))
+         {
+            if (blockIt->getState().contains(BlockStateDetectorSweep< FlagField_T >::fullFreeSurface))
+            {
+               weightSum[blockForest->getProcessRank(blockIt->getId())] += real_c(blockWeightFullFreeSurface_);
+            }
+            else
+            {
+               if (blockIt->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyLBM))
+               {
+                  weightSum[blockForest->getProcessRank(blockIt->getId())] += real_c(blockWeightOnlyLBM_);
+               }
+               else
+               {
+                  if (blockIt->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyGasAndBoundary))
+                  {
+                     weightSum[blockForest->getProcessRank(blockIt->getId())] += real_c(blockWeightOnlyGasAndBoundary_);
+                  }
+               }
+            }
+         }
+      }
+
+      mpi::reduceInplace< real_t >(weightSum, mpi::SUM, 0);
+
+      return weightSum;
+   }
+
+   void print(const std::vector< real_t >& weightSum)
+   {
+      WALBERLA_ROOT_SECTION()
+      {
+         const std::vector< real_t >::const_iterator max = std::max_element(weightSum.cbegin(), weightSum.end());
+         const std::vector< real_t >::const_iterator min = std::min_element(weightSum.cbegin(), weightSum.end());
+         const real_t sum = std::accumulate(weightSum.cbegin(), weightSum.end(), real_c(0));
+         const real_t avg = sum / real_c(MPIManager::instance()->numProcesses());
+
+         WALBERLA_LOG_INFO("Load balancing:");
+         WALBERLA_LOG_INFO("\t Average weight per process " << avg);
+         WALBERLA_LOG_INFO("\t Maximum weight per process " << *max);
+         WALBERLA_LOG_INFO("\t Minimum weight per process " << *min);
+      }
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   uint_t blockWeightFullFreeSurface_;
+   uint_t blockWeightOnlyLBM_;
+   uint_t blockWeightOnlyGasAndBoundary_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class ProcessLoadEvaluator
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/MaxVelocityComputer.h b/src/lbm_generated/free_surface/MaxVelocityComputer.h
new file mode 100644
index 0000000000000000000000000000000000000000..c568f637ed47c4d79fc6c78ead51590b21d8ef71
--- /dev/null
+++ b/src/lbm_generated/free_surface/MaxVelocityComputer.h
@@ -0,0 +1,113 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file MaxVelocityComputer.h
+//! \ingroup free_surface
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute the maximum velocity of all liquid and interface cells (in each direction) in the system.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename FreeSurfaceBoundaryHandling_T, typename PdfField_T, typename FlagField_T >
+class MaxVelocityComputer
+{
+ public:
+   MaxVelocityComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                       const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                       const ConstBlockDataID& pdfFieldID, uint_t frequency,
+                       const std::shared_ptr< Vector3< real_t > >& maxVelocity)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID),
+        maxVelocity_(maxVelocity), frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      if (frequency_ == uint_c(0)) { return; }
+
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      if (executionCounter_ == uint_c(0)) { getMaxVelocity(blockForest, freeSurfaceBoundaryHandling); }
+      else
+      {
+         // only evaluate in given frequencies
+         if (executionCounter_ % frequency_ == uint_c(0)) { getMaxVelocity(blockForest, freeSurfaceBoundaryHandling); }
+      }
+
+      ++executionCounter_;
+   }
+
+   void getMaxVelocity(const std::shared_ptr< const StructuredBlockForest >& blockForest,
+                       const std::shared_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling)
+   {
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      real_t maxVelocityX = real_c(0);
+      real_t maxVelocityY = real_c(0);
+      real_t maxVelocityZ = real_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+         const PdfField_T* const pdfField   = blockIt->template getData< const PdfField_T >(pdfFieldID_);
+
+            WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField,
+                                       omp parallel for schedule(static) reduction(max:maxVelocityX)
+                                                                         reduction(max:maxVelocityY)
+                                                                         reduction(max:maxVelocityZ),
+            {
+            if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt))
+            {
+               const Vector3< real_t > velocity = pdfField->getVelocity(pdfFieldIt.cell());
+
+               if (velocity[0] > maxVelocityX) { maxVelocityX = velocity[0]; }
+               if (velocity[1] > maxVelocityY) { maxVelocityY = velocity[1]; }
+               if (velocity[2] > maxVelocityZ) { maxVelocityZ = velocity[2]; }
+            }
+            }) // WALBERLA_FOR_ALL_CELLS_OMP
+      }
+
+      Vector3< real_t > maxVelocity(maxVelocityX, maxVelocityY, maxVelocityZ);
+      mpi::allReduceInplace< real_t >(maxVelocity, mpi::MAX);
+
+      *maxVelocity_ = maxVelocity;
+   };
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+
+   const ConstBlockDataID pdfFieldID_;
+
+   std::shared_ptr< Vector3< real_t > > maxVelocity_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class MaxVelocityComputer
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/SurfaceMeshWriter.h b/src/lbm_generated/free_surface/SurfaceMeshWriter.h
new file mode 100644
index 0000000000000000000000000000000000000000..f0071d49a95e875d804131196ae7935754006b8f
--- /dev/null
+++ b/src/lbm_generated/free_surface/SurfaceMeshWriter.h
@@ -0,0 +1,176 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SurfaceMeshWriter.h
+//! \ingroup free_surface
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Free surface-specific class for writing the free surface as triangle mesh.
+//
+//======================================================================================================================
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "core/Filesystem.h"
+
+#include "field/AddToStorage.h"
+
+#include "geometry/mesh/TriangleMeshIO.h"
+
+#include "postprocessing/FieldToSurfaceMesh.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+namespace abortIfNullptr
+{
+
+// helper function to check validity of std::weak_ptr in constructors' initializer list; WALBERLA_CHECK_NOT_NULLPTR()
+// does not work there, because the macro terminates with ";"
+template< typename T >
+void abortIfNullptr(const std::weak_ptr< T >& weakPointer)
+{
+   if (weakPointer.lock() == nullptr) { WALBERLA_ABORT("Weak pointer has expired."); }
+}
+} // namespace abortIfNullptr
+
+/***********************************************************************************************************************
+ * Write free surface as triangle mesh.
+ *
+ * Internally, a clone of the fill level field is stored and all cells not marked as liquidInterfaceGasFlagIDSet are set
+ * to "obstacleFillLevel" in the cloned field. This is done to avoid writing e.g. obstacle cells that were possibly
+ * assigned a fill level of 1 to not make them detect as gas cells in the bubble model.
+ **********************************************************************************************************************/
+template< typename ScalarField_T, typename FlagField_T >
+class SurfaceMeshWriter
+{
+ public:
+   SurfaceMeshWriter(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID,
+                     const ConstBlockDataID& flagFieldID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet,
+                     real_t obstacleFillLevel, uint_t writeFrequency, const std::string& baseFolder)
+      : blockForest_(blockForest), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID),
+        liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFillLevel_(obstacleFillLevel),
+        writeFrequency_(writeFrequency), baseFolder_(baseFolder), executionCounter_(uint_c(0)),
+        fillFieldCloneID_(
+           (abortIfNullptr::abortIfNullptr(blockForest),
+            field::addCloneToStorage< ScalarField_T >(blockForest_.lock(), fillFieldID_, "Fill level field clone")))
+   {}
+
+   // config block must be named "MeshOutputParameters"
+   SurfaceMeshWriter(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID,
+                     const ConstBlockDataID& flagFieldID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet,
+                     real_t obstacleFillLevel, const std::weak_ptr< Config >& config)
+      : SurfaceMeshWriter(
+           blockForest, fillFieldID, flagFieldID, liquidInterfaceGasFlagIDSet, obstacleFillLevel,
+           (abortIfNullptr::abortIfNullptr(config),
+            config.lock()->getOneBlock("MeshOutputParameters").getParameter< uint_t >("writeFrequency")),
+           (abortIfNullptr::abortIfNullptr(config),
+            config.lock()->getOneBlock("MeshOutputParameters").getParameter< std::string >("baseFolder")))
+   {}
+
+   void operator()()
+   {
+      if (writeFrequency_ == uint_c(0)) { return; }
+
+      if (executionCounter_ == uint_c(0))
+      {
+         createBaseFolder();
+         writeMesh();
+      }
+      else { writeMesh(); }
+
+      ++executionCounter_;
+   }
+
+ private:
+   void createBaseFolder() const
+   {
+      WALBERLA_ROOT_SECTION()
+      {
+         const filesystem::path basePath(baseFolder_);
+         if (filesystem::exists(basePath)) { filesystem::remove_all(basePath); }
+         filesystem::create_directories(basePath);
+      }
+      WALBERLA_MPI_BARRIER();
+   }
+
+   void writeMesh()
+   {
+      // only write mesh in given frequency
+      if (executionCounter_ % writeFrequency_ != uint_c(0)) { return; }
+
+      // rank=0 is just an arbitrary choice here
+      const int targetRank = 0;
+
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // update clone of fill level field and set fill level of all non-liquid, -interface, or -gas cells to zero
+      updateFillFieldClone(blockForest);
+
+      const auto surfaceMesh = postprocessing::realFieldToSurfaceMesh< ScalarField_T >(
+         blockForest, fillFieldCloneID_, real_c(0.5), uint_c(0), true, targetRank, MPI_COMM_WORLD);
+
+      WALBERLA_EXCLUSIVE_WORLD_SECTION(targetRank)
+      {
+         geometry::writeMesh(baseFolder_ + "/" + "simulation_step_" + std::to_string(executionCounter_) + ".obj",
+                             *surfaceMesh);
+      }
+   }
+
+   // update clone of fill level field and set fill level of all non-liquid, -interface, or -gas cells to zero;
+   // explicitly use shared_ptr instead of weak_ptr to avoid checking the latter's validity (is done in writeMesh()
+   // already)
+   void updateFillFieldClone(const shared_ptr< StructuredBlockForest >& blockForest)
+   {
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         ScalarField_T* const fillFieldClone  = blockIt->template getData< ScalarField_T >(fillFieldCloneID_);
+         const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_);
+         const FlagField_T* const flagField   = blockIt->template getData< const FlagField_T >(flagFieldID_);
+
+         const auto liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_);
+
+         WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillFieldClone, fillField->nrOfGhostLayers(), {
+            const typename ScalarField_T::Ptr fillFieldClonePtr(*fillFieldClone, x, y, z);
+            const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z);
+            const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+            // set fill level to zero in every non-liquid, -interface, or -gas cell
+            if (!isPartOfMaskSet(flagFieldPtr, liquidInterfaceGasFlagMask)) { *fillFieldClonePtr = obstacleFillLevel_; }
+            else
+            {
+               // copy fill level from fill level field
+               *fillFieldClonePtr = *fillFieldPtr;
+            }
+         }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+      }
+   }
+
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+   ConstBlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+   Set< FlagUID > liquidInterfaceGasFlagIDSet_;
+   real_t obstacleFillLevel_;
+   uint_t writeFrequency_;
+   std::string baseFolder_;
+   uint_t executionCounter_;
+
+   BlockDataID fillFieldCloneID_;
+}; // class SurfaceMeshWriter
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/TotalMassComputer.h b/src/lbm_generated/free_surface/TotalMassComputer.h
new file mode 100644
index 0000000000000000000000000000000000000000..f91af951654bba34ee5b09c9293ea3336fa5c866
--- /dev/null
+++ b/src/lbm_generated/free_surface/TotalMassComputer.h
@@ -0,0 +1,149 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file TotalMassComputer.h
+//! \ingroup free_surface
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute the total mass of the system (including mass from the excessMassField).
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/StringUtility.h"
+
+#include "lbm_generated/free_surface/dynamics/SurfaceDynamicsHandler.h"
+#include "lbm_generated/free_surface/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm_generated/free_surface/surface_geometry/Utility.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename PdfField_T, typename FlagField_T, typename ScalarField_T >
+class TotalMassComputer
+{
+ public:
+   TotalMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                     const ConstBlockDataID& flagFieldID, const FlagInfo<FlagField_T>& flagInfo,
+                     const ConstBlockDataID& pdfFieldID, const ConstBlockDataID& fillFieldID, uint_t frequency,
+                     const std::shared_ptr< real_t >& totalMass)
+      : blockForest_(blockForest), flagFieldID_(flagFieldID), flagInfo_(flagInfo), pdfFieldID_(pdfFieldID),
+        fillFieldID_(fillFieldID), totalMass_(totalMass), frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   TotalMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                     const ConstBlockDataID& flagFieldID, const FlagInfo<FlagField_T>& flagInfo,
+                     const ConstBlockDataID& pdfFieldID, const ConstBlockDataID& fillFieldID,
+                     const ConstBlockDataID& excessMassFieldID, uint_t frequency,
+                     const std::shared_ptr< real_t >& totalMass, const std::shared_ptr< real_t >& excessMass)
+      : blockForest_(blockForest), flagFieldID_(flagFieldID), flagInfo_(flagInfo), pdfFieldID_(pdfFieldID),
+        fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass),
+        excessMass_(excessMass), frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      if (frequency_ == uint_c(0)) { return; }
+
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // only evaluate in given frequencies
+      if (executionCounter_ % frequency_ == uint_c(0) || executionCounter_ == uint_c(0))
+      {
+         computeMass(blockForest, flagFieldID_, flagInfo_);
+      }
+
+      ++executionCounter_;
+   }
+
+   void computeMass(const std::shared_ptr< const StructuredBlockForest >& blockForest,
+                    const ConstBlockDataID& flagFieldID, const FlagInfo<FlagField_T>& flagInfo)
+   {
+      real_t mass       = real_c(0);
+      real_t excessMass = real_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         const FlagField_T* const flagField   = blockIt->template getData< const FlagField_T >(flagFieldID);
+         const PdfField_T* const pdfField     = blockIt->template getData< const PdfField_T >(pdfFieldID_);
+         const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_);
+
+         // if provided, also consider mass stored in excessMassField
+         if (excessMassFieldID_ != ConstBlockDataID())
+         {
+            const ScalarField_T* const excessMassField =
+               blockIt->template getData< const ScalarField_T >(excessMassFieldID_);
+
+            WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, fillFieldIt, fillField,
+                                       excessMassFieldIt, excessMassField,
+                                       omp parallel for schedule(static) reduction(+:mass),
+            {
+               if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt))
+               {
+                  //TODO Implement
+                  const real_t density = 1.0; // pdfField->getDensity(pdfFieldIt.cell());
+                  mass += *fillFieldIt * density + *excessMassFieldIt;
+
+                  if (excessMass_ != nullptr) { excessMass += *excessMassFieldIt; }
+               }
+            }) // WALBERLA_FOR_ALL_CELLS_OMP
+         }
+         else
+         {
+            WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, fillFieldIt, fillField,
+                                       omp parallel for schedule(static) reduction(+:mass),
+            {
+               if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt))
+               {
+                  //TODO Implement
+                  const real_t density = 1.0; // pdfField->getDensity(pdfFieldIt.cell());
+                  mass += *fillFieldIt * density;
+               }
+            }) // WALBERLA_FOR_ALL_CELLS_OMP
+         }
+      }
+
+      mpi::allReduceInplace< real_t >(mass, mpi::SUM);
+      *totalMass_ = mass;
+
+      if (excessMass_ != nullptr)
+      {
+         mpi::allReduceInplace< real_t >(excessMass, mpi::SUM);
+         *excessMass_ = excessMass;
+      }
+   };
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   ConstBlockDataID flagFieldID_;
+   FlagInfo<FlagField_T> flagInfo_;
+
+   const ConstBlockDataID pdfFieldID_;
+   const ConstBlockDataID fillFieldID_;
+   const ConstBlockDataID excessMassFieldID_ = ConstBlockDataID();
+
+   std::shared_ptr< real_t > totalMass_;
+   std::shared_ptr< real_t > excessMass_ = nullptr; // mass stored in the excessMassField
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class TotalMassComputer
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/VtkWriter.h b/src/lbm_generated/free_surface/VtkWriter.h
new file mode 100644
index 0000000000000000000000000000000000000000..ef523a2627eee3437318228f0743dc41df4d979d
--- /dev/null
+++ b/src/lbm_generated/free_surface/VtkWriter.h
@@ -0,0 +1,183 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file VtkWriter.h
+//! \ingroup free_surface
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Free surface-specific VTK writer function.
+//
+//======================================================================================================================
+
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "field/adaptors/AdaptorCreators.h"
+#include "field/communication/PackInfo.h"
+#include "field/vtk/FlagFieldCellFilter.h"
+#include "field/vtk/FlagFieldMapping.h"
+#include "field/vtk/VTKWriter.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "vtk/Initialization.h"
+
+#include "FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Add VTK output to time loop that includes all relevant free surface information. It must be configured via
+ * config-file.
+ **********************************************************************************************************************/
+template< typename FlagInfo_T, typename PdfField_T, typename FlagField_T,
+          typename ScalarField_T, typename VectorField_T,
+          typename VectorFieldFlattened_T = GhostLayerField< real_t, 3 > >
+void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, SweepTimeloop& timeloop,
+                  const std::weak_ptr< Config >& configPtr,
+                  const FlagInfo_T& flagInfo, const BlockDataID& pdfFieldID,
+                  const BlockDataID& flagFieldID, const BlockDataID& fillFieldID,
+                  const BlockDataID& forceDensityFieldID, const BlockDataID& curvatureFieldID,
+                  const BlockDataID& normalFieldID, const BlockDataID& obstacleNormalFieldID)
+{
+   // TODO density and velocity are not added to VTK Output.
+   using value_type       = typename FlagField_T::value_type;
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   const auto config = configPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(config);
+
+   // define VTK output (see src/vtk/Initialization.cpp, line 574 for usage)
+   const auto vtkConfigFunc = [&](std::vector< std::shared_ptr< vtk::BlockCellDataWriterInterface > >& writers,
+                                  std::map< std::string, vtk::VTKOutput::CellFilter >& filters,
+                                  std::map< std::string, vtk::VTKOutput::BeforeFunction >& beforeFuncs) {
+      using field::VTKWriter;
+      writers.push_back(std::make_shared< VTKWriter< PdfField_T, float > >(pdfFieldID, "pdf"));
+      writers.push_back(std::make_shared< VTKWriter< FlagField_T, float > >(flagFieldID, "flag"));
+      writers.push_back(std::make_shared< VTKWriter< ScalarField_T, float > >(fillFieldID, "fill_level"));
+      writers.push_back(std::make_shared< VTKWriter< ScalarField_T, float > >(curvatureFieldID, "curvature"));
+      writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(normalFieldID, "normal"));
+      writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(obstacleNormalFieldID, "obstacle_normal"));
+
+      if (forceDensityFieldID != BlockDataID())
+      {
+         writers.push_back(std::make_shared< VTKWriter< VectorFieldFlattened_T, float > >(forceDensityFieldID, "force_density"));
+      }
+
+      // map flagIDs to integer values
+      const auto flagMapper =
+         std::make_shared< field::FlagFieldMapping< FlagField_T, value_type > >(flagFieldID, "mapped_flag");
+      flagMapper->addMapping(flagIDs::liquidFlagID, numeric_cast< value_type >(1));
+      flagMapper->addMapping(flagIDs::interfaceFlagID, numeric_cast< value_type >(2));
+      flagMapper->addMapping(flagIDs::gasFlagID, numeric_cast< value_type >(3));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, numeric_cast< value_type >(4));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, numeric_cast< value_type >(6));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, numeric_cast< value_type >(6));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, numeric_cast< value_type >(7));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, numeric_cast< value_type >(8));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, numeric_cast< value_type >(9));
+//      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, numeric_cast< value_type >(10));
+
+      writers.push_back(flagMapper);
+
+      // filter for writing only liquid and interface cells to VTK
+      auto liquidInterfaceFilter = field::FlagFieldCellFilter< FlagField_T >(flagFieldID);
+      liquidInterfaceFilter.addFlag(flagIDs::liquidFlagID);
+      liquidInterfaceFilter.addFlag(flagIDs::interfaceFlagID);
+      filters["liquidInterfaceFilter"] = liquidInterfaceFilter;
+
+      // communicate fields to update the ghost layer
+      auto preVTKComm = blockforest::communication::UniformBufferedScheme< stencil::D3Q27 >(blockForest);
+      preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID));
+      preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< FlagField_T > >(flagFieldID));
+      preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID));
+      preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(curvatureFieldID));
+      preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< VectorField_T > >(normalFieldID));
+      preVTKComm.addPackInfo(
+         std::make_shared< field::communication::PackInfo< VectorField_T > >(obstacleNormalFieldID));
+      preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< VectorFieldFlattened_T > >(forceDensityFieldID));
+
+      beforeFuncs["ghost_layer_synchronization"] = preVTKComm;
+
+      // set velocity and density to zero in obstacle and gas cells (only for visualization purposes); the PDF values in
+      // these cells are not important and thus not set during the simulation;
+      // only enable this functionality if the non-liquid and non-interface cells are not excluded anyway
+      const auto vtkConfigBlock        = config->getOneBlock("VTK");
+      const auto fluidFieldConfigBlock = vtkConfigBlock.getBlock("fluid_field");
+      if (fluidFieldConfigBlock)
+      {
+         auto inclusionFiltersConfigBlock = fluidFieldConfigBlock.getBlock("inclusion_filters");
+
+         // liquidInterfaceFilter limits VTK-output to only liquid and interface cells
+         if (!inclusionFiltersConfigBlock.isDefined("liquidInterfaceFilter"))
+         {
+            class ZeroSetter
+            {
+             public:
+               ZeroSetter(const weak_ptr< StructuredBlockForest >& blockForest, const BlockDataID& pdfFieldID,
+                          const ConstBlockDataID& flagFieldID,
+                          const FlagInfo_T& flagInfo)
+                  : blockForest_(blockForest), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), flagInfo_(flagInfo)
+               {}
+
+               void operator()()
+               {
+                  auto blockForest = blockForest_.lock();
+                  WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+                  for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+                  {
+                     PdfField_T* const pdfField         = blockIt->template getData< PdfField_T >(pdfFieldID_);
+                     const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID_);
+                     WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(pdfField, uint_c(1), {
+                        const typename PdfField_T::Ptr pdfFieldPtr(*pdfField, x, y, z);
+                        const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+//                        if (flagInfo_.isGas(*flagFieldPtr) || flagInfo_.isObstacle(*flagFieldPtr))
+//                        {
+//                           pdfField->setDensityAndVelocity(pdfFieldPtr.cell(), Vector3< real_t >(real_c(0)),
+//                                                           real_c(1.0));
+//                        }
+                     }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+                  }
+               }
+
+             private:
+               weak_ptr< StructuredBlockForest > blockForest_;
+               BlockDataID pdfFieldID_;
+               ConstBlockDataID flagFieldID_;
+               FlagInfo_T flagInfo_;
+            };
+
+            beforeFuncs["gas_cell_zero_setter"] = ZeroSetter(blockForest, pdfFieldID, flagFieldID, flagInfo);
+         }
+      }
+   };
+
+   // add VTK output to timeloop
+   std::map< std::string, vtk::SelectableOutputFunction > vtkOutputFunctions;
+   vtk::initializeVTKOutput(vtkOutputFunctions, vtkConfigFunc, blockForest, config);
+   for (auto output = vtkOutputFunctions.begin(); output != vtkOutputFunctions.end(); ++output)
+   {
+      timeloop.addFuncBeforeTimeStep(output->second.outputFunction, std::string("VTK: ") + output->first,
+                                     output->second.requiredGlobalStates, output->second.incompatibleGlobalStates);
+   }
+}
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/CMakeLists.txt b/src/lbm_generated/free_surface/dynamics/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f6f2fc658d31f95028dabb69c7a7fb4ddb59424f
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/CMakeLists.txt
@@ -0,0 +1,17 @@
+target_sources( lbm_generated
+        PRIVATE
+        CellConversionSweep.h
+        ConversionFlagsResetSweep.h
+        ExcessMassDistributionModel.h
+        ExcessMassDistributionSweep.h
+        ExcessMassDistributionSweep.impl.h
+        ForceDensitySweep.h
+        PdfReconstructionModel.h
+        PdfRefillingModel.h
+        PdfRefillingSweep.h
+        PdfRefillingSweep.impl.h
+        StreamReconstructAdvectSweep.h
+        SurfaceDynamicsHandler.h
+        )
+
+add_subdirectory( functionality )
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/CellConversionSweep.h b/src/lbm_generated/free_surface/dynamics/CellConversionSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..2d360357ed34c687e12a356868c5307445dc8622
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/CellConversionSweep.h
@@ -0,0 +1,285 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file CellConversionSweep.h
+//! \ingroup dynamics
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Convert cells to/from interface cells.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/FlagField.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Convert cells to/from interface
+ * - expects that a previous sweep has set the convertToLiquidFlag and convertToGasFlag flags
+ * - notifies the bubble model that conversions occurred
+ * - all cells that have been converted, have the "converted" flag set
+ * - PDF field is required in order to initialize the velocity in new gas cells with information from surrounding cells
+ *
+ * A) Interface -> Liquid/Gas
+ *     - always converts interface to liquid
+ *     - converts interface to gas only if no newly created liquid cell is in neighborhood
+ * B) Liquid/Gas -> Interface (to obtain a closed interface layer)
+ *     - "old" liquid cells in the neighborhood of newly converted cells in A) are converted to interface
+ *     - "old" gas cells in the neighborhood of newly converted cells in A) are converted to interface
+ *     The term "old" refers to cells that were not converted themselves in the same time step.
+ * C) GAS -> INTERFACE (due to inflow boundary condition)
+ * D) LIQUID/GAS -> INTERFACE (due to wetting; only when using local triangulation for curvature computation)
+ *
+ * For gas cells that were converted to interface in B), the flag "convertedFromGasToInterface" is set to signal another
+ * sweep (i.e. "PdfRefillingSweep.h") that the this cell's PDFs need to be reinitialized.
+ *
+ * For gas cells that were converted to interface in C), the cell's PDFs are reinitialized with equilibrium constructed
+ * with the inflow velocity.
+ *
+ * More information can be found in the dissertation of N. Thuerey, 2007, section 4.3.
+ * ********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T >
+class CellConversionSweep
+{
+ public:
+   using flag_t      = typename FlagField_T::flag_t;
+   using PdfField_T  = lbm_generated::PdfField< StorageSpecification_T >;
+   using Stencil_T   = typename StorageSpecification_T::Stencil;
+
+   CellConversionSweep(BlockDataID flagFieldID, BlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo)
+      : flagFieldID_(flagFieldID), pdfFieldID_(pdfFieldID), flagInfo_(flagInfo)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      // PdfField_T* const pdfField         = block->getData< PdfField_T >(pdfFieldID_);
+      FlagField_T* const flagField       = block->getData< FlagField_T >(flagFieldID_);
+
+      // A) INTERFACE -> LIQUID/GAS
+      // convert interface cells that have filled/emptied to liquid/gas (cflagInfo_. dissertation of N. Thuerey, 2007,
+      // section 4.3)
+      // the conversion is also performed in the first ghost layer, since B requires an up-to-date first ghost layer;
+      // explicitly avoid OpenMP, as bubble IDs are set here
+      WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, {
+         const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+         // 1) convert interface cells to liquid
+         if (isFlagSet(flagFieldPtr, flagInfo_.convertToLiquidFlag))
+         {
+            flagField->removeFlag(x, y, z, flagInfo_.interfaceFlag);
+            flagField->addFlag(x, y, z, flagInfo_.liquidFlag);
+            flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+         }
+
+         // 2) convert interface cells to gas only if no newly converted liquid cell from 1) is in neighborhood to
+         // ensure a closed interface
+         if (isFlagSet(flagFieldPtr, flagInfo_.convertToGasFlag) &&
+             !isFlagInNeighborhood< Stencil_T >(flagFieldPtr, flagInfo_.convertToLiquidFlag))
+         {
+            flagField->removeFlag(x, y, z, flagInfo_.interfaceFlag);
+            flagField->addFlag(x, y, z, flagInfo_.gasFlag);
+            flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP
+
+      // B) LIQUID/GAS -> INTERFACE
+      // convert those liquid/gas cells to interface that are in the neighborhood of the newly created liquid/gas cells
+      // from A; this maintains a closed interface layer;
+      // explicitly avoid OpenMP, as bubble IDs are set here
+      WALBERLA_FOR_ALL_CELLS_XYZ_OMP(flagField, omp critical, {
+         const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+         // only consider "old" liquid cells, i.e., cells that have not been converted in this time step
+         if (flagInfo_.isLiquid(flagFieldPtr) && !flagInfo_.hasConverted(flagFieldPtr))
+         {
+            const flag_t newGasFlagMask = flagInfo_.convertedFlag | flagInfo_.gasFlag; // flag newly converted gas cell
+
+            // the state of ghost layer cells becomes relevant here
+            for (auto d = StorageSpecification_T::Stencil::beginNoCenter(); d != StorageSpecification_T::Stencil::end(); ++d)
+               if (isMaskSet(flagFieldPtr.neighbor(*d), newGasFlagMask)) // newly converted gas cell is in neighborhood
+               {
+                  // convert the current cell to interface
+                  flagField->removeFlag(x, y, z, flagInfo_.liquidFlag);
+                  flagField->addFlag(x, y, z, flagInfo_.interfaceFlag);
+                  flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+
+                  // current cell was already converted to interface, flags of other neighbors are not relevant
+                  break;
+               }
+         }
+         // only consider "old" gas cells, i.e., cells that have not been converted in this time step
+         else
+         {
+            if (flagInfo_.isGas(flagFieldPtr) && !flagInfo_.hasConverted(flagFieldPtr))
+            {
+               const flag_t newLiquidFlagMask =
+                  flagInfo_.convertedFlag | flagInfo_.liquidFlag; // flag of newly converted liquid cell
+
+               // the state of ghost layer cells is relevant here
+               for (auto d = StorageSpecification_T::Stencil::beginNoCenter(); d != StorageSpecification_T::Stencil::end(); ++d)
+
+                  // newly converted liquid cell is in neighborhood
+                  if (isMaskSet(flagFieldPtr.neighbor(*d), newLiquidFlagMask))
+                  {
+                     // convert the current cell to interface
+                     flagField->removeFlag(x, y, z, flagInfo_.gasFlag);
+                     flagField->addFlag(x, y, z, flagInfo_.interfaceFlag);
+                     flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+                     flagField->addFlag(x, y, z, flagInfo_.convertFromGasToInterfaceFlag);
+
+                     // current cell was already converted to interface, flags of other neighbors are not relevant
+                     break;
+                  }
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP
+
+      // C) GAS -> INTERFACE (due to inflow boundary condition)
+      // convert gas cells to interface cells near inflow boundaries;
+      // explicitly avoid OpenMP, such that cell conversions are performed sequentially
+      convertedFromGasToInterfaceDueToInflow.clear();
+      WALBERLA_FOR_ALL_CELLS_XYZ_OMP(flagField, omp critical, {
+         const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+         if (flagInfo_.isConvertToInterfaceForInflow(flagFieldPtr) && !flagInfo_.hasConverted(flagFieldPtr))
+         {
+            // newly converted liquid cell is in neighborhood
+            flagField->removeFlag(flagInfo_.convertToInterfaceForInflowFlag, x, y, z);
+
+            // convert the current cell to interface
+            flagField->removeFlag(x, y, z, flagInfo_.gasFlag);
+            flagField->addFlag(x, y, z, flagInfo_.interfaceFlag);
+            flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+            convertedFromGasToInterfaceDueToInflow.insert(flagFieldPtr.cell());
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP
+
+      // D) LIQUID/GAS -> INTERFACE (due to wetting; only active when using local triangulation for curvature
+      // computation)
+      // convert liquid/gas to interface cells where the interface cell is required for a smooth
+      // continuation of the wetting surface (see dissertation of S. Donath, 2011, section 6.3.5.3);
+      // explicitly avoid OpenMP, as bubble IDs are set here
+      WALBERLA_FOR_ALL_CELLS_XYZ_OMP(flagField, omp critical, {
+         const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+         // only consider wetting and non-interface cells
+         if (flagInfo_.isKeepInterfaceForWetting(flagFieldPtr) && !flagInfo_.isInterface(flagFieldPtr))
+         {
+            // convert liquid cell to interface
+            if (isFlagSet(flagFieldPtr, flagInfo_.liquidFlag))
+            {
+               flagField->removeFlag(x, y, z, flagInfo_.liquidFlag);
+               flagField->addFlag(x, y, z, flagInfo_.interfaceFlag);
+               flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+               flagField->removeFlag(x, y, z, flagInfo_.keepInterfaceForWettingFlag);
+            }
+            else
+            {
+               // convert gas cell to interface
+               if (isFlagSet(flagFieldPtr, flagInfo_.gasFlag))
+               {
+                  flagField->removeFlag(x, y, z, flagInfo_.gasFlag);
+                  flagField->addFlag(x, y, z, flagInfo_.interfaceFlag);
+                  flagField->addFlag(x, y, z, flagInfo_.convertedFlag);
+                  flagField->removeFlag(x, y, z, flagInfo_.keepInterfaceForWettingFlag);
+                  flagField->addFlag(x, y, z, flagInfo_.convertFromGasToInterfaceFlag);
+               }
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP
+
+      // initialize PDFs of interface cells that were created due to an inflow boundary; the PDFs are set to equilibrium
+      // with density=1 and velocity of the inflow boundary
+
+      // TODO: should be activated again in the future
+      // initializeFromInflow(convertedFromGasToInterfaceDueToInflow, flagField, pdfField);
+   }
+
+ protected:
+   /********************************************************************************************************************
+    * Initializes PDFs in cells that are converted to interface due to a neighboring inflow boundary.
+    *
+    * The PDFs of these cells are set to equilibrium values using density=1 and the average velocity of neighboring
+    * inflow boundaries. An inflow cell is used for averaging only if the velocity actually flows towards the newly
+    * created interface cell. In other words, the velocity direction is compared to the converted cell's direction with
+    * respect to the inflow location.
+    *
+    * REMARK: The inflow boundary condition must implement function "getValue()" that returns the prescribed velocity
+    * (see e.g. UBB).
+    *******************************************************************************************************************/
+//   void initializeFromInflow(const std::set< Cell >& cells, FlagField_T* flagField, PdfField_T* pdfField)
+//   {
+//      for (auto setIt = cells.begin(); setIt != cells.end(); ++setIt)
+//      {
+//         const Cell& cell = *setIt;
+//
+//         Vector3< real_t > u(real_c(0.0));
+//         uint_t numNeighbors = uint_c(0);
+//
+//         // get UBB inflow boundary
+//
+//         for (auto i = StorageSpecification_T::Stencil::beginNoCenter(); i != StorageSpecification_T::Stencil::end(); ++i)
+//         {
+//            using namespace stencil;
+//            const Cell neighborCell(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz());
+//
+//            const flag_t neighborFlag = flagField->get(neighborCell);
+//
+//            // neighboring cell is inflow
+//            if (isPartOfMaskSet(neighborFlag, flagInfo_.inflowFlagMask))
+//            {
+//               // get direction towards cell containing inflow boundary
+//               const Vector3< int > dir = Vector3< int >(-i.cx(), -i.cy(), -i.cz());
+//
+//               const Vector3< real_t > inflowVel = ubbInflow.getValue(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz());
+//
+//               // skip directions in which the corresponding velocity component is zero
+//               if (realIsEqual(inflowVel[0], real_c(0), real_c(1e-14)) && dir[0] != 0) { continue; }
+//               if (realIsEqual(inflowVel[1], real_c(0), real_c(1e-14)) && dir[1] != 0) { continue; }
+//               if (realIsEqual(inflowVel[2], real_c(0), real_c(1e-14)) && dir[2] != 0) { continue; }
+//
+//               // skip directions in which the corresponding velocity component is in opposite direction
+//               if (inflowVel[0] > real_c(0) && dir[0] < 0) { continue; }
+//               if (inflowVel[1] > real_c(0) && dir[1] < 0) { continue; }
+//               if (inflowVel[2] > real_c(0) && dir[2] < 0) { continue; }
+//
+//               // use inflow velocity to get average velocity
+//               u += inflowVel;
+//               numNeighbors++;
+//            }
+//         }
+//         if (numNeighbors > uint_c(0)) { u /= real_c(numNeighbors); } // else: velocity is zero
+//
+//         pdfField->setDensityAndVelocity(cell, u, real_c(1)); // set density=1
+//      }
+//   }
+
+   BlockDataID flagFieldID_;
+   BlockDataID pdfFieldID_;
+
+   FlagInfo< FlagField_T > flagInfo_;
+
+   std::set< Cell > convertedFromGasToInterfaceDueToInflow;
+}; // class CellConversionSweep
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/ConversionFlagsResetSweep.h b/src/lbm_generated/free_surface/dynamics/ConversionFlagsResetSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..f3df3bbfa0aa4213dc822d8a68fa0beac06c5a21
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/ConversionFlagsResetSweep.h
@@ -0,0 +1,70 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ResetFlagSweep.h
+//! \ingroup dynamics
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Reset all free surface flags that mark cell conversions.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/logging/Logging.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Reset all free surface flags that signal cell conversions. The flag "keepInterfaceForWettingFlag" is explicitly not
+ * reset since this flag must persist in the next time step.
+ **********************************************************************************************************************/
+template< typename FlagField_T >
+class ConversionFlagsResetSweep
+{
+ public:
+   ConversionFlagsResetSweep(BlockDataID flagFieldID, const FlagInfo< FlagField_T >& flagInfo)
+      : flagFieldID_(flagFieldID), flagInfo_(flagInfo)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      FlagField_T* const flagField = block->getData< FlagField_T >(flagFieldID_);
+
+      // reset all conversion flags (except flagInfo_.keepInterfaceForWettingFlag)
+      const flag_t allConversionFlags = flagInfo_.convertToGasFlag | flagInfo_.convertToLiquidFlag |
+                                        flagInfo_.convertedFlag | flagInfo_.convertFromGasToInterfaceFlag |
+                                        flagInfo_.convertToInterfaceForInflowFlag;
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField,
+                             { removeMask(flagFieldIt, allConversionFlags); }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+ private:
+   using flag_t = typename FlagField_T::flag_t;
+
+   BlockDataID flagFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+}; // class ConversionFlagsResetSweep
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionModel.h b/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..809dbbdc0e725f2e832ee34bfdec98125013a695
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionModel.h
@@ -0,0 +1,238 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ExcessMassDistributionModel.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Class that specifies how excessive mass is distributed.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/StringUtility.h"
+#include "core/stringToNum.h"
+
+#include <string>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Class that specifies how excessive mass is distributed after cell conversions from interface to liquid or interface
+ * to gas.
+ * For example, when converting an interface cell with fill level 1.1 to liquid with fill level 1,0, an excessive mass
+ * corresponding to the fill level 0.1 must be distributed.
+ *
+ * Available models:
+ *  - EvenlyAllInterface:
+ *       Excess mass is distributed evenly among all neighboring interface cells (see dissertations of T. Pohl, S.
+ *       Donath, S. Bogner).
+ *
+ *  - EvenlyNewInterface:
+ *       Excess mass is distributed evenly among newly converted neighboring interface cells (see Koerner et al., 2005).
+ *       Falls back to EvenlyAllInterface if not applicable.
+ *
+ *  - EvenlyOldInterface:
+ *       Excess mass is distributed evenly among old neighboring interface cells, i.e., cells that are non-newly
+ *       converted to interface. Falls back to EvenlyAllInterface if not applicable.
+ *
+ *  - WeightedAllInterface:
+ *       Excess mass is distributed weighted with the direction of the interface normal among all neighboring interface
+ *       cells (see dissertation of N. Thuerey, 2007). Falls back to EvenlyAllInterface if not applicable.
+ *
+ *  - WeightedNewInterface:
+ *       Excess mass is distributed weighted with the direction of the interface normal among newly converted
+ *       neighboring interface cells. Falls back to WeightedAllInterface if not applicable.
+ *
+ *  - WeightedOldInterface:
+ *       Excess mass is distributed weighted with the direction of the interface normal among old neighboring interface
+ *       cells, i.e., cells that are non-newly converted to interface. Falls back to WeightedAllInterface if not
+ * applicable.
+ *
+ *  - EvenlyAllInterfaceAndLiquid:
+ *      Excess mass is distributed evenly among all neighboring interface and liquid cells (see p.47 in master thesis of
+ *      M. Lehmann, 2019). The excess mass distributed to liquid cells does neither modify the cell's density nor fill
+ *      level. Instead, it is stored in an additional excess mass field. Therefore, not only the converted interface
+ *      cells' excess mass is distributed, but also the excess mass of liquid cells stored in this additional field.
+ *
+ *  - EvenlyAllInterfaceFallbackLiquid:
+ *      Similar to EvenlyAllInterfaceAndLiquid, however, excess mass is preferably distributed to interface cells. It is
+ *      distributed to liquid cells only if there are no neighboring interface cells available.
+ *
+ *  - EvenlyNewInterfaceFallbackLiquid:
+ *      Similar to EvenlyAllInterfaceFallbackLiquid, however, excess mass is preferably distributed to newly
+ *      converted interface cells. If there are no newly converted interface cells, the excess mass is distributed to
+ *      old interface cells. The excess mass is distributed to neighboring liquid cells only if there are no neighboring
+ *      interface cells available.
+ * ********************************************************************************************************************/
+class ExcessMassDistributionModel
+{
+ public:
+   enum class ExcessMassModel {
+      EvenlyAllInterface,
+      EvenlyNewInterface,
+      EvenlyOldInterface,
+      WeightedAllInterface,
+      WeightedNewInterface,
+      WeightedOldInterface,
+      EvenlyAllInterfaceAndLiquid,
+      EvenlyAllInterfaceFallbackLiquid,
+      EvenlyNewInterfaceFallbackLiquid
+   };
+
+   ExcessMassDistributionModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName))
+   {}
+
+   ExcessMassDistributionModel(const ExcessMassModel& modelType)
+      : modelName_(chooseName(modelType)), modelType_(modelType)
+   {
+      switch (modelType_)
+      {
+      case ExcessMassModel::EvenlyAllInterface:
+         break;
+      case ExcessMassModel::EvenlyNewInterface:
+         break;
+      case ExcessMassModel::EvenlyOldInterface:
+         break;
+      case ExcessMassModel::WeightedAllInterface:
+         break;
+      case ExcessMassModel::WeightedNewInterface:
+         break;
+      case ExcessMassModel::WeightedOldInterface:
+         break;
+      case ExcessMassModel::EvenlyAllInterfaceAndLiquid:
+         break;
+      case ExcessMassModel::EvenlyAllInterfaceFallbackLiquid:
+         break;
+      case ExcessMassModel::EvenlyNewInterfaceFallbackLiquid:
+         break;
+      }
+   }
+
+   inline ExcessMassModel getModelType() const { return modelType_; }
+   inline std::string getModelName() const { return modelName_; }
+   inline std::string getFullModelSpecification() const { return getModelName(); }
+
+   inline bool isEvenlyType() const
+   {
+      return modelType_ == ExcessMassModel::EvenlyAllInterface || modelType_ == ExcessMassModel::EvenlyNewInterface ||
+             modelType_ == ExcessMassModel::EvenlyOldInterface;
+   }
+
+   inline bool isWeightedType() const
+   {
+      return modelType_ == ExcessMassModel::WeightedAllInterface ||
+             modelType_ == ExcessMassModel::WeightedNewInterface || modelType_ == ExcessMassModel::WeightedOldInterface;
+   }
+
+   inline bool isEvenlyAllInterfaceFallbackLiquidType() const
+   {
+      return modelType_ == ExcessMassModel::EvenlyAllInterfaceAndLiquid ||
+             modelType_ == ExcessMassModel::EvenlyAllInterfaceFallbackLiquid ||
+             modelType_ == ExcessMassModel::EvenlyNewInterfaceFallbackLiquid;
+      ;
+   }
+
+   static inline std::initializer_list< const ExcessMassModel > getTypeIterator() { return listOfAllEnums; }
+
+ private:
+   ExcessMassModel chooseType(const std::string& modelName)
+   {
+      if (!string_icompare(modelName, "EvenlyAllInterface")) { return ExcessMassModel::EvenlyAllInterface; }
+
+      if (!string_icompare(modelName, "EvenlyNewInterface")) { return ExcessMassModel::EvenlyNewInterface; }
+
+      if (!string_icompare(modelName, "EvenlyOldInterface")) { return ExcessMassModel::EvenlyOldInterface; }
+
+      if (!string_icompare(modelName, "WeightedAllInterface")) { return ExcessMassModel::WeightedAllInterface; }
+
+      if (!string_icompare(modelName, "WeightedNewInterface")) { return ExcessMassModel::WeightedNewInterface; }
+
+      if (!string_icompare(modelName, "WeightedOldInterface")) { return ExcessMassModel::WeightedOldInterface; }
+
+      if (!string_icompare(modelName, "EvenlyAllInterfaceAndLiquid"))
+      {
+         return ExcessMassModel::EvenlyAllInterfaceAndLiquid;
+      }
+
+      if (!string_icompare(modelName, "EvenlyAllInterfaceFallbackLiquid"))
+      {
+         return ExcessMassModel::EvenlyAllInterfaceFallbackLiquid;
+      }
+
+      if (!string_icompare(modelName, "EvenlyNewInterfaceFallbackLiquid"))
+      {
+         return ExcessMassModel::EvenlyNewInterfaceFallbackLiquid;
+      }
+
+      WALBERLA_ABORT("The specified PDF reinitialization model " << modelName << " is not available.");
+   }
+
+   std::string chooseName(ExcessMassModel const& modelType) const
+   {
+      std::string modelName;
+      switch (modelType)
+      {
+      case ExcessMassModel::EvenlyAllInterface:
+         modelName = "EvenlyAllInterface";
+         break;
+      case ExcessMassModel::EvenlyNewInterface:
+         modelName = "EvenlyNewInterface";
+         break;
+      case ExcessMassModel::EvenlyOldInterface:
+         modelName = "EvenlyOldInterface";
+         break;
+      case ExcessMassModel::WeightedAllInterface:
+         modelName = "WeightedAllInterface";
+         break;
+      case ExcessMassModel::WeightedNewInterface:
+         modelName = "WeightedNewInterface";
+         break;
+      case ExcessMassModel::WeightedOldInterface:
+         modelName = "WeightedOldInterface";
+         break;
+
+      case ExcessMassModel::EvenlyAllInterfaceAndLiquid:
+         modelName = "EvenlyAllInterfaceAndLiquid";
+         break;
+      case ExcessMassModel::EvenlyAllInterfaceFallbackLiquid:
+         modelName = "EvenlyAllInterfaceFallbackLiquid";
+         break;
+      case ExcessMassModel::EvenlyNewInterfaceFallbackLiquid:
+         modelName = "EvenlyNewInterfaceFallbackLiquid";
+         break;
+      }
+      return modelName;
+   }
+
+   std::string modelName_;
+   ExcessMassModel modelType_;
+   static constexpr std::initializer_list< const ExcessMassModel > listOfAllEnums = {
+      ExcessMassModel::EvenlyAllInterface,
+      ExcessMassModel::EvenlyNewInterface,
+      ExcessMassModel::EvenlyOldInterface,
+      ExcessMassModel::WeightedAllInterface,
+      ExcessMassModel::WeightedNewInterface,
+      ExcessMassModel::WeightedOldInterface,
+      ExcessMassModel::EvenlyAllInterfaceAndLiquid,
+      ExcessMassModel::EvenlyAllInterfaceFallbackLiquid,
+      ExcessMassModel::EvenlyNewInterfaceFallbackLiquid
+   };
+
+}; // class ExcessMassDistributionModel
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionSweep.h b/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..b7fd343845d36eeeb9258abb0e106da429915390
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionSweep.h
@@ -0,0 +1,213 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ExcessMassDistributionSweep.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Distribute excess mass, i.e., mass that is undistributed after conversions from interface to liquid or gas.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/logging/Logging.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FieldClone.h"
+#include "field/FlagField.h"
+#include "field/GhostLayerField.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include "ExcessMassDistributionModel.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Distribute excess mass, i.e., mass that is undistributed after cells have been converted from interface to
+ * gas/liquid. For example, when converting an interface cell with fill level 1.1 to liquid with fill level 1.0, an
+ * excessive mass corresponding to the fill level 0.1 must be distributed to conserve mass.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepBase
+{
+ public:
+   ExcessMassDistributionSweepBase(const ExcessMassDistributionModel& excessMassDistributionModel,
+                                   BlockDataID fillFieldID, ConstBlockDataID flagFieldID, ConstBlockDataID pdfFieldID,
+                                   const FlagInfo< FlagField_T >& flagInfo)
+      : excessMassDistributionModel_(excessMassDistributionModel), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID),
+        pdfFieldID_(pdfFieldID), flagInfo_(flagInfo)
+   {}
+
+   virtual void operator()(IBlock* const block) = 0;
+
+   virtual ~ExcessMassDistributionSweepBase() = default;
+
+ protected:
+   /********************************************************************************************************************
+    * Determines the number of a cell's
+    *  - neighboring newly-converted interface cells
+    *  - neighboring interface cells (regardless if newly converted or not)
+    *******************************************************************************************************************/
+   void getNumberOfInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& newInterfaceNeighbors,
+                                      uint_t& interfaceNeighbors);
+
+   /********************************************************************************************************************
+    * Determines the number of a cell's neighboring liquid and interface cells.
+    *******************************************************************************************************************/
+   void getNumberOfLiquidAndInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& liquidNeighbors,
+                                               uint_t& interfaceNeighbors, uint_t& newInterfaceNeighbors);
+
+   ExcessMassDistributionModel excessMassDistributionModel_;
+   BlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+   ConstBlockDataID pdfFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+}; // class ExcessMassDistributionSweep
+
+/***********************************************************************************************************************
+ * Distribute the excess mass evenly among either
+ *  - all neighboring interface cells (see dissertations of T. Pohl, S. Donath, S. Bogner).
+ *  - newly converted interface cells (see Koerner et al., 2005)
+ *  - old, i.e., non-newly converted interface cells
+ *
+ * If either no newly converted interface cell or old interface cell is available in the neighborhood, the
+ * respective other approach is used as fallback.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepInterfaceEvenly
+   : public ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExcessMassDistributionSweepBase_T =
+      ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >;
+
+   ExcessMassDistributionSweepInterfaceEvenly(const ExcessMassDistributionModel& excessMassDistributionModel,
+                                              BlockDataID fillFieldID, ConstBlockDataID flagFieldID,
+                                              ConstBlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo)
+      : ExcessMassDistributionSweepBase_T(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo)
+   {}
+
+   ~ExcessMassDistributionSweepInterfaceEvenly() override = default;
+
+   void operator()(IBlock* const block) override;
+
+ private:
+   template< typename PdfField_T >
+   void distributeMassEvenly(ScalarField_T* fillField, const FlagField_T* flagField, const PdfField_T* pdfField,
+                             const Cell& cell, real_t excessFill);
+}; // class ExcessMassDistributionSweepInterfaceEvenly
+
+/***********************************************************************************************************************
+ * Distribute the excess mass weighted with the direction of the interface normal among either
+ *  - all neighboring interface cells (see section 4.3 in dissertation of N. Thuerey, 2007)
+ *  - newly converted interface cells
+ *  - old, i.e., non-newly converted interface cells
+ *
+ * If either no newly converted interface cell or old interface cell is available in the neighborhood, the
+ * respective other approach is used as fallback.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepInterfaceWeighted
+   : public ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExcessMassDistributionSweepBase_T =
+      ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >;
+
+   ExcessMassDistributionSweepInterfaceWeighted(const ExcessMassDistributionModel& excessMassDistributionModel,
+                                                BlockDataID fillFieldID, ConstBlockDataID flagFieldID,
+                                                ConstBlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                                                ConstBlockDataID normalFieldID)
+      : ExcessMassDistributionSweepBase_T(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo),
+        normalFieldID_(normalFieldID)
+   {}
+
+   ~ExcessMassDistributionSweepInterfaceWeighted() override = default;
+
+   void operator()(IBlock* const block) override;
+
+ private:
+   template< typename PdfField_T >
+   void distributeMassWeighted(ScalarField_T* fillField, const FlagField_T* flagField, const PdfField_T* pdfField,
+                               const VectorField_T* normalField, const Cell& cell, bool isNewLiquid, real_t excessFill);
+
+   /********************************************************************************************************************
+    * Returns vector with weights for excess mass distribution among neighboring cells.
+    *******************************************************************************************************************/
+   void getExcessMassWeights(const FlagField_T* flagField, const VectorField_T* normalField, const Cell& cell,
+                             bool isNewLiquid, bool useWeightedOld, bool useWeightedAll, bool useWeightedNew,
+                             std::vector< real_t >& weights);
+
+   /********************************************************************************************************************
+    * Computes the weights for distributing the excess mass based on the direction of the interface normal (see equation
+    * (4.9) in dissertation of N. Thuerey, 2007)
+    *******************************************************************************************************************/
+   void computeWeightWithNormal(real_t n_dot_ci, bool isNewLiquid, typename StorageSpecification_T::Stencil::iterator dir,
+                                std::vector< real_t >& weights);
+
+   ConstBlockDataID normalFieldID_;
+
+}; // class ExcessMassDistributionSweepInterfaceWeighted
+
+/***********************************************************************************************************************
+ * Distribute the excess mass evenly among
+ *  - all neighboring liquid and interface cells (see p. 47 in master thesis of M. Lehmann, 2019)
+ *  - all neighboring interface cells and only to liquid cells if there exists no neighboring interface cell
+ *  - new neighboring interface cells, if not available to old interface cells and only to liquid cells if there exists
+ *    no neighboring interface cell
+ *
+ * Neither the fill level, nor the density of liquid cells is modified. Instead, the excess mass is stored in an
+ * additional field.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepInterfaceAndLiquid
+   : public ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExcessMassDistributionSweepBase_T =
+      ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >;
+
+   ExcessMassDistributionSweepInterfaceAndLiquid(const ExcessMassDistributionModel& excessMassDistributionModel,
+                                                 BlockDataID fillFieldID, ConstBlockDataID flagFieldID,
+                                                 ConstBlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                                                 BlockDataID excessMassFieldID)
+      : ExcessMassDistributionSweepBase_T(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo),
+        excessMassFieldID_(excessMassFieldID), excessMassFieldClone_(excessMassFieldID)
+   {}
+
+   ~ExcessMassDistributionSweepInterfaceAndLiquid() override = default;
+
+   void operator()(IBlock* const block) override;
+
+ private:
+   template< typename PdfField_T >
+   void distributeMassInterfaceAndLiquid(ScalarField_T* fillField, ScalarField_T* dstExcessMassField,
+                                         const FlagField_T* flagField, const PdfField_T* pdfField, const Cell& cell,
+                                         real_t excessMass);
+
+   BlockDataID excessMassFieldID_;
+   field::FieldClone< ScalarField_T, true > excessMassFieldClone_;
+
+}; // class ExcessMassDistributionSweepInterfaceAndLiquid
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "ExcessMassDistributionSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionSweep.impl.h b/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..8a8db33b232c6ff79c5d5fde83a839455c70fcf4
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
@@ -0,0 +1,638 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ExcessMassDistributionSweep.impl.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Distribute excess mass, i.e., mass that is undistributed after conversions from interface to liquid or gas.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/logging/Logging.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+#include "field/GhostLayerField.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include "ExcessMassDistributionSweep.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceEvenly< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                 VectorField_T >::operator()(IBlock* const block)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(Base_T::flagFieldID_);
+   ScalarField_T* const fillField     = block->getData< ScalarField_T >(Base_T::fillFieldID_);
+   const lbm_generated::PdfField< StorageSpecification_T >* const pdfField =
+      block->getData< const lbm_generated::PdfField< StorageSpecification_T > >(Base_T::pdfFieldID_);
+
+   // disable OpenMP to avoid mass distribution to neighboring cells before they have distributed their excess mass
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(fillField, uint_c(1), omp critical, {
+      const Cell cell(x, y, z);
+
+      if (flagField->isFlagSet(cell, Base_T::flagInfo_.convertedFlag))
+      {
+         // identify cells that were converted to gas/liquid in this time step
+         const bool newGas = flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.gasFlag);
+         const bool newLiquid =
+            flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.liquidFlag);
+
+         if (newGas || newLiquid)
+         {
+            // a cell can not be converted to both gas and liquid
+            WALBERLA_ASSERT(!(newGas && newLiquid));
+
+            // calculate excess fill level
+            const real_t excessFill = newGas ? fillField->get(cell) : (fillField->get(cell) - real_c(1.0));
+
+            distributeMassEvenly(fillField, flagField, pdfField, cell, excessFill);
+
+            if (newGas) { fillField->get(cell) = real_c(0.0); }
+            else { fillField->get(cell) = real_c(1.0); }
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   getNumberOfLiquidAndInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& liquidNeighbors,
+                                          uint_t& interfaceNeighbors, uint_t& newInterfaceNeighbors)
+{
+   newInterfaceNeighbors = uint_c(0);
+   interfaceNeighbors    = uint_c(0);
+   liquidNeighbors       = uint_c(0);
+
+   for (auto d = StorageSpecification_T::Stencil::beginNoCenter(); d != StorageSpecification_T::Stencil::end(); ++d)
+   {
+      const Cell neighborCell = Cell(cell.x() + d.cx(), cell.y() + d.cy(), cell.z() + d.cz());
+      auto neighborFlags      = flagField->get(neighborCell);
+
+      if (isFlagSet(neighborFlags, flagInfo_.interfaceFlag))
+      {
+         ++interfaceNeighbors;
+         if (isFlagSet(neighborFlags, flagInfo_.convertedFlag)) { ++newInterfaceNeighbors; }
+      }
+      else
+      {
+         if (isFlagSet(neighborFlags, flagInfo_.liquidFlag)) { ++liquidNeighbors; }
+      }
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                      VectorField_T >::getNumberOfInterfaceNeighbors(const FlagField_T* flagField,
+                                                                                     const Cell& cell,
+                                                                                     uint_t& newInterfaceNeighbors,
+                                                                                     uint_t& interfaceNeighbors)
+{
+   interfaceNeighbors    = uint_c(0);
+   newInterfaceNeighbors = uint_c(0);
+
+   for (auto d = StorageSpecification_T::Stencil::beginNoCenter(); d != StorageSpecification_T::Stencil::end(); ++d)
+   {
+      const Cell neighborCell = Cell(cell.x() + d.cx(), cell.y() + d.cy(), cell.z() + d.cz());
+      auto neighborFlags      = flagField->get(neighborCell);
+
+      if (isFlagSet(neighborFlags, flagInfo_.interfaceFlag))
+      {
+         ++interfaceNeighbors;
+         if (isFlagSet(neighborFlags, flagInfo_.convertedFlag)) { ++newInterfaceNeighbors; }
+      }
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+template< typename PdfField_T >
+void ExcessMassDistributionSweepInterfaceEvenly< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                 VectorField_T >::distributeMassEvenly(ScalarField_T* fillField,
+                                                                                       const FlagField_T* flagField,
+                                                                                       const PdfField_T* pdfField,
+                                                                                       const Cell& cell,
+                                                                                       real_t excessFill)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   bool useEvenlyAll = Base_T::excessMassDistributionModel_.getModelType() ==
+                       ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterface;
+   bool useEvenlyNew = Base_T::excessMassDistributionModel_.getModelType() ==
+                       ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterface;
+   bool useEvenlyOld = Base_T::excessMassDistributionModel_.getModelType() ==
+                       ExcessMassDistributionModel::ExcessMassModel::EvenlyOldInterface;
+
+   // get number of interface neighbors
+   uint_t newInterfaceNeighbors = uint_c(0);
+   uint_t interfaceNeighbors    = uint_c(0);
+   Base_T::getNumberOfInterfaceNeighbors(flagField, cell, newInterfaceNeighbors, interfaceNeighbors);
+   const uint_t oldInterfaceNeighbors = interfaceNeighbors - newInterfaceNeighbors;
+
+   if (interfaceNeighbors == uint_c(0))
+   {
+      WALBERLA_LOG_WARNING(
+         "No interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained.");
+      return;
+   }
+
+   // get density of the current cell
+   // TODO Implement
+   const real_t density = real_c(1.0); //pdfField->getDensity(cell);
+
+   // compute mass to be distributed to neighboring cells
+   real_t deltaMass = real_c(0);
+   if ((useEvenlyOld && oldInterfaceNeighbors > uint_c(0)) || newInterfaceNeighbors == uint_c(0))
+   {
+      useEvenlyOld = true;
+      useEvenlyAll = false;
+      useEvenlyNew = false;
+
+      deltaMass = excessFill / real_c(oldInterfaceNeighbors) * density;
+   }
+   else
+   {
+      if (useEvenlyNew || oldInterfaceNeighbors == uint_c(0))
+      {
+         useEvenlyOld = false;
+         useEvenlyAll = false;
+         useEvenlyNew = true;
+
+         deltaMass = excessFill / real_c(newInterfaceNeighbors) * density;
+      }
+      else
+      {
+         useEvenlyOld = false;
+         useEvenlyAll = true;
+         useEvenlyNew = false;
+
+         deltaMass = excessFill / real_c(interfaceNeighbors) * density;
+      }
+   }
+
+   // distribute the excess mass
+   for (auto pushDir = StorageSpecification_T::Stencil::beginNoCenter(); pushDir != StorageSpecification_T::Stencil::end(); ++pushDir)
+   {
+      const Cell neighborCell = Cell(cell.x() + pushDir.cx(), cell.y() + pushDir.cy(), cell.z() + pushDir.cz());
+
+      // do not push mass in the direction of the second ghost layer
+      // - inner domain: 0 to *Size()-1
+      // - inner domain incl. first ghost layer: -1 to *Size()
+      if (neighborCell.x() < cell_idx_c(-1) || neighborCell.y() < cell_idx_c(-1) || neighborCell.z() < cell_idx_c(-1) ||
+          neighborCell.x() > cell_idx_c(fillField->xSize()) || neighborCell.y() > cell_idx_c(fillField->ySize()) ||
+          neighborCell.z() > cell_idx_c(fillField->zSize()))
+      {
+         continue;
+      }
+
+      // only push mass to neighboring interface cells
+      if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag))
+      {
+         // get density of neighboring interface cell
+         //TODO Implement
+         const real_t neighborDensity = real_c(1.0); //pdfField->getDensity(neighborCell);
+
+         if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && (useEvenlyAll || useEvenlyNew))
+         {
+            // push mass to newly converted interface cell
+            fillField->get(neighborCell) += deltaMass / neighborDensity;
+         }
+         else
+         {
+            if (!flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && (useEvenlyOld || useEvenlyAll))
+            {
+               // push mass to old, i.e., non-newly converted interface cells
+               fillField->getNeighbor(cell, *pushDir) += deltaMass / neighborDensity;
+            }
+         }
+      }
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                   VectorField_T >::operator()(IBlock* const block)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(Base_T::flagFieldID_);
+   ScalarField_T* const fillField     = block->getData< ScalarField_T >(Base_T::fillFieldID_);
+   const lbm_generated::PdfField< StorageSpecification_T >* const pdfField =
+      block->getData< const lbm_generated::PdfField< StorageSpecification_T > >(Base_T::pdfFieldID_);
+   const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_);
+
+   // disable OpenMP to avoid mass distribution to neighboring cells before they have distributed their excess mass
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(fillField, uint_c(1), omp critical, {
+      const Cell cell(x, y, z);
+
+      if (flagField->isFlagSet(cell, Base_T::flagInfo_.convertedFlag))
+      {
+         // identify cells that were converted to gas/liquid in this time step
+         const bool newGas = flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.gasFlag);
+         const bool newLiquid =
+            flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.liquidFlag);
+
+         if (newGas || newLiquid)
+         {
+            // a cell can not be converted to both gas and liquid
+            WALBERLA_ASSERT(!(newGas && newLiquid));
+
+            // calculate excess fill level
+            const real_t excessFill = newGas ? fillField->get(cell) : (fillField->get(cell) - real_c(1.0));
+
+            distributeMassWeighted(fillField, flagField, pdfField, normalField, cell, newLiquid, excessFill);
+
+            if (newGas) { fillField->get(cell) = real_c(0.0); }
+            else { fillField->get(cell) = real_c(1.0); }
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+template< typename PdfField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   distributeMassWeighted(ScalarField_T* fillField, const FlagField_T* flagField, const PdfField_T* pdfField,
+                          const VectorField_T* normalField, const Cell& cell, bool isNewLiquid, real_t excessFill)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   bool useWeightedAll = Base_T::excessMassDistributionModel_.getModelType() ==
+                         ExcessMassDistributionModel::ExcessMassModel::WeightedAllInterface;
+   bool useWeightedNew = Base_T::excessMassDistributionModel_.getModelType() ==
+                         ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface;
+   bool useWeightedOld = Base_T::excessMassDistributionModel_.getModelType() ==
+                         ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface;
+
+   // get number of interface neighbors
+   uint_t newInterfaceNeighbors = uint_c(0);
+   uint_t interfaceNeighbors    = uint_c(0);
+   Base_T::getNumberOfInterfaceNeighbors(flagField, cell, newInterfaceNeighbors, interfaceNeighbors);
+   const uint_t oldInterfaceNeighbors = interfaceNeighbors - newInterfaceNeighbors;
+
+   if (interfaceNeighbors == uint_c(0))
+   {
+      WALBERLA_LOG_WARNING(
+         "No interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained.");
+      return;
+   }
+
+   // check applicability of the chosen model
+   if ((useWeightedOld && oldInterfaceNeighbors > uint_c(0)) || newInterfaceNeighbors == uint_c(0))
+   {
+      useWeightedOld = true;
+      useWeightedAll = false;
+      useWeightedNew = false;
+   }
+   else
+   {
+      if (useWeightedNew || oldInterfaceNeighbors == uint_c(0))
+      {
+         useWeightedOld = false;
+         useWeightedAll = false;
+         useWeightedNew = true;
+      }
+      else
+      {
+         useWeightedOld = false;
+         useWeightedAll = true;
+         useWeightedNew = false;
+      }
+   }
+
+   // get normal-direction-based weights of the excess mass
+   std::vector< real_t > weights(StorageSpecification_T::Stencil::Size, real_c(0));
+   getExcessMassWeights(flagField, normalField, cell, isNewLiquid, useWeightedOld, useWeightedAll, useWeightedNew,
+                        weights);
+
+   // get the sum of all weights
+   real_t weightSum = real_c(0);
+   for (const auto& w : weights)
+   {
+      weightSum += w;
+   }
+
+   // if there are either no old or no newly converted interface cells in normal direction, distribute mass to whatever
+   // interface cell is available in normal direction
+   if (realIsEqual(weightSum, real_c(0), real_c(1e-14)) && (useWeightedOld || useWeightedNew))
+   {
+      useWeightedOld = false;
+      useWeightedAll = true;
+      useWeightedNew = false;
+
+      // recompute mass weights since other type of interface cells are now considered also
+      getExcessMassWeights(flagField, normalField, cell, isNewLiquid, useWeightedOld, useWeightedAll, useWeightedNew,
+                           weights);
+
+      // update sum of all weights
+      for (const auto& w : weights)
+      {
+         weightSum += w;
+      }
+
+      // if no interface cell is available in normal direction, distribute mass evenly to all neighboring interface
+      // cells
+      if (realIsEqual(weightSum, real_c(0), real_c(1e-14)))
+      {
+         WALBERLA_LOG_WARNING_ON_ROOT(
+            "Excess mass can not be distributed with a weighted approach since no interface cell is available in "
+            "normal direction. Distributing excess mass evenly among all surrounding interface cells.");
+
+         // manually set weights to 1 to get equal weight in any direction
+         for (auto& w : weights)
+         {
+            w = real_c(1);
+         }
+
+         // weight sum is now the number of neighboring interface cells
+         weightSum = real_c(interfaceNeighbors);
+      }
+   }
+
+   WALBERLA_ASSERT_GREATER(
+      weightSum, real_c(0),
+      "Sum of all weights is zero in ExcessMassDistribution. This means that no neighboring interface cell is "
+      "available for distributing the excess mass to. This error should have been caught earlier.");
+
+   // TODO Implement
+   const real_t excessMass = excessFill * real_c(1.0); //pdfField->getDensity(cell);
+
+   // distribute the excess mass
+   for (auto pushDir = StorageSpecification_T::Stencil::beginNoCenter(); pushDir != StorageSpecification_T::Stencil::end(); ++pushDir)
+   {
+      const Cell neighborCell = Cell(cell.x() + pushDir.cx(), cell.y() + pushDir.cy(), cell.z() + pushDir.cz());
+
+      // do not push mass in the direction of the second ghost layer
+      // - inner domain: 0 to *Size()-1
+      // - inner domain incl. first ghost layer: -1 to *Size()
+      if (neighborCell.x() < cell_idx_c(-1) || neighborCell.y() < cell_idx_c(-1) || neighborCell.z() < cell_idx_c(-1) ||
+          neighborCell.x() > cell_idx_c(fillField->xSize()) || neighborCell.y() > cell_idx_c(fillField->ySize()) ||
+          neighborCell.z() > cell_idx_c(fillField->zSize()))
+      {
+         continue;
+      }
+
+      // only push mass to neighboring interface cells
+      if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag))
+      {
+         // get density of neighboring interface cell
+         // TODO Implement
+         const real_t neighborDensity = real_c(1.0); // pdfField->getDensity(neighborCell);
+
+         if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && (useWeightedAll || useWeightedNew))
+         {
+            // push mass to newly converted interface cell
+            const real_t deltaMass = excessMass * weights[pushDir.toIdx()] / weightSum;
+            fillField->get(neighborCell) += deltaMass / neighborDensity;
+         }
+         else
+         {
+            if (!flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) &&
+                (useWeightedOld || useWeightedAll))
+            {
+               // push mass to old, i.e., non-newly converted interface cells
+               const real_t deltaMass = excessMass * weights[pushDir.toIdx()] / weightSum;
+               fillField->getNeighbor(cell, *pushDir) += deltaMass / neighborDensity;
+            }
+         }
+      }
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   getExcessMassWeights(const FlagField_T* flagField, const VectorField_T* normalField, const Cell& cell,
+                        bool isNewLiquid, bool useWeightedOld, bool useWeightedAll, bool useWeightedNew,
+                        std::vector< real_t >& weights)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   // iterate all neighboring cells
+   for (auto d = StorageSpecification_T::Stencil::beginNoCenter(); d != StorageSpecification_T::Stencil::end(); ++d)
+   {
+      const Cell neighborCell = Cell(cell.x() + d.cx(), cell.y() + d.cy(), cell.z() + d.cz());
+      auto neighborFlags      = flagField->get(neighborCell);
+
+      if (isFlagSet(neighborFlags, Base_T::flagInfo_.interfaceFlag))
+      {
+         // compute dot product of normal direction and lattice direction to neighboring cell
+         const real_t n_dot_ci =
+            Vector3(normalField->get(cell, 0), normalField->get(cell, 1), normalField->get(cell, 2)) * Vector3< real_t >(real_c(d.cx()), real_c(d.cy()), real_c(d.cz()));
+
+         if (useWeightedAll || (useWeightedOld && !isFlagSet(neighborFlags, Base_T::flagInfo_.convertedFlag)))
+         {
+            computeWeightWithNormal(n_dot_ci, isNewLiquid, d, weights);
+         }
+         else
+         {
+            if (useWeightedNew && isFlagSet(neighborFlags, Base_T::flagInfo_.convertedFlag))
+            {
+               computeWeightWithNormal(n_dot_ci, isNewLiquid, d, weights);
+            }
+            else { weights[d.toIdx()] = real_c(0); }
+         }
+      }
+      else
+      {
+         // no interface cell in this direction, weight is zero
+         weights[d.toIdx()] = real_c(0);
+      }
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   computeWeightWithNormal(real_t n_dot_ci, bool isNewLiquid, typename StorageSpecification_T::Stencil::iterator dir,
+                           std::vector< real_t >& weights)
+{
+   // dissertation of N. Thuerey, 2007, equation (4.9)
+   if (isNewLiquid)
+   {
+      if (n_dot_ci > real_c(0)) { weights[dir.toIdx()] = n_dot_ci; }
+      else { weights[dir.toIdx()] = real_c(0); }
+   }
+   else // cell was converted from interface to gas
+   {
+      if (n_dot_ci < real_c(0)) { weights[dir.toIdx()] = -n_dot_ci; }
+      else { weights[dir.toIdx()] = real_c(0); }
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceAndLiquid< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                    VectorField_T >::operator()(IBlock* const block)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(Base_T::flagFieldID_);
+   ScalarField_T* const fillField     = block->getData< ScalarField_T >(Base_T::fillFieldID_);
+   const lbm_generated::PdfField< StorageSpecification_T >* const pdfField =
+      block->getData< const lbm_generated::PdfField< StorageSpecification_T > >(Base_T::pdfFieldID_);
+
+   ScalarField_T* const srcExcessMassField = block->getData< ScalarField_T >(excessMassFieldID_);
+   ScalarField_T* const dstExcessMassField = excessMassFieldClone_.get(block);
+
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(dstExcessMassField, uint_c(1), {
+      dstExcessMassField->get(x, y, z) = real_c(0);
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+
+   // disable OpenMP to avoid mass distribution to neighboring cells before they have distributed their excess mass
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(fillField, uint_c(1), omp critical, {
+      const Cell cell(x, y, z);
+
+      if (flagField->isFlagSet(cell, Base_T::flagInfo_.convertedFlag))
+      {
+         // identify cells that were converted to gas/liquid in this time step
+         const bool newGas = flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.gasFlag);
+         const bool newLiquid =
+            flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.liquidFlag);
+
+         if (newGas || newLiquid)
+         {
+            // a cell can not be converted to both gas and liquid
+            WALBERLA_ASSERT(!(newGas && newLiquid));
+
+            // calculate excess fill level
+            const real_t excessFill = newGas ? fillField->get(cell) : (fillField->get(cell) - real_c(1.0));
+
+            // store excess mass such that it can be distributed below (no += here because cell was an interface cell
+            // that can not have an excess mass stored in the field; any excess mass is added to the interface cell's
+            // fill level)
+            // TODO Implement
+            srcExcessMassField->get(cell) = excessFill * real_c(1.0); //pdfField->getDensity(cell);
+
+            if (newGas) { fillField->get(cell) = real_c(0.0); }
+            else { fillField->get(cell) = real_c(1.0); }
+         }
+      }
+
+      if (!realIsEqual(srcExcessMassField->get(cell), real_c(0), real_c(1e-14)))
+      {
+         distributeMassInterfaceAndLiquid(fillField, dstExcessMassField, flagField, pdfField, cell,
+                                          srcExcessMassField->get(cell));
+      }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+
+   srcExcessMassField->swapDataPointers(dstExcessMassField);
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+template< typename PdfField_T >
+void ExcessMassDistributionSweepInterfaceAndLiquid< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   distributeMassInterfaceAndLiquid(ScalarField_T* fillField, ScalarField_T* dstExcessMassField,
+                                    const FlagField_T* flagField, const PdfField_T* pdfField, const Cell& cell,
+                                    real_t excessMass)
+{
+   using Base_T = ExcessMassDistributionSweepBase_T;
+
+   // get number of liquid and interface neighbors
+   uint_t liquidNeighbors       = uint_c(0);
+   uint_t interfaceNeighbors    = uint_c(0);
+   uint_t newInterfaceNeighbors = uint_c(0);
+   Base_T::getNumberOfLiquidAndInterfaceNeighbors(flagField, cell, liquidNeighbors, interfaceNeighbors,
+                                                  newInterfaceNeighbors);
+   const uint_t liquidAndInterfaceNeighbors = liquidNeighbors + interfaceNeighbors;
+
+   if (liquidAndInterfaceNeighbors == uint_c(0))
+   {
+      WALBERLA_LOG_WARNING(
+         "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained.");
+      return;
+   }
+
+   // check if there are neighboring new interface cells
+   const bool preferNewInterface = Base_T::excessMassDistributionModel_.getModelType() ==
+                                      ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterfaceFallbackLiquid &&
+                                   newInterfaceNeighbors > uint_c(0);
+
+   // check if there are neighboring interface cells
+   const bool preferInterface = (Base_T::excessMassDistributionModel_.getModelType() ==
+                                    ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceFallbackLiquid ||
+                                 !preferNewInterface) &&
+                                interfaceNeighbors > uint_c(0) &&
+                                Base_T::excessMassDistributionModel_.getModelType() !=
+                                   ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceAndLiquid;
+
+   // compute mass to be distributed to neighboring cells
+   real_t deltaMass;
+   if (preferNewInterface) { deltaMass = excessMass / real_c(newInterfaceNeighbors); }
+   else
+   {
+      if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); }
+      else { deltaMass = excessMass / real_c(liquidAndInterfaceNeighbors); }
+   }
+
+   // distribute the excess mass
+   for (auto pushDir = StorageSpecification_T::Stencil::beginNoCenter(); pushDir != StorageSpecification_T::Stencil::end(); ++pushDir)
+   {
+      const Cell neighborCell = Cell(cell.x() + pushDir.cx(), cell.y() + pushDir.cy(), cell.z() + pushDir.cz());
+
+      // do not push mass to cells in the ghost layer (done by the process from which the ghost layer is synchronized)
+      // - inner domain: 0 to *Size()-1
+      // - inner domain incl. first ghost layer: -1 to *Size()
+      if (neighborCell.x() <= cell_idx_c(-1) || neighborCell.y() <= cell_idx_c(-1) ||
+          neighborCell.z() <= cell_idx_c(-1) || neighborCell.x() >= cell_idx_c(fillField->xSize()) ||
+          neighborCell.y() >= cell_idx_c(fillField->ySize()) || neighborCell.z() >= cell_idx_c(fillField->zSize()))
+      {
+         continue;
+      }
+
+      // distribute excess mass to newly converted interface cell
+      if (flagField->isMaskSet(neighborCell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.interfaceFlag))
+      {
+         // get density of neighboring interface cell
+         // TODO Implement
+         const real_t neighborDensity = real_c(1.0); //pdfField->getDensity(neighborCell);
+
+         // add excess mass directly to fill level for newly converted neighboring interface cells
+         fillField->get(neighborCell) += deltaMass / neighborDensity;
+      }
+      else
+      {
+         // distribute excess mass to old interface cell
+         if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag) && !preferNewInterface)
+         {
+            // get density of neighboring interface cell
+            // TODO Implement
+            const real_t neighborDensity = real_c(1.0); //pdfField->getDensity(neighborCell);
+
+            // add excess mass directly to fill level for neighboring interface cells
+            fillField->get(neighborCell) += deltaMass / neighborDensity;
+         }
+         else
+         {
+            // distribute excess mass to liquid cell
+            if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface &&
+                !preferNewInterface)
+            {
+               // add excess mass to excessMassField for neighboring liquid cells
+               dstExcessMassField->get(neighborCell) += deltaMass;
+            }
+         }
+      }
+   }
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/ForceDensitySweep.h b/src/lbm_generated/free_surface/dynamics/ForceDensitySweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..b30d6aa3c98dfff7dd569f5005e3a4505aa246de
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/ForceDensitySweep.h
@@ -0,0 +1,107 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ForceDensitySweep.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Weight force in interface cells with fill level and density (equation 15 in Koerner et al., 2005).
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/logging/Logging.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+#include "field/GhostLayerField.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Update the force density field as in equation 15 in Koerner et al., 2005. with "acceleration * density * fill level".
+ * Differs from the version above by using a flattened vector field (GhostLayerField< real_t, 3 >). This is necessary
+ * because Pystencils does not support VectorField_T (GhostLayerField< Vector3<real_t>, 1 >).
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename VectorField_T, typename ScalarField_T >
+class ForceDensityCodegenSweep
+{
+ public:
+   ForceDensityCodegenSweep(BlockDataID forceDensityFieldID, ConstBlockDataID pdfFieldID, ConstBlockDataID flagFieldID,
+                            ConstBlockDataID fillFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                            const Vector3< real_t >& globalAcceleration)
+      : forceDensityFieldID_(forceDensityFieldID), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID),
+        fillFieldID_(fillFieldID), flagInfo_(flagInfo), globalAcceleration_(globalAcceleration)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      using PdfField_T = lbm_generated::PdfField< StorageSpecification_T >;
+
+      VectorField_T* const forceDensityField = block->getData< VectorField_T >(forceDensityFieldID_);
+      const PdfField_T* const pdfField                = block->getData< const PdfField_T >(pdfFieldID_);
+      const FlagField_T* const flagField              = block->getData< const FlagField_T >(flagFieldID_);
+      const ScalarField_T* const fillField            = block->getData< const ScalarField_T >(fillFieldID_);
+
+      WALBERLA_FOR_ALL_CELLS(forceDensityFieldIt, forceDensityField, pdfFieldIt, pdfField, flagFieldIt, flagField,
+                             fillFieldIt, fillField, {flag_t flag = *flagFieldIt;
+
+                                // set force density in cells to acceleration * density * fillLevel (see equation 15
+                                // in Koerner et al., 2005);
+                                if (flagInfo_.isInterface(flag))
+                                {
+                                   // TODO do this more flexible
+                                   real_t density = real_c(0.0);
+                                   for (uint_t i = 0; i < 19; ++i)
+                                      density += pdfFieldIt.getF(i);
+
+                                   forceDensityFieldIt[0] = globalAcceleration_[0] * *fillFieldIt * density;
+                                   forceDensityFieldIt[1] = globalAcceleration_[1] * *fillFieldIt * density;
+                                   forceDensityFieldIt[2] = globalAcceleration_[2] * *fillFieldIt * density;
+                                }
+                                else
+                                {
+                                   if (flagInfo_.isLiquid(flag))
+                                   {
+                                      real_t density = real_c(0.0);
+                                      for (uint_t i = 0; i < 19; ++i)
+                                         density += pdfFieldIt.getF(i);
+                                      forceDensityFieldIt[0] = globalAcceleration_[0] * density;
+                                      forceDensityFieldIt[1] = globalAcceleration_[1] * density;
+                                      forceDensityFieldIt[2] = globalAcceleration_[2] * density;
+                                   }
+                                }
+                             }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+ private:
+   using flag_t = typename FlagField_T::flag_t;
+
+   BlockDataID forceDensityFieldID_;
+   ConstBlockDataID pdfFieldID_;
+   ConstBlockDataID flagFieldID_;
+   ConstBlockDataID fillFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+   Vector3< real_t > globalAcceleration_;
+}; // class ForceDensitySweep
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/PdfReconstructionModel.h b/src/lbm_generated/free_surface/dynamics/PdfReconstructionModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..b782c101e4422b6958a73a4ba2c56560dc6d0f59
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/PdfReconstructionModel.h
@@ -0,0 +1,173 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file PdfReconstructionModel.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Class that specifies the number of reconstructed PDFs at the free surface interface.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/StringUtility.h"
+#include "core/stringToNum.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Class that specifies the number of reconstructed PDFs at the free surface interface. PDFs need to be reconstructed
+ * as they might be missing, i.e., PDFs streaming from gas to interface are not available inherently.
+ *
+ * Available models:
+ * - NormalBasedKeepCenter: reconstruct all PDFs for which n * c_i >= 0 (approach by Koerner et al., 2005); some
+ *                           already available PDFs will be overwritten
+ *
+ * - NormalBasedReconstructCenter: reconstruct all PDFs for which n * c_i >= 0 (including the center PDF); some already
+ *                                 available PDFs coming from liquid will be overwritten
+ *
+ * - OnlyMissing: reconstruct only missing PDFs (no already available PDF gets overwritten)
+ *
+ * - All: reconstruct all PDFs (any already available PDF is overwritten)
+ *
+ * - OnlyMissingMin-N-largest: Reconstruct only missing PDFs but at least N (and therefore potentially overwrite
+ *                             available PDFs); "smallest" or "largest" specifies whether PDFs with smallest or largest
+ *                             n * c_i get overwritten first. This model is motivated by the dissertation of Simon
+ *                             Bogner, 2017, section 4.2.1, where it is argued that at least 3 PDFs must be
+ *                             reconstructed, as otherwise the free surface boundary condition is claimed to be
+ *                             underdetermined. However, a mathematical proof for this statement is not given.
+ *
+ * - OnlyMissingMin-N-smallest: see comment at "OnlyMissingMin-N-largest"
+ *
+ * - OnlyMissingMin-N-normalBasedKeepCenter: see comment at "OnlyMissingMin-N-largest"; if less than N PDFs are unknown,
+ *                                           reconstruct according to the model "NormalBasedKeepCenter"
+ * ********************************************************************************************************************/
+class PdfReconstructionModel
+{
+ public:
+   enum class ReconstructionModel {
+      NormalBasedReconstructCenter,
+      NormalBasedKeepCenter,
+      OnlyMissing,
+      All,
+      OnlyMissingMin,
+   };
+
+   enum class FallbackModel {
+      Largest,
+      Smallest,
+      NormalBasedKeepCenter,
+   };
+
+   PdfReconstructionModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName))
+   {
+      if (modelType_ == ReconstructionModel::OnlyMissingMin)
+      {
+         const std::vector< std::string > substrings = string_split(modelName, "-");
+
+         modelName_         = substrings[0];                        // "OnlyMissingMin"
+         numMinReconstruct_ = stringToNum< uint_t >(substrings[1]); // N
+         fallbackModelName_ = substrings[2]; // "smallest" or "largest" or "normalBasedKeepCenter"
+         fallbackModel_     = chooseFallbackModel(fallbackModelName_);
+
+         if (fallbackModel_ != FallbackModel::Largest && fallbackModel_ != FallbackModel::Smallest &&
+             fallbackModel_ != FallbackModel::NormalBasedKeepCenter)
+         {
+            WALBERLA_ABORT("The specified PDF reconstruction fallback-model " << modelName << " is not available.");
+         }
+      }
+   }
+
+   inline ReconstructionModel getModelType() const { return modelType_; }
+   inline std::string getModelName() const { return modelName_; }
+   inline uint_t getNumMinReconstruct() const { return numMinReconstruct_; }
+   inline FallbackModel getFallbackModel() const { return fallbackModel_; }
+   inline std::string getFallbackModelName() const { return fallbackModelName_; }
+   inline std::string getFullModelSpecification() const
+   {
+      if (modelType_ == ReconstructionModel::OnlyMissingMin)
+      {
+         return modelName_ + "-" + std::to_string(numMinReconstruct_) + "-" + fallbackModelName_;
+      }
+      else { return modelName_; }
+   }
+
+ private:
+   ReconstructionModel chooseType(const std::string& modelName)
+   {
+      if (!string_icompare(modelName, "NormalBasedReconstructCenter"))
+      {
+         return ReconstructionModel::NormalBasedReconstructCenter;
+      }
+
+      else
+      {
+         if (!string_icompare(modelName, "NormalBasedKeepCenter"))
+         {
+            return ReconstructionModel::NormalBasedKeepCenter;
+         }
+         else
+         {
+            if (!string_icompare(modelName, "OnlyMissing")) { return ReconstructionModel::OnlyMissing; }
+            else
+            {
+               if (!string_icompare(modelName, "All")) { return ReconstructionModel::All; }
+               else
+               {
+                  if (!string_icompare(string_split(modelName, "-")[0], "OnlyMissingMin"))
+                  {
+                     return ReconstructionModel::OnlyMissingMin;
+                  }
+                  else
+                  {
+                     WALBERLA_ABORT("The specified PDF reconstruction model " << modelName << " is not available.");
+                  }
+               }
+            }
+         }
+      }
+   }
+
+   FallbackModel chooseFallbackModel(const std::string& fallbackModelName)
+   {
+      if (!string_icompare(fallbackModelName, "largest")) { return FallbackModel::Largest; }
+      else
+      {
+         if (!string_icompare(fallbackModelName, "smallest")) { return FallbackModel::Smallest; }
+         else
+         {
+            if (!string_icompare(fallbackModelName, "normalBasedKeepCenter"))
+            {
+               return FallbackModel::NormalBasedKeepCenter;
+            }
+            else
+            {
+               WALBERLA_ABORT("The specified PDF reconstruction fallback-model " << fallbackModelName
+                                                                                 << " is not available.");
+            }
+         }
+      }
+   }
+
+   std::string modelName_;
+   ReconstructionModel modelType_;
+   uint_t numMinReconstruct_;
+   std::string fallbackModelName_;
+   FallbackModel fallbackModel_;
+}; // class PdfReconstructionModel
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/PdfRefillingModel.h b/src/lbm_generated/free_surface/dynamics/PdfRefillingModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..0215d4b33c92977a249fca76f31fd73d26fbe27b
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/PdfRefillingModel.h
@@ -0,0 +1,147 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file PdfRefillingModel.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \author Michael Zikeli
+//! \brief Defines how cells are refilled (i.e. PDFs reinitialized) after the cell was converted from gas to interface.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/StringUtility.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ *   Class that specifies how PDFs are reinitialized in cells that are converted from gas to interface.
+ *
+ *   Available options are:
+ *       - EquilibriumRefilling:
+ *              initialize PDFs with equilibrium using average density and velocity from neighboring cells; default
+ *              approach used in any known publication with free surface LBM
+ *
+ *       - AverageRefilling:
+ *              initialize PDFs with average PDFs (in the respective directions) of neighboring cells
+ *
+ *       - EquilibriumAndNonEquilibriumRefilling:
+ *              initialize PDFs with EquilibriumRefilling and add the non-equilibrium contribution of neighboring cells
+ *
+ *       - ExtrapolationRefilling:
+ *              initialize PDFs with PDFs extrapolated (in surface normal direction) from neighboring cells
+ *
+ *       - GradsMomentsRefilling:
+ *              initialize PDFs with EquilibriumRefilling and add the contribution of the non-equilibrium pressure
+ *              tensor
+ *
+ * See src/lbm/free_surface/dynamics/PdfRefillingSweep.h for a detailed description of the models.
+ *
+ * The models and their implementation are inspired by the equivalent functionality of the lbm-particle coupling, see
+ * src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h.
+ **********************************************************************************************************************/
+class PdfRefillingModel
+{
+ public:
+   enum class RefillingModel {
+      EquilibriumRefilling,
+      AverageRefilling,
+      EquilibriumAndNonEquilibriumRefilling,
+      ExtrapolationRefilling,
+      GradsMomentsRefilling
+   };
+
+   PdfRefillingModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName)) {}
+
+   PdfRefillingModel(const RefillingModel& modelType) : modelName_(chooseName(modelType)), modelType_(modelType)
+   {
+      switch (modelType_)
+      {
+      case RefillingModel::EquilibriumRefilling:
+         break;
+      case RefillingModel::AverageRefilling:
+         break;
+      case RefillingModel::EquilibriumAndNonEquilibriumRefilling:
+         break;
+      case RefillingModel::ExtrapolationRefilling:
+         break;
+      case RefillingModel::GradsMomentsRefilling:
+         break;
+      }
+   }
+
+   inline RefillingModel getModelType() const { return modelType_; }
+   inline std::string getModelName() const { return modelName_; }
+   inline std::string getFullModelSpecification() const { return getModelName(); }
+
+   static inline std::initializer_list< const RefillingModel > getTypeIterator() { return listOfAllEnums; }
+
+ private:
+   RefillingModel chooseType(const std::string& modelName)
+   {
+      if (!string_icompare(modelName, "EquilibriumRefilling")) { return RefillingModel::EquilibriumRefilling; }
+
+      if (!string_icompare(modelName, "AverageRefilling")) { return RefillingModel::AverageRefilling; }
+
+      if (!string_icompare(modelName, "EquilibriumAndNonEquilibriumRefilling"))
+      {
+         return RefillingModel::EquilibriumAndNonEquilibriumRefilling;
+      }
+
+      if (!string_icompare(modelName, "ExtrapolationRefilling")) { return RefillingModel::ExtrapolationRefilling; }
+
+      if (!string_icompare(modelName, "GradsMomentsRefilling")) { return RefillingModel::GradsMomentsRefilling; }
+
+      WALBERLA_ABORT("The specified PDF reinitialization model " << modelName << " is not available.");
+   }
+
+   std::string chooseName(RefillingModel const& modelType) const
+   {
+      std::string modelName;
+      switch (modelType)
+      {
+      case RefillingModel::EquilibriumRefilling:
+         modelName = "EquilibriumRefilling";
+         break;
+      case RefillingModel::AverageRefilling:
+         modelName = "AverageRefilling";
+         break;
+      case RefillingModel::EquilibriumAndNonEquilibriumRefilling:
+         modelName = "EquilibriumAndNonEquilibriumRefilling";
+         break;
+      case RefillingModel::ExtrapolationRefilling:
+         modelName = "ExtrapolationRefilling";
+         break;
+      case RefillingModel::GradsMomentsRefilling:
+         modelName = "GradsMomentsRefilling";
+         break;
+      }
+      return modelName;
+   }
+
+   std::string modelName_;
+   RefillingModel modelType_;
+   static constexpr std::initializer_list< const RefillingModel > listOfAllEnums = {
+      RefillingModel::EquilibriumRefilling, RefillingModel::AverageRefilling,
+      RefillingModel::EquilibriumAndNonEquilibriumRefilling, RefillingModel::ExtrapolationRefilling,
+      RefillingModel::GradsMomentsRefilling
+   };
+
+}; // class PdfRefillingModel
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/PdfRefillingSweep.h b/src/lbm_generated/free_surface/dynamics/PdfRefillingSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..dcbe431e144713c185d201cd11d3243c189c30bd
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/PdfRefillingSweep.h
@@ -0,0 +1,447 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file PdfRefillingSweep.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \author Michael Zikeli
+//! \brief Sweeps for refilling cells (i.e. reinitializing PDFs) after the cell was converted from gas to interface.
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/StringUtility.h"
+#include "core/cell/Cell.h"
+#include "core/debug/CheckFunctions.h"
+#include "core/logging/Logging.h"
+#include "core/math/Vector3.h"
+
+#include "domain_decomposition/IBlock.h"
+
+#include "field/GhostLayerField.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+#include "lbm_generated/free_surface/dynamics/PdfRefillingModel.h"
+#include "lbm_generated/free_surface/surface_geometry/NormalSweep.h"
+
+#include "stencil/D3Q6.h"
+
+#include <functional>
+#include <vector>
+
+#include "PdfRefillingModel.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Base class for all sweeps to reinitialize (refill) all PDFs in cells that were converted from gas to interface.
+ * This is required since gas cells do not have PDFs. The sweep expects that a previous sweep has set the
+ * "convertedFromGasToInterface" flag and reinitializes the PDFs in all cells with this flag according to the specified
+ * model.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T >
+class RefillingSweepBase
+{
+ public:
+   using PdfField_T = lbm_generated::PdfField< StorageSpecification_T >;
+   using flag_t     = typename FlagField_T::flag_t;
+   using Stencil_T  = typename StorageSpecification_T::Stencil;
+
+   RefillingSweepBase(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                      const FlagInfo< FlagField_T >& flagInfo, bool useDataFromGhostLayers)
+      : pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), flagInfo_(flagInfo),
+        useDataFromGhostLayers_(useDataFromGhostLayers)
+   {}
+
+   virtual void operator()(IBlock* const block) = 0;
+
+   virtual ~RefillingSweepBase() = default;
+
+   real_t getAverageDensityAndVelocity(const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField,
+                                       const FlagInfo< FlagField_T >& flagInfo, Vector3< real_t >& avgVelocity)
+   {
+      std::vector< bool > validStencilIndices(Stencil_T::Size, false);
+      return getAverageDensityAndVelocity(cell, pdfField, flagField, flagInfo, avgVelocity, validStencilIndices);
+   }
+
+   // also stores stencil indices of valid neighboring cells (liquid/ non-newly converted interface) in a vector
+   real_t getAverageDensityAndVelocity(const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField,
+                                       const FlagInfo< FlagField_T >& flagInfo, Vector3< real_t >& avgVelocity,
+                                       std::vector< bool >& validStencilIndices);
+
+   // returns the averaged PDFs of valid neighboring cells (liquid/ non-newly converted interface)
+   std::vector< real_t > getAveragePdfs(const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField,
+                                        const FlagInfo< FlagField_T >& flagInfo);
+
+ protected:
+   BlockDataID pdfFieldID_;
+   ConstBlockDataID flagFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+   bool useDataFromGhostLayers_;
+}; // class RefillingSweepBase
+
+/***********************************************************************************************************************
+ * Base class for refilling models that need to obtain information by extrapolation from neighboring cells.
+ *
+ * The parameter "useDataFromGhostLayers" is useful, when reducedCommunication is used, i.e., when not all PDFs are
+ * communicated in the PDF field but only those that are actually required in a certain direction. This is currently not
+ * used in the free surface part of waLBerla: In SurfaceDynamicsHandler, SimpleCommunication uses the default PackInfo
+ * of the GhostLayerField for PDF communication. The optimized version would be using the PdfFieldPackInfo (in
+ * src/lbm/communication).
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExtrapolationRefillingSweepBase : public RefillingSweepBase< StorageSpecification_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< StorageSpecification_T, FlagField_T >;
+   using PdfField_T           = typename RefillingSweepBase_T::PdfField_T;
+   using flag_t               = typename RefillingSweepBase_T::flag_t;
+   using Stencil_T            = typename RefillingSweepBase_T::Stencil_T;
+
+   ExtrapolationRefillingSweepBase(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                                   const ConstBlockDataID& fillFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                                   uint_t extrapolationOrder, bool useDataFromGhostLayers)
+      : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers), fillFieldID_(fillFieldID),
+        extrapolationOrder_(extrapolationOrder)
+   {}
+
+   virtual ~ExtrapolationRefillingSweepBase() = default;
+
+   virtual void operator()(IBlock* const block) = 0;
+
+   /********************************************************************************************************************
+    * Find the lattice direction in the given stencil that corresponds best to the provided direction.
+    *
+    * Mostly copied from src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h.
+    *******************************************************************************************************************/
+   Vector3< cell_idx_t > findCorrespondingLatticeDirection(const Vector3< real_t >& direction);
+
+   /********************************************************************************************************************
+    * The extrapolation direction is chosen such that it most closely resembles the surface normal in the cell.
+    *
+    * The normal has to be recomputed here and MUST NOT be taken directly from the normal field because the fill levels
+    * will have changed since the last computation of the normal. As the normal is computed from the fill levels, it
+    * must be recomputed to have an up-to-date normal.
+    *******************************************************************************************************************/
+   Vector3< cell_idx_t > findExtrapolationDirection(const Cell& cell, const FlagField_T& flagField,
+                                                    const ScalarField_T& fillField);
+
+   /********************************************************************************************************************
+    * Determine the number of applicable (liquid or interface) cells for extrapolation in the given extrapolation
+    * direction.
+    *******************************************************************************************************************/
+   uint_t getNumberOfExtrapolationCells(const Cell& cell, const FlagField_T& flagField, const PdfField_T& pdfField,
+                                        const Vector3< cell_idx_t >& extrapolationDirection);
+
+   /********************************************************************************************************************
+    * Get the non-equilibrium part of all PDFs in "cell" and store them in a std::vector<real_t>.
+    *******************************************************************************************************************/
+   std::vector< real_t > getNonEquilibriumPdfsInCell(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField);
+
+   /********************************************************************************************************************
+    * Get all PDFs in "cell" and store them in a std::vector<real_t>.
+    *******************************************************************************************************************/
+   std::vector< real_t > getPdfsInCell(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField);
+
+   /********************************************************************************************************************
+    * Set the PDFs in cell "x" according to the following linear combination:
+    *   f(x,q) = f(x,q) + 3 * f^{get}(x+e,q) - 3 * f^{get}(x+2e,q) + 1 * f^{get}(x+3e,q)
+    *       with x: cell position
+    *            q: index of the respective PDF
+    *            e: extrapolation direction
+    *            f^{get}: PDF specified by getPdfFunc
+    *       "includeThisCell" defines whether f(x,q) is included
+    *
+    * Note: this function does NOT assert out of bounds access, i.e., it may only be called for cells that have at least
+    *       three neighboring cells in extrapolationDirection.
+    *******************************************************************************************************************/
+   void applyQuadraticExtrapolation(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField) >&
+         getPdfFunc);
+
+   /********************************************************************************************************************
+    * Set the PDFs in cell (x) according to the following linear combination:
+    *   f(x,q) = f(x,q) + 2 * f^{get}(x+1e,q) - 1 * f^{get}(x+2e,q)
+    *       with x: cell position
+    *            q: index of the respective PDF
+    *            e: extrapolation direction
+    *            f^{get}: PDF specified by getPdfFunc
+    *       "includeThisCell" defines whether f(x,q) is included
+    *
+    * Note: this function does NOT assert out of bounds access, i.e., it may only be called for cells that have at least
+    *       two neighboring cells in extrapolationDirection.
+    *******************************************************************************************************************/
+   void applyLinearExtrapolation(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField) >&
+         getPdfFunc);
+
+   /********************************************************************************************************************
+    * Set the PDFs in cell (x) according to the following linear combination:
+    *   f(x,q) = f(x,q) + 1 * f^{get}(x+1e,q)
+    *       with x: cell position
+    *            q: index of the respective PDF
+    *            e: extrapolation direction
+    *            f^{get}: PDF specified by getPdfFunc
+    *       "includeThisCell" defines whether f(x,q) is included
+    *
+    * Note: this function does NOT assert out of bounds access, i.e., it may only be called for cells that have at least
+    *       a neighboring cell in extrapolationDirection.
+    *******************************************************************************************************************/
+   void applyConstantExtrapolation(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField) >&
+         getPdfFunc);
+
+ protected:
+   ConstBlockDataID fillFieldID_;
+   uint_t extrapolationOrder_;
+}; // class ExtrapolationRefillingSweepBase
+
+/***********************************************************************************************************************
+ * EquilibriumRefillingSweep:
+ * PDFs are initialized with the equilibrium based on the average density and velocity from neighboring liquid and
+ * (non-newly converted) interface cells.
+ *
+ * Reference: dissertation of N. Thuerey, 2007, section 4.3
+ *
+ *  f(x,q) = f^{eq}(x+e,q)
+ *      with x: cell position
+ *           q: index of the respective PDF
+ *           e: direction of a valid neighbor
+ **********************************************************************************************************************/
+
+template< typename StorageSpecification_T, typename FlagField_T >
+class EquilibriumRefillingSweep : public RefillingSweepBase< StorageSpecification_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< StorageSpecification_T, FlagField_T >;
+   using PdfField_T           = typename RefillingSweepBase_T::PdfField_T;
+   using flag_t               = typename RefillingSweepBase_T::flag_t;
+   using Stencil_T            = typename RefillingSweepBase_T::Stencil_T;
+
+   EquilibriumRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                             const FlagInfo< FlagField_T >& flagInfo, bool useDataFromGhostLayers)
+      : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers)
+   {}
+
+   ~EquilibriumRefillingSweep() override = default;
+
+   void operator()(IBlock* const block) override;
+}; // class EquilibriumRefillingSweep
+
+/***********************************************************************************************************************
+ * AverageRefillingSweep:
+ * PDFs are initialized with the average of the PDFs in the same direction from applicable neighboring cells.
+ *
+ *  f_i(x,q) = \sum_N( f_i(x+e,q) ) / N
+ *      with x: cell position
+ *           i: PDF index, i.e., direction
+ *           q: index of the respective PDF
+ *           e: direction of a valid neighbor
+ *           N: number of applicable neighbors
+ *           sum_N: sum over all applicable neighbors
+ *
+ * Reference: not available in literature (as of 06/2022).
+ **********************************************************************************************************************/
+
+template< typename StorageSpecification_T, typename FlagField_T >
+class AverageRefillingSweep : public RefillingSweepBase< StorageSpecification_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< StorageSpecification_T, FlagField_T >;
+   using PdfField_T           = typename RefillingSweepBase_T::PdfField_T;
+   using flag_t               = typename RefillingSweepBase_T::flag_t;
+   using Stencil_T            = typename RefillingSweepBase_T::Stencil_T;
+
+   AverageRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                         const FlagInfo< FlagField_T >& flagInfo, bool useDataFromGhostLayers)
+      : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers)
+   {}
+
+   ~AverageRefillingSweep() override = default;
+
+   void operator()(IBlock* const block) override;
+}; // class AverageRefillingSweep
+
+/***********************************************************************************************************************
+ * EquilibriumAndNonEquilibriumRefilling:
+ * First reconstruct the equilibrium values according to the "EquilibriumRefilling". Then extrapolate the
+ * non-equilibrium part of the PDFs in the direction of the surface normal and add it to the (equilibrium-)
+ * reinitialized PDFs.
+ *
+ * Reference: equation (51) in  Peng et al., "Implementation issues and benchmarking of lattice Boltzmann method for
+ *            moving rigid particle simulations in a viscous flow", 2015, doi: 10.1016/j.camwa.2015.08.027)
+ *
+ *  f_q(x,t+dt) = f_q^{eq}(x,t) + f_q^{neq}(x+e*dt,t+dt)
+ *      with x: cell position
+ *           q: index of the respective PDF
+ *           e: direction of a valid neighbor/ extrapolation direction
+ *           t: current time step
+ *           dt: time step width
+ *           f^{eq}: equilibrium PDF with velocity and density averaged from neighboring cells
+ *           f^{neq}: non-equilibrium PDF (f - f^{eq})
+ *
+ * Note: Analogously as in the "ExtrapolationRefilling", the expression "f_q^{neq}(x+e*dt,t+dt)" can also be obtained by
+ *       extrapolation (in literature, only zeroth order extrapolation is used/documented):
+ *          - zeroth order: f_q^{neq}(x+e*dt,t+dt)
+ *          - first order: 2 * f_q^{neq}(x+e*dt,t+dt) - 1 * f_q^{neq}(x+2e*dt,t+dt)
+ *          - second order: 3 * f_q^{neq}(x+e*dt,t+dt) - 3 * f_q^{neq}(x+2e*dt,t+dt) + f_q^{neq}(x+3e*dt,t+dt)
+ * If not enough cells are available for the chosen extrapolation order, the algorithm falls back to the corresponding
+ * lower order. If even zeroth order can not be applied, only f_q^{eq}(x,t) is considered which corresponds to
+ * "EquilibriumRefilling".
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class EquilibriumAndNonEquilibriumRefillingSweep
+   : public ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExtrapolationRefillingSweepBase_T =
+      ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >;
+   using RefillingSweepBase_T = typename ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T;
+   using PdfField_T           = typename ExtrapolationRefillingSweepBase_T::PdfField_T;
+   using flag_t               = typename ExtrapolationRefillingSweepBase_T::flag_t;
+   using Stencil_T            = typename ExtrapolationRefillingSweepBase_T::Stencil_T;
+
+   EquilibriumAndNonEquilibriumRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                                              const ConstBlockDataID& fillFieldID,
+                                              const FlagInfo< FlagField_T >& flagInfo, uint_t extrapolationOrder,
+                                              bool useDataFromGhostLayers)
+      : ExtrapolationRefillingSweepBase_T(pdfFieldID, flagFieldID, fillFieldID, flagInfo, extrapolationOrder,
+                                          useDataFromGhostLayers)
+   {}
+
+   ~EquilibriumAndNonEquilibriumRefillingSweep() override = default;
+
+   void operator()(IBlock* const block) override;
+}; // class EquilibriumAndNonEquilibriumRefillingSweep
+
+/***********************************************************************************************************************
+ * ExtrapolationRefilling:
+ * Extrapolate the PDFs of one or more cells in the direction of the surface normal.
+ *
+ * Reference: equation (50) in  Peng et al., "Implementation issues and benchmarking of lattice Boltzmann method for
+ *            moving rigid particle simulations in a viscous flow", 2015, doi: 10.1016/j.camwa.2015.08.027)
+ *
+ *  f_q(x,t+dt) = 3 * f_q(x+e*dt,t+dt) - 3 * f_q(x+2e*dt,t+dt) + f_q(x+3e*dt,t+dt)
+ *        with x: cell position
+ *             q: index of the respective PDF
+ *             e: direction of a valid neighbor/ extrapolation direction
+ *             t: current time step
+ *             dt: time step width
+ * Note: The equation contains a second order extrapolation, however other options are also available. If not enough
+ *       cells are available for second order extrapolation, the algorithm falls back to the next applicable
+ *       lower order:
+ *           - second order: 3 * f_q(x+e*dt,t+dt) - 3 * f_q(x+2e*dt,t+dt) + f_q(x+3e*dt,t+dt)
+ *           - first order: 2 * f_q(x+e*dt,t+dt) - 1 * f_q(x+2e*dt,t+dt)
+ *           - zeroth order: f_q(x+e*dt,t+dt)
+ * If even zeroth order can not be applied, only f_q^{eq}(x,t) is considered which corresponds to
+ * "EquilibriumRefilling".
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExtrapolationRefillingSweep
+   : public ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExtrapolationRefillingSweepBase_T =
+      ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >;
+   using RefillingSweepBase_T = typename ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T;
+   using PdfField_T           = typename ExtrapolationRefillingSweepBase_T::PdfField_T;
+   using flag_t               = typename ExtrapolationRefillingSweepBase_T::flag_t;
+   using Stencil_T            = typename ExtrapolationRefillingSweepBase_T::Stencil_T;
+
+   ExtrapolationRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                               const ConstBlockDataID& fillFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                               uint_t extrapolationOrder, bool useDataFromGhostLayers)
+      : ExtrapolationRefillingSweepBase_T(pdfFieldID, flagFieldID, fillFieldID, flagInfo, extrapolationOrder,
+                                          useDataFromGhostLayers)
+   {}
+
+   ~ExtrapolationRefillingSweep() override = default;
+
+   void operator()(IBlock* const block) override;
+}; // class ExtrapolationRefillingSweep
+
+/***********************************************************************************************************************
+ * GradsMomentsRefilling:
+ * Reconstruct missing PDFs based on Grad's moment closure.
+ *
+ * References: - equation (11) in Chikatamarla et al., "Grad’s approximation for missing data in lattice Boltzmann
+ *               simulations", 2006, doi: 10.1209/epl/i2005-10535-x
+ *             - equation (10) in Dorscher et al., "Grad’s approximation for moving and stationary walls in entropic
+ *               lattice Boltzmann simulations", 2015, doi: 10.1016/j.jcp.2015.04.017
+ *
+ * The following equation is a rewritten and easier version of the equation in the above references:
+ *  f_q(x,t+dt) = f_q^{eq}(x,t) +
+ *                w_q * rho / 2 / cs^2 / omega * (du_a / dx_b + du_b / dx_a)(cs^2 * delta_{ab} - c_{q,a}c_{q,b} )
+ *      with x: cell position
+ *           q: index of the respective PDF
+ *           t: current time step
+ *           f^{eq}: equilibrium PDF with velocity and density averaged from neighboring cells
+ *           w_q: lattice weight
+ *           rho: density averaged from neighboring cells
+ *           cs: lattice speed of sound
+ *           omega: relaxation rate
+ *           du_a / dx_b: gradient of the velocity (in index notation)
+ *           delta_{ab}: Kronecker delta (in index notation)
+ *           c_q{q,a}: lattice velocity (in index notation)
+ *
+ * The velocity gradient is computed using a first order central finite difference scheme if two neighboring cells
+ * are available. Otherwise, a first order upwind scheme is applied.
+ * IMPORTANT REMARK: The current implementation only works for dx=1 (this is assumed in the gradient computation).
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T >
+class GradsMomentsRefillingSweep : public RefillingSweepBase< StorageSpecification_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< StorageSpecification_T, FlagField_T >;
+   using PdfField_T           = typename RefillingSweepBase_T::PdfField_T;
+   using flag_t               = typename RefillingSweepBase_T::flag_t;
+   using Stencil_T            = typename RefillingSweepBase_T::Stencil_T;
+
+   GradsMomentsRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID,
+                              const FlagInfo< FlagField_T >& flagInfo, real_t relaxRate, bool useDataFromGhostLayers)
+
+      : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers), relaxRate_(relaxRate)
+   {}
+
+   ~GradsMomentsRefillingSweep() override = default;
+
+   void operator()(IBlock* const block) override;
+
+   // compute the gradient of the velocity in the specified direction
+   // - using a first order central finite difference scheme if two valid neighboring cells are available
+   // - using a first order upwind scheme if only one valid neighboring cell is available
+   // - assuming a gradient of zero if no valid neighboring cell is available
+   Vector3< real_t > getVelocityGradient(stencil::Direction direction, const Cell& cell, const PdfField_T* pdfField,
+                                         const Vector3< real_t >& avgVelocity,
+                                         const std::vector< bool >& validStencilIndices);
+
+ private:
+   real_t relaxRate_;
+}; // class GradsMomentApproximationRefilling
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "PdfRefillingSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/PdfRefillingSweep.impl.h b/src/lbm_generated/free_surface/dynamics/PdfRefillingSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..19a71c5b9dde769fd6ac176a1c0b9ee6e65c6707
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/PdfRefillingSweep.impl.h
@@ -0,0 +1,727 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file PdfRefillingSweep.impl.h
+//! \ingroup dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \author Michael Zikeli
+//! \brief Sweeps for refilling cells (i.e. reinitializing PDFs) after the cell was converted from gas to interface.
+//
+//======================================================================================================================
+
+#include "core/DataTypes.h"
+#include "core/cell/Cell.h"
+#include "core/debug/CheckFunctions.h"
+#include "core/logging/Logging.h"
+#include "core/math/Vector3.h"
+#include "core/math/Matrix3.h"
+
+#include "domain_decomposition/IBlock.h"
+
+#include "field/GhostLayerField.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+#include "lbm_generated/free_surface/dynamics/PdfRefillingModel.h"
+#include "lbm_generated/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm_generated/macroscopics/DensityAndMomentumDensity.h"
+#include "lbm_generated/macroscopics/Equilibrium.h"
+
+#include "stencil/D3Q6.h"
+#include "stencil/Directions.h"
+
+#include <set>
+#include <vector>
+
+#include "PdfRefillingSweep.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename StorageSpecification_T, typename FlagField_T >
+real_t RefillingSweepBase< StorageSpecification_T, FlagField_T >::getAverageDensityAndVelocity(
+   const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, const FlagInfo< FlagField_T >& flagInfo,
+   Vector3< real_t >& avgVelocity, std::vector< bool >& validStencilIndices)
+{
+   real_t rho = real_c(0.0);
+   Vector3< real_t > u(real_c(0.0));
+   uint_t numNeighbors = uint_c(0);
+
+   // do not use data from ghost layer if optimized communication is used (see comment in PdfRefillingSweep.h at
+   // ExtrapolationRefillingSweepBase)
+   const CellInterval localDomain = useDataFromGhostLayers_ ? pdfField.xyzSizeWithGhostLayer() : pdfField.xyzSize();
+
+   for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i)
+   {
+      const Cell neighborCell(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz());
+
+      const flag_t neighborFlag        = flagField.get(neighborCell);
+      const flag_t liquidInterfaceMask = flagInfo.interfaceFlag | flagInfo.liquidFlag;
+
+      // only use neighboring cell if
+      // - neighboring cell is part of the block-local domain
+      // - neighboring cell is liquid or interface
+      // - not newly converted from G->I
+      const bool useNeighbor = isPartOfMaskSet(neighborFlag, liquidInterfaceMask) &&
+                               !flagInfo.hasConvertedFromGasToInterface(flagField.get(neighborCell)) &&
+                               localDomain.contains(neighborCell);
+
+      // calculate the average of valid neighbor cells to calculate an average density and velocity.
+      if (useNeighbor)
+      {
+         numNeighbors++;
+         Vector3< real_t > neighborU;
+         real_t neighborRho;
+         neighborRho = lbm_generated::getDensityAndMomentumDensity<StorageSpecification_T, PdfField_T>(neighborU, pdfField, neighborCell[0], neighborCell[1], neighborCell[2]);
+         neighborU /= neighborRho;
+         u += neighborU;
+         rho += neighborRho;
+
+         validStencilIndices[i.toIdx()] = true;
+      }
+   }
+
+   // normalize the newly calculated velocity and density
+   if (numNeighbors != uint_c(0))
+   {
+      u /= real_c(numNeighbors);
+      rho /= real_c(numNeighbors);
+   }
+   else
+   {
+      u   = Vector3< real_t >(0.0);
+      rho = real_c(1.0);
+      WALBERLA_LOG_WARNING_ON_ROOT("There are no valid neighbors for the refilling of (block-local) cell: " << cell);
+   }
+
+   avgVelocity = u;
+   return rho;
+}
+
+template< typename StorageSpecification_T, typename FlagField_T >
+std::vector< real_t > RefillingSweepBase< StorageSpecification_T, FlagField_T >::getAveragePdfs(
+   const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, const FlagInfo< FlagField_T >& flagInfo)
+{
+   uint_t numNeighbors = uint_c(0);
+
+   // do not use data from ghost layer if optimized communication is used (see comment in PdfRefillingSweep.h at
+   // ExtrapolationRefillingSweepBase)
+   const CellInterval localDomain = useDataFromGhostLayers_ ? pdfField.xyzSizeWithGhostLayer() : pdfField.xyzSize();
+
+   std::vector< real_t > pdfSum(Stencil_T::Size, real_c(0));
+
+   for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i)
+   {
+      const Cell neighborCell(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz());
+
+      const flag_t neighborFlag        = flagField.get(neighborCell);
+      const flag_t liquidInterfaceMask = flagInfo.interfaceFlag | flagInfo.liquidFlag;
+
+      // only use neighboring cell if
+      // - neighboring cell is part of the block-local domain
+      // - neighboring cell is liquid or interface
+      // - not newly converted from G->I
+      const bool useNeighbor = isPartOfMaskSet(neighborFlag, liquidInterfaceMask) &&
+                               !flagInfo.hasConvertedFromGasToInterface(flagField.get(neighborCell)) &&
+                               localDomain.contains(neighborCell);
+
+      // calculate the average of valid neighbor cells to calculate an average of PDFs in each direction
+      if (useNeighbor)
+      {
+         ++numNeighbors;
+         for (auto pdfDir = Stencil_T::begin(); pdfDir != Stencil_T::end(); ++pdfDir)
+         {
+            pdfSum[pdfDir.toIdx()] += pdfField.get(neighborCell, *pdfDir);
+         }
+      }
+   }
+
+   // average the PDFs of all neighboring cells
+   if (numNeighbors != uint_c(0))
+   {
+      for (auto& pdf : pdfSum)
+      {
+         pdf /= real_c(numNeighbors);
+      }
+   }
+   else
+   {
+      // fall back to EquilibriumRefilling by setting PDFs according to equilibrium with velocity=0 and density=1
+      lbm_generated::Equilibrium< StorageSpecification_T >::set(pdfSum, Vector3< real_t >(real_c(0)), real_c(1));
+
+      WALBERLA_LOG_WARNING_ON_ROOT("There are no valid neighbors for the refilling of (block-local) cell: " << cell);
+   }
+
+   return pdfSum;
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+Vector3< cell_idx_t > ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   findCorrespondingLatticeDirection(const Vector3< real_t >& direction)
+{
+   if (direction == Vector3< real_t >(real_c(0))) { return direction; }
+
+   stencil::Direction bestFittingDirection = stencil::C; // arbitrary default initialization
+   real_t scalarProduct                    = real_c(0);
+
+   for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+   {
+      // compute inner product <dir,c_i>
+      const real_t scalarProductTmp = direction[0] * stencil::cNorm[0][*dir] + direction[1] * stencil::cNorm[1][*dir] +
+                                      direction[2] * stencil::cNorm[2][*dir];
+      if (scalarProductTmp > scalarProduct)
+      {
+         // largest scalar product is the best fitting lattice direction, i.e., this direction has the smallest angle to
+         // the given direction
+         scalarProduct        = scalarProductTmp;
+         bestFittingDirection = *dir;
+      }
+   }
+
+   return Vector3< cell_idx_t >(stencil::cx[bestFittingDirection], stencil::cy[bestFittingDirection],
+                                stencil::cz[bestFittingDirection]);
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+Vector3< cell_idx_t >
+   ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                    VectorField_T >::findExtrapolationDirection(const Cell& cell,
+                                                                                const FlagField_T& flagField,
+                                                                                const ScalarField_T& fillField)
+{
+   Vector3< real_t > normal = Vector3< real_t >(real_c(0));
+
+   // get flag mask for obstacle boundaries
+   const flag_t obstacleFlagMask = flagField.getMask(RefillingSweepBase_T::flagInfo_.getObstacleIDSet());
+
+   // get flag mask for liquid, interface, and gas cells
+   const flag_t liquidInterfaceGasMask = flagField.getMask(flagIDs::liquidInterfaceGasFlagIDs);
+
+   const typename ScalarField_T::ConstPtr fillPtr(fillField, cell.x(), cell.y(), cell.z());
+   const typename FlagField_T::ConstPtr flagPtr(flagField, cell.x(), cell.y(), cell.z());
+
+   if (!isFlagInNeighborhood< Stencil_T >(flagPtr, obstacleFlagMask))
+   {
+      normal_computation::computeNormal<
+         typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type >(
+         normal, fillPtr, flagPtr, liquidInterfaceGasMask);
+   }
+   else
+   {
+      normal_computation::computeNormalNearSolidBoundary<
+         typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type >(
+         normal, fillPtr, flagPtr, liquidInterfaceGasMask, obstacleFlagMask);
+   }
+
+   // normalize normal (in contrast to the usual definition in FSlbm_generated, it points from gas to fluid here)
+   normal = normal.getNormalizedOrZero();
+
+   // find the lattice direction that most closely resembles the normal direction
+   // Note: the normal must point from gas to fluid because the extrapolation direction is also defined to point from
+   // gas to fluid
+   return findCorrespondingLatticeDirection(normal);
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+uint_t ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   getNumberOfExtrapolationCells(const Cell& cell, const FlagField_T& flagField, const PdfField_T& pdfField,
+                                 const Vector3< cell_idx_t >& extrapolationDirection)
+{
+   // skip center cell
+   if (extrapolationDirection == Vector3< cell_idx_t >(cell_idx_c(0))) { return uint_c(0); }
+
+   // do not use data from ghost layer if optimized communication is used (see comment in PdfRefillingSweep.h at
+   // ExtrapolationRefillingSweepBase)
+   const CellInterval localDomain =
+      (RefillingSweepBase_T::useDataFromGhostLayers_) ? pdfField.xyzSizeWithGhostLayer() : pdfField.xyzSize();
+
+   // for extrapolation order n, n+1 applicable cells must be available in the desired direction
+   const uint_t maxExtrapolationCells = extrapolationOrder_ + uint_c(1);
+
+   for (uint_t numCells = uint_c(1); numCells <= maxExtrapolationCells; ++numCells)
+   {
+      // potential cell used for extrapolation
+      const Cell checkCell = cell + Cell(cell_idx_c(numCells) * extrapolationDirection);
+
+      const flag_t neighborFlag = flagField.get(checkCell);
+      const flag_t liquidInterfaceMask =
+         RefillingSweepBase_T::flagInfo_.interfaceFlag | RefillingSweepBase_T::flagInfo_.liquidFlag;
+
+      // only use cell if the cell is
+      // - inside domain
+      // - liquid or interface
+      // - not a cell that has also just been converted from gas to interface
+      if (!localDomain.contains(checkCell) || !isPartOfMaskSet(neighborFlag, liquidInterfaceMask) ||
+          RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagField.get(checkCell)))
+      {
+         return numCells - uint_c(1);
+      }
+   }
+
+   return maxExtrapolationCells;
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+std::vector< real_t > ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   getNonEquilibriumPdfsInCell(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField)
+{
+   std::vector< real_t > nonEquilibriumPartOfPdfs(Stencil_T::Size);
+
+   Vector3< real_t > velocity;
+   // TODO Implement
+   // const real_t density = pdfField.getDensityAndVelocity(velocity, cell);
+   const real_t density = real_c(1.0);
+
+   // compute non-equilibrium of each PDF
+   for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir)
+   {
+      nonEquilibriumPartOfPdfs[dir.toIdx()] =
+         pdfField.get(cell, dir.toIdx()) - lbm_generated::Equilibrium< StorageSpecification_T >::get(*dir, velocity, density);
+   }
+   return nonEquilibriumPartOfPdfs;
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+std::vector< real_t >
+   ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::getPdfsInCell(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField)
+{
+   std::vector< real_t > Pdfs(Stencil_T::Size);
+
+   for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir)
+   {
+      Pdfs[dir.toIdx()] = pdfField.get(cell, dir.toIdx());
+   }
+   return Pdfs;
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   applyQuadraticExtrapolation(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField) >&
+         getPdfFunc)
+{
+   // store the PDFs of the cells participating in the extrapolation
+   std::vector< real_t > pdfsXf(StorageSpecification_T::Stencil::Size);   // cell + 1 * extrapolationDirection
+   std::vector< real_t > pdfsXff(StorageSpecification_T::Stencil::Size);  // cell + 2 * extrapolationDirection
+   std::vector< real_t > pdfsXfff(StorageSpecification_T::Stencil::Size); // cell + 3 * extrapolationDirection
+
+   // determines if the PDFs of the current cell are also considered
+   const real_t centerFactor = includeThisCell ? real_c(1) : real_c(0);
+
+   // get PDFs
+   pdfsXf   = getPdfFunc(cell + Cell(cell_idx_c(1) * extrapolationDirection), pdfField);
+   pdfsXff  = getPdfFunc(cell + Cell(cell_idx_c(2) * extrapolationDirection), pdfField);
+   pdfsXfff = getPdfFunc(cell + Cell(cell_idx_c(3) * extrapolationDirection), pdfField);
+
+   // compute the resulting PDF values of "cell" according to second order extrapolation
+   for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir)
+   {
+      pdfField.get(cell, dir.toIdx()) = centerFactor * pdfField.get(cell, dir.toIdx()) +
+                                        real_c(3) * pdfsXf[dir.toIdx()] - real_c(3) * pdfsXff[dir.toIdx()] +
+                                        real_c(1) * pdfsXfff[dir.toIdx()];
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   applyLinearExtrapolation(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField) >&
+         getPdfFunc)
+{
+   // store the PDFs of the cells participating in the interpolation
+   std::vector< real_t > pdfsXf(Stencil_T::Size);  // cell + 1 * extrapolationDirection
+   std::vector< real_t > pdfsXff(Stencil_T::Size); // cell + 2 * extrapolationDirection
+
+   // determines if the PDFs of the current cell are also considered
+   const real_t centerFactor = includeThisCell ? real_c(1) : real_c(0);
+
+   // get PDFs
+   pdfsXf  = getPdfFunc(cell + Cell(cell_idx_c(1) * extrapolationDirection), pdfField);
+   pdfsXff = getPdfFunc(cell + Cell(cell_idx_c(2) * extrapolationDirection), pdfField);
+
+   // compute the resulting PDF values of "cell" according to first order extrapolation
+   for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir)
+   {
+      pdfField.get(cell, dir.toIdx()) = centerFactor * pdfField.get(cell, dir.toIdx()) +
+                                        real_c(2) * pdfsXf[dir.toIdx()] - real_c(1) * pdfsXff[dir.toIdx()];
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweepBase< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::
+   applyConstantExtrapolation(
+      const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm_generated::PdfField< StorageSpecification_T >& pdfField) >&
+         getPdfFunc)
+{
+   // store the PDFs of the cells participating in the interpolation
+   std::vector< real_t > pdfsXf(Stencil_T::Size); // cell + 1 * extrapolationDirection
+
+   // determines if the PDFs of the current cell are also considered
+   const real_t centerFactor = includeThisCell ? real_c(1) : real_c(0);
+
+   // get PDFs
+   pdfsXf = getPdfFunc(cell + Cell(cell_idx_c(1) * extrapolationDirection), pdfField);
+
+   // compute the resulting PDF values of "cell" according to zeroth order extrapolation
+   for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir)
+   {
+      pdfField.get(cell, dir.toIdx()) =
+         centerFactor * pdfField.get(cell, dir.toIdx()) + real_c(1) * pdfsXf[dir.toIdx()];
+   }
+}
+
+template< typename StorageSpecification_T, typename FlagField_T >
+void EquilibriumRefillingSweep< StorageSpecification_T, FlagField_T >::operator()(IBlock* const block)
+{
+   PdfField_T* const pdfField         = block->getData< PdfField_T >(RefillingSweepBase_T::pdfFieldID_);
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(RefillingSweepBase_T::flagFieldID_);
+
+   WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+      if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt))
+      {
+         const Cell cell = pdfFieldIt.cell();
+
+         Vector3< real_t > avgVelocity;
+         real_t avgDensity;
+         avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity(cell, *pdfField, *flagField,
+                                                                         RefillingSweepBase_T::flagInfo_, avgVelocity);
+
+         // TODO add again
+         // pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity);
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename StorageSpecification_T, typename FlagField_T >
+void AverageRefillingSweep< StorageSpecification_T, FlagField_T >::operator()(IBlock* const block)
+{
+   PdfField_T* const pdfField         = block->getData< PdfField_T >(RefillingSweepBase_T::pdfFieldID_);
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(RefillingSweepBase_T::flagFieldID_);
+
+   WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+      if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt))
+      {
+         const Cell cell = pdfFieldIt.cell();
+
+         // compute average PDFs (in each direction) from all applicable neighboring cells
+         const std::vector< real_t > pdfAverage =
+            RefillingSweepBase_T::getAveragePdfs(cell, *pdfField, *flagField, RefillingSweepBase_T::flagInfo_);
+
+         for (auto pdfDir = Stencil_T::begin(); pdfDir != Stencil_T::end(); ++pdfDir)
+         {
+            pdfField->get(cell, *pdfDir) = pdfAverage[pdfDir.toIdx()];
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void EquilibriumAndNonEquilibriumRefillingSweep< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                 VectorField_T >::operator()(IBlock* const block)
+{
+   PdfField_T* const pdfField =
+      block->getData< PdfField_T >(ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T::pdfFieldID_);
+   const FlagField_T* const flagField =
+      block->getData< const FlagField_T >(ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T::flagFieldID_);
+   const ScalarField_T* const fillField =
+      block->getData< const ScalarField_T >(ExtrapolationRefillingSweepBase_T::fillFieldID_);
+
+   // function to fetch relevant PDFs
+   auto getPdfFunc = std::bind(&ExtrapolationRefillingSweepBase_T::getNonEquilibriumPdfsInCell, this,
+                               std::placeholders::_1, std::placeholders::_2);
+
+   WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+      if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt))
+      {
+         const Cell cell = pdfFieldIt.cell();
+
+         // apply EquilibriumRefilling first
+         Vector3< real_t > avgVelocity;
+         real_t avgDensity;
+         avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity(cell, *pdfField, *flagField,
+                                                                         RefillingSweepBase_T::flagInfo_, avgVelocity);
+
+         // TODO Implement
+         // pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity);
+
+         // find valid cells for extrapolation
+         const Vector3< cell_idx_t > extrapolationDirection =
+            ExtrapolationRefillingSweepBase_T::findExtrapolationDirection(cell, *flagField, *fillField);
+         const uint_t numberOfCellsForExtrapolation = ExtrapolationRefillingSweepBase_T::getNumberOfExtrapolationCells(
+            cell, *flagField, *pdfField, extrapolationDirection);
+
+         // add non-equilibrium part of PDF (which might be obtained by extrapolation)
+         if (numberOfCellsForExtrapolation >= uint_c(3))
+         {
+            ExtrapolationRefillingSweepBase_T::applyQuadraticExtrapolation(cell, *pdfField, extrapolationDirection,
+                                                                           true, getPdfFunc);
+         }
+         else
+         {
+            if (numberOfCellsForExtrapolation >= uint_c(2))
+            {
+               ExtrapolationRefillingSweepBase_T::applyLinearExtrapolation(cell, *pdfField, extrapolationDirection,
+                                                                           true, getPdfFunc);
+            }
+            else
+            {
+               if (numberOfCellsForExtrapolation >= uint_c(1))
+               {
+                  ExtrapolationRefillingSweepBase_T::applyConstantExtrapolation(cell, *pdfField, extrapolationDirection,
+                                                                                true, getPdfFunc);
+               }
+               // else: do nothing here; this corresponds to using EquilibriumRefilling (done already at the beginning)
+            }
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweep< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(
+   IBlock* const block)
+{
+   PdfField_T* const pdfField = block->getData< PdfField_T >(ExtrapolationRefillingSweepBase_T::pdfFieldID_);
+   const FlagField_T* const flagField =
+      block->getData< const FlagField_T >(ExtrapolationRefillingSweepBase_T::flagFieldID_);
+   const ScalarField_T* const fillField =
+      block->getData< const ScalarField_T >(ExtrapolationRefillingSweepBase_T::fillFieldID_);
+
+   WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+      if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt))
+      {
+         const Cell cell = pdfFieldIt.cell();
+
+         // find valid cells for extrapolation
+         const Vector3< cell_idx_t > extrapolationDirection =
+            ExtrapolationRefillingSweepBase_T::findExtrapolationDirection(cell, *flagField, *fillField);
+         const uint_t numberOfCellsForExtrapolation = ExtrapolationRefillingSweepBase_T::getNumberOfExtrapolationCells(
+            cell, *flagField, *pdfField, extrapolationDirection);
+
+         // function to fetch relevant PDFs
+         auto getPdfFunc = std::bind(&ExtrapolationRefillingSweepBase_T::getPdfsInCell, this, std::placeholders::_1,
+                                     std::placeholders::_2);
+
+         if (numberOfCellsForExtrapolation >= uint_c(3))
+         {
+            ExtrapolationRefillingSweepBase_T::applyQuadraticExtrapolation(cell, *pdfField, extrapolationDirection,
+                                                                           false, getPdfFunc);
+         }
+         else
+         {
+            if (numberOfCellsForExtrapolation >= uint_c(2))
+            {
+               ExtrapolationRefillingSweepBase_T::applyLinearExtrapolation(cell, *pdfField, extrapolationDirection,
+                                                                           false, getPdfFunc);
+            }
+            else
+            {
+               if (numberOfCellsForExtrapolation >= uint_c(1))
+               {
+                  ExtrapolationRefillingSweepBase_T::applyConstantExtrapolation(cell, *pdfField, extrapolationDirection,
+                                                                                false, getPdfFunc);
+               }
+               else
+               {
+                  // if not enough cells for extrapolation are available, use EquilibriumRefilling
+                  Vector3< real_t > avgVelocity;
+                  real_t avgDensity;
+                  avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity(
+                     cell, *pdfField, *flagField, RefillingSweepBase_T::flagInfo_, avgVelocity);
+                  // TODO Implement
+                  // pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity);
+               }
+            }
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename StorageSpecification_T, typename FlagField_T >
+Vector3< real_t > GradsMomentsRefillingSweep< StorageSpecification_T, FlagField_T >::getVelocityGradient(
+   stencil::Direction direction, const Cell& cell, const PdfField_T* pdfField, const Vector3< real_t >& avgVelocity,
+   const std::vector< bool >& validStencilIndices)
+{
+   stencil::Direction dir;
+   stencil::Direction invDir;
+
+   switch (direction)
+   {
+   case stencil::E:
+   case stencil::W:
+      dir    = stencil::E;
+      invDir = stencil::W;
+      break;
+   case stencil::N:
+   case stencil::S:
+      dir    = stencil::N;
+      invDir = stencil::S;
+      break;
+   case stencil::T:
+   case stencil::B:
+      dir    = stencil::T;
+      invDir = stencil::B;
+      break;
+   default:
+      WALBERLA_ABORT("Velocity gradient for GradsMomentsRefilling can not be computed in a direction other than in x-, "
+                     "y-, or z-direction.");
+   }
+
+   Vector3< real_t > velocityGradient(real_c(0));
+
+   // apply central finite differences if both neighboring cells are available
+   if (validStencilIndices[Stencil_T::idx[dir]] && validStencilIndices[Stencil_T::idx[invDir]])
+   {
+      // TODO Implement
+      const Vector3< real_t > neighborVelocity1 = Vector3(0,0,0); // pdfField->getVelocity(cell + dir);
+      const Vector3< real_t > neighborVelocity2 = Vector3(0,0,0); // pdfField->getVelocity(cell + invDir);
+      velocityGradient[0] = real_c(0.5) * (neighborVelocity1[0] - neighborVelocity2[0]); // assuming dx = 1
+      velocityGradient[1] = real_c(0.5) * (neighborVelocity1[1] - neighborVelocity2[1]); // assuming dx = 1
+      velocityGradient[2] = real_c(0.5) * (neighborVelocity1[2] - neighborVelocity2[2]); // assuming dx = 1
+   }
+   else
+   {
+      // apply first order upwind scheme
+      stencil::Direction upwindDirection = (avgVelocity[0] > real_c(0)) ? invDir : dir;
+
+      stencil::Direction sourceDirection = stencil::C; // arbitrary default initialization
+
+      if (validStencilIndices[Stencil_T::idx[upwindDirection]]) { sourceDirection = upwindDirection; }
+      else
+      {
+         if (validStencilIndices[Stencil_T::idx[stencil::inverseDir[upwindDirection]]])
+         {
+            sourceDirection = stencil::inverseDir[upwindDirection];
+         }
+      }
+
+      if (sourceDirection == dir)
+      {
+         auto neighborVelocity = Vector3(0,0,0); // pdfField->getVelocity(cell + sourceDirection);
+         velocityGradient[0]   = neighborVelocity[0] - avgVelocity[0]; // assuming dx = 1
+         velocityGradient[1]   = neighborVelocity[1] - avgVelocity[1]; // assuming dx = 1
+         velocityGradient[2]   = neighborVelocity[2] - avgVelocity[2]; // assuming dx = 1
+      }
+      else
+      {
+         if (sourceDirection == invDir)
+         {
+            auto neighborVelocity = Vector3(0,0,0); // pdfField->getVelocity(cell + sourceDirection);
+            velocityGradient[0]   = avgVelocity[0] - neighborVelocity[0]; // assuming dx = 1
+            velocityGradient[1]   = avgVelocity[1] - neighborVelocity[1]; // assuming dx = 1
+            velocityGradient[2]   = avgVelocity[2] - neighborVelocity[2]; // assuming dx = 1
+         }
+         // else: no stencil direction is valid, velocityGradient is zero
+      }
+   }
+
+   return velocityGradient;
+}
+
+template< typename StorageSpecification_T, typename FlagField_T >
+void GradsMomentsRefillingSweep< StorageSpecification_T, FlagField_T >::operator()(IBlock* const block)
+{
+   PdfField_T* pdfField               = block->getData< PdfField_T >(RefillingSweepBase_T::pdfFieldID_);
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(RefillingSweepBase_T::flagFieldID_);
+
+   WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+      if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt))
+      {
+         const Cell cell = pdfFieldIt.cell();
+
+         // get average density and velocity from valid neighboring cells and store the directions of valid neighbors
+         std::vector< bool > validStencilIndices(Stencil_T::Size, false);
+         Vector3< real_t > avgVelocity;
+         real_t avgDensity;
+         avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity(
+            cell, *pdfField, *flagField, RefillingSweepBase_T::flagInfo_, avgVelocity, validStencilIndices);
+
+         // get velocity gradients
+         // - using a first order central finite differences (if two neighboring cells are available)
+         // - using a first order upwind scheme (if only one neighboring cell is available)
+         // - assuming a zero gradient if no valid neighboring cell is available
+         // velocityGradient(u) =
+         // | du1/dx1 du2/dx1 du3/dx1 |   | 0 1 2 |   | 0,0  0,1  0,2 |
+         // | du1/dx2 du2/dx2 du3/dx2 | = | 3 4 5 | = | 1,0  1,1  1,2 |
+         // | du1/dx3 du2/dx3 du3/dx3 |   | 6 7 8 |   | 2,0  2,1  2,2 |
+         const Vector3< real_t > gradientX =
+            getVelocityGradient(stencil::E, cell, pdfField, avgVelocity, validStencilIndices);
+         const Vector3< real_t > gradientY =
+            getVelocityGradient(stencil::N, cell, pdfField, avgVelocity, validStencilIndices);
+         Vector3< real_t > gradientZ = Vector3< real_t >(real_c(0));
+         if (Stencil_T::D == 3)
+         {
+            gradientZ = getVelocityGradient(stencil::T, cell, pdfField, avgVelocity, validStencilIndices);
+         }
+
+         Matrix3< real_t > velocityGradient(gradientX, gradientY, gradientZ);
+
+         // compute non-equilibrium pressure tensor (equation (13) in Dorschner et al.); rho is not included in the
+         // pre-factor here, but will be considered later
+         Matrix3< real_t > pressureTensorNeq(real_c(0));
+
+         // in equation (13) in Dorschner et al., 2*beta is used in the pre-factor; note that 2*beta=omega (relaxation
+         // rate related to kinematic viscosity)
+         const real_t preFac = -real_c(1) / (real_c(3) * relaxRate_);
+
+         for (uint_t j = uint_c(0); j != uint_c(3); ++j)
+         {
+            for (uint_t i = uint_c(0); i != uint_c(3); ++i)
+            {
+               pressureTensorNeq(i, j) += preFac * (velocityGradient(i, j) + velocityGradient(j, i));
+            }
+         }
+
+         // set PDFs according to equation (10) in Dorschner et al.; this is equivalent to setting the PDFs to
+         // equilibrium f^{eq} and adding a contribution of the non-equilibrium pressure tensor P^{neq}
+         for (auto q = Stencil_T::begin(); q != Stencil_T::end(); ++q)
+         {
+            const real_t velCi = real_c(stencil::cx[*q]) * avgVelocity[0] + real_c(stencil::cy[*q]) * avgVelocity[1] +  real_c(stencil::cz[*q]) * avgVelocity[2];
+
+            real_t contributionFromPneq = real_c(0);
+            for (uint_t j = uint_c(0); j != uint_c(3); ++j)
+            {
+               for (uint_t i = uint_c(0); i != uint_c(3); ++i)
+               {
+                  // P^{neq}_{a,b} * c_{q,a} * c_{q,b}
+                  contributionFromPneq +=
+                     pressureTensorNeq(i, j) * real_c(stencil::c[i][*q]) * real_c(stencil::c[j][*q]);
+               }
+            }
+
+            // - P^{neq}_{a,b} * cs^2 * delta_{a,b}
+            contributionFromPneq -=
+               (pressureTensorNeq(0, 0) + pressureTensorNeq(1, 1) + pressureTensorNeq(2, 2)) / real_c(3);
+
+            // compute f^{eq} and add contribution from P^{neq}
+            const real_t pdf = StorageSpecification_T::w[q.toIdx()] * avgDensity *
+                               (real_c(1) + real_c(3) * velCi - real_c(1.5) * avgVelocity.sqrLength() +
+                                real_c(4.5) * velCi * velCi + real_c(4.5) * contributionFromPneq);
+            pdfField->get(cell, *q) = pdf;
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/StreamReconstructAdvectSweep.h b/src/lbm_generated/free_surface/dynamics/StreamReconstructAdvectSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..85a7292ce028189f90e2353ef960433510810ae6
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/StreamReconstructAdvectSweep.h
@@ -0,0 +1,167 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file StreamReconstructAdvectSweep.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Sweep for reconstruction of PDFs, streaming of PDFs (only in interface cells), advection of mass, update of
+//!        bubble volumes and marking interface cells for conversion.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/math/Vector3.h"
+#include "core/mpi/Reduce.h"
+#include "core/timing/TimingPool.h"
+
+#include "field/FieldClone.h"
+#include "field/FlagField.h"
+
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include "PdfReconstructionModel.h"
+#include "functionality/AdvectMass.h"
+#include "functionality/FindInterfaceCellConversion.h"
+#include "functionality/GetOredNeighborhood.h"
+#include "functionality/ReconstructInterfaceCellABB.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename StorageSpecification_T, typename SweepCollection_T, typename FlagField_T, typename FlagInfo_T,
+          typename ScalarField_T, typename VectorField_T >
+class StreamReconstructAdvectSweep
+{
+ public:
+   using flag_t     = typename FlagInfo_T::flag_t;
+   using PdfField_T = lbm_generated::PdfField< StorageSpecification_T >;
+
+   StreamReconstructAdvectSweep(SweepCollection_T& sweepCollection, real_t sigma, BlockDataID fillFieldID, BlockDataID flagFieldID,
+                                BlockDataID pdfField, ConstBlockDataID normalFieldID, ConstBlockDataID curvatureFieldID,
+                                const FlagInfo_T& flagInfo,
+                                const PdfReconstructionModel& pdfReconstructionModel, bool useSimpleMassExchange,
+                                real_t cellConversionThreshold, real_t cellConversionForceThreshold)
+      : sweepCollection_(sweepCollection), sigma_(sigma), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID),
+        pdfFieldID_(pdfField), normalFieldID_(normalFieldID), curvatureFieldID_(curvatureFieldID), flagInfo_(flagInfo),
+        neighborhoodFlagFieldClone_(flagFieldID), fillFieldClone_(fillFieldID),
+        pdfFieldClone_(pdfField), pdfReconstructionModel_(pdfReconstructionModel),
+        useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold),
+        cellConversionForceThreshold_(cellConversionForceThreshold)
+   {}
+
+   void operator()(IBlock* const block);
+
+ protected:
+   SweepCollection_T & sweepCollection_;
+   real_t sigma_; // surface tension
+
+   BlockDataID fillFieldID_;
+   BlockDataID flagFieldID_;
+   BlockDataID pdfFieldID_;
+
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID curvatureFieldID_;
+
+   FlagInfo_T flagInfo_;
+
+   // efficient clones of fields to provide temporary fields (for writing to)
+   field::FieldClone< FlagField_T, true > neighborhoodFlagFieldClone_;
+   field::FieldClone< ScalarField_T, true > fillFieldClone_;
+   field::FieldClone< PdfField_T, true > pdfFieldClone_;
+
+   PdfReconstructionModel pdfReconstructionModel_;
+   bool useSimpleMassExchange_;
+   real_t cellConversionThreshold_;
+   real_t cellConversionForceThreshold_;
+}; // class StreamReconstructAdvectSweep
+
+template< typename StorageSpecification_T, typename SweepCollection_T, typename FlagField_T, typename FlagInfo_T,
+          typename ScalarField_T, typename VectorField_T >
+void StreamReconstructAdvectSweep< StorageSpecification_T, SweepCollection_T, FlagField_T, FlagInfo_T, ScalarField_T,
+                                   VectorField_T >::operator()(IBlock* const block)
+{
+   // fetch data
+   ScalarField_T* const fillSrcField = block->getData< ScalarField_T >(fillFieldID_);
+   PdfField_T* const pdfSrcField     = block->getData< PdfField_T >(pdfFieldID_);
+
+   const ScalarField_T* const curvatureField = block->getData< const ScalarField_T >(curvatureFieldID_);
+   const VectorField_T* const normalField    = block->getData< const VectorField_T >(normalFieldID_);
+   FlagField_T* const flagField              = block->getData< FlagField_T >(flagFieldID_);
+
+   // temporary fields that act as destination fields
+   PdfField_T* const pdfDstField            = pdfFieldClone_.get(block);
+   FlagField_T* const neighborhoodFlagField = neighborhoodFlagFieldClone_.get(block);
+   ScalarField_T* const fillDstField        = fillFieldClone_.get(block);
+
+   // combine all neighbor flags using bitwise OR and write them to the neighborhood field
+   // this is simply a pre-computation of often required values
+   // IMPORTANT REMARK: the "OredNeighborhood" is also required for each cell in the first ghost layer; this requires
+   // access to all first ghost layer cell's neighbors (i.e., to the second ghost layer)
+   WALBERLA_CHECK_GREATER_EQUAL(flagField->nrOfGhostLayers(), uint_c(2));
+   getOredNeighborhood< typename StorageSpecification_T::Stencil >(flagField, neighborhoodFlagField);
+
+   // explicitly avoid OpenMP due to bubble model update (reportFillLevelChange)
+   WALBERLA_FOR_ALL_CELLS_OMP(
+      pdfDstFieldIt, pdfDstField, pdfSrcFieldIt, pdfSrcField, fillSrcFieldIt, fillSrcField, fillDstFieldIt,
+      fillDstField, flagFieldIt, flagField, neighborhoodFlagFieldIt, neighborhoodFlagField, normalFieldIt, normalField,
+      curvatureFieldIt, curvatureField, omp critical, {
+         if (flagInfo_.isInterface(flagFieldIt))
+         {
+            // get density (rhoGas) for interface PDF reconstruction
+            const real_t rhoGas        = computeDeltaRhoLaplacePressure(sigma_, *curvatureFieldIt);
+
+            // reconstruct missing PDFs coming from gas neighbors according to the specified model (see dissertation of
+            // N. Thuerey, 2007, section 4.2); reconstruction includes streaming of PDFs to interface cells (no LBM
+            // stream required here)
+            (reconstructInterfaceCellLegacy< StorageSpecification_T, FlagField_T >) (flagField, pdfSrcFieldIt, flagFieldIt,
+                                                                             normalFieldIt, flagInfo_, rhoGas,
+                                                                             pdfDstFieldIt, pdfReconstructionModel_);
+         }
+         else // treat non-interface cells
+         {
+            // manually adjust the fill level to avoid outdated fill levels being copied from fillSrcField
+            if (flagInfo_.isGas(flagFieldIt)) { *fillDstFieldIt = real_c(0); }
+            else
+            {
+               if (flagInfo_.isLiquid(flagFieldIt))
+               {
+                  const Cell cell = pdfSrcFieldIt.cell();
+                  const CellInterval ci(cell, cell);
+                  sweepCollection_.streamCellInterval(pdfSrcField, pdfDstField, ci);
+
+                  *fillDstFieldIt = real_c(1);
+               }
+               else // flag is e.g. obstacle or outflow
+               {
+                  *fillDstFieldIt = *fillSrcFieldIt;
+               }
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP
+
+   pdfSrcField->swapDataPointers(pdfDstField);
+   fillSrcField->swapDataPointers(fillDstField);
+
+   // mark interface cell for conversion
+   findInterfaceCellConversions< StorageSpecification_T >(fillSrcField, flagField, neighborhoodFlagField, flagInfo_,
+                                                  cellConversionThreshold_, cellConversionForceThreshold_);
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/SurfaceDynamicsHandler.h b/src/lbm_generated/free_surface/dynamics/SurfaceDynamicsHandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..18e1425a8d1cb8a47aaf7e2ecf3bf088cde85d18
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/SurfaceDynamicsHandler.h
@@ -0,0 +1,344 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SurfaceDynamicsHandler.h
+//! \ingroup surface_dynamics
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Handles the free surface dynamics (mass advection, lbm_generated, boundary condition, cell conversion etc.).
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/AddToStorage.h"
+#include "field/FlagField.h"
+
+#include "lbm_generated/blockforest/SimpleCommunication.h"
+#include "lbm_generated/blockforest/UpdateSecondGhostLayer.h"
+#include "lbm_generated/free_surface/BlockStateDetectorSweep.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "CellConversionSweep.h"
+#include "ConversionFlagsResetSweep.h"
+#include "ExcessMassDistributionModel.h"
+#include "ExcessMassDistributionSweep.h"
+#include "ForceDensitySweep.h"
+#include "PdfReconstructionModel.h"
+#include "PdfRefillingModel.h"
+#include "PdfRefillingSweep.h"
+#include "StreamReconstructAdvectSweep.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename StorageSpecification_T, typename SweepCollection_T, typename BoundaryCollection_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T>
+class SurfaceDynamicsHandler
+{
+ protected:
+   using Communication_T = lbm_generated::SimpleCommunication< typename StorageSpecification_T::Stencil >;
+
+   // communication in corner directions (D2Q9/D3Q27) is required for all fields but the PDF field
+   using CommunicationStencil_T =
+      typename std::conditional< StorageSpecification_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+   using CommunicationCorner_T = lbm_generated::SimpleCommunication< CommunicationStencil_T >;
+
+ public:
+   SurfaceDynamicsHandler(const std::shared_ptr< StructuredBlockForest >& blockForest,
+                          SweepCollection_T & sweepCollection, BoundaryCollection_T & boundaryCollection,
+                          FlagInfo_T& flagInfo, BlockDataID pdfFieldID,
+                          BlockDataID flagFieldID, BlockDataID fillFieldID, BlockDataID forceDensityFieldID,
+                          ConstBlockDataID normalFieldID, ConstBlockDataID curvatureFieldID,
+                          const std::string& pdfReconstructionModel, const std::string& pdfRefillingModel,
+                          const std::string& excessMassDistributionModel, real_t relaxationRate,
+                          const Vector3< real_t >& globalAcceleration, real_t surfaceTension,
+                          bool useSimpleMassExchange, real_t cellConversionThreshold,
+                          real_t cellConversionForceThreshold)
+      : blockForest_(blockForest), sweepCollection_(sweepCollection), boundaryCollection_(boundaryCollection), flagInfo_(flagInfo), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID),
+        forceDensityFieldID_(forceDensityFieldID), normalFieldID_(normalFieldID), curvatureFieldID_(curvatureFieldID),
+        pdfReconstructionModel_(pdfReconstructionModel), pdfRefillingModel_({ pdfRefillingModel }),
+        excessMassDistributionModel_({ excessMassDistributionModel }), relaxationRate_(relaxationRate),
+        globalAcceleration_(globalAcceleration), surfaceTension_(surfaceTension),
+        useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold),
+        cellConversionForceThreshold_(cellConversionForceThreshold)
+   {
+      WALBERLA_CHECK(StorageSpecification_T::compressible,
+                     "The free surface lattice Boltzmann extension works only with compressible lbm models.");
+
+      if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType())
+      {
+         // add additional field for storing excess mass in liquid cells
+         excessMassFieldID_ =
+            field::addToStorage< ScalarField_T >(blockForest_, "Excess mass", real_c(0), field::fzyx, uint_c(1));
+      }
+
+      if (StorageSpecification_T::Stencil::D == uint_t(2))
+      {
+         WALBERLA_LOG_INFO_ON_ROOT(
+            "IMPORTANT REMARK: You are using a D2Q9 stencil in SurfaceDynamicsHandler. In the FSlbm_generated, a D2Q9 setup is "
+            "not identical to a D3Q19 setup with periodicity in the third direction but only identical to the same "
+            "D3Q27 setup. This comes from the distribution of excess mass, where the number of available neighbors "
+            "differs for D2Q9 and a periodic D3Q19 setup.")
+      }
+   }
+
+   ConstBlockDataID getConstExcessMassFieldID() const { return excessMassFieldID_; }
+
+   /********************************************************************************************************************
+    * The order of these sweeps is similar to page 40 in the dissertation of T. Pohl, 2008.
+    *******************************************************************************************************************/
+   void addSweeps(SweepTimeloop& timeloop) const
+   {
+      using StateSweep = BlockStateDetectorSweep< FlagField_T >;
+      const auto blockStateUpdate = StateSweep(blockForest_, flagInfo_, flagFieldID_);
+
+      // empty sweeps required for using selectors (e.g. StateSweep::onlyGasAndBoundary)
+      const auto emptySweep = [](IBlock*) {};
+
+      // add standard waLBerla boundary handling
+      timeloop.add() << Sweep(boundaryCollection_.getSweep(), "Sweep: boundary handling",
+                              Set< SUID >::emptySet(), StateSweep::onlyGasAndBoundary)
+                     << Sweep(emptySweep, "Empty sweep: boundary handling", StateSweep::onlyGasAndBoundary);
+
+      if (!(floatIsEqual(globalAcceleration_[0], real_c(0), real_c(1e-14)) &&
+            floatIsEqual(globalAcceleration_[1], real_c(0), real_c(1e-14)) &&
+            floatIsEqual(globalAcceleration_[2], real_c(0), real_c(1e-14))))
+      {
+         // add sweep for weighting force in interface cells with fill level and density
+         // different versions for codegen because pystencils does not support 'Ghostlayerfield<Vector3(), 1>'
+         const ForceDensityCodegenSweep< StorageSpecification_T, FlagField_T, VectorField_T, ScalarField_T >
+            forceDensityCodegenSweep(forceDensityFieldID_, pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo_,
+                                     globalAcceleration_);
+         timeloop.add() << Sweep(forceDensityCodegenSweep, "Sweep: force weighting", Set< SUID >::emptySet(),
+                                 StateSweep::onlyGasAndBoundary)
+                        << Sweep(emptySweep, "Empty sweep: force weighting", StateSweep::onlyGasAndBoundary)
+                        << AfterFunction(CommunicationCorner_T(blockForest_, forceDensityFieldID_),
+                                         "Communication: after force weighting sweep");
+      }
+
+      // sweep for
+      // - reconstruction of PDFs in interface cells
+      // - streaming of PDFs in interface cells (and liquid cells on the same block)
+      // - advection of mass
+      // - update bubble volumes
+      // - marking interface cells for conversion
+      const StreamReconstructAdvectSweep< StorageSpecification_T, SweepCollection_T,
+                                          FlagField_T, FlagInfo<FlagField_T>,
+                                          ScalarField_T, VectorField_T >
+         streamReconstructAdvectSweep(sweepCollection_, surfaceTension_, fillFieldID_,
+                                      flagFieldID_, pdfFieldID_, normalFieldID_, curvatureFieldID_, flagInfo_,
+                                      pdfReconstructionModel_, useSimpleMassExchange_,
+                                      cellConversionThreshold_, cellConversionForceThreshold_);
+      // sweep acts only on blocks with at least one interface cell (due to StateSweep::fullFreeSurface)
+      timeloop.add()
+         << Sweep(streamReconstructAdvectSweep, "Sweep: StreamReconstructAdvect", StateSweep::fullFreeSurface)
+         << Sweep(emptySweep, "Empty sweep: StreamReconstructAdvect")
+         // do not communicate PDFs here:
+         // - stream on blocks with "StateSweep::fullFreeSurface" was performed here using post-collision PDFs
+         // - stream on other blocks is performed below and should also use post-collision PDFs
+         // => if PDFs were communicated here, the ghost layer of other blocks would have post-stream PDFs
+         << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, flagFieldID_),
+                          "Communication: after StreamReconstructAdvect sweep")
+         << AfterFunction(lbm_generated::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                          "Second ghost layer update: after StreamReconstructAdvect sweep (fill level field)")
+         << AfterFunction(lbm_generated::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_),
+                          "Second ghost layer update: after StreamReconstructAdvect sweep (flag field)");
+
+      timeloop.add() << Sweep(sweepCollection_.collide(), "Sweep: collision (generated)", StateSweep::fullFreeSurface)
+                     << Sweep(sweepCollection_.streamCollide(), "Sweep: streamCollide (generated)", StateSweep::onlyLBM)
+                     << Sweep(emptySweep, "Empty sweep: streamCollide (generated)")
+                     << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                      "Communication: after streamCollide (generated)");
+
+      // convert cells
+      // - according to the flags from StreamReconstructAdvectSweep (interface -> gas/liquid)
+      // - to ensure a closed layer of interface cells (gas/liquid -> interface)
+      // - detect and register bubble merges/splits (bubble volumes are already updated in StreamReconstructAdvectSweep)
+      // - convert cells and initialize PDFs near inflow boundaries
+      const CellConversionSweep< StorageSpecification_T, FlagField_T, ScalarField_T >
+         cellConvSweep(flagFieldID_, pdfFieldID_, flagInfo_);
+      timeloop.add() << Sweep(cellConvSweep, "Sweep: cell conversion", StateSweep::fullFreeSurface)
+                     << Sweep(emptySweep, "Empty sweep: cell conversion")
+                     << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                      "Communication: after cell conversion sweep (PDF field)")
+                     // communicate the flag field also in corner directions
+                     << AfterFunction(CommunicationCorner_T(blockForest_, flagFieldID_),
+                                      "Communication: after cell conversion sweep (flag field)")
+                     << AfterFunction(lbm_generated::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_),
+                                      "Second ghost layer update: after cell conversion sweep (flag field)");
+
+      // reinitialize PDFs, i.e., refill cells that were converted from gas to interface
+      // - when the flag "convertedFromGasToInterface" has been set (by CellConversionSweep)
+      // - according to the method specified with pdfRefillingModel_
+      switch (pdfRefillingModel_.getModelType())
+      { // the scope for each "case" is required since variables are defined within "case"
+      case PdfRefillingModel::RefillingModel::EquilibriumRefilling: {
+         const EquilibriumRefillingSweep< StorageSpecification_T, FlagField_T > equilibriumRefillingSweep(
+            pdfFieldID_, flagFieldID_, flagInfo_, true);
+         timeloop.add() << Sweep(equilibriumRefillingSweep, "Sweep: EquilibriumRefilling", StateSweep::fullFreeSurface)
+                        << Sweep(emptySweep, "Empty sweep: EquilibriumRefilling")
+                        << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                         "Communication: after EquilibriumRefilling sweep");
+         break;
+      }
+      case PdfRefillingModel::RefillingModel::AverageRefilling: {
+         const AverageRefillingSweep< StorageSpecification_T, FlagField_T > averageRefillingSweep(pdfFieldID_, flagFieldID_,
+                                                                                          flagInfo_, true);
+         timeloop.add() << Sweep(averageRefillingSweep, "Sweep: AverageRefilling", StateSweep::fullFreeSurface)
+                        << Sweep(emptySweep, "Empty sweep: AverageRefilling")
+                        << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                         "Communication: after AverageRefilling sweep");
+         break;
+      }
+      case PdfRefillingModel::RefillingModel::EquilibriumAndNonEquilibriumRefilling: {
+         // default: extrapolation order: 0
+         const EquilibriumAndNonEquilibriumRefillingSweep< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+            equilibriumAndNonEquilibriumRefillingSweep(pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo_, uint_c(0),
+                                                       true);
+         timeloop.add() << Sweep(equilibriumAndNonEquilibriumRefillingSweep,
+                                 "Sweep: EquilibriumAndNonEquilibriumRefilling sweep", StateSweep::fullFreeSurface)
+                        << Sweep(emptySweep, "Empty sweep: EquilibriumAndNonEquilibriumRefilling")
+                        << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                         "Communication: after EquilibriumAndNonEquilibriumRefilling sweep");
+         break;
+      }
+      case PdfRefillingModel::RefillingModel::ExtrapolationRefilling: {
+         // default: extrapolation order: 2
+         const ExtrapolationRefillingSweep< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+            extrapolationRefillingSweep(pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo_, uint_c(2), true);
+         timeloop.add() << Sweep(extrapolationRefillingSweep, "Sweep: ExtrapolationRefilling",
+                                 StateSweep::fullFreeSurface)
+                        << Sweep(emptySweep, "Empty sweep: ExtrapolationRefilling")
+                        << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                         "Communication: after ExtrapolationRefilling sweep");
+         break;
+      }
+      case PdfRefillingModel::RefillingModel::GradsMomentsRefilling: {
+         const GradsMomentsRefillingSweep< StorageSpecification_T, FlagField_T > gradsMomentRefillingSweep(
+            pdfFieldID_, flagFieldID_, flagInfo_, relaxationRate_, true);
+         timeloop.add() << Sweep(gradsMomentRefillingSweep, "Sweep: GradsMomentRefilling", StateSweep::fullFreeSurface)
+                        << Sweep(emptySweep, "Empty sweep: GradsMomentRefilling")
+                        << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                         "Communication: after GradsMomentRefilling sweep");
+         break;
+      }
+      default:
+         WALBERLA_ABORT("The specified pdf refilling model is not available.");
+      }
+
+      // distribute excess mass:
+      // - excess mass: mass that is free after conversion from interface to gas/liquid cells
+      // - update the bubble model
+      // IMPORTANT REMARK: this sweep computes the mass via the density, i.e., the PDF field must be up-to-date and the
+      // PdfRefillingSweep must have been performed
+      if (excessMassDistributionModel_.isEvenlyType())
+      {
+         const ExcessMassDistributionSweepInterfaceEvenly< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T >
+            distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo_);
+         timeloop.add() << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface)
+                        << Sweep(emptySweep, "Empty sweep: distribute excess mass")
+                        << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_),
+                                         "Communication: after excess mass distribution sweep")
+                        << AfterFunction(lbm_generated::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                              "Second ghost layer update: after excess mass distribution sweep (fill level field)");
+      }
+      else
+      {
+         if (excessMassDistributionModel_.isWeightedType())
+         {
+            const ExcessMassDistributionSweepInterfaceWeighted< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                                VectorField_T >
+               distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo_,
+                                   normalFieldID_);
+            timeloop.add() << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface)
+                           << Sweep(emptySweep, "Empty sweep: distribute excess mass")
+                           << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_),
+                                            "Communication: after excess mass distribution sweep")
+                           << AfterFunction(lbm_generated::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                                 "Second ghost layer update: after excess mass distribution sweep (fill level field)");
+         }
+         else
+         {
+            if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType())
+            {
+               const ExcessMassDistributionSweepInterfaceAndLiquid< StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                                    VectorField_T >
+                  distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo_,
+                                      excessMassFieldID_);
+               timeloop.add()
+                  // perform this sweep also on "onlylbm_generated" blocks because liquid cells also exchange excess mass here
+                  << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface)
+                  << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::onlyLBM)
+                  << Sweep(emptySweep, "Empty sweep: distribute excess mass")
+                  << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, excessMassFieldID_),
+                                   "Communication: after excess mass distribution sweep")
+                  << AfterFunction(lbm_generated::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                                   "Second ghost layer update: after excess mass distribution sweep (fill level field)");
+            }
+         }
+      }
+
+      // reset all flags that signal cell conversions (except "keepInterfaceForWettingFlag")
+      ConversionFlagsResetSweep< FlagField_T > resetConversionFlagsSweep(flagFieldID_, flagInfo_);
+      timeloop.add() << Sweep(resetConversionFlagsSweep, "Sweep: conversion flag reset", StateSweep::fullFreeSurface)
+                     << Sweep(emptySweep, "Empty sweep: conversion flag reset")
+                     << AfterFunction(CommunicationCorner_T(blockForest_, flagFieldID_),
+                                      "Communication: after excess mass distribution sweep")
+                     << AfterFunction(lbm_generated::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_),
+                                      "Second ghost layer update: after excess mass distribution sweep (flag field)");
+
+      // update block states
+      timeloop.add() << Sweep(blockStateUpdate, "Sweep: block state update");
+   }
+
+ private:
+   std::shared_ptr< StructuredBlockForest > blockForest_;
+
+   SweepCollection_T & sweepCollection_;
+   BoundaryCollection_T & boundaryCollection_;
+   FlagInfo_T& flagInfo_;
+
+   BlockDataID pdfFieldID_;
+   BlockDataID flagFieldID_;
+   BlockDataID fillFieldID_;
+   BlockDataID forceDensityFieldID_;
+
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID curvatureFieldID_;
+
+   PdfReconstructionModel pdfReconstructionModel_;
+   PdfRefillingModel pdfRefillingModel_;
+   ExcessMassDistributionModel excessMassDistributionModel_;
+   real_t relaxationRate_;
+   Vector3< real_t > globalAcceleration_;
+   real_t surfaceTension_;
+   bool useSimpleMassExchange_;
+   real_t cellConversionThreshold_;
+   real_t cellConversionForceThreshold_;
+
+   BlockDataID excessMassFieldID_ = BlockDataID();
+}; // class SurfaceDynamicsHandler
+
+} // namespace free_surface_generated
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/dynamics/functionality/AdvectMass.h b/src/lbm_generated/free_surface/dynamics/functionality/AdvectMass.h
new file mode 100644
index 0000000000000000000000000000000000000000..ad8ba33ba445256969070b28bc2263d22dd75efb
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/functionality/AdvectMass.h
@@ -0,0 +1,315 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file AdvectMass.h
+//! \ingroup dynamics
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Calculate the mass advection for a single interface cell.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/Abort.h"
+#include "core/logging/Logging.h"
+#include "core/timing/TimingPool.h"
+
+#include "domain_decomposition/IBlock.h"
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include <type_traits>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Calculate the mass advection for a single interface cell and returns the mass delta for this cell.
+ *
+ * With "useSimpleMassExchange==true", mass exchange is performed according to the paper from Koerner et al., 2005,
+ * equation (9). That is, mass is exchanged regardless of the cells' neighborhood.
+ *  Mass exchange of interface cell with
+ * - obstacle cell: no mass is exchanged
+ * - gas cell: no mass is exchanged
+ * - liquid cell: mass exchange is determined by the difference between incoming and outgoing PDFs
+ * - interface cell: The mass exchange is determined by the difference between incoming and outgoing PDFs weighted with
+ *                   the average fill level of both interface cells. This is basically a linear estimate of the wetted
+ *                   area between the two cells.
+ * - free slip cell: mass is exchanged with the cell where the mirrored PDFs are coming from
+ * - inflow cell: mass exchange as with liquid cell
+ * - outflow cell: mass exchange as with interface cell (outflow cell is assumed to have the same fill level)
+ *
+ * With  "useSimpleMassExchange==false", mass is exchanged according to the dissertation of N. Thuerey,
+ * 2007, sections 4.1 and 4.4, and table 4.1. However, here, the fill level field and density are used for the
+ * computations instead of storing an additional mass field.
+ * To ensure a single interface layer, an interface cell is
+ * - forced to empty if it has no fluid neighbors.
+ * - forced to fill if it has no gas neighbors.
+ * This is done by modifying the exchanged PDFs accordingly. If an interface cell is
+ * - forced to empty, only outgoing PDFs but no incoming PDFs are allowed.
+ * - forced to fill, only incoming PDFs but no outgoing PDFs are allowed.
+ * A more detailed description is available in the dissertation of N. Thuerey, 2007, section 4.4 and table 4.1.
+ *
+ * neighIt is an iterator pointing to a pre-computed flag field that contains the bitwise OR'ed neighborhood flags of
+ * the current cell. See free_surface::getOredNeighborhood for more information.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ConstScalarIt_T, typename ConstPdfIt_T,
+          typename ConstFlagIt_T, typename ConstNeighIt_T, typename FlagInfo_T >
+real_t advectMass(const FlagField_T* flagField, const ConstScalarIt_T& fillSrc, const ConstPdfIt_T& pdfFieldIt,
+                  const ConstFlagIt_T& flagFieldIt, const ConstNeighIt_T& neighIt, const FlagInfo_T& flagInfo,
+                  bool useSimpleMassExchange)
+{
+   using flag_c_t = typename std::remove_const< typename ConstFlagIt_T::value_type >::type;
+   using flag_n_t = typename std::remove_const< typename ConstNeighIt_T::value_type >::type;
+   using flag_i_t = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+   static_assert(std::is_same< flag_i_t, flag_c_t >::value && std::is_same< flag_i_t, flag_n_t >::value,
+                 "Flag types have to be equal.");
+
+   WALBERLA_ASSERT(flagInfo.isInterface(flagFieldIt), "Function advectMass() must only be called for interface cell.");
+
+   // determine the type of the current interface cell (similar to section 4.4 in the dissertation of N. Thuerey,
+   // 2007) neighIt is the pre-computed bitwise OR-ed neighborhood of the cell
+   bool localNoGasNeig   = !flagInfo.isGas(*neighIt); // this cell has no gas neighbor (should be converted to liquid)
+   bool localNoFluidNeig = !flagInfo.isLiquid(*neighIt); // this cell has no fluid neighbor (should be converted to gas)
+   bool localStandardCell = !(localNoGasNeig || localNoFluidNeig); // this cell has both gas and fluid neighbors
+
+   // evaluate flag of this cell (flagFieldIt) and not the neighborhood flags (neighIt)
+   bool localWettingCell = flagInfo.isKeepInterfaceForWetting(*flagFieldIt); // this cell should be kept for wetting
+
+   if (localNoFluidNeig && localNoGasNeig &&
+       !localWettingCell) // this cell has only interface neighbors (interface layer of 3 cells width)
+   {
+      // WALBERLA_LOG_WARNING("Interface layer of 3 cells width at cell " << fillSrc.cell());
+      // set this cell to standard for enabling regular mass exchange
+      localNoGasNeig    = false;
+      localNoFluidNeig  = false;
+      localStandardCell = true;
+   }
+
+   real_t deltaMass = real_c(0);
+   for (auto dir = StorageSpecification_T::Stencil::beginNoCenter(); dir != StorageSpecification_T::Stencil::end(); ++dir)
+   {
+      flag_c_t neighFlag = flagFieldIt.neighbor(*dir);
+
+      bool isFreeSlip = false; // indicates whether dir points to a free slip cell
+
+      // from the viewpoint of the current cell, direction where the PDFs are actually coming from when there is a free
+      // slip cell in direction dir; explicitly not a Cell object to emphasize that this denotes a direction
+      Vector3< cell_idx_t > freeSlipDir;
+
+      // determine type of cell where the mirrored PDFs at free slip boundary are coming from
+      if (flagInfo.isFreeSlip(neighFlag))
+      {
+         // REMARK: the following implementation is based on lbm/boundary/FreeSlip.h
+
+         // get components of inverse direction of dir
+         const int ix = stencil::cx[stencil::inverseDir[*dir]];
+         const int iy = stencil::cy[stencil::inverseDir[*dir]];
+         const int iz = stencil::cz[stencil::inverseDir[*dir]];
+
+         int wnx = 0; // compute "normal" vector of free slip wall
+         int wny = 0;
+         int wnz = 0;
+
+         // from the current cell, go into neighboring cell in direction dir and from there determine the type of the
+         // neighboring cell in ix, iy and iz direction
+         const auto flagFieldFreeSlipPtrX = typename FlagField_T::ConstPtr(
+            *flagField, flagFieldIt.x() + dir.cx() + ix, flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+         if (flagInfo.isLiquid(*flagFieldFreeSlipPtrX) || flagInfo.isInterface(*flagFieldFreeSlipPtrX)) { wnx = ix; }
+
+         const auto flagFieldFreeSlipPtrY = typename FlagField_T::ConstPtr(
+            *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy() + iy, flagFieldIt.z() + dir.cz());
+         if (flagInfo.isLiquid(*flagFieldFreeSlipPtrY) || flagInfo.isInterface(*flagFieldFreeSlipPtrY)) { wny = iy; }
+
+         const auto flagFieldFreeSlipPtrZ = typename FlagField_T::ConstPtr(
+            *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz() + iz);
+         if (flagInfo.isLiquid(*flagFieldFreeSlipPtrZ) || flagInfo.isInterface(*flagFieldFreeSlipPtrZ)) { wnz = iz; }
+
+         // flagFieldFreeSlipPtr denotes the cell from which the PDF is coming from
+         const auto flagFieldFreeSlipPtr =
+            typename FlagField_T::ConstPtr(*flagField, flagFieldIt.x() + dir.cx() + wnx,
+                                           flagFieldIt.y() + dir.cy() + wny, flagFieldIt.z() + dir.cz() + wnz);
+
+         // no mass must be exchanged if:
+         // - PDFs are not coming from liquid or interface cells
+         // - PDFs are mirrored from this cell (deltaPdf=0)
+         if ((!flagInfo.isLiquid(*flagFieldFreeSlipPtr) && !flagInfo.isInterface(*flagFieldFreeSlipPtr)) ||
+             flagFieldFreeSlipPtr.cell() == flagFieldIt.cell())
+         {
+            continue;
+         }
+         else
+         {
+            // update neighFlag such that it does contain the flags of the cell that mirrored at the free slip boundary;
+            // PDFs from the boundary cell can be used because they were correctly updated by the boundary handling
+            neighFlag = *flagFieldFreeSlipPtr;
+
+            // direction of the cell that is mirrored at the free slip boundary
+            freeSlipDir = Vector3< cell_idx_t >(cell_idx_c(dir.cx() + wnx), cell_idx_c(dir.cy() + wny),
+                                                cell_idx_c(dir.cz() + wnz));
+            isFreeSlip  = true;
+         }
+      }
+
+      // no mass exchange with gas and obstacle cells that are neither inflow nor outflow
+      if (flagInfo.isGas(neighFlag) ||
+          (flagInfo.isObstacle(neighFlag) && !flagInfo.isInflow(neighFlag) && !flagInfo.isOutflow(neighFlag)))
+      {
+         continue;
+      }
+
+      // PDF pointing from neighbor to current cell
+      real_t neighborPdf = real_c(0);
+      if (!isFreeSlip) { neighborPdf = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); }
+      else
+      {
+         // get PDF reflected at free slip boundary condition
+         stencil::Direction neighborPdfDir = *dir;
+
+         if (freeSlipDir[0] != cell_idx_c(0)) { neighborPdfDir = stencil::mirrorX[neighborPdfDir]; }
+         if (freeSlipDir[1] != cell_idx_c(0)) { neighborPdfDir = stencil::mirrorY[neighborPdfDir]; }
+         if (freeSlipDir[2] != cell_idx_c(0)) { neighborPdfDir = stencil::mirrorZ[neighborPdfDir]; }
+         neighborPdf = pdfFieldIt.neighbor(freeSlipDir[0], freeSlipDir[1], freeSlipDir[2],
+                                           StorageSpecification_T::Stencil::idx[neighborPdfDir]);
+      }
+
+      // PDF pointing to neighbor
+      const real_t localPdf = pdfFieldIt.getF(dir.toIdx());
+
+      // mass exchange with liquid cells (inflow cells are considered to be liquid)
+      if (flagInfo.isLiquid(neighFlag) || flagInfo.isInflow(neighFlag))
+      {
+         // mass exchange is difference between incoming and outgoing PDFs (see equation (9) in Koerner et al., 2005)
+         deltaMass += neighborPdf - localPdf;
+         continue;
+      }
+
+      // assert cells that are neither gas, obstacle nor interface
+      WALBERLA_ASSERT(flagInfo.isInterface(neighFlag) || flagInfo.isOutflow(neighFlag),
+                      "In cell " << fillSrc.cell() << ", flag of neighboring cell "
+                                 << Cell(fillSrc.x() + dir.cx(), fillSrc.y() + dir.cy(), fillSrc.z() + dir.cz())
+                                 << " is not plausible.");
+
+      // direction of the cell from which the PDFs are coming from
+      const Vector3< cell_idx_t > relevantDir =
+         isFreeSlip ? freeSlipDir :
+                      Vector3< cell_idx_t >(cell_idx_c(dir.cx()), cell_idx_c(dir.cy()), cell_idx_c(dir.cz()));
+
+      // determine the type of the neighboring cell (similar to section 4.4 in the dissertation of N. Thuerey, 2007)
+      bool neighborNoGasNeig    = !flagInfo.isGas(neighIt.neighbor(relevantDir[0], relevantDir[1], relevantDir[2]));
+      bool neighborNoFluidNeig  = !flagInfo.isLiquid(neighIt.neighbor(relevantDir[0], relevantDir[1], relevantDir[2]));
+      bool neighborStandardCell = !(neighborNoGasNeig || neighborNoFluidNeig);
+      bool neighborWettingCell  = flagInfo.isKeepInterfaceForWetting(flagFieldIt.neighbor(
+         relevantDir[0], relevantDir[1],
+         relevantDir[2])); // evaluate flag of this cell (flagFieldIt) and not the neighborhood flags (neighIt)
+
+      if (neighborNoGasNeig && neighborNoFluidNeig &&
+          !neighborWettingCell) // neighboring cell has only interface neighbors
+      {
+         // WALBERLA_LOG_WARNING("Interface layer of 3 cells width at cell " << fillSrc.cell());
+         //  set neighboring cell to standard for enabling regular mass exchange
+         neighborNoGasNeig    = false;
+         neighborNoFluidNeig  = false;
+         neighborStandardCell = true;
+      }
+
+      const real_t localFill    = *fillSrc;
+      const real_t neighborFill = fillSrc.neighbor(relevantDir[0], relevantDir[1], relevantDir[2]);
+
+      real_t fillAvg  = real_c(0);
+      real_t deltaPdf = real_c(0); // deltaMass = fillAvg * deltaPdf (see equation (9) in Koerner et al., 2005)
+
+      // both cells are interface cells (standard mass exchange)
+      // see paper of C. Koerner et al., 2005, equation (9)
+      // see dissertation of N. Thuerey, 2007, table 4.1: (standard at x) <-> (standard at x_nb);
+      if (useSimpleMassExchange || (localStandardCell && (neighborStandardCell || neighborWettingCell)) ||
+          (neighborStandardCell && (localStandardCell || localWettingCell)) || flagInfo.isOutflow(neighFlag))
+      {
+         if (flagInfo.isOutflow(neighFlag))
+         {
+            fillAvg = localFill; // use local fill level only, since outflow cells do not have a meaningful fill level
+         }
+         else { fillAvg = real_c(0.5) * (neighborFill + localFill); }
+
+         deltaPdf = neighborPdf - localPdf;
+      }
+      else
+      {
+         // see dissertation of N. Thuerey, 2007, table 4.1:
+         //    (standard at x) <-> (no empty neighbors at x_nb)
+         //    (no fluid neighbors at x) <-> (standard cell at x_nb)
+         //    (no fluid neighbors at x) <-> (no empty neighbors at x_nb)
+         // => push local, i.e., this cell empty (if it is not a cell needed for wetting)
+         if (((localStandardCell && neighborNoGasNeig) || (localNoFluidNeig && !neighborNoFluidNeig)) &&
+             !localWettingCell)
+         {
+            fillAvg  = real_c(0.5) * (neighborFill + localFill);
+            deltaPdf = -localPdf;
+         }
+         else
+         {
+            // see dissertation of N. Thuerey, 2007, table 4.1:
+            //    (standard at x) <-> (no fluid neighbors at x_nb)
+            //    (no empty neighbors at x) <-> (standard cell at x_nb)
+            //    (no empty neighbors at x) <-> (no fluid neighbors at x_nb)
+            // => push neighboring cell empty (if it is not a cell needed for wetting)
+            if (((localStandardCell && neighborNoFluidNeig) || (localNoGasNeig && !neighborNoGasNeig)) &&
+                !neighborWettingCell)
+            {
+               fillAvg  = real_c(0.5) * (neighborFill + localFill);
+               deltaPdf = neighborPdf;
+            }
+            else
+            {
+               // see dissertation of N. Thuerey, 2007, table 4.1:
+               //    (no fluid neighbors at x) <-> (no fluid neighbors at x_nb)
+               //    (no empty neighbors at x) <-> (no empty neighbors at x_nb)
+               if ((localNoFluidNeig && neighborNoFluidNeig) || (localNoGasNeig && neighborNoGasNeig))
+               {
+                  fillAvg  = real_c(0.5) * (neighborFill + localFill);
+                  deltaPdf = (neighborPdf - localPdf);
+               }
+               else
+               {
+                  // treat remaining cases that were not covered above and include wetting cells
+                  if (localWettingCell || neighborWettingCell)
+                  {
+                     fillAvg  = real_c(0.5) * (neighborFill + localFill);
+                     deltaPdf = (neighborPdf - localPdf);
+                  }
+                  else
+                  {
+                     WALBERLA_ABORT("Unknown mass advection combination of flags (loc=" << *flagFieldIt << ", neig="
+                                                                                        << neighFlag << ")");
+                  }
+               }
+            }
+         }
+      }
+      // this cell's deltaMass is the sum over all stencil directions (see dissertation of N. Thuerey, 2007, equation
+      // (4.4))
+      deltaMass += fillAvg * deltaPdf;
+   }
+
+   return deltaMass;
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/functionality/CMakeLists.txt b/src/lbm_generated/free_surface/dynamics/functionality/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..244965327dd3c2eba13dfb06559e47aeadc17c29
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/functionality/CMakeLists.txt
@@ -0,0 +1,8 @@
+target_sources( lbm_generated
+        PRIVATE
+        AdvectMass.h
+        FindInterfaceCellConversion.h
+        GetLaplacePressure.h
+        GetOredNeighborhood.h
+        ReconstructInterfaceCellABB.h
+        )
diff --git a/src/lbm_generated/free_surface/dynamics/functionality/FindInterfaceCellConversion.h b/src/lbm_generated/free_surface/dynamics/functionality/FindInterfaceCellConversion.h
new file mode 100644
index 0000000000000000000000000000000000000000..d8db238c583dd46ea854add9c0e401be40d061eb
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/functionality/FindInterfaceCellConversion.h
@@ -0,0 +1,257 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file FindInterfaceCellConversion.h
+//! \ingroup dynamics
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Find and mark interface cells for conversion to gas/liquid.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/debug/Debug.h"
+#include "core/logging/Logging.h"
+#include "core/timing/TimingPool.h"
+
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include <type_traits>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Finds interface cell conversions to gas/liquid and sets corresponding conversion flag that marks this cell for
+ * conversion.
+ *
+ * This version uses the cached OR'ed flag neighborhood given in neighborFlags. See free_surface::getOredNeighborhood.
+ *
+ * This function decides by looking at the fill level which interface cells should be converted to gas or liquid cells.
+ * It does not change the state directly, it only sets "conversion suggestions" by setting flags called
+ * 'convertToGasFlag' and 'convertToLiquidFlag'.
+ *
+ * An interface is first classified as one of the following types (same classification as in free_surface::advectMass)
+ * - _to gas_   : if cell has no liquid cell in neighborhood, this cell should become gas
+ * - _to liquid_: if cell has no gas cell in neighborhood, this cell should become liquid
+ * - _pure interface_: if not '_to gas_' and not '_to liquid_'
+ *
+ * This classification up to now depends only on the neighborhood flags, not on the fill level.
+ *
+ *  _Pure interface_ cells are marked for to-gas-conversion if their fill level is lower than
+ *  "0 - cellConversionThreshold" and marked for to-liquid-conversion if the fill level is higher than
+ *  "1 + cellConversionThreshold". The threshold is introduced to prevent oscillating cell conversions.
+ *  The value of the offset is chosen heuristically (see dissertation of N. Thuerey, 2007: 1e-3, dissertation of
+ *  T. Pohl, 2008: 1e-2).
+ *
+ * Additionally, interface cells without fluid neighbors are marked for conversion to gas if an:
+ * - interface cell is almost empty (fill < cellConversionForceThreshold) AND
+ * - interface cell has no neighboring interface cells
+ * OR
+ * - interface cell has neighboring cell with outflow boundary condition
+ *
+ * Similarly, interface cells without gas neighbors are marked for conversion to liquid if:
+ * - interface cell is almost full (fill > 1.0-cellConversionForceThreshold) AND
+ * - interface cell has no neighboring interface cells
+ *
+ * The value of cellConversionForceThreshold is chosen heuristically: 1e-1 (see dissertation of N. Thuerey, 2007,
+ * section 4.4).
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename ScalarField_T, typename FlagField_T,
+          typename ScalarIt_T, typename FlagIt_T >
+void findInterfaceCellConversion(const ScalarIt_T& fillFieldIt,
+                                 FlagIt_T& flagFieldIt, const typename FlagField_T::flag_t& neighborFlags,
+                                 const FlagInfo< FlagField_T >& flagInfo, real_t cellConversionThreshold,
+                                 real_t cellConversionForceThreshold)
+{
+   static_assert(std::is_same< typename FlagField_T::value_type, typename FlagIt_T::value_type >::value,
+                 "The given flagFieldIt does not seem to be an iterator of the provided FlagField_T.");
+
+   static_assert(std::is_floating_point< typename ScalarIt_T::value_type >::value,
+                 "The given fillFieldIt has to be a floating point value.");
+
+   cellConversionThreshold      = std::abs(cellConversionThreshold);
+   cellConversionForceThreshold = std::abs(cellConversionForceThreshold);
+
+   WALBERLA_ASSERT_LESS(cellConversionThreshold, cellConversionForceThreshold);
+
+   // in the neighborhood of inflow boundaries, convert gas cells to interface cells depending on the direction of the
+   // prescribed inflow velocity
+   // TODO implement again
+   /*
+   if (field::isFlagSet(neighborFlags, flagInfo.inflowFlagMask) && field::isFlagSet(flagFieldIt, flagInfo.gasFlag))
+   {
+      // get UBB inflow boundary
+      auto ubbInflow = handling->template getBoundaryCondition<
+         typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::UBB_Inflow_T >(
+         handling->getBoundaryUID(
+            FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowFlagID));
+
+      for (auto d = StorageSpecification_T::Stencil::beginNoCenter(); d != StorageSpecification_T::Stencil::end(); ++d)
+      {
+         if (field::isMaskSet(flagFieldIt.neighbor(*d), flagInfo.inflowFlagMask))
+         {
+            // get direction of cell containing inflow boundary
+            const Vector3< int > dir = Vector3< int >(-d.cx(), -d.cy(), -d.cz());
+
+            // get velocity from UBB inflow boundary
+            const Vector3< real_t > inflowVel =
+               ubbInflow.getValue(flagFieldIt.x() + d.cx(), flagFieldIt.y() + d.cy(), flagFieldIt.z() + d.cz());
+
+            // skip directions in which the corresponding velocity component is zero
+            if (realIsEqual(inflowVel[0], real_c(0), real_c(1e-14)) && dir[0] != 0) { continue; }
+            if (realIsEqual(inflowVel[1], real_c(0), real_c(1e-14)) && dir[1] != 0) { continue; }
+            if (realIsEqual(inflowVel[2], real_c(0), real_c(1e-14)) && dir[2] != 0) { continue; }
+
+            // skip directions in which the corresponding velocity component is in opposite direction
+            if (inflowVel[0] > real_c(0) && dir[0] < 0) { continue; }
+            if (inflowVel[1] > real_c(0) && dir[1] < 0) { continue; }
+            if (inflowVel[2] > real_c(0) && dir[2] < 0) { continue; }
+
+            // set conversion flag to remaining cells
+            field::addFlag(flagFieldIt, flagInfo.convertToInterfaceForInflowFlag);
+         }
+      }
+      return;
+   }*/
+
+   // only interface cells are converted directly (except for cells near inflow boundaries, see above)
+   if (!field::isFlagSet(flagFieldIt, flagInfo.interfaceFlag)) { return; }
+
+   // interface cell is empty and should be converted to gas
+   if (*fillFieldIt < -cellConversionThreshold)
+   {
+      if (field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag))
+      {
+         field::removeFlag(flagFieldIt, flagInfo.keepInterfaceForWettingFlag);
+      }
+
+      field::addFlag(flagFieldIt, flagInfo.convertToGasFlag);
+      return;
+   }
+
+   // interface cell is full and should be converted to liquid
+   if (*fillFieldIt > real_c(1.0) + cellConversionThreshold)
+   {
+      if (field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag))
+      {
+         field::removeFlag(flagFieldIt, flagInfo.keepInterfaceForWettingFlag);
+      }
+
+      field::addFlag(flagFieldIt, flagInfo.convertToLiquidFlag);
+      return;
+   }
+
+   // interface cell has no liquid neighbor and should be converted to gas (see dissertation of N. Thuerey, 2007
+   // section 4.4)
+   if (!field::isFlagSet(neighborFlags, flagInfo.liquidFlag) &&
+       !field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag) &&
+       !field::isFlagSet(neighborFlags, flagInfo.inflowFlagMask))
+   {
+      // interface cell is almost empty
+      if (*fillFieldIt < cellConversionForceThreshold && field::isFlagSet(neighborFlags, flagInfo.interfaceFlag))
+      {
+         // mass is not necessarily lost as it can be distributed to a neighboring interface cell
+         field::addFlag(flagFieldIt, flagInfo.convertToGasFlag);
+         return;
+      }
+
+      // interface cell has no other interface neighbors; conversion might lead to loss in mass (depending on the excess
+      // mass distribution model)
+      if (!field::isFlagSet(neighborFlags, flagInfo.interfaceFlag))
+      {
+         field::addFlag(flagFieldIt, flagInfo.convertToGasFlag);
+         return;
+      }
+   }
+
+   // interface cell has no gas neighbor and should be converted to liquid (see dissertation of N. Thuerey, 2007
+   // section 4.4)
+   if (!field::isFlagSet(neighborFlags, flagInfo.gasFlag) &&
+       !field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag))
+   {
+      // interface cell is almost full
+      if (*fillFieldIt > real_c(1.0) - cellConversionForceThreshold &&
+          field::isFlagSet(neighborFlags, flagInfo.interfaceFlag))
+      {
+         // mass is not necessarily gained as it can be taken from a neighboring interface cell
+         field::addFlag(flagFieldIt, flagInfo.convertToLiquidFlag);
+         return;
+      }
+
+      // interface cell has no other interface neighbors; conversion might lead to gain in mass (depending on the excess
+      // mass distribution model)
+      if (!field::isFlagSet(neighborFlags, flagInfo.interfaceFlag))
+      {
+         field::addFlag(flagFieldIt, flagInfo.convertToLiquidFlag);
+         return;
+      }
+   }
+}
+
+/***********************************************************************************************************************
+ * Triggers findInterfaceCellConversion() for each cell of the given fields and recomputes the OR'ed flag neighborhood
+ * info.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename ScalarField_T, typename FlagField_T >
+void findInterfaceCellConversions(const ScalarField_T* fillField,
+                                  FlagField_T* flagField, const FlagInfo< FlagField_T >& flagInfo,
+                                  real_t cellConversionThreshold, real_t cellConversionForceThreshold)
+{
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, {
+      const typename FlagField_T::Ptr flagFieldPtr(*flagField, x, y, z);
+      const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z);
+
+      if (field::isFlagSet(flagFieldPtr, flagInfo.interfaceFlag))
+      {
+         const typename FlagField_T::value_type neighborFlags =
+            field::getOredNeighborhood< typename StorageSpecification_T::Stencil >(flagFieldPtr);
+
+         (findInterfaceCellConversion< StorageSpecification_T, ScalarField_T,
+                                       FlagField_T >) (fillFieldPtr, flagFieldPtr, neighborFlags, flagInfo,
+                                                       cellConversionThreshold, cellConversionForceThreshold);
+      }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+/***********************************************************************************************************************
+ * Triggers findInterfaceCellConversion() for each cell of the given fields using the cached OR'ed flag neighborhood
+ * given in neighField.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename ScalarField_T, typename FlagField_T,
+          typename NeighField_T >
+void findInterfaceCellConversions(const ScalarField_T* fillField,
+                                  FlagField_T* flagField, const NeighField_T* neighField,
+                                  const FlagInfo< FlagField_T >& flagInfo, real_t cellConversionThreshold,
+                                  real_t cellConversionForceThreshold)
+{
+   WALBERLA_ASSERT_EQUAL_2(flagField->xyzSize(), fillField->xyzSize());
+   WALBERLA_ASSERT_EQUAL_2(flagField->xyzSize(), neighField->xyzSize());
+
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, {
+      const typename FlagField_T::Ptr flagFieldPtr(*flagField, x, y, z);
+      const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z);
+
+      (findInterfaceCellConversion< StorageSpecification_T, ScalarField_T,
+                                    FlagField_T >) (fillFieldPtr, flagFieldPtr, neighField->get(x, y, z),
+                                                    flagInfo, cellConversionThreshold, cellConversionForceThreshold);
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/functionality/GetLaplacePressure.h b/src/lbm_generated/free_surface/dynamics/functionality/GetLaplacePressure.h
new file mode 100644
index 0000000000000000000000000000000000000000..6f3f1bf1c7ad96b90ac2a9b0471b51bad713ad39
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/functionality/GetLaplacePressure.h
@@ -0,0 +1,61 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file GetLaplacePressure.h
+//! \ingroup dynamics
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute difference in density due to Laplace pressure.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/logging/Logging.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Compute difference in density due to Laplace pressure.
+ **********************************************************************************************************************/
+inline real_t computeDeltaRhoLaplacePressure(real_t sigma, real_t curvature, real_t maxDeltaRho = real_c(0.1))
+{
+   static const real_t inv_cs2  = real_c(3); // 1.0 / (cs * cs)
+   const real_t laplacePressure = real_c(2) * sigma * curvature;
+   real_t deltaRho              = inv_cs2 * laplacePressure;
+
+   if (deltaRho > maxDeltaRho)
+   {
+      WALBERLA_LOG_WARNING("Too large density variation of " << deltaRho << " due to laplacePressure "
+                                                             << laplacePressure << " with curvature " << curvature
+                                                             << ". Will be limited to " << maxDeltaRho << ".\n");
+      deltaRho = maxDeltaRho;
+   }
+   if (deltaRho < -maxDeltaRho)
+   {
+      WALBERLA_LOG_WARNING("Too large density variation of " << deltaRho << " due to laplacePressure "
+                                                             << laplacePressure << " with curvature " << curvature
+                                                             << ". Will be limited to " << -maxDeltaRho << ".\n");
+      deltaRho = -maxDeltaRho;
+   }
+
+   return deltaRho;
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/functionality/GetOredNeighborhood.h b/src/lbm_generated/free_surface/dynamics/functionality/GetOredNeighborhood.h
new file mode 100644
index 0000000000000000000000000000000000000000..28008819ba81eae7c1eb997b32ae778987ed91f4
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/functionality/GetOredNeighborhood.h
@@ -0,0 +1,62 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file GetOredNeighborhood.h
+//! \ingroup dynamics
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Combines the flags of all neighboring cells using bitwise OR.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/debug/Debug.h"
+#include "core/timing/TimingPool.h"
+
+#include "field/FlagField.h"
+
+#include <type_traits>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Combines the flags of all neighboring cells using bitwise OR.
+ * Flags are read from flagField and stored in neighborhoodFlagField. Every cell contains the bitwise OR of the
+ * neighboring flags in flagField.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T >
+void getOredNeighborhood(const FlagField_T* flagField, FlagField_T* neighborhoodFlagField)
+{
+   WALBERLA_ASSERT_GREATER_EQUAL(flagField->nrOfGhostLayers(), 2);
+   WALBERLA_ASSERT_EQUAL(neighborhoodFlagField->xyzSize(), flagField->xyzSize());
+   WALBERLA_ASSERT_EQUAL(neighborhoodFlagField->xyzAllocSize(), flagField->xyzAllocSize());
+
+   // REMARK: here is the reason why the flag field MUST have two ghost layers;
+   // the "OredNeighborhood" of the first ghost layer is determined, such that the first ghost layer's neighbors (i.e.,
+   // the second ghost layer) must be available
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(flagField, uint_c(1), {
+      const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+      const typename FlagField_T::Ptr neighborhoodFlagFieldPtr(*neighborhoodFlagField, x, y, z);
+
+      *neighborhoodFlagFieldPtr = field::getOredNeighborhood< Stencil_T >(flagFieldPtr);
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOSTLAYER_XYZ
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h b/src/lbm_generated/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h
new file mode 100644
index 0000000000000000000000000000000000000000..a88f914681f880b76c2092fccd9582886bc93d1d
--- /dev/null
+++ b/src/lbm_generated/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h
@@ -0,0 +1,439 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ReconstructInterfaceCellABB.h
+//! \ingroup dynamics
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Free surface boundary condition as in Koerner et al., 2005. Similar to anti-bounce-back pressure condition.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "lbm_generated/field/PdfField.h"
+#include "lbm_generated/free_surface/dynamics/PdfReconstructionModel.h"
+
+#include "stencil/Directions.h"
+
+#include "GetLaplacePressure.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+// get index of largest entry in n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false
+uint_t getIndexOfMaximum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable,
+                         const std::vector< real_t >& n_dot_ci);
+
+// get index of smallest entry in n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false
+uint_t getIndexOfMinimum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable,
+                         const std::vector< real_t >& n_dot_ci);
+
+// reconstruct PDFs according to pressure anti bounce back boundary condition (page 31, equation 4.5 in dissertation of
+// N. Thuerey, 2007)
+template< typename StorageSpecification_T, typename ConstPdfIt_T >
+inline real_t reconstructPressureAntiBounceBack(const stencil::Iterator< typename StorageSpecification_T::Stencil >& dir,
+                                                const ConstPdfIt_T& pdfFieldIt, const Vector3< real_t >& u,
+                                                real_t rhoGas, real_t dir_independent)
+{
+   const real_t vel = real_c(dir.cx()) * u[0] + real_c(dir.cy()) * u[1] + real_c(dir.cz()) * u[2];
+
+   // compute f^{eq}_i + f^{eq}_{\overline{i}} using rhoGas (without linear terms as they cancel out)
+   const real_t tmp =
+      real_c(2.0) * StorageSpecification_T::w[dir.toIdx()] * rhoGas * (dir_independent + real_c(4.5) * vel * vel);
+
+   // reconstruct PDFs (page 31, equation 4.5 in dissertation of N. Thuerey, 2007)
+   return tmp - pdfFieldIt.getF(dir.toIdx());
+}
+
+/***********************************************************************************************************************
+ * Free surface boundary condition as described in the publication of Koerner et al., 2005. Missing PDFs are
+ * reconstructed according to an anti-bounce-back pressure boundary condition at the free surface.
+ *
+ * Be aware that in Koerner et al., 2005, the PDFs are reconstructed in gas cells (neighboring to interface cells).
+ * These PDFs are then streamed into the interface cells in the LBM stream. Here, we directly reconstruct the PDFs in
+ * interface cells such that our implementation follows the notation in the dissertation of N. Thuerey, 2007, page 31.
+ * ********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ConstPdfIt_T, typename ConstFlagIt_T,
+          typename ConstVectorIt_T, typename OutputArray_T >
+void reconstructInterfaceCellLegacy(const FlagField_T* flagField, const ConstPdfIt_T& pdfFieldIt,
+                                    const ConstFlagIt_T& flagFieldIt, const ConstVectorIt_T& normalFieldIt,
+                                    const FlagInfo< FlagField_T >& flagInfo, const real_t rhoGas, OutputArray_T& f,
+                                    const PdfReconstructionModel& pdfReconstructionModel)
+{
+   using Stencil_T = typename StorageSpecification_T::Stencil;
+
+   // get velocity and density in interface cell
+   Vector3< real_t > u;
+   auto pdfField = dynamic_cast< const lbm_generated::PdfField< StorageSpecification_T >* >(pdfFieldIt.getField());
+   WALBERLA_ASSERT_NOT_NULLPTR(pdfField);
+   const real_t rho = lbm_generated::getDensityAndMomentumDensity<StorageSpecification_T>(u, *pdfField, pdfFieldIt.x(), pdfFieldIt.y(), pdfFieldIt.z());
+   u /= rho;
+
+   const real_t dir_independent =
+      real_c(1.0) - real_c(1.5) * u.sqrLength(); // direction independent value used for PDF reconstruction
+
+   // get type of the model that determines the PDF reconstruction
+   const PdfReconstructionModel::ReconstructionModel reconstructionModel = pdfReconstructionModel.getModelType();
+
+   // vector that stores the dot product between interface normal and lattice direction for each lattice direction
+   std::vector< real_t > n_dot_ci;
+
+   // vector that stores which index from loop "for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end();
+   // ++dir)" is currently available, i.e.:
+   // - reconstructed (or scheduled for reconstruction)
+   // - coming from boundary condition
+   // - available due to fluid or interface neighbor
+   std::vector< bool > isPdfAvailable;
+
+   // vector that stores which index from loop "for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end();
+   // ++dir)" points to a neighboring interface or fluid cell
+   std::vector< bool > isInterfaceOrLiquid;
+
+   // count number of reconstructed links
+   uint_t numReconstructed = uint_c(0);
+
+   for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir)
+   {
+      const auto neighborFlag = flagFieldIt.neighbor(*dir);
+
+      if (flagInfo.isObstacle(neighborFlag))
+      {
+         // free slip boundaries need special treatment because PDFs traveling from gas cells into the free slip
+         // boundary must be reconstructed, for instance:
+         // [I][G]  with I: interface cell; G: gas cell; f: free slip cell
+         // [f][f]
+         // During streaming, the interface cell's PDF with direction (-1, 1) is coming from the right free slip cell.
+         // For a free slip boundary, this PDF is identical to the PDF with direction (-1, -1) in the gas cell. Since
+         // gas-side PDFs are not available, such PDFs must be reconstructed.
+         // Non-gas cells do not need to be treated here as they are treated correctly by the boundary handling.
+
+         if (flagInfo.isFreeSlip(neighborFlag))
+         {
+            // REMARK: the following implementation is based on lbm/boundary/FreeSlip.h
+
+            // get components of inverse direction of dir
+            const int ix = stencil::cx[stencil::inverseDir[*dir]];
+            const int iy = stencil::cy[stencil::inverseDir[*dir]];
+            const int iz = stencil::cz[stencil::inverseDir[*dir]];
+
+            int wnx = 0; // compute "normal" vector of free slip wall
+            int wny = 0;
+            int wnz = 0;
+
+            // from the current cell, go into neighboring cell in direction dir and from there check whether the
+            // neighbors in ix, iy and iz are gas cells
+            const auto flagFieldFreeSlipPtrX = typename FlagField_T::ConstPtr(
+               *flagField, flagFieldIt.x() + dir.cx() + ix, flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+            if (flagInfo.isGas(*flagFieldFreeSlipPtrX)) { wnx = ix; }
+
+            const auto flagFieldFreeSlipPtrY = typename FlagField_T::ConstPtr(
+               *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy() + iy, flagFieldIt.z() + dir.cz());
+            if (flagInfo.isGas(*flagFieldFreeSlipPtrY)) { wny = iy; }
+
+            const auto flagFieldFreeSlipPtrZ = typename FlagField_T::ConstPtr(
+               *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz() + iz);
+            if (flagInfo.isGas(*flagFieldFreeSlipPtrZ)) { wnz = iz; }
+
+            if (wnx != 0 || wny != 0 || wnz != 0)
+            {
+               // boundaryNeighbor denotes the cell from which the PDF is coming from
+               const auto flagFieldFreeSlipPtr =
+                  typename FlagField_T::ConstPtr(*flagField, flagFieldIt.x() + dir.cx() + wnx,
+                                                 flagFieldIt.y() + dir.cy() + wny, flagFieldIt.z() + dir.cz() + wnz);
+
+               if (flagInfo.isGas(*flagFieldFreeSlipPtr))
+               {
+                  // reconstruct PDF
+                  f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< StorageSpecification_T, ConstPdfIt_T >(
+                     dir, pdfFieldIt, u, rhoGas, dir_independent);
+                  isPdfAvailable.push_back(true);
+                  isInterfaceOrLiquid.push_back(false);
+                  n_dot_ci.push_back(
+                     real_c(0)); // dummy entry for having index as in vectors isPDFAvailable and isInterfaceOrLiquid
+                  ++numReconstructed;
+                  continue;
+               }
+               // else: do nothing here, i.e., make usual obstacle boundary treatment below
+            } // else: concave corner, all surrounding PDFs are known, i.e., make usual obstacle boundary treatment
+              // below
+         }
+
+         f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); // use PDFs defined by boundary handling
+         isPdfAvailable.push_back(true);
+         isInterfaceOrLiquid.push_back(false);
+         n_dot_ci.push_back(
+            real_c(0)); // dummy entry for having same indices as in vectors isPDFAvailable and isInterfaceOrLiquid
+         continue;
+      }
+      else
+      {
+         if (flagInfo.isGas(neighborFlag))
+         {
+            f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< StorageSpecification_T, ConstPdfIt_T >(
+               dir, pdfFieldIt, u, rhoGas, dir_independent);
+            isPdfAvailable.push_back(true);
+            isInterfaceOrLiquid.push_back(false);
+            n_dot_ci.push_back(
+               real_c(0)); // dummy entry for having index as in vectors isPDFAvailable and isInterfaceOrLiquid
+            ++numReconstructed;
+            continue;
+         }
+      }
+
+      // dot product between interface normal and lattice direction
+      real_t dotProduct = normalFieldIt.getF(0) * real_c(dir.cx()) + normalFieldIt.getF(1) * real_c(dir.cy()) + normalFieldIt.getF(2) * real_c(dir.cz());
+
+      // avoid numerical inaccuracies in the computation of the scalar product n_dot_ci
+      if (realIsEqual(dotProduct, real_c(0), real_c(1e-14))) { dotProduct = real_c(0); }
+
+      // approach from Koerner; reconstruct all PDFs in direction opposite to interface normal and center PDF (not
+      // stated in the paper explicitly but follows from n*e_i>=0 for i=0)
+      if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedReconstructCenter)
+      {
+         if (dotProduct >= real_c(0))
+         {
+            f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< StorageSpecification_T, ConstPdfIt_T >(
+               dir, pdfFieldIt, u, rhoGas, dir_independent);
+         }
+         else
+         {
+            // regular LBM stream with PDFs from neighbor
+            f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx());
+         }
+         continue;
+      }
+
+      if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedKeepCenter)
+      {
+         if (*dir == stencil::C)
+         {
+            // use old center PDF
+            f[Stencil_T::idx[stencil::C]] = pdfFieldIt[Stencil_T::idx[stencil::C]];
+         }
+         else
+         {
+            if (dotProduct >= real_c(0))
+            {
+               f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< StorageSpecification_T, ConstPdfIt_T >(
+                  dir, pdfFieldIt, u, rhoGas, dir_independent);
+            }
+            else
+            {
+               // regular LBM stream with PDFs from neighbor
+               f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx());
+            }
+         }
+         continue;
+      }
+
+      // reconstruct all non-obstacle PDFs, including those that come from liquid and are already known
+      if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::All)
+      {
+         f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< StorageSpecification_T, ConstPdfIt_T >(dir, pdfFieldIt, u,
+                                                                                               rhoGas, dir_independent);
+         continue;
+      }
+
+      // reconstruct only those gas-side PDFs that are really missing
+      if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissing)
+      {
+         // regular LBM stream with PDFs from neighboring interface or liquid cell
+         f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx());
+         continue;
+      }
+
+      // reconstruct only those gas-side PDFs that are really missing but make sure that at least a
+      // specified number of PDFs are reconstructed (even if available PDFs are overwritten)
+      if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin)
+      {
+         isPdfAvailable.push_back(false); // PDF has not yet been marked for reconstruction
+         isInterfaceOrLiquid.push_back(true);
+         n_dot_ci.push_back(dotProduct);
+         continue;
+      }
+
+      WALBERLA_ABORT("Unknown pdfReconstructionModel.")
+   }
+
+   if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin)
+   {
+      WALBERLA_ASSERT_EQUAL(Stencil_T::Size, uint_c(n_dot_ci.size()));
+      WALBERLA_ASSERT_EQUAL(Stencil_T::Size, uint_c(isInterfaceOrLiquid.size()));
+      WALBERLA_ASSERT_EQUAL(Stencil_T::Size, uint_c(isPdfAvailable.size()));
+
+      const uint_t numMinReconstruct = pdfReconstructionModel.getNumMinReconstruct();
+
+      // number of remaining PDFs that need to be reconstructed according to the specified model (number can be negative
+      // => do not use uint_t)
+      int numRemainingReconstruct = int_c(numMinReconstruct) - int_c(numReconstructed);
+
+      // count the number of neighboring cells that are interface or liquid (and not obstacle or gas)
+      const uint_t numLiquidNeighbors =
+         uint_c(std::count_if(isInterfaceOrLiquid.begin(), isInterfaceOrLiquid.end(), [](bool a) { return a; }));
+
+      // // REMARK: this was commented because it regularly occurred in practical simulations (e.g. BreakingDam)
+      //    if (numRemainingReconstruct > int_c(0) && numRemainingReconstruct < int_c(numLiquidNeighbors))
+      //    {
+      //       // There are less neighboring liquid and interface cells than needed to reconstruct PDFs in the
+      //       // free surface boundary condition. You have probably specified a large minimum number of PDFs to be
+      //       // reconstructed. This happens e.g. near solid boundaries (especially corners). There, the number of
+      //       // surrounding non-obstacle cells might be less than the number of PDFs that you want to have
+      //       // reconstructed.
+      //       WALBERLA_LOG_WARNING_ON_ROOT("Less PDFs reconstructed in cell "
+      //                                     << pdfFieldIt.cell()
+      //                                     << " than specified in the PDF reconstruction model. See comment in "
+      //                                        "source code of ReconstructInterfaceCellABB.h for further information. "
+      //                                        "Here, as many PDFs as possible are reconstructed now.");
+      //    }
+
+      // count additionally reconstructed PDFs (that come from interface or liquid)
+      uint_t numAdditionalReconstructed = uint_c(0);
+
+      // define which PDFs to additionally reconstruct (overwrite known information) according to the specified model
+      while (numRemainingReconstruct > int_c(0) && numAdditionalReconstructed < numLiquidNeighbors)
+      {
+         if (pdfReconstructionModel.getFallbackModel() == PdfReconstructionModel::FallbackModel::Largest)
+         {
+            // get index of largest n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false
+            const uint_t maxIndex    = getIndexOfMaximum(isInterfaceOrLiquid, isPdfAvailable, n_dot_ci);
+            isPdfAvailable[maxIndex] = true; // reconstruct this PDF later
+            ++numReconstructed;
+            ++numAdditionalReconstructed;
+         }
+         else
+         {
+            if (pdfReconstructionModel.getFallbackModel() == PdfReconstructionModel::FallbackModel::Smallest)
+            {
+               // get index of smallest n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false
+               const uint_t minIndex    = getIndexOfMinimum(isInterfaceOrLiquid, isPdfAvailable, n_dot_ci);
+               isPdfAvailable[minIndex] = true; // reconstruct this PDF later
+               ++numReconstructed;
+               ++numAdditionalReconstructed;
+            }
+            else
+            {
+               // use approach from Koerner
+               if (pdfReconstructionModel.getFallbackModel() ==
+                   PdfReconstructionModel::FallbackModel::NormalBasedKeepCenter)
+               {
+                  uint_t index = uint_c(0);
+                  for (const real_t& value : n_dot_ci)
+                  {
+                     if (value >= real_c(0) && index > uint_c(1)) // skip center PDF with index=0
+                     {
+                        isPdfAvailable[index] = true; // reconstruct this PDF later
+                     }
+
+                     ++index;
+                  }
+                  break; // exit while loop
+               }
+               else { WALBERLA_ABORT("Unknown fallbackModel in pdfReconstructionModel.") }
+            }
+         }
+
+         numRemainingReconstruct = int_c(numMinReconstruct) - int_c(numReconstructed);
+      }
+
+      // reconstruct additional PDFs
+      uint_t index = uint_c(0);
+      for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir, ++index)
+      {
+         const auto neighborFlag = flagFieldIt.neighbor(*dir);
+
+         // skip links pointing to obstacle and gas neighbors; they were treated above already
+         if (flagInfo.isObstacle(neighborFlag) || flagInfo.isGas(neighborFlag)) { continue; }
+         else
+         {
+            // reconstruct links that were marked for reconstruction
+            if (isPdfAvailable[index] && isInterfaceOrLiquid[index])
+            {
+               f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< StorageSpecification_T, ConstPdfIt_T >(
+                  dir, pdfFieldIt, u, rhoGas, dir_independent);
+            }
+            else
+            {
+               if (!isPdfAvailable[index] && isInterfaceOrLiquid[index])
+               {
+                  // regular LBM stream with PDFs from neighbor
+                  f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx());
+                  continue;
+               }
+               WALBERLA_ABORT("Error in PDF reconstruction. This point should never be reached.")
+            }
+         }
+      }
+   }
+}
+
+uint_t getIndexOfMaximum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable,
+                         const std::vector< real_t >& n_dot_ci)
+{
+   real_t maximum = -std::numeric_limits< real_t >::max();
+   uint_t index   = std::numeric_limits< uint_t >::max();
+
+   for (uint_t i = uint_c(0); i != isInterfaceOrLiquid.size(); ++i)
+   {
+      if (isInterfaceOrLiquid[i] && !isPdfAvailable[i])
+      {
+         const real_t absValue = std::abs(n_dot_ci[i]);
+         if (absValue > maximum)
+         {
+            maximum = absValue;
+            index   = i;
+         }
+      }
+   }
+
+   // less Pdfs available for being reconstructed than specified by the user; these assertions should never fail, as the
+   // conditionals in reconstructInterfaceCellLegacy() should avoid calling this function
+   WALBERLA_ASSERT(maximum > -real_c(std::numeric_limits< real_t >::min()));
+   WALBERLA_ASSERT(index != std::numeric_limits< uint_t >::max());
+
+   return index;
+}
+
+uint_t getIndexOfMinimum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable,
+                         const std::vector< real_t >& n_dot_ci)
+{
+   real_t minimum = std::numeric_limits< real_t >::max();
+   uint_t index   = std::numeric_limits< uint_t >::max();
+
+   for (uint_t i = uint_c(0); i != isInterfaceOrLiquid.size(); ++i)
+   {
+      if (isInterfaceOrLiquid[i] && !isPdfAvailable[i])
+      {
+         const real_t absValue = std::abs(n_dot_ci[i]);
+         if (absValue < minimum)
+         {
+            minimum = absValue;
+            index   = i;
+         }
+      }
+   }
+
+   // fewer PDFs available for being reconstructed than specified by the user; these assertions should never fail, as
+   // the conditionals in reconstructInterfaceCellLegacy() should avoid calling this function
+   WALBERLA_ASSERT(minimum < real_c(std::numeric_limits< real_t >::max()));
+   WALBERLA_ASSERT(index != std::numeric_limits< uint_t >::max());
+
+   return index;
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/CMakeLists.txt b/src/lbm_generated/free_surface/surface_geometry/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..daa509840ebebf9dd176593797702f74d770844b
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/CMakeLists.txt
@@ -0,0 +1,22 @@
+target_sources( lbm_generated
+        PRIVATE
+        ContactAngle.h
+        CurvatureModel.h
+        CurvatureModel.impl.h
+        CurvatureSweep.h
+        CurvatureSweep.impl.h
+        DetectWettingSweep.h
+        ExtrapolateNormalsSweep.h
+        ExtrapolateNormalsSweep.impl.h
+        NormalSweep.h
+        NormalSweep.impl.h
+        ObstacleFillLevelSweep.h
+        ObstacleFillLevelSweep.impl.h
+        ObstacleNormalSweep.h
+        ObstacleNormalSweep.impl.h
+        SmoothingSweep.h
+        SmoothingSweep.impl.h
+        SurfaceGeometryHandler.h
+        Utility.cpp
+        Utility.h
+        )
diff --git a/src/lbm_generated/free_surface/surface_geometry/ContactAngle.h b/src/lbm_generated/free_surface/surface_geometry/ContactAngle.h
new file mode 100644
index 0000000000000000000000000000000000000000..641f4282e06babcd0eb1b59718f7984437860f10
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ContactAngle.h
@@ -0,0 +1,54 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ContactAngle.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Class to avoid re-computing sine and cosine of the contact angle.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Constants.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Class to avoid re-computing sine and cosine of the contact angle.
+ **********************************************************************************************************************/
+class ContactAngle
+{
+ public:
+   ContactAngle(real_t angleInDegrees)
+      : angleDegrees_(angleInDegrees), angleRadians_(math::pi / real_c(180) * angleInDegrees),
+        sinAngle_(std::sin(angleRadians_)), cosAngle_(std::cos(angleRadians_))
+   {}
+
+   inline real_t getInDegrees() const { return angleDegrees_; }
+   inline real_t getInRadians() const { return angleRadians_; }
+   inline real_t getSin() const { return sinAngle_; }
+   inline real_t getCos() const { return cosAngle_; }
+
+ private:
+   real_t angleDegrees_;
+   real_t angleRadians_;
+   real_t sinAngle_;
+   real_t cosAngle_;
+}; // class ContactAngle
+} // namespace free_surface_generated
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/CurvatureModel.h b/src/lbm_generated/free_surface/surface_geometry/CurvatureModel.h
new file mode 100644
index 0000000000000000000000000000000000000000..08c8b7e58fdd85d269c2a3b6c709da855b166408
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/CurvatureModel.h
@@ -0,0 +1,84 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file CurvatureModel.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Collection of sweeps required for using a specific curvature model.
+//
+//======================================================================================================================
+
+#pragma once
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+// forward declaration
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T >
+class SurfaceGeometryHandler;
+
+namespace curvature_model
+{
+/***********************************************************************************************************************
+ * Collection of sweeps for computing the curvature using a finite difference-based (Parker-Youngs) approach according
+ * to:
+ * dissertation of S. Bogner, 2017 (section 4.4.2.1)
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T,typename VectorField_T, typename FlagInfo_T >
+class FiniteDifferenceMethod
+{
+ private:
+   using SurfaceGeometryHandler_T = SurfaceGeometryHandler< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >;
+
+ public:
+   void addSweeps(SweepTimeloop& timeloop, const SurfaceGeometryHandler_T& geometryHandler);
+}; // class FiniteDifferenceMethod
+
+/***********************************************************************************************************************
+ * Collection of sweeps for computing the curvature using local triangulation according to:
+ * - dissertation of T. Pohl, 2008 (section 2.5)
+ * - dissertation of S. Donath, 2011 (wetting model, section 6.3.3)
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T >
+class LocalTriangulation
+{
+ private:
+   using SurfaceGeometryHandler_T = SurfaceGeometryHandler< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >;
+
+ public:
+   void addSweeps(SweepTimeloop& timeloop, const SurfaceGeometryHandler_T& geometryHandler);
+}; // class LocalTriangulation
+
+/***********************************************************************************************************************
+ * Collection of sweeps for computing the curvature with a simplistic finite difference method. This approach is not
+ * documented in literature and neither thoroughly tested or validated.
+ * Use it with caution and preferably for testing purposes only.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T >
+class SimpleFiniteDifferenceMethod
+{
+ private:
+   using SurfaceGeometryHandler_T = SurfaceGeometryHandler< StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >;
+
+ public:
+   void addSweeps(SweepTimeloop& timeloop, const SurfaceGeometryHandler_T& geometryHandler);
+}; // class SimpleFiniteDifferenceMethod
+
+} // namespace curvature_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "CurvatureModel.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/CurvatureModel.impl.h b/src/lbm_generated/free_surface/surface_geometry/CurvatureModel.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..7a1b4f07901605cca2a7e8a99b5942c3ee11ce21
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/CurvatureModel.impl.h
@@ -0,0 +1,258 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file CurvatureModel.impl.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Collection of sweeps required for using a specific curvature model.
+//
+//======================================================================================================================
+
+#include "lbm_generated/blockforest/UpdateSecondGhostLayer.h"
+
+#include "CurvatureModel.h"
+#include "CurvatureSweep.h"
+#include "DetectWettingSweep.h"
+#include "ExtrapolateNormalsSweep.h"
+#include "NormalSweep.h"
+#include "ObstacleFillLevelSweep.h"
+#include "ObstacleNormalSweep.h"
+#include "SmoothingSweep.h"
+#include "SurfaceGeometryHandler.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+namespace curvature_model
+{
+// empty sweep required for using selectors (e.g. StateSweep::fullFreeSurface)
+struct emptySweep
+{
+   void operator()(IBlock*) {}
+};
+
+template< typename Stencil_T, typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T>
+void FiniteDifferenceMethod< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >::addSweeps(
+   SweepTimeloop& timeloop, const FiniteDifferenceMethod::SurfaceGeometryHandler_T& geometryHandler)
+{
+   using Communication_T = typename SurfaceGeometryHandler_T::Communication_T;
+   using StateSweep      = typename SurfaceGeometryHandler_T::StateSweep;
+
+   // layout for allocating the smoothed fill level field
+   field::Layout fillFieldLayout = field::fzyx;
+
+   // check if an obstacle cell is in a non-periodic outermost global ghost layer; used to check if two ghost layers are
+   // required for the fill level field
+   const Vector3< bool > isObstacleInGlobalGhostLayerXYZ = geometryHandler.isObstacleInGlobalGhostLayer();
+
+   bool isObstacleInGlobalGhostLayer = false;
+   if ((!geometryHandler.blockForest_->isXPeriodic() && isObstacleInGlobalGhostLayerXYZ[0]) ||
+       (!geometryHandler.blockForest_->isYPeriodic() && isObstacleInGlobalGhostLayerXYZ[1]) ||
+       (!geometryHandler.blockForest_->isZPeriodic() && isObstacleInGlobalGhostLayerXYZ[2]))
+   {
+      isObstacleInGlobalGhostLayer = true;
+   }
+
+   for (auto blockIt = geometryHandler.blockForest_->begin(); blockIt != geometryHandler.blockForest_->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(geometryHandler.fillFieldID_);
+      // check if two ghost layers are required for the fill level field
+      if (isObstacleInGlobalGhostLayer && fillField->nrOfGhostLayers() < uint_c(2) && geometryHandler.enableWetting_)
+      {
+         WALBERLA_ABORT(
+            "With wetting enabled, the curvature computation with the finite difference method requires two ghost "
+            "layers in the fill level field whenever solid obstacles are located in a global outermost ghost layer. "
+            "For more information, see the remark in the description of ObstacleFillLevelSweep.h");
+      }
+
+      // get layout of fill level field (to be used in allocating the smoothed fill level field; cloning would
+      // waste memory, as the fill level field might have two ghost layers, whereas the smoothed fill level field needs
+      // only one ghost layer)
+      fillFieldLayout = fillField->layout();
+   }
+
+   // IMPORTANT REMARK: ObstacleNormalSweep and ObstacleFillLevelSweep must be executed on all blocks, because the
+   // SmoothingSweep requires meaningful values in the ghost layers.
+
+   // add field for smoothed fill levels
+   BlockDataID smoothFillFieldID = field::addToStorage< ScalarField_T >(
+      geometryHandler.blockForest_, "Smooth fill level field", real_c(0), fillFieldLayout, uint_c(1));
+
+   if (geometryHandler.enableWetting_)
+   {
+      // compute obstacle normals
+      ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstacleNormalSweep(
+         geometryHandler.obstacleNormalFieldID_, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID,
+         flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, false, true, true);
+      timeloop.add() << Sweep(obstacleNormalSweep, "Sweep: obstacle normal computation")
+                     << AfterFunction(
+                           Communication_T(geometryHandler.blockForest_, geometryHandler.obstacleNormalFieldID_),
+                           "Communication: after obstacle normal sweep");
+
+      // reflect fill level into obstacle cells such that they can be used for smoothing the fill level field and for
+      // computing the interface normal; MUST be performed BEFORE SmoothingSweep
+      ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > obstacleFillLevelSweep(
+         smoothFillFieldID, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_,
+         geometryHandler.obstacleNormalFieldID_, flagIDs::liquidInterfaceGasFlagIDs,
+         geometryHandler.obstacleFlagIDSet_);
+      timeloop.add() << Sweep(obstacleFillLevelSweep, "Sweep: obstacle fill level computation")
+                     << AfterFunction(Communication_T(geometryHandler.blockForest_, smoothFillFieldID),
+                                      "Communication: after obstacle fill level sweep");
+   }
+
+   // smooth fill level field for decreasing error in finite difference normal and curvature computation (see
+   // dissertation of S. Bogner, 2017 (section 4.4.2.1))
+   SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep(
+      smoothFillFieldID, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, flagIDs::liquidInterfaceGasFlagIDs,
+      geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_);
+   // IMPORTANT REMARK: SmoothingSweep must be executed on all blocks, because the algorithm works on all liquid,
+   // interface and gas cells. This is necessary since the normals are not only computed in interface cells, but also in
+   // the neighborhood of interface cells. Therefore, meaningful values for the fill levels of the second neighbors of
+   // interface cells are also required in NormalSweep.
+   timeloop.add() << Sweep(smoothingSweep, "Sweep: fill level smoothing")
+                  << AfterFunction(Communication_T(geometryHandler.blockForest_, smoothFillFieldID),
+                                   "Communication: after smoothing sweep");
+
+   // compute interface normals (using smoothed fill level field)
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalSweep(
+      geometryHandler.normalFieldID_, smoothFillFieldID, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID,
+      flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, true, geometryHandler.enableWetting_,
+      true, geometryHandler.enableWetting_);
+   timeloop.add() << Sweep(normalSweep, "Sweep: normal computation", StateSweep::fullFreeSurface)
+                  << Sweep(emptySweep(), "Empty sweep: normal")
+                  << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_),
+                                   "Communication: after normal sweep");
+
+   if (geometryHandler.computeCurvature_)
+   {
+      // compute interface curvature using finite differences according to Brackbill et al.
+      CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvSweep(
+         geometryHandler.curvatureFieldID_, geometryHandler.normalFieldID_, geometryHandler.obstacleNormalFieldID_,
+         geometryHandler.flagFieldID_, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+         geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_, geometryHandler.contactAngle_);
+      timeloop.add() << Sweep(curvSweep, "Sweep: curvature computation (finite difference method)",
+                              StateSweep::fullFreeSurface)
+                     << Sweep(emptySweep(), "Empty sweep: curvature")
+                     << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.curvatureFieldID_),
+                                      "Communication: after curvature sweep");
+   }
+}
+
+template< typename Stencil_T, typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T>
+void LocalTriangulation< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >::addSweeps(
+   SweepTimeloop& timeloop, const LocalTriangulation::SurfaceGeometryHandler_T& geometryHandler)
+{
+   using Communication_T = typename SurfaceGeometryHandler_T::Communication_T;
+   using StateSweep      = typename SurfaceGeometryHandler_T::StateSweep;
+
+   // compute interface normals
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalSweep(
+      geometryHandler.normalFieldID_, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_,
+      flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, false, false,
+      true, false);
+   timeloop.add() << Sweep(normalSweep, "Sweep: normal computation", StateSweep::fullFreeSurface)
+                  << Sweep(emptySweep(), "Empty sweep: normal")
+                  << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_),
+                                   "Communication: after normal sweep");
+
+   // compute obstacle normals
+   ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstacleNormalSweep(
+      geometryHandler.obstacleNormalFieldID_, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID,
+      flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, true, false, false);
+   timeloop.add() << Sweep(obstacleNormalSweep, "Sweep: obstacle normal computation", StateSweep::fullFreeSurface)
+                  << Sweep(emptySweep(), "Empty sweep: obstacle normal")
+                  << AfterFunction(
+                        Communication_T(geometryHandler.blockForest_, geometryHandler.obstacleNormalFieldID_),
+                        "Communication: after obstacle normal sweep");
+
+   if (geometryHandler.computeCurvature_)
+   {
+      // compute interface curvature using local triangulation according to dissertation of T. Pohl, 2008
+      CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvSweep(
+         geometryHandler.blockForest_, geometryHandler.curvatureFieldID_, geometryHandler.normalFieldID_,
+         geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, geometryHandler.obstacleNormalFieldID_,
+         flagIDs::interfaceFlagID, geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_,
+         geometryHandler.contactAngle_);
+      timeloop.add() << Sweep(curvSweep, "Sweep: curvature computation (local triangulation)",
+                              StateSweep::fullFreeSurface)
+                     << Sweep(emptySweep(), "Empty sweep: curvature")
+                     << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.curvatureFieldID_),
+                                      "Communication: after curvature sweep");
+   }
+
+   // sweep for detecting cells that need to be converted to interface cells for continuing the wetting
+   // surface correctly
+   // IMPORTANT REMARK: this MUST NOT be performed when using finite differences for curvature computation and can
+   // otherwise lead to instabilities and errors
+   if (geometryHandler.enableWetting_)
+   {
+      DetectWettingSweep<Stencil_T, FlagField_T, ScalarField_T, VectorField_T >
+         detWetSweep(geometryHandler.flagFieldID_, geometryHandler.getFlagInfo(),
+                     geometryHandler.normalFieldID_, geometryHandler.fillFieldID_);
+      timeloop.add() << Sweep(detWetSweep, "Sweep: wetting detection", StateSweep::fullFreeSurface)
+                     << Sweep(emptySweep(), "Empty sweep: wetting detection")
+                     << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.flagFieldID_),
+                                      "Communication: after wetting detection sweep")
+                     << AfterFunction(lbm_generated::UpdateSecondGhostLayer< FlagField_T >(geometryHandler.blockForest_,
+                                                                                         geometryHandler.flagFieldID_),
+                                      "Second ghost layer update: after wetting detection sweep (flag field)");
+   }
+}
+
+template< typename Stencil_T, typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T>
+void SimpleFiniteDifferenceMethod< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >::addSweeps(
+   SweepTimeloop& timeloop, const SimpleFiniteDifferenceMethod::SurfaceGeometryHandler_T& geometryHandler)
+{
+   using Communication_T = typename SurfaceGeometryHandler_T::Communication_T;
+   using StateSweep      = typename SurfaceGeometryHandler_T::StateSweep;
+
+   // compute interface normals
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalSweep(
+      geometryHandler.normalFieldID_, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_,
+      flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, false, false,
+      false, false);
+   timeloop.add() << Sweep(normalSweep, "Sweep: normal computation", StateSweep::fullFreeSurface)
+                  << Sweep(emptySweep(), "Empty sweep: normal")
+                  << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_),
+                                   "Communication: after normal sweep");
+
+   // extrapolation of normals to interface neighboring cells (required for computing the curvature with finite
+   // differences)
+   ExtrapolateNormalsSweep< Stencil_T, FlagField_T, VectorField_T > extNormalsSweep(
+      geometryHandler.normalFieldID_, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID);
+   timeloop.add() << Sweep(extNormalsSweep, "Sweep: normal extrapolation", StateSweep::fullFreeSurface)
+                  << Sweep(emptySweep(), "Empty sweep: normal extrapolation")
+                  << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_),
+                                   "Communication: after normal extrapolation sweep");
+
+   if (geometryHandler.computeCurvature_)
+   {
+      // curvature computation using finite differences
+      CurvatureSweepSimpleFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvSweep(
+         geometryHandler.curvatureFieldID_, geometryHandler.normalFieldID_, geometryHandler.flagFieldID_,
+         flagIDs::interfaceFlagID, geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_,
+         geometryHandler.contactAngle_);
+      timeloop.add() << Sweep(curvSweep, "Sweep: curvature computation (simple finite difference method)",
+                              StateSweep::fullFreeSurface)
+                     << Sweep(emptySweep(), "Empty sweep: curvature")
+                     << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.curvatureFieldID_),
+                                      "Communication: after curvature sweep ");
+   }
+}
+
+} // namespace curvature_model
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/CurvatureSweep.h b/src/lbm_generated/free_surface/surface_geometry/CurvatureSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..8f1a1994ae18543e9e4cc9affb1cf2b0c3bd7eda
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/CurvatureSweep.h
@@ -0,0 +1,217 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file CurvatureSweep.h
+//! \ingroup surface_geometry
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Sweeps for computing the interface curvature.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "core/logging/Logging.h"
+#include "core/math/Constants.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q27.h"
+#include "stencil/Directions.h"
+
+#include <type_traits>
+#include <vector>
+
+#include "ContactAngle.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Compute the interface curvature using a finite difference scheme (Parker-Youngs approach) as described in
+ * - dissertation of S. Bogner, 2017 (section 4.4.2.1)
+ * which is based on
+ * - Brackbill, Kothe and Zemach, "A continuum method ...", 1992
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class CurvatureSweepFiniteDifferences
+{
+ protected:
+   using vector_t = Vector3<real_t>;
+   using flag_t   = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+ public:
+   CurvatureSweepFiniteDifferences(const BlockDataID& curvatureFieldID, const ConstBlockDataID& normalFieldID,
+                                   const ConstBlockDataID& obstacleNormalFieldID, const ConstBlockDataID& flagFieldID,
+                                   const FlagUID& interfaceFlagID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet,
+                                   const Set< FlagUID >& obstacleFlagIDSet, bool enableWetting,
+                                   const ContactAngle& contactAngle)
+      : curvatureFieldID_(curvatureFieldID), normalFieldID_(normalFieldID),
+        obstacleNormalFieldID_(obstacleNormalFieldID), flagFieldID_(flagFieldID), enableWetting_(enableWetting),
+        contactAngle_(contactAngle), interfaceFlagID_(interfaceFlagID),
+        liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFlagIDSet_(obstacleFlagIDSet)
+   {}
+
+   void operator()(IBlock* const block);
+
+   /********************************************************************************************************************
+    * Returns an adjusted interface normal according to the wetting model from the dissertation of S. Bogner, 2017
+    * (section 4.4.2.1).
+    *******************************************************************************************************************/
+   template< typename VectorIt_T >
+   Vector3< real_t > getNormalWithWetting(VectorIt_T normalFieldIt, VectorIt_T obstacleNormalFieldIt,
+                                          const stencil::Direction dir)
+   {
+      vector_t normal = Vector3<real_t>(  normalFieldIt.getF(0),
+                                          normalFieldIt.getF(1),
+                                          normalFieldIt.getF(2));
+
+      vector_t nw = Vector3(obstacleNormalFieldIt.neighbor(dir, 0),
+                            obstacleNormalFieldIt.neighbor(dir, 1),
+                            obstacleNormalFieldIt.neighbor(dir, 1));
+
+
+      // get reversed interface normal
+      const Vector3< real_t > n = -normal;
+
+      // get n_t: vector tangent to the wall and normal to the contact line; obtained by subtracting the wall-normal
+      // component from the interface normal n
+      Vector3< real_t > nt = n - (nw * n) * nw; // "(nw * n) * nw" is orthogonal projection of nw on n
+      nt                   = nt.getNormalizedOrZero();
+
+      // compute interface normal at wall according to wetting model (equation 4.21 in dissertation of S. Bogner,
+      // 2017)
+      const Vector3< real_t > nWall = nw * contactAngle_.getCos() + nt * contactAngle_.getSin();
+
+      // extrapolate into obstacle cell to obtain boundary value; expression comes from 1D extrapolation formula
+      // IMPORTANT REMARK: corner and diagonal directions must use different formula, Bogner did not consider this in
+      // his implementation; here, nevertheless Bogner's approach is used
+      const Vector3< real_t > nWallExtrapolated = n + real_c(2) * (nWall - n);
+
+      return -nWallExtrapolated;
+   }
+
+ private:
+   BlockDataID curvatureFieldID_;
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID obstacleNormalFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   bool enableWetting_;
+   ContactAngle contactAngle_;
+
+   FlagUID interfaceFlagID_;
+   Set< FlagUID > liquidInterfaceGasFlagIDSet_;
+   Set< FlagUID > obstacleFlagIDSet_;
+}; // class CurvatureSweepFiniteDifferences
+
+/***********************************************************************************************************************
+ * Compute the interface curvature using local triangulation as described in
+ * - dissertation of T. Pohl, 2008 (section 2.5)
+ * - dissertation of S. Donath, 2011 (wetting model, section 6.3.3)
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class CurvatureSweepLocalTriangulation
+{
+ protected:
+   using vector_t = Vector3<real_t>;
+
+ public:
+   CurvatureSweepLocalTriangulation(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                                    const BlockDataID& curvatureFieldID, const ConstBlockDataID& normalFieldID,
+                                    const ConstBlockDataID& fillFieldID, const ConstBlockDataID& flagFieldID,
+                                    const ConstBlockDataID& obstacleNormalFieldID, const FlagUID& interfaceFlagID,
+                                    const Set< FlagUID >& obstacleFlagIDSet, bool enableWetting,
+                                    const ContactAngle& contactAngle)
+      : blockForest_(blockForest), curvatureFieldID_(curvatureFieldID), normalFieldID_(normalFieldID),
+        fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), obstacleNormalFieldID_(obstacleNormalFieldID),
+        enableWetting_(enableWetting), contactAngle_(contactAngle), interfaceFlagID_(interfaceFlagID),
+        obstacleFlagIDSet_(obstacleFlagIDSet)
+   {
+      if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 >)
+      {
+         WALBERLA_ABORT(
+            "Curvature computation with local triangulation using a D2Q9 stencil has not been thoroughly tested.");
+      }
+   }
+
+   void operator()(IBlock* const block);
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   BlockDataID curvatureFieldID_;
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+   ConstBlockDataID obstacleNormalFieldID_;
+
+   bool enableWetting_;
+   ContactAngle contactAngle_;
+
+   FlagUID interfaceFlagID_;
+   Set< FlagUID > obstacleFlagIDSet_;
+}; // class CurvatureSweepLocalTriangulation
+
+/***********************************************************************************************************************
+ * Compute the interface curvature with a simplistic finite difference method. This approach is not documented in
+ * literature and neither thoroughly tested or validated.
+ * Use it with caution and preferably for testing purposes only.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class CurvatureSweepSimpleFiniteDifferences
+{
+ protected:
+   using vector_t = Vector3<real_t>;
+
+ public:
+   CurvatureSweepSimpleFiniteDifferences(const BlockDataID& curvatureFieldID, const ConstBlockDataID& normalFieldID,
+                                         const ConstBlockDataID& flagFieldID, const FlagUID& interfaceFlagID,
+                                         const Set< FlagUID >& obstacleFlagIDSet, bool enableWetting,
+                                         const ContactAngle& contactAngle)
+      : curvatureFieldID_(curvatureFieldID), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID),
+        enableWetting_(enableWetting), contactAngle_(contactAngle), interfaceFlagID_(interfaceFlagID),
+        obstacleFlagIDSet_(obstacleFlagIDSet)
+   {
+      WALBERLA_LOG_WARNING_ON_ROOT(
+         "You are using curvature computation based on a simplistic finite difference method. This "
+         "was implemented for testing purposes only and has not been thoroughly "
+         "validated and tested in the current state of the code. Use it with caution.");
+   }
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID curvatureFieldID_;
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   bool enableWetting_;
+   ContactAngle contactAngle_;
+
+   FlagUID interfaceFlagID_;
+   Set< FlagUID > obstacleFlagIDSet_;
+}; // class CurvatureSweepSimpleFiniteDifferences
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "CurvatureSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/CurvatureSweep.impl.h b/src/lbm_generated/free_surface/surface_geometry/CurvatureSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..53eb0c6a8893ce7034bc3c91764c7bdae791230b
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/CurvatureSweep.impl.h
@@ -0,0 +1,520 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file CurvatureSweep.impl.h
+//! \ingroup surface_geometry
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Sweeps for computing the interface curvature.
+//
+//======================================================================================================================
+
+#include "core/debug/CheckFunctions.h"
+#include "core/logging/Logging.h"
+#include "core/math/Matrix3.h"
+#include "core/math/Utility.h"
+#include "core/math/Vector3.h"
+
+#include "field/FlagField.h"
+
+#include "stencil/D3Q27.h"
+#include "stencil/Directions.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include "ContactAngle.h"
+#include "CurvatureSweep.h"
+#include "Utility.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(
+   IBlock* const block)
+{
+   // get fields
+   ScalarField_T* const curvatureField            = block->getData< ScalarField_T >(curvatureFieldID_);
+   const VectorField_T* const normalField         = block->getData< const VectorField_T >(normalFieldID_);
+   const VectorField_T* const obstacleNormalField = block->getData< const VectorField_T >(obstacleNormalFieldID_);
+   const FlagField_T* const flagField             = block->getData< const FlagField_T >(flagFieldID_);
+
+   // get flags
+   const flag_t interfaceFlag              = flagField->getFlag(interfaceFlagID_);
+   const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_);
+   const flag_t obstacleFlagMask           = flagField->getMask(obstacleFlagIDSet_);
+
+   WALBERLA_FOR_ALL_CELLS(
+      flagFieldIt, flagField, normalFieldIt, normalField, obstacleNormalFieldIt, obstacleNormalField, curvatureFieldIt,
+      curvatureField, {
+         real_t& curv = *curvatureFieldIt;
+         curv         = real_c(0.0);
+         vector_t normal = Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2));
+
+         if (isFlagSet(flagFieldIt, interfaceFlag)) // only treat interface cells
+         {
+            real_t weightSum = real_c(0);
+
+            if (normal.sqrLength() < real_c(1e-14))
+            {
+               WALBERLA_LOG_WARNING("Invalid normal detected in CurvatureSweep.")
+               continue;
+            }
+
+            // Parker-Youngs central finite difference approximation of curvature (see dissertation
+            // of S. Bogner, 2017, section 4.4.2.1)
+            for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+            {
+               const Vector3< real_t > dirVector = Vector3< real_t >(real_c(dir.cx()), real_c(dir.cy()), real_c(dir.cz()));
+
+               Vector3< real_t > neighborNormal;
+
+               if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), liquidInterfaceGasFlagMask | obstacleFlagMask))
+               {
+                  // get interface normal of neighbor in direction dir with respect to wetting model
+                  if (enableWetting_ && isPartOfMaskSet(flagFieldIt.neighbor(*dir), obstacleFlagMask))
+                  {
+                     neighborNormal = getNormalWithWetting(normalFieldIt, obstacleNormalFieldIt, *dir);
+                     neighborNormal = neighborNormal.getNormalizedOrZero();
+                  }
+                  else
+                  {
+                     if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), liquidInterfaceGasFlagMask))
+                     {
+                        neighborNormal = Vector3(normalFieldIt.neighbor(*dir, 0), normalFieldIt.neighbor(*dir, 1), normalFieldIt.neighbor(*dir, 2));
+                     }
+                     else
+                     {
+                        // skip remainder of this direction such that it is not considered in curvature computation
+                        continue;
+                     }
+                  }
+               }
+
+               // equation (35) in Brackbill et al. discretized with finite difference method
+               // according to Parker-Youngs
+               if constexpr (Stencil_T::D == uint_t(2))
+               {
+                  const real_t weight =
+                     real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]);
+                  weightSum += weight;
+                  curv += weight * (dirVector * neighborNormal);
+               }
+               else
+               {
+                  const real_t weight = real_c(stencil::gaussianMultipliers[dir.toIdx()]);
+                  weightSum += weight;
+                  curv += weight * (dirVector * neighborNormal);
+               }
+            }
+
+            // divide by sum of weights in Parker-Youngs approximation; sum does not contain weights of directions in
+            // which there is no liquid, interface, gas, or obstacle cell (only when wetting is enabled, otherwise
+            // obstacle cell is also not considered); must be done like this because otherwise such non-valid
+            // directions would implicitly influence the finite difference scheme by assuming a normal of zero
+            curv /= weightSum;
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(
+   IBlock* const block)
+{
+   const auto blockForest = blockForest_.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   // struct for storing relevant information of each neighboring interface cell (POD type)
+   using Neighbor = struct
+   {
+      Vector3< real_t > diff;     // difference (distance in coordinates) between this and neighboring interface point
+      Vector3< real_t > diffNorm; // normalized difference between this and neighboring interface point
+      Vector3< real_t > normal;   // interface normal of neighboring interface point
+      real_t dist2;               // square of the distance between this and neighboring interface point
+      real_t area;                // sum of the area of the two triangles that this neighboring interface is part of
+      real_t sort;                // angle that is used to sort the order neighboring points accordingly
+      bool valid;                 // validity, used to remove triangles with too narrow angles
+      bool wall;                  // used to include wetting effects near solid cells
+   };
+
+   // get fields
+   ScalarField_T* const curvatureField            = block->getData< ScalarField_T >(curvatureFieldID_);
+   const VectorField_T* const normalField         = block->getData< const VectorField_T >(normalFieldID_);
+   const ScalarField_T* const fillField           = block->getData< const ScalarField_T >(fillFieldID_);
+   const FlagField_T* const flagField             = block->getData< const FlagField_T >(flagFieldID_);
+   const VectorField_T* const obstacleNormalField = block->getData< const VectorField_T >(obstacleNormalFieldID_);
+
+   // get flags
+   auto interfaceFlag    = flagField->getFlag(interfaceFlagID_);
+   auto obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_);
+
+   WALBERLA_FOR_ALL_CELLS(
+      flagFieldIt, flagField, normalFieldIt, normalField, fillFieldIt, fillField, obstacleNormalFieldIt,
+      obstacleNormalField, curvatureFieldIt, curvatureField, {
+         real_t& curv = *curvatureFieldIt;
+         curv         = real_c(0.0);
+         std::vector< Neighbor > neighbors;
+         Vector3< real_t > meanInterfaceNormal;
+         vector_t normal = Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2));
+
+         // compute curvature only in interface points
+         if (!isFlagSet(flagFieldIt, interfaceFlag)) { continue; }
+
+         // normal of this cell also contributes to mean normal
+         meanInterfaceNormal = normal;
+
+         // iterate over all neighboring cells (Eq. 2.18 in dissertation of T. Pohl, 2008)
+         for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+         {
+            auto neighborFlags = flagFieldIt.neighbor(*dir);
+
+            if (isFlagSet(neighborFlags, interfaceFlag) || // Eq. 2.19 in dissertation of T. Pohl, 2008
+                (isPartOfMaskSet(neighborFlags, obstacleFlagMask) &&
+                 dir.toIdx() <= uint_c(18))) // obstacle in main direction or diagonal direction (not corner direction)
+            {
+               Vector3< real_t > neighborNormal;
+               const real_t neighborFillLevel = fillFieldIt.neighbor(*dir);
+
+               // if loop was entered because of neighboring solid cell, normal of this solid cell points towards the
+               // currently processed interface cell
+               Vector3< real_t > wallNormal(real_c(-dir.cx()), real_c(-dir.cy()), real_c(-dir.cz()));
+
+               if (isPartOfMaskSet(neighborFlags, obstacleFlagMask))
+               {
+                  neighborNormal =
+                     Vector3< real_t >(real_c(1)); // temporarily guarantees "neighborNormal.sqrLength()>0"
+                  wallNormal.getNormalized();
+               }
+               else { neighborNormal = Vector3(normalFieldIt.neighbor(*dir, 0), normalFieldIt.neighbor(*dir, 1), normalFieldIt.neighbor(*dir, 2)); }
+
+               if (neighborNormal.sqrLength() > real_c(0))
+               {
+                  Neighbor n;
+
+                  // get global coordinate (with respect to whole simulation domain) of the currently processed cell
+                  Vector3< real_t > globalCellCoordinate =
+                     blockForest->getBlockLocalCellCenter(*block, flagFieldIt.cell()) - Vector3< real_t >(real_c(0.5));
+
+                  for (auto dir2 = Stencil_T::beginNoCenter(); dir2 != Stencil_T::end(); ++dir2)
+                  {
+                     // stay in the close neighborhood of the currently processed interface cell
+                     if ((dir.cx() != 0 && dir2.cx() != 0) || (dir.cy() != 0 && dir2.cy() != 0) ||
+                         (dir.cz() != 0 && dir2.cz() != 0))
+                     {
+                        continue;
+                     }
+
+                     if (isPartOfMaskSet(neighborFlags, obstacleFlagMask) && enableWetting_)
+                     {
+                        // get flags of neighboring cell in direction dir2
+                        auto neighborFlagsInDir2 = flagFieldIt.neighbor(*dir2);
+
+                        // the currently processed interface cell i has a neighboring solid cell j in direction dir;
+                        // get the flags of j's neighboring cell in direction dir2
+                        // i.e., from the current cell, go to neighbor in dir; from there, go to next cell in dir2
+                        auto neighborNeighborFlags =
+                           flagFieldIt.neighbor(dir.cx() + dir2.cx(), dir.cy() + dir2.cy(), dir.cz() + dir2.cz());
+
+                        // true if the currently processed interface cell i has a neighboring interface cell j (in
+                        // dir2), which (j) has a neighboring obstacle cell in the same direction as i does (in dir)
+                        if (isFlagSet(neighborFlagsInDir2, interfaceFlag) &&
+                            isPartOfMaskSet(neighborNeighborFlags, obstacleFlagMask))
+                        {
+                           // get the normal of the currently processed interface cell's neighboring interface cell
+                           // (in direction 2)
+                           Vector3< real_t > neighborNormalDir2 = Vector3(normalFieldIt.neighbor(*dir2, 0), normalFieldIt.neighbor(*dir2, 1), normalFieldIt.neighbor(*dir2, 2));
+                           Vector3< real_t > neighborGlobalCoordDir2 = globalCellCoordinate;
+                           neighborGlobalCoordDir2[0] += real_c(dir2.cx());
+                           neighborGlobalCoordDir2[1] += real_c(dir2.cy());
+                           neighborGlobalCoordDir2[2] += real_c(dir2.cz());
+
+                           // get neighboring interface point, i.e., location of interface within cell
+
+
+                           Vector3< real_t > neighborGlobalInterfacePoint =
+                              getInterfacePoint(Vector3(normalFieldIt.neighbor(*dir2, 0), normalFieldIt.neighbor(*dir2, 1), normalFieldIt.neighbor(*dir2, 2)), fillFieldIt.neighbor(*dir2));
+
+                           // transform to global coordinates, i.e., neighborInterfacePoint specifies the global
+                           // location of the interface point in the currently processed interface cell's neighbor in
+                           // direction dir2
+                           neighborGlobalInterfacePoint += neighborGlobalCoordDir2;
+
+                           // get the mean (averaged over multiple solid cells) wall normal of the neighbor in
+                           // direction dir2
+                           Vector3< real_t > obstacleNormal = Vector3(obstacleNormalFieldIt.neighbor(*dir2, 0), obstacleNormalFieldIt.neighbor(*dir2, 1), obstacleNormalFieldIt.neighbor(*dir2, 2));
+                           obstacleNormal *= real_c(-1);
+                           if (obstacleNormal.sqrLength() < real_c(1e-10)) { obstacleNormal = wallNormal; }
+
+                           Vector3< real_t > neighborPoint;
+
+                           bool result = computeArtificalWallPoint(
+                              neighborGlobalInterfacePoint, neighborGlobalCoordDir2, neighborNormalDir2, wallNormal,
+                              obstacleNormal, contactAngle_, neighborPoint, neighborNormal);
+                           if (!result) { continue; }
+                           n.wall  = true;
+                           n.diff  = neighborPoint - neighborGlobalInterfacePoint;
+                           n.dist2 = n.diff.sqrLength();
+                        }
+                        else { continue; }
+                     }
+                     else
+                     // will be entered if:
+                     // isFlagSet(neighborFlags, interfaceFlag) && !isPartOfMaskSet(neighborFlags, obstacleFlagMask)
+                     {
+                        n.wall = false;
+
+                        // get neighboring interface point, i.e., location of interface within cell
+                        n.diff = getInterfacePoint(neighborNormal, neighborFillLevel);
+
+                        // get distance between this cell (0,0,0) and neighboring interface point + (dx,dy,dz)
+                        n.diff += Vector3< real_t >(real_c(dir.cx()), real_c(dir.cy()), real_c(dir.cz()));
+
+                        // get distance between this and neighboring interface point
+                        n.diff -= getInterfacePoint(Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2)), *fillFieldIt);
+                        n.dist2 = n.diff.sqrLength();
+                     }
+
+                     // exclude neighboring interface points that are too close or too far away from this cell's
+                     // interface point
+                     if (n.dist2 >= real_c(0.64) && n.dist2 <= real_c(3.24)) // Eq. 2.20, 0.64 = 0.8^2; 3.24 = 1.8^2
+                     {
+                        n.normal   = neighborNormal;
+                        n.diffNorm = n.diff.getNormalized();
+                        n.area     = real_c(0);
+                        n.sort     = real_c(0);
+                        n.valid    = true;
+
+                        neighbors.push_back(n);
+                     }
+
+                     // if there is no obstacle, loop should be interrupted immediately
+                     if (!isPartOfMaskSet(neighborFlags, obstacleFlagMask))
+                     {
+                        // interrupt loop
+                        break;
+                     }
+                  }
+               }
+            }
+         }
+
+         // remove degenerated triangles, see dissertation of T. Pohl, 2008, p. 27
+         for (auto nIt1 = ++neighbors.begin(); !neighbors.empty() && nIt1 != neighbors.end(); ++nIt1)
+         {
+            if (!nIt1->valid) { continue; }
+
+            for (auto nIt2 = neighbors.begin(); nIt2 != nIt1; ++nIt2)
+            {
+               if (!nIt2->valid) { continue; }
+
+               // triangle is degenerated if angle between surface normals is less than 30° (heuristically chosen
+               // in dissertation of T. Pohl, 2008, p. 27); greater sign is correct here due to cos(29) > cos(30)
+               if (nIt1->diffNorm * nIt2->diffNorm >
+                   real_c(0.866)) // cos(30°) = 0.866, as in dissertation of T. Pohl, 2008, p. 27
+               {
+                  const real_t diff = nIt1->dist2 - nIt2->dist2;
+
+                  if (diff < real_c(1e-4)) { nIt1->valid = nIt1->wall ? true : false; }
+                  if (diff > real_c(-1e-4)) { nIt2->valid = nIt2->wall ? true : false; }
+               }
+            }
+         }
+
+         // remove invalid neighbors
+         neighbors.erase(std::remove_if(neighbors.begin(), neighbors.end(), [](const Neighbor& a) { return !a.valid; }),
+                         neighbors.end());
+
+         if (neighbors.size() < 4)
+         {
+            // WALBERLA_LOG_WARNING_ON_ROOT(
+            //    "Not enough faces in curvature reconstruction, setting curvature in this cell to zero.");
+            curv = real_c(0); // not documented in literature but taken from S. Donath's code
+            continue;         // process next cell in WALBERLA_FOR_ALL_CELLS
+         }
+
+         // compute mean normal
+         for (auto const& n : neighbors)
+         {
+            meanInterfaceNormal += n.normal;
+         }
+         meanInterfaceNormal = meanInterfaceNormal.getNormalized();
+
+         // compute xAxis and yAxis that define a coordinate system on a tangent plane for sorting neighbors
+         // T_i' = (I - N * N^t) * (p_i - p); projection of neighbors.diff[0] onto tangent plane (Figure 2.14 in
+         // dissertation of T. Pohl, 2008)
+         Vector3< real_t > xAxis =
+            (Matrix3< real_t >::makeIdentityMatrix() - dyadicProduct(meanInterfaceNormal, meanInterfaceNormal)) *
+            neighbors[0].diff;
+
+         // T_i = T_i' / ||T_i'||
+         xAxis = xAxis.getNormalized();
+
+         // get vector that is orthogonal to xAxis and meanInterfaceNormal
+         const Vector3< real_t > yAxis = cross(xAxis, meanInterfaceNormal);
+
+         for (auto& n : neighbors)
+         {
+            // get cosine of angles between n.diff and axes of the new coordinate system
+            const real_t cosAngX = xAxis * n.diff;
+            const real_t cosAngY = yAxis * n.diff;
+
+            // sort the neighboring interface points using atan2 (which is not just atan(wy/wx), see Wikipedia)
+            n.sort = std::atan2(cosAngY, cosAngX);
+         }
+
+         std::sort(neighbors.begin(), neighbors.end(),
+                   [](const Neighbor& a, const Neighbor& b) { return a.sort < b.sort; });
+
+         Vector3< real_t > meanTriangleNormal(real_c(0));
+         for (auto nIt1 = neighbors.begin(); neighbors.size() > uint_c(1) && nIt1 != neighbors.end(); ++nIt1)
+         {
+            // index of second neighbor starts over at 0: (k + 1) mod N_P
+            auto nIt2 = (nIt1 != (neighbors.end() - 1)) ? (nIt1 + 1) : neighbors.begin();
+
+            // N_f_k (with real length, i.e., not normalized yet)
+            const Vector3< real_t > triangleNormal = cross(nIt1->diff, nIt2->diff);
+
+            // |f_k| (multiplication with 0.5, since triangleNormal.length() is area of parallelogram and not
+            // triangle)
+            const real_t area = real_c(0.5) * triangleNormal.length();
+
+            // lambda_i' = |f_{(i-1+N_p) mod N_p}| + |f_i|
+            nIt1->area += area; // from the view of na, this is |f_i|
+            nIt2->area += area; // from the view of nb, this is |f_{(i-1+N_p) mod N_p}|, i.e., area of the face
+                                // from the neighbor with smaller index
+
+            // N' = sum(|f_k| * N_f_k)
+            meanTriangleNormal += area * triangleNormal;
+         }
+
+         if (meanTriangleNormal.length() < real_c(1e-10))
+         {
+            // WALBERLA_LOG_WARNING_ON_ROOT("Invalid meanTriangleNormal, setting curvature in this cell to zero.");
+            curv = real_c(0); // not documented in literature but taken from S. Donath's code
+            continue;         // process next cell in WALBERLA_FOR_ALL_CELLS
+         }
+
+         // N = N' / ||N'||
+         meanTriangleNormal = meanTriangleNormal.getNormalized();
+
+         // I - N * N^t; matrix for projection of vector on tangent plane
+         const Matrix3< real_t > projMatrix =
+            Matrix3< real_t >::makeIdentityMatrix() - dyadicProduct(meanTriangleNormal, meanTriangleNormal);
+
+         // M
+         Matrix3< real_t > mMatrix(real_c(0));
+
+         // sum(lambda_i')
+         real_t neighborAreaSum = real_c(0);
+
+         // M = sum(lambda_i' * kappa_i * T_i * T_i^t)
+         for (auto& n : neighbors)
+         {
+            if (n.area > real_c(0))
+            {
+               // kappa_i = 2 * N^t * (p_i - p) / ||p_i - p||^2
+               const real_t kappa = real_c(2) * (meanTriangleNormal * n.diff) / n.dist2;
+
+               // T_i' = (I - N * N^t) * (p_i - p)
+               Vector3< real_t > tVector = projMatrix * n.diff;
+
+               // T_i = T_i' / ||T_i'||
+               tVector = tVector.getNormalized();
+
+               // T_i * T_i^t
+               const Matrix3< real_t > auxMat = dyadicProduct(tVector, tVector);
+
+               // M += T_i * T_i^t * kappa_i * lambda_i'
+               mMatrix += auxMat * kappa * n.area;
+
+               // sum(lambda_i')
+               neighborAreaSum += n.area;
+            }
+         }
+
+         // M = M * 1 / sum(lambda_i')
+         mMatrix = mMatrix * (real_c(1) / neighborAreaSum);
+
+         // kappa = tr(M)
+         curv = (mMatrix(0, 0) + mMatrix(1, 1) + mMatrix(2, 2));
+      }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void CurvatureSweepSimpleFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(
+   IBlock* const block)
+{
+   // get fields
+   ScalarField_T* const curvatureField    = block->getData< ScalarField_T >(curvatureFieldID_);
+   const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_);
+   const FlagField_T* const flagField     = block->getData< const FlagField_T >(flagFieldID_);
+
+   // get flags
+   auto interfaceFlag    = flagField->getFlag(interfaceFlagID_);
+   auto obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_);
+
+   WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, curvatureFieldIt, curvatureField, {
+      real_t& curv = *curvatureFieldIt;
+      curv         = real_c(0.0);
+
+      // interface cells
+      if (isFlagSet(flagFieldIt, interfaceFlag))
+      {
+         // this cell is next to a wall/obstacle
+         if (enableWetting_ && isFlagInNeighborhood< Stencil_T >(flagFieldIt, obstacleFlagMask))
+         {
+            // compute wall/obstacle curvature
+            vector_t obstacleNormal(real_c(0), real_c(0), real_c(0));
+            for (auto it = Stencil_T::beginNoCenter(); it != Stencil_T::end(); ++it)
+            {
+               // calculate obstacle normal with central finite difference approximation of the surface's gradient (see
+               // dissertation of S. Donath, 2011, section 6.3.5.2)
+               if (isPartOfMaskSet(flagFieldIt.neighbor(*it), obstacleFlagMask))
+               {
+                  obstacleNormal[0] -= real_c(it.cx());
+                  obstacleNormal[1] -= real_c(it.cy());
+                  obstacleNormal[2] -= real_c(it.cz());
+               }
+            }
+
+            if (obstacleNormal.sqrLength() > real_c(0))
+            {
+               obstacleNormal = obstacleNormal.getNormalized();
+
+               // IMPORTANT REMARK:
+               // the following wetting model is not documented in literature and not tested for correctness; use it
+               // with caution
+               curv = -real_c(0.25) * (contactAngle_.getCos() - (Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2))) * obstacleNormal);
+            }
+         }
+         else // no obstacle cell is in next neighborhood
+         {
+            // central finite difference approximation of curvature (see dissertation of S. Bogner, 2017,
+            // section 4.4.2.1)
+            curv = normalFieldIt.neighbor(1, 0, 0, 0) - normalFieldIt.neighbor(-1, 0, 0, 0) +
+                   normalFieldIt.neighbor(0, 1, 0, 1) - normalFieldIt.neighbor(0, -1, 0, 1) +
+                   normalFieldIt.neighbor(0, 0, 1, 2) - normalFieldIt.neighbor(0, 0, -1, 2);
+
+            curv *= real_c(0.25);
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/DetectWettingSweep.h b/src/lbm_generated/free_surface/surface_geometry/DetectWettingSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..694f678d73f89ecb874e07b4330f1c55fa454b6f
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/DetectWettingSweep.h
@@ -0,0 +1,317 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file DetectWettingSweep.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Sweep for detecting cells that need to be converted to interface to obtain a smooth wetting interface.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "core/math/Constants.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+
+#include "stencil/D2Q4.h"
+#include "stencil/D3Q19.h"
+
+#include <type_traits>
+#include <vector>
+
+#include "ContactAngle.h"
+#include "Utility.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Sweep for detecting interface cells that need to be created in order to obtain a smooth interface continuation in
+ * case of wetting.
+ *
+ * See dissertation of S. Donath, 2011 section 6.3.5.3.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class DetectWettingSweep
+{
+ protected:
+   using FlagUIDSet = Set< FlagUID >;
+
+   using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type;
+
+   // restrict stencil because surface continuation in corner directions is not meaningful
+   using WettingStencil_T = typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q19 >::type;
+
+ public:
+   DetectWettingSweep(const BlockDataID& flagFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                      const ConstBlockDataID& normalFieldID, const ConstBlockDataID& fillFieldID)
+      : flagFieldID_(flagFieldID), flagInfo_(flagInfo), normalFieldID_(normalFieldID), fillFieldID_(fillFieldID)
+   {}
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID flagFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID fillFieldID_;
+
+}; // class DetectWettingSweep
+
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void DetectWettingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(
+   IBlock* const block)
+{
+//   // get free surface boundary handling
+//   // get fields
+//   auto normalField = block->getData< const VectorField_T >(normalFieldID_);
+//   auto fillField   = block->getData< const ScalarField_T >(fillFieldID_);
+//   FlagField_T flagField     = block->getData< FlagField_T >(flagFieldID_);
+//
+//   // get flags
+//   const FlagInfo< FlagField_T >& flagInfo = flagInfo_;
+//   using flag_t                            = typename FlagField_T::flag_t;
+//
+//   const flag_t liquidInterfaceGasFlagMask = flagInfo_.liquidFlag | flagInfo_.interfaceFlag | flagInfo_.gasFlag;
+//
+//   WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, fillFieldIt, fillField, {
+//      // skip non-interface cells
+//      if (!isFlagSet(flagFieldIt, flagInfo.interfaceFlag)) { continue; }
+//
+//      // skip cells that have no solid cell in their neighborhood
+//      if (!isFlagInNeighborhood< WettingStencil_T >(flagFieldIt, flagInfo.obstacleFlagMask)) { continue; }
+//
+//      // restrict maximal and minimal angle such that the surface continuation does not become too flat
+//      if (*fillFieldIt < real_c(0.005) || *fillFieldIt > real_c(0.995)) { continue; }
+//
+//      const Vector3< real_t > interfacePointLocation = getInterfacePoint(Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2)), *fillFieldIt);
+//
+//      for (auto dir = WettingStencil_T::beginNoCenter(); dir != WettingStencil_T::end(); ++dir)
+//      {
+//         const Cell neighborCell =
+//            Cell(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+//         const flag_t neighborFlag = flagField->get(neighborCell);
+//
+//         // skip neighboring cells that
+//         // - are not liquid, gas or interface
+//         // - are already marked for conversion to interface due to wetting
+//         // IMPORTANT REMARK: It is crucial that interface cells are NOT skipped here. Since the
+//         // "keepInterfaceForWettingFlag" flag is cleared in all cells after performing the conversion, interface cells
+//         // that were converted due to wetting must still get this flag to avoid being prematurely converted back.
+//         if (!isPartOfMaskSet(neighborFlag, liquidInterfaceGasFlagMask) ||
+//             isFlagSet(neighborFlag, flagInfo.keepInterfaceForWettingFlag))
+//         {
+//            continue;
+//         }
+//
+//         // skip neighboring cells that do not have solid cells in their neighborhood
+//         bool hasObstacle = false;
+//         for (auto dir2 = WettingStencil_T::beginNoCenter(); dir2 != WettingStencil_T::end(); ++dir2)
+//         {
+//            const Cell neighborNeighborCell =
+//               Cell(flagFieldIt.x() + dir.cx() + dir2.cx(), flagFieldIt.y() + dir.cy() + dir2.cy(),
+//                    flagFieldIt.z() + dir.cz() + dir2.cz());
+//            const flag_t neighborNeighborFlag = flagField->get(neighborNeighborCell);
+//
+//            if (isPartOfMaskSet(neighborNeighborFlag, flagInfo.obstacleFlagMask))
+//            {
+//               hasObstacle = true;
+//               break; // exit dir2 loop
+//            }
+//         }
+//         if (!hasObstacle) { continue; }
+//
+//         // check cell edges for intersection with the interface surface plane and mark the respective neighboring for
+//         // conversion
+//         // bottom south
+//         if ((dir.cx() == 0 && dir.cy() == -1 && dir.cz() == -1) ||
+//             (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) || (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+//                                                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+//                                                                Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2)), interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               // TODO look carefully at boundary handling and use correct method
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // bottom north
+//         if ((dir.cx() == 0 && dir.cy() == 1 && dir.cz() == -1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) ||
+//             (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+//                                                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+//                                                                Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2)), interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // bottom west
+//         if ((dir.cx() == -1 && dir.cy() == 0 && dir.cz() == -1) ||
+//             (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) || (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+//                                                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // bottom east
+//         if ((dir.cx() == 1 && dir.cy() == 0 && dir.cz() == -1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) ||
+//             (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+//                                                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // top south
+//         if ((dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) ||
+//             (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+//                                                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // top north
+//         if ((dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) ||
+//             (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(1)),
+//                                                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // top west
+//         if ((dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) ||
+//             (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+//                                                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // top east
+//         if ((dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) ||
+//             (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(1)),
+//                                                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // south-west
+//         if ((dir.cx() == -1 && dir.cy() == -1 && dir.cz() == 0) ||
+//             (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0) || (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+//                                                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // south-east
+//         if ((dir.cx() == 1 && dir.cy() == -1 && dir.cz() == 0) || (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0) ||
+//             (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+//                                                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // north-west
+//         if ((dir.cx() == -1 && dir.cy() == 1 && dir.cz() == 0) || (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0) ||
+//             (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+//                                                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//
+//         // north-east
+//         if ((dir.cx() == 1 && dir.cy() == 1 && dir.cz() == 0) || (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0) ||
+//             (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0))
+//         {
+//            const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)),
+//                                                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+//                                                                *normalFieldIt, interfacePointLocation);
+//            if (intersection > real_c(0))
+//            {
+//               flagField->addFlag(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz(), flagInfo.keepInterfaceForWettingFlag);
+//               continue;
+//            }
+//         }
+//      }
+//   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/ExtrapolateNormalsSweep.h b/src/lbm_generated/free_surface/surface_geometry/ExtrapolateNormalsSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..417419043936c1779cae92a3edad06555c34e666
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ExtrapolateNormalsSweep.h
@@ -0,0 +1,71 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ExtrapolateNormalsSweep.h
+//! \ingroup surface_geometry
+//! \author Martin Bauer
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Extrapolate interface normals to neighboring cells in D3Q27 direction.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+
+#include <type_traits>
+#include <vector>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Approximates the normals of non-interface cells using the normals of all neighboring interface cells in D3Q27
+ * direction.
+ * The approximation is computed by summing the weighted normals of all neighboring interface cells. The weights are
+ * chosen as in the Parker-Youngs approximation.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename VectorField_T >
+class ExtrapolateNormalsSweep
+{
+ protected:
+   using FlagUIDSet = Set< FlagUID >;
+
+   using vector_t = Vector3<real_t>;
+   using flag_t   = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+ public:
+   ExtrapolateNormalsSweep(const BlockDataID& normalFieldID, const ConstBlockDataID& flagFieldID,
+                           const FlagUID& interfaceFlagID)
+      : normalFieldID_(normalFieldID), flagFieldID_(flagFieldID), interfaceFlagID_(interfaceFlagID)
+   {}
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID normalFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   FlagUID interfaceFlagID_;
+}; // class ExtrapolateNormalsSweep
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "ExtrapolateNormalsSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h b/src/lbm_generated/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..d2f553e303c1cbc5d5db227c10e880b097cb5a7a
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h
@@ -0,0 +1,68 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ExtrapolateNormalsSweep.impl.h
+//! \ingroup surface_geometry
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Extrapolate interface normals to neighboring cells in D3Q27 direction.
+//
+//======================================================================================================================
+
+#include "core/math/Vector3.h"
+
+#include "field/GhostLayerField.h"
+
+#include "stencil/D3Q27.h"
+
+#include "ExtrapolateNormalsSweep.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename Stencil_T, typename FlagField_T, typename VectorField_T >
+void ExtrapolateNormalsSweep< Stencil_T, FlagField_T, VectorField_T >::operator()(IBlock* const block)
+{
+   VectorField_T* const normalField   = block->getData< VectorField_T >(normalFieldID_);
+   const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_);
+
+   const auto interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+   // compute normals in interface neighboring cells, i.e., in D3Q27 direction of interface cells
+   WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, {
+      if (!isFlagSet(flagFieldIt, interfaceFlag) && isFlagInNeighborhood< stencil::D3Q27 >(flagFieldIt, interfaceFlag))
+      {
+         vector_t normal = Vector3(normalFieldIt.getF(0), normalFieldIt.getF(1), normalFieldIt.getF(2));
+         normal.set(real_c(0), real_c(0), real_c(0));
+
+         // approximate the normal of non-interface cells with the normal of neighboring interface cells (weights as
+         // in Parker-Youngs approximation)
+         for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i)
+         {
+            if (isFlagSet(flagFieldIt.neighbor(*i), interfaceFlag))
+            {
+               normal += real_c(stencil::gaussianMultipliers[i.toIdx()]) * Vector3(normalFieldIt.neighbor(*i, 0), normalFieldIt.neighbor(*i, 1), normalFieldIt.neighbor(*i, 2));
+            }
+         }
+
+         // normalize the normal
+         normal = normal.getNormalizedOrZero();
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/NormalSweep.h b/src/lbm_generated/free_surface/surface_geometry/NormalSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..47ff64fca7bc101952223dbb875dccf5b0fc9ef9
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/NormalSweep.h
@@ -0,0 +1,109 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file NormalSweep.h
+//! \ingroup surface_geometry
+//! \author Martin Bauer <martin.bauer@fau.de>
+//! \author Daniela Anderl
+//! \author Stefan Donath
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute interface normal.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+
+#include <type_traits>
+#include <vector>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Compute normals in interface cells by taking the derivative of the fill level field using the Parker-Youngs
+ * approximation. Near boundary cells, a modified Parker-Youngs approximation is applied with the cell being shifted by
+ * 0.5 away from the boundary.
+ *
+ * Details can be found in the Dissertation of S. Donath, page 21f.
+ *
+ * IMPORTANT REMARK: In this FSLBM implementation, the normal is defined to point from liquid to gas.
+ *
+ * More general: compute the gradient of a given scalar field on cells that are marked with a specific flag.
+ **********************************************************************************************************************/
+
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class NormalSweep
+{
+ protected:
+   using vector_t = Vector3<real_t>;
+   using scalar_t = typename std::remove_const< typename ScalarField_T::value_type >::type;
+   using flag_t   = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+ public:
+   NormalSweep(const BlockDataID& normalFieldID, const ConstBlockDataID& fillFieldID,
+               const ConstBlockDataID& flagFieldID, const FlagUID& interfaceFlagID,
+               const Set< FlagUID >& liquidInterfaceGasFlagIDSet, const Set< FlagUID >& obstacleFlagIDSet,
+               bool computeInInterfaceNeighbors, bool includeObstacleNeighbors, bool modifyNearObstacles,
+               bool computeInGhostLayer)
+      : normalFieldID_(normalFieldID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID),
+        interfaceFlagID_(interfaceFlagID), liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet),
+        obstacleFlagIDSet_(obstacleFlagIDSet), computeInInterfaceNeighbors_(computeInInterfaceNeighbors),
+        includeObstacleNeighbors_(includeObstacleNeighbors), modifyNearObstacles_(modifyNearObstacles),
+        computeInGhostLayer_(computeInGhostLayer)
+   {}
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID normalFieldID_;
+   ConstBlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   FlagUID interfaceFlagID_;
+   Set< FlagUID > liquidInterfaceGasFlagIDSet_;
+   Set< FlagUID > obstacleFlagIDSet_;
+
+   bool computeInInterfaceNeighbors_;
+   bool includeObstacleNeighbors_;
+   bool modifyNearObstacles_;
+   bool computeInGhostLayer_;
+}; // class NormalSweep
+
+// namespace to use these functions outside NormalSweep, e.g., in ReinitializationSweep
+namespace normal_computation
+{
+// compute the normal using Parker-Youngs approximation (see dissertation of S. Donath, 2011, section 2.3.3.1.1)
+template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t >
+void computeNormal(vector_t& normal, const ScalarFieldIt_T& fillFieldIt, const FlagFieldIt_T& flagFieldIt,
+                   const flag_t& validNeighborFlagMask);
+
+// near solid boundary cells, compute a Parker-Youngs approximation around a virtual (constructed) midpoint that is
+// displaced by a distance of 0.5 away from the boundary (see dissertation of S. Donath, 2011, section 6.3.5.1)
+template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t >
+void computeNormalNearSolidBoundary(vector_t& normal, const ScalarFieldIt_T& fillFieldIt,
+                                    const FlagFieldIt_T& flagFieldIt, const flag_t& validNeighborFlagMask,
+                                    const flag_t& obstacleFlagMask);
+} // namespace normal_computation
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "NormalSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/NormalSweep.impl.h b/src/lbm_generated/free_surface/surface_geometry/NormalSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..e144416fe478618775f9d7665b6c9a9c18cfaec7
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/NormalSweep.impl.h
@@ -0,0 +1,458 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file NormalSweep.impl.h
+//! \ingroup surface_geometry
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute interface normal.
+//
+//======================================================================================================================
+
+#include "core/logging/Logging.h"
+#include "core/math/Vector3.h"
+
+#include "field/iterators/IteratorMacros.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+#include <type_traits>
+
+#include "NormalSweep.h"
+namespace walberla
+{
+namespace free_surface_generated
+{
+
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(IBlock* const block)
+{
+   // fetch fields
+   VectorField_T* const normalField     = block->getData< VectorField_T >(normalFieldID_);
+   const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_);
+   const FlagField_T* const flagField   = block->getData< const FlagField_T >(flagFieldID_);
+
+   // two ghost layers are required in the flag field
+   WALBERLA_ASSERT_EQUAL(flagField->nrOfGhostLayers(), uint_c(2));
+
+   // get flags
+   const flag_t interfaceFlag              = flagField->getFlag(interfaceFlagID_);
+   const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_);
+   const flag_t obstacleFlagMask           = flagField->getMask(obstacleFlagIDSet_);
+
+   // evaluate flags in D2Q19 neighborhood for 2D, and in D3Q27 neighborhood for 3D simulations
+   using NeighborhoodStencil_T =
+      typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+
+   // include ghost layer because solid cells might be located in the (outermost global) ghost layer
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(normalField, uint_c(1), {
+      if (!computeInGhostLayer_ && (!flagField->isInInnerPart(Cell(x, y, z)))) { continue; }
+
+      const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+      const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z);
+
+      const bool computeNormalInCell =
+         isFlagSet(flagFieldPtr, interfaceFlag) ||
+         (computeInInterfaceNeighbors_ && isFlagInNeighborhood< NeighborhoodStencil_T >(flagFieldPtr, interfaceFlag));
+
+      vector_t normal = Vector3(normalField->get(x, y, z, 0),
+                                normalField->get(x, y, z, 1),
+                                normalField->get(x, y, z, 2)) ;
+
+      if (computeNormalInCell)
+      {
+         if (includeObstacleNeighbors_)
+         {
+            // requires meaningful fill level values in obstacle cells, as set by ObstacleFillLevelSweep when using
+            // curvature computation via the finite difference method
+            normal_computation::computeNormal< Stencil_T >(normal, fillFieldPtr, flagFieldPtr,
+                                                           liquidInterfaceGasFlagMask | obstacleFlagMask);
+         }
+         else
+         {
+            if (modifyNearObstacles_ && isFlagInNeighborhood< Stencil_T >(flagFieldPtr, obstacleFlagMask))
+            {
+               // near solid boundary cells, compute a Parker-Youngs approximation around a virtual (constructed)
+               // midpoint that is displaced by a distance of 0.5 away from the boundary (see dissertation of S. Donath,
+               // 2011, section 6.3.5.1); use only for curvature computation based on local triangulation
+               normal_computation::computeNormalNearSolidBoundary< Stencil_T >(
+                  normal, fillFieldPtr, flagFieldPtr, liquidInterfaceGasFlagMask, obstacleFlagMask);
+            }
+            else
+            {
+               normal_computation::computeNormal< Stencil_T >(normal, fillFieldPtr, flagFieldPtr,
+                                                              liquidInterfaceGasFlagMask);
+            }
+         }
+
+         // normalize and negate normal (to make it point from liquid to gas)
+         normal = real_c(-1) * normal.getNormalizedOrZero();
+      }
+      else { normal.set(real_c(0), real_c(0), real_c(0)); }
+   }); // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+namespace normal_computation
+{
+template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t >
+void computeNormal(vector_t& normal, const ScalarFieldIt_T& fillFieldIt, const FlagFieldIt_T& flagFieldIt,
+                   const flag_t& validNeighborFlagMask)
+{
+   // All computations are performed in double precision here (and truncated later). This is done to avoid an issue
+   // observed with the Intel 19 compiler when built in "DebugOptimized" mode with single precision. There, the result
+   // is dependent on the order of the "tmp_*" variables' definitions. It is assumed that an Intel-specific optimization
+   // leads to floating point inaccuracies.
+   normal = vector_t(real_c(0));
+
+   // avoid accessing neighbors that are out-of-range, i.e., restrict neighbor access to first ghost layer
+   const bool useW = flagFieldIt.x() >= cell_idx_c(0);
+   const bool useE = flagFieldIt.x() < cell_idx_c(flagFieldIt.getField()->xSize());
+   const bool useS = flagFieldIt.y() >= cell_idx_c(0);
+   const bool useN = flagFieldIt.y() < cell_idx_c(flagFieldIt.getField()->ySize());
+
+   // loops are unrolled for improved computational performance
+   // IMPORTANT REMARK: the non-unrolled implementation was observed to give different results at O(1e-15); this
+   // accumulated and lead to inaccuracies, e.g., a drop wetting a surface became asymmetrical and started to move
+   // sideways
+   if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 >)
+   {
+      // get fill level in neighboring cells
+      const double tmp_S  = useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(0, -1, 0)) :
+                               static_cast< double >(0);
+      const double tmp_N  = useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(0, 1, 0)) :
+                               static_cast< double >(0);
+      const double tmp_W  = useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(-1, 0, 0)) :
+                               static_cast< double >(0);
+      const double tmp_E  = useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(1, 0, 0)) :
+                               static_cast< double >(0);
+      const double tmp_SW = useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(-1, -1, 0)) :
+                               static_cast< double >(0);
+      const double tmp_SE = useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(1, -1, 0)) :
+                               static_cast< double >(0);
+      const double tmp_NW = useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(-1, 1, 0)) :
+                               static_cast< double >(0);
+      const double tmp_NE = useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 0), validNeighborFlagMask) ?
+                               static_cast< double >(fillFieldIt.neighbor(1, 1, 0)) :
+                               static_cast< double >(0);
+
+      // compute normal with Parker-Youngs approximation (PY)
+      const double weight_1 = real_c(4);
+      normal[0]             = real_c(weight_1 * (tmp_E - tmp_W));
+      normal[1]             = real_c(weight_1 * (tmp_N - tmp_S));
+      normal[2]             = real_c(0);
+
+      const double weight_2 = real_c(2);
+      normal[0] += real_c(weight_2 * ((tmp_NE + tmp_SE) - (tmp_NW + tmp_SW)));
+      normal[1] += real_c(weight_2 * ((tmp_NE + tmp_NW) - (tmp_SE + tmp_SW)));
+   }
+   else
+   {
+      const bool useB = flagFieldIt.z() >= cell_idx_c(0);
+      const bool useT = flagFieldIt.z() < cell_idx_c(flagFieldIt.getField()->zSize());
+
+      if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >)
+      {
+         // get fill level in neighboring cells
+         const double tmp_S  = useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, -1, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_N  = useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, 1, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_W  = useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(-1, 0, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_E  = useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(1, 0, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_SW = useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(-1, -1, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_SE = useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(1, -1, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_NW = useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(-1, 1, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_NE = useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 0), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(1, 1, 0)) :
+                                  static_cast< double >(0);
+         const double tmp_B  = useB && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, -1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, 0, -1)) :
+                                  static_cast< double >(0);
+         const double tmp_T  = useT && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, 1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, 0, 1)) :
+                                  static_cast< double >(0);
+         const double tmp_BS = useB && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, -1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, -1, -1)) :
+                                  static_cast< double >(0);
+         const double tmp_BN = useB && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, -1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, 1, -1)) :
+                                  static_cast< double >(0);
+         const double tmp_BW = useB && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, -1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(-1, 0, -1)) :
+                                  static_cast< double >(0);
+         const double tmp_BE = useB && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, -1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(1, 0, -1)) :
+                                  static_cast< double >(0);
+         const double tmp_TS = useT && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, -1, 1)) :
+                                  static_cast< double >(0);
+         const double tmp_TN = useT && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(0, 1, 1)) :
+                                  static_cast< double >(0);
+         const double tmp_TW = useT && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(-1, 0, 1)) :
+                                  static_cast< double >(0);
+         const double tmp_TE = useT && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 1), validNeighborFlagMask) ?
+                                  static_cast< double >(fillFieldIt.neighbor(1, 0, 1)) :
+                                  static_cast< double >(0);
+
+         // compute normal with Parker-Youngs approximation (PY)
+         const double weight_1 = real_c(4);
+         normal[0]             = real_c(weight_1 * (tmp_E - tmp_W));
+         normal[1]             = real_c(weight_1 * (tmp_N - tmp_S));
+         normal[2]             = real_c(weight_1 * (tmp_T - tmp_B));
+
+         const double weight_2 = real_c(2);
+         normal[0] += real_c(weight_2 * ((tmp_NE + tmp_SE + tmp_TE + tmp_BE) - (tmp_NW + tmp_SW + tmp_TW + tmp_BW)));
+         normal[1] += real_c(weight_2 * ((tmp_NE + tmp_NW + tmp_TN + tmp_BN) - (tmp_SE + tmp_SW + tmp_TS + tmp_BS)));
+         normal[2] += real_c(weight_2 * ((tmp_TN + tmp_TS + tmp_TE + tmp_TW) - (tmp_BN + tmp_BS + tmp_BE + tmp_BW)));
+      }
+      else
+      {
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >)
+         {
+            // get fill level in neighboring cells
+            const double tmp_S = useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 0), validNeighborFlagMask) ?
+                                    static_cast< double >(fillFieldIt.neighbor(0, -1, 0)) :
+                                    static_cast< double >(0);
+            const double tmp_N = useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 0), validNeighborFlagMask) ?
+                                    static_cast< double >(fillFieldIt.neighbor(0, 1, 0)) :
+                                    static_cast< double >(0);
+            const double tmp_W = useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 0), validNeighborFlagMask) ?
+                                    static_cast< double >(fillFieldIt.neighbor(-1, 0, 0)) :
+                                    static_cast< double >(0);
+            const double tmp_E = useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 0), validNeighborFlagMask) ?
+                                    static_cast< double >(fillFieldIt.neighbor(1, 0, 0)) :
+                                    static_cast< double >(0);
+            const double tmp_SW =
+               useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 0), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, -1, 0)) :
+                  static_cast< double >(0);
+            const double tmp_SE =
+               useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 0), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, -1, 0)) :
+                  static_cast< double >(0);
+            const double tmp_NW =
+               useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 0), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, 1, 0)) :
+                  static_cast< double >(0);
+            const double tmp_NE =
+               useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 0), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, 1, 0)) :
+                  static_cast< double >(0);
+            const double tmp_B = useB && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, -1), validNeighborFlagMask) ?
+                                    static_cast< double >(fillFieldIt.neighbor(0, 0, -1)) :
+                                    static_cast< double >(0);
+            const double tmp_T = useT && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, 1), validNeighborFlagMask) ?
+                                    static_cast< double >(fillFieldIt.neighbor(0, 0, 1)) :
+                                    static_cast< double >(0);
+            const double tmp_BS =
+               useB && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(0, -1, -1)) :
+                  static_cast< double >(0);
+            const double tmp_BN =
+               useB && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(0, 1, -1)) :
+                  static_cast< double >(0);
+            const double tmp_BW =
+               useB && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, 0, -1)) :
+                  static_cast< double >(0);
+            const double tmp_BE =
+               useB && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, 0, -1)) :
+                  static_cast< double >(0);
+            const double tmp_TS =
+               useT && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(0, -1, 1)) :
+                  static_cast< double >(0);
+            const double tmp_TN =
+               useT && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(0, 1, 1)) :
+                  static_cast< double >(0);
+            const double tmp_TW =
+               useT && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, 0, 1)) :
+                  static_cast< double >(0);
+            const double tmp_TE =
+               useT && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, 0, 1)) :
+                  static_cast< double >(0);
+            const double tmp_BSW =
+               useB && useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, -1, -1)) :
+                  static_cast< double >(0);
+            const double tmp_BNW =
+               useB && useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, 1, -1)) :
+                  static_cast< double >(0);
+            const double tmp_BSE =
+               useB && useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, -1, -1)) :
+                  static_cast< double >(0);
+            const double tmp_BNE =
+               useB && useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, -1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, 1, -1)) :
+                  static_cast< double >(0);
+            const double tmp_TSW =
+               useT && useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, -1, 1)) :
+                  static_cast< double >(0);
+            const double tmp_TNW =
+               useT && useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(-1, 1, 1)) :
+                  static_cast< double >(0);
+            const double tmp_TSE =
+               useT && useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, -1, 1)) :
+                  static_cast< double >(0);
+            const double tmp_TNE =
+               useT && useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 1), validNeighborFlagMask) ?
+                  static_cast< double >(fillFieldIt.neighbor(1, 1, 1)) :
+                  static_cast< double >(0);
+
+            // compute normal with Parker-Youngs approximation (PY)
+            const double weight_1 = real_c(4);
+            normal[0]             = real_c(weight_1 * (tmp_E - tmp_W));
+            normal[1]             = real_c(weight_1 * (tmp_N - tmp_S));
+            normal[2]             = real_c(weight_1 * (tmp_T - tmp_B));
+
+            const double weight_2 = real_c(2);
+            normal[0] += real_c(weight_2 * ((tmp_NE + tmp_SE + tmp_TE + tmp_BE) - (tmp_NW + tmp_SW + tmp_TW + tmp_BW)));
+            normal[1] += real_c(weight_2 * ((tmp_NE + tmp_NW + tmp_TN + tmp_BN) - (tmp_SE + tmp_SW + tmp_TS + tmp_BS)));
+            normal[2] += real_c(weight_2 * ((tmp_TN + tmp_TS + tmp_TE + tmp_TW) - (tmp_BN + tmp_BS + tmp_BE + tmp_BW)));
+
+            // weight (=1) corresponding to Parker-Youngs approximation
+            normal[0] += real_c((tmp_TNE + tmp_TSE + tmp_BNE + tmp_BSE) - (tmp_TNW + tmp_TSW + tmp_BNW + tmp_BSW));
+            normal[1] += real_c((tmp_TNE + tmp_TNW + tmp_BNE + tmp_BNW) - (tmp_TSE + tmp_TSW + tmp_BSE + tmp_BSW));
+            normal[2] += real_c((tmp_TNE + tmp_TNW + tmp_TSE + tmp_TSW) - (tmp_BNE + tmp_BNW + tmp_BSE + tmp_BSW));
+         }
+         else { WALBERLA_ABORT("The chosen stencil type is not implemented in computeNormal()."); }
+      }
+   }
+}
+
+template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t >
+void computeNormalNearSolidBoundary(vector_t& normal, const ScalarFieldIt_T& fillFieldIt,
+                                    const FlagFieldIt_T& flagFieldIt, const flag_t& validNeighborFlagMask,
+                                    const flag_t& obstacleFlagMask)
+{
+   Vector3< real_t > midPoint(real_c(0));
+
+   // construct the virtual midpoint
+   for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+   {
+      if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), validNeighborFlagMask) &&
+          isPartOfMaskSet(flagFieldIt.neighbor(dir.inverseDir()), obstacleFlagMask))
+      {
+         if constexpr (Stencil_T::D == uint_t(2))
+         {
+            midPoint[0] += real_c(dir.cx()) *
+                           real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]);
+            midPoint[1] += real_c(dir.cy()) *
+                           real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]);
+            midPoint[2] += real_c(0);
+         }
+         else
+         {
+            midPoint[0] += real_c(dir.cx()) * real_c(stencil::gaussianMultipliers[dir.toIdx()]);
+            midPoint[1] += real_c(dir.cy()) * real_c(stencil::gaussianMultipliers[dir.toIdx()]);
+            midPoint[2] += real_c(dir.cz()) * real_c(stencil::gaussianMultipliers[dir.toIdx()]);
+         }
+      }
+   }
+
+   // restrict the displacement of the virtual midpoint to an absolute value of 0.5
+   for (uint_t i = uint_c(0); i != uint_c(3); ++i)
+   {
+      if (midPoint[i] > real_c(0.0)) { midPoint[i] = real_c(0.5); }
+      else
+      {
+         if (midPoint[i] < real_c(0.0)) { midPoint[i] = real_c(-0.5); }
+         // else midPoint[i] == 0
+      }
+   }
+
+   normal.set(real_c(0), real_c(0), real_c(0));
+
+   // use standard Parker-Youngs approximation (PY) for all cells without solid boundary neighbors
+   // otherwise shift neighboring cell by virtual midpoint (also referred to as narrower PY)
+   for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+   {
+      // skip directions that have obstacleFlagMask set in this and the opposing direction; this is NOT documented in
+      // literature, however, it avoids that an undefined fill level is used from the wall cells
+      if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), obstacleFlagMask) &&
+          isPartOfMaskSet(flagFieldIt.neighbor(dir.inverseDir()), obstacleFlagMask))
+      {
+         continue;
+      }
+
+      cell_idx_t modCx = dir.cx();
+      cell_idx_t modCy = dir.cy();
+      cell_idx_t modCz = dir.cz();
+
+      // shift neighboring cells by midpoint if they are solid boundary cells
+      if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), obstacleFlagMask) ||
+          isPartOfMaskSet(flagFieldIt.neighbor(dir.inverseDir()), obstacleFlagMask))
+      {
+         // truncate cells towards 0, i.e., make the access pattern narrower
+         modCx = cell_idx_c(real_c(modCx) + midPoint[0]);
+         modCy = cell_idx_c(real_c(modCy) + midPoint[1]);
+         modCz = cell_idx_c(real_c(modCz) + midPoint[2]);
+      }
+
+      real_t fill;
+
+      if (isPartOfMaskSet(flagFieldIt.neighbor(modCx, modCy, modCz), validNeighborFlagMask))
+      {
+         // compute normal with formula from regular Parker-Youngs approximation
+         if constexpr (Stencil_T::D == uint_t(2))
+         {
+            fill = fillFieldIt.neighbor(modCx, modCy, modCz) *
+                   real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]);
+         }
+         else { fill = fillFieldIt.neighbor(modCx, modCy, modCz) * real_c(stencil::gaussianMultipliers[dir.toIdx()]); }
+
+         normal[0] += real_c(dir.cx()) * fill;
+         normal[1] += real_c(dir.cy()) * fill;
+         normal[2] += real_c(dir.cz()) * fill;
+      }
+      else { normal = Vector3< real_t >(real_c(0)); }
+   }
+}
+} // namespace normal_computation
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/ObstacleFillLevelSweep.h b/src/lbm_generated/free_surface/surface_geometry/ObstacleFillLevelSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..031eca256b8305bf9c48dd5276a45cda0ba7eb34
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ObstacleFillLevelSweep.h
@@ -0,0 +1,91 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ObstacleFillLevelSweep.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Reflect fill levels into obstacle cells (for finite difference curvature computation).
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Reflect fill levels into obstacle cells by averaging the fill levels from fluid cells with weights according to the
+ * surface normal.
+ *
+ * See dissertation of S. Bogner, 2017 (section 4.4.2.1).
+ *
+ * IMPORTANT REMARK: If an obstacle is located in a non-periodic outermost ghost layer, the fill level field must have
+ * two ghost layers. That is, the ObstacleFillLevelSweep computes the fill level obstacle of cells located in the
+ * outermost global ghost layer. For this, the all neighboring cells' fill levels are required.
+ * A single ghost layer is not sufficient, because the computed values by ObstacleFillLevelSweep (located in an
+ * outermost global ghost layer) are not communicated themselves. In the example below, the values A, D, E, and H are
+ * located in a global outermost ghost layer. Only directions without # shall be communicated and * marks ghost layers
+ * in the directions to be communicated. In this example, only B, C, F, and G will be communicated as expected. In
+ * contrast, A, D, E, and H will not be communicated.
+ *
+ *  Block 1      Block 2                            Block 1      Block 2
+ *  ######       ######                             ######       ######
+ *  # A |*       *| E #                             # A |*       *| E #
+ *  # ----       -----#                             # ----       -----#
+ *  # B |*       *| F #     ===> communication      # B |F       B| F #
+ *  # C |*       *| G #                             # C |G       C| G #
+ *  # ----       -----#                             # ----       -----#
+ *  # D |*       *| H #                             # D |*       *| H #
+ *  ######       ######                             ######       ######
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ObstacleFillLevelSweep
+{
+ protected:
+   using FlagUIDSet = Set< FlagUID >;
+
+   using vector_t = Vector3<real_t>;
+   using flag_t   = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+ public:
+   ObstacleFillLevelSweep(const BlockDataID& fillFieldDstID, const ConstBlockDataID& fillFieldSrcID,
+                          const ConstBlockDataID& flagFieldID, const ConstBlockDataID& obstacleNormalFieldID,
+                          const FlagUIDSet& liquidInterfaceGasFlagIDSet, const FlagUIDSet& obstacleFlagIDSet)
+      : fillFieldDstID_(fillFieldDstID), fillFieldSrcID_(fillFieldSrcID), flagFieldID_(flagFieldID),
+        obstacleNormalFieldID_(obstacleNormalFieldID), liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet),
+        obstacleFlagIDSet_(obstacleFlagIDSet)
+   {}
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID fillFieldDstID_;
+   ConstBlockDataID fillFieldSrcID_;
+   ConstBlockDataID flagFieldID_;
+   ConstBlockDataID obstacleNormalFieldID_;
+
+   FlagUIDSet liquidInterfaceGasFlagIDSet_;
+   FlagUIDSet obstacleFlagIDSet_;
+}; // class ObstacleFillLevelSweep
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "ObstacleFillLevelSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h b/src/lbm_generated/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..c6fa9765cbd13319b0d447a4a9c494e2511471b3
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h
@@ -0,0 +1,92 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ObstacleFillLevelSweep.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Reflect fill levels into obstacle cells (for finite difference curvature computation).
+//
+//======================================================================================================================
+
+#include "core/debug/CheckFunctions.h"
+#include "core/math/Utility.h"
+#include "core/math/Vector3.h"
+
+#include "field/FlagField.h"
+
+#include <algorithm>
+#include <cmath>
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(IBlock* const block)
+{
+   // get fields
+   const ScalarField_T* const fillFieldSrc        = block->getData< const ScalarField_T >(fillFieldSrcID_);
+   ScalarField_T* const fillFieldDst              = block->getData< ScalarField_T >(fillFieldDstID_);
+   const FlagField_T* const flagField             = block->getData< const FlagField_T >(flagFieldID_);
+   const VectorField_T* const obstacleNormalField = block->getData< const VectorField_T >(obstacleNormalFieldID_);
+
+   // get flags
+   const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_);
+   const flag_t obstacleFlagMask           = flagField->getMask(obstacleFlagIDSet_);
+
+   // equation (4.22) in dissertation of S. Bogner, 2017 (section 4.4.2.1); include ghost layer because solid cells
+   // might be located in the (outermost global) ghost layer
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillFieldDst, uint_c(1), {
+      // IMPORTANT REMARK: do not restrict this algorithm to obstacle cells that are direct neighbors of interface
+      // cells; the succeeding SmoothingSweep uses the values computed here and must be executed for an at least
+      // two-cell neighborhood of interface cells
+      const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+      const typename ScalarField_T::ConstPtr fillFieldSrcPtr(*fillFieldSrc, x, y, z);
+      const typename ScalarField_T::Ptr fillFieldDstPtr(*fillFieldDst, x, y, z);
+
+      if (isPartOfMaskSet(flagFieldPtr, obstacleFlagMask) &&
+          isFlagInNeighborhood< Stencil_T >(flagFieldPtr, liquidInterfaceGasFlagMask))
+      {
+         vector_t obstacleNormal = Vector3(obstacleNormalField->get(x, y, z, 0),
+                                           obstacleNormalField->get(x, y, z, 1),
+                                           obstacleNormalField->get(x, y, z, 2)) ;
+
+         WALBERLA_CHECK_GREATER(obstacleNormal.length(), real_c(0),
+                                "An obstacleNormal of an obstacle cell was found to be zero in obstacleNormalSweep. "
+                                "This is not plausible.");
+
+         real_t sum       = real_c(0);
+         real_t weightSum = real_c(0);
+         for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+         {
+            if (isPartOfMaskSet(flagFieldPtr.neighbor(*dir), liquidInterfaceGasFlagMask))
+            {
+               const Vector3< real_t > dirVector =
+                  Vector3< real_t >(real_c(dir.cx()), real_c(dir.cy()), real_c(dir.cz())).getNormalized();
+
+               const real_t weight = std::abs(obstacleNormal * dirVector);
+
+               sum += weight * fillFieldSrcPtr.neighbor(*dir);
+
+               weightSum += weight;
+            }
+         }
+
+         *fillFieldDstPtr = weightSum > real_c(0) ? sum / weightSum : real_c(0);
+      }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/ObstacleNormalSweep.h b/src/lbm_generated/free_surface/surface_geometry/ObstacleNormalSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..579f3a23f6d206681b2f0d568ecee7d35e8d60e7
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ObstacleNormalSweep.h
@@ -0,0 +1,98 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ObstacleNormalSweep.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute a mean obstacle normal in interface cells near solid boundary cells.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/logging/Logging.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/FlagField.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Compute a mean obstacle normal in interface cells near obstacle cells, and/or in obstacle cells. This reduces the
+ * influence of a stair-case approximated wall in the wetting model.
+ *
+ * - computeInInterfaceCells: Compute the obstacle normal in interface cells. Required when using curvature computation
+ *                            based on local triangulation (not with finite difference method).
+ * - computeInObstacleCells: Compute the obstacle normal in obstacle cells. Required when using curvature computation
+ *                           based on the finite difference method (not with local triangulation).
+ *
+ * Details can be found in the dissertation of S. Donath, 2011 section 6.3.5.2.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename VectorField_T >
+class ObstacleNormalSweep
+{
+ protected:
+   using vector_t = Vector3<real_t>;
+   using flag_t   = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+ public:
+   ObstacleNormalSweep(const BlockDataID& obstacleNormalFieldID, const ConstBlockDataID& flagFieldID,
+                       const FlagUID& interfaceFlagID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet,
+                       const Set< FlagUID >& obstacleFlagIDSet, bool computeInInterfaceCells,
+                       bool computeInObstacleCells, bool computeInGhostLayer)
+      : obstacleNormalFieldID_(obstacleNormalFieldID), flagFieldID_(flagFieldID), interfaceFlagID_(interfaceFlagID),
+        liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFlagIDSet_(obstacleFlagIDSet),
+        computeInInterfaceCells_(computeInInterfaceCells), computeInObstacleCells_(computeInObstacleCells),
+        computeInGhostLayer_(computeInGhostLayer)
+   {
+      if (!computeInInterfaceCells_ && !computeInObstacleCells_)
+      {
+         WALBERLA_LOG_WARNING_ON_ROOT(
+            "In ObstacleNormalSweep, you specified to neither compute the obstacle normal in interface cells, nor in "
+            "obstacle cells. That is, ObstacleNormalSweep will do nothing. Please check if this is what you really "
+            "want.");
+      }
+   }
+
+   void operator()(IBlock* const block);
+
+ private:
+   template< typename FlagFieldIt_T >
+   void computeObstacleNormalInInterfaceCell(vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt,
+                                             const flag_t& validNeighborFlagMask);
+
+   template< typename FlagFieldIt_T >
+   void computeObstacleNormalInObstacleCell(vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt,
+                                            const flag_t& liquidInterfaceGasFlagMask);
+
+   BlockDataID obstacleNormalFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   FlagUID interfaceFlagID_;
+   Set< FlagUID > liquidInterfaceGasFlagIDSet_;
+   Set< FlagUID > obstacleFlagIDSet_;
+
+   bool computeInInterfaceCells_;
+   bool computeInObstacleCells_;
+   bool computeInGhostLayer_;
+}; // class ObstacleNormalSweep
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "ObstacleNormalSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/ObstacleNormalSweep.impl.h b/src/lbm_generated/free_surface/surface_geometry/ObstacleNormalSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..aacc785076ad3ea617404404be062ffba89a8491
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/ObstacleNormalSweep.impl.h
@@ -0,0 +1,149 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ObstacleNormalSweep.impl.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute a mean obstacle normal in interface cells near solid boundary cells.
+//
+//======================================================================================================================
+
+#include "core/math/Vector3.h"
+
+#include "field/iterators/IteratorMacros.h"
+
+#include "stencil/D3Q27.h"
+
+#include "ObstacleNormalSweep.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename Stencil_T, typename FlagField_T, typename VectorField_T >
+void ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T >::operator()(IBlock* const block)
+{
+   // do nothing if obstacle normal must not be computed anywhere
+   if (!computeInInterfaceCells_ && !computeInObstacleCells_) { return; }
+
+   // fetch fields
+   VectorField_T* const obstacleNormalField = block->getData< VectorField_T >(obstacleNormalFieldID_);
+   const FlagField_T* const flagField       = block->getData< const FlagField_T >(flagFieldID_);
+
+   // two ghost layers are required in the flag field
+   WALBERLA_ASSERT_EQUAL(flagField->nrOfGhostLayers(), uint_c(2));
+
+   // get flags
+   const flag_t interfaceFlag              = flagField->getFlag(interfaceFlagID_);
+   const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_);
+   const flag_t obstacleFlagMask           = flagField->getMask(obstacleFlagIDSet_);
+
+   // include ghost layer because solid cells might be located in the (outermost global) ghost layer
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(obstacleNormalField, uint_c(1), {
+      if (!computeInGhostLayer_ && (!flagField->isInInnerPart(Cell(x, y, z)))) { continue; }
+
+      const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z);
+
+      const bool computeInInterfaceCell = computeInInterfaceCells_ && isPartOfMaskSet(flagFieldPtr, interfaceFlag) &&
+                                          isFlagInNeighborhood< Stencil_T >(flagFieldPtr, obstacleFlagMask);
+
+      const bool computeInObstacleCell = computeInObstacleCells_ && isPartOfMaskSet(flagFieldPtr, obstacleFlagMask) &&
+                                         isFlagInNeighborhood< Stencil_T >(flagFieldPtr, liquidInterfaceGasFlagMask);
+
+      // IMPORTANT REMARK: do not restrict this algorithm to obstacle cells that are direct neighbors of interface
+      // cells; the succeeding ObstacleFillLevelSweep and SmoothingSweep use the values computed here and the latter
+      // must work on an at least two-cell neighborhood of interface cells
+
+      vector_t obstacleNormal = Vector3(obstacleNormalField->get(x, y, z, 0),
+                                        obstacleNormalField->get(x, y, z, 1),
+                                        obstacleNormalField->get(x, y, z, 2)) ;
+
+      if (computeInInterfaceCell)
+      {
+         WALBERLA_ASSERT(!computeInObstacleCell);
+
+         // compute mean obstacle, i.e., mean wall normal in interface cell (see dissertation of S. Donath, 2011,
+         // section 6.3.5.2)
+         computeObstacleNormalInInterfaceCell(obstacleNormal, flagFieldPtr, obstacleFlagMask);
+      }
+      else
+      {
+         if (computeInObstacleCell)
+         {
+            WALBERLA_ASSERT(!computeInInterfaceCell);
+
+            // compute mean obstacle normal in obstacle cell
+            computeObstacleNormalInObstacleCell(obstacleNormal, flagFieldPtr, liquidInterfaceGasFlagMask);
+         }
+         else
+         {
+            // set obstacle normal of all other cells to zero
+            obstacleNormal.set(real_c(0), real_c(0), real_c(0));
+         }
+      }
+
+      // normalize mean obstacle normal
+      const real_t sqrObstNormal = obstacleNormal.sqrLength();
+      if (sqrObstNormal > real_c(0))
+      {
+         const real_t invlength = -real_c(1) / real_c(std::sqrt(sqrObstNormal));
+         obstacleNormal *= invlength;
+      }
+   }); // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+template< typename Stencil_T, typename FlagField_T, typename VectorField_T >
+template< typename FlagFieldIt_T >
+void ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T >::computeObstacleNormalInInterfaceCell(
+   vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt, const flag_t& obstacleFlagMask)
+{
+   uint_t obstCount = uint_c(0);
+   obstacleNormal   = vector_t(real_c(0));
+
+   for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i)
+   {
+      // only consider directions in which there is an obstacle cell
+      if (isPartOfMaskSet(flagFieldIt.neighbor(*i), obstacleFlagMask))
+      {
+         obstacleNormal += vector_t(real_c(-i.cx()), real_c(-i.cy()), real_c(-i.cz()));
+         ++obstCount;
+      }
+   }
+   obstacleNormal = obstCount > uint_c(0) ? obstacleNormal / real_c(obstCount) : vector_t(real_c(0));
+}
+
+template< typename Stencil_T, typename FlagField_T, typename VectorField_T >
+template< typename FlagFieldIt_T >
+void ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T >::computeObstacleNormalInObstacleCell(
+   vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt, const flag_t& liquidInterfaceGasFlagMask)
+{
+   uint_t obstCount = uint_c(0);
+   obstacleNormal   = vector_t(real_c(0));
+
+   for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i)
+   {
+      // only consider directions in which there is a liquid, interface, or gas cell
+      if (isPartOfMaskSet(flagFieldIt.neighbor(*i), liquidInterfaceGasFlagMask))
+      {
+         obstacleNormal += vector_t(real_c(i.cx()), real_c(i.cy()), real_c(i.cz()));
+
+         ++obstCount;
+      }
+   }
+   obstacleNormal = obstCount > uint_c(0) ? obstacleNormal / real_c(obstCount) : vector_t(real_c(0));
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/SmoothingSweep.h b/src/lbm_generated/free_surface/surface_geometry/SmoothingSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..b849f0334099f8b72e103161e033fecc1b1a02a0
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/SmoothingSweep.h
@@ -0,0 +1,113 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SmoothingSweep.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Smooth fill levels (used for finite difference curvature computation).
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+// forward declaration
+template< typename Stencil_T >
+class KernelK8;
+
+/***********************************************************************************************************************
+ * Smooth fill levels such that interface-neighboring cells get assigned a new fill level. This is required for
+ * computing the interface curvature using the finite difference method.
+ *
+ * The same smoothing kernel is used as in the dissertation of S. Bogner, 2017, i.e., the K8 kernel with support
+ * radius 2 from
+ * Williams, Kothe and Puckett, "Accuracy and Convergence of Continuum Surface Tension Models", 1998.
+ **********************************************************************************************************************/
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class SmoothingSweep
+{
+ protected:
+   using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type;
+   using flag_t   = typename std::remove_const< typename FlagField_T::value_type >::type;
+
+ public:
+   SmoothingSweep(const BlockDataID& smoothFillFieldID, const ConstBlockDataID& fillFieldID,
+                  const ConstBlockDataID& flagFieldID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet,
+                  const Set< FlagUID >& obstacleFlagIDSet, bool includeObstacleNeighbors)
+      : smoothFillFieldID_(smoothFillFieldID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID),
+        liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFlagIDSet_(obstacleFlagIDSet),
+        includeObstacleNeighbors_(includeObstacleNeighbors), smoothingKernel_(KernelK8< Stencil_T >(real_c(2.0)))
+   {}
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID smoothFillFieldID_;
+   ConstBlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   Set< FlagUID > liquidInterfaceGasFlagIDSet_;
+   Set< FlagUID > obstacleFlagIDSet_;
+
+   bool includeObstacleNeighbors_;
+
+   KernelK8< Stencil_T > smoothingKernel_;
+}; // class SmoothingSweep
+
+/***********************************************************************************************************************
+ * K8 kernel from Williams, Kothe and Puckett, "Accuracy and Convergence of Continuum Surface Tension Models", 1998.
+ **********************************************************************************************************************/
+template< typename Stencil_T >
+class KernelK8
+{
+ public:
+   KernelK8(real_t epsilon) : epsilon_(epsilon) { stencilSize_ = uint_c(std::ceil(epsilon_) - real_c(1)); }
+
+   // equation (11) in Williams et al. (normalization constant A=1 here, result must be normalized outside the kernel)
+   inline real_t kernelFunction(const Vector3< real_t >& dirVec) const
+   {
+      const real_t r_sqr   = dirVec.sqrLength();
+      const real_t eps_sqr = epsilon_ * epsilon_;
+
+      if (r_sqr < eps_sqr)
+      {
+         real_t result = real_c(1.0) - (r_sqr / (eps_sqr));
+         result        = result * result * result * result;
+
+         return result;
+      }
+      else { return real_c(0.0); }
+   }
+
+   inline real_t getSupportRadius() const { return epsilon_; }
+   inline uint_t getStencilSize() const { return static_cast< uint_t >(stencilSize_); }
+
+ private:
+   real_t epsilon_;     // support radius of the kernel
+   uint_t stencilSize_; // size of the stencil which defines included neighbors in smoothing
+
+}; // class KernelK8
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "SmoothingSweep.impl.h"
\ No newline at end of file
diff --git a/src/lbm_generated/free_surface/surface_geometry/SmoothingSweep.impl.h b/src/lbm_generated/free_surface/surface_geometry/SmoothingSweep.impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..eab1f39df1aab47c83f2a02f1be1047d338e5794
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/SmoothingSweep.impl.h
@@ -0,0 +1,159 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SmoothingSweep.impl.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Smooth fill levels (used for finite difference curvature computation).
+//
+//======================================================================================================================
+
+#include "core/debug/CheckFunctions.h"
+#include "core/math/Utility.h"
+#include "core/math/Vector3.h"
+
+#include "field/FlagField.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include "ContactAngle.h"
+#include "SmoothingSweep.h"
+#include "Utility.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(IBlock* const block)
+{
+   // get fields
+   ScalarField_T* const smoothFillField = block->getData< ScalarField_T >(smoothFillFieldID_);
+   const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_);
+   const FlagField_T* const flagField   = block->getData< const FlagField_T >(flagFieldID_);
+
+   // get flags
+   const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_);
+   const flag_t obstacleFlagMask           = flagField->getMask(obstacleFlagIDSet_);
+
+   // const KernelK8< Stencil_T > smoothingKernel(real_c(2.0));
+
+   const uint_t kernelSize = smoothingKernel_.getStencilSize();
+
+   WALBERLA_CHECK_GREATER_EQUAL(
+      smoothFillField->nrOfGhostLayers(), kernelSize,
+      "Support radius of smoothing kernel results in a smoothing stencil size that exceeds the ghost layers.");
+
+   // including ghost layers is not necessary, even if solid cells are located in the (outermost global) ghost layer,
+   // because fill level in obstacle cells is set by ObstacleFillLevelSweep
+   WALBERLA_FOR_ALL_CELLS(smoothFillFieldIt, smoothFillField, fillFieldIt, fillField, flagFieldIt, flagField, {
+      // IMPORTANT REMARK: do not restrict this algorithm to interface cells and their neighbors; when the normals are
+      // computed in the neighborhood of interface cells, the second neighbors of interface cells are also required
+
+      // mollify fill level in interface, liquid, and gas according to equation (9) in Williams et al.
+      if (isPartOfMaskSet(flagFieldIt, liquidInterfaceGasFlagMask))
+      {
+         real_t normalizationConstant = real_c(0);
+         real_t smoothedFillLevel     = real_c(0);
+         if constexpr (Stencil_T::D == uint_t(2))
+         {
+            for (int j = -int_c(kernelSize); j <= int_c(kernelSize); ++j)
+            {
+               for (int i = -int_c(kernelSize); i <= int_c(kernelSize); ++i)
+               {
+                  const Vector3< real_t > dirVector(real_c(i), real_c(j), real_c(0));
+
+                  if (isPartOfMaskSet(flagFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(0)),
+                                      obstacleFlagMask))
+                  {
+                     if (includeObstacleNeighbors_)
+                     {
+                        // in solid cells, use values from smoothed fill field (instead of regular fill field) that have
+                        // been set by ObstacleFillLevelSweep
+                        smoothedFillLevel += smoothingKernel_.kernelFunction(dirVector) *
+                                             smoothFillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(0));
+
+                        if (dirVector.length() < smoothingKernel_.getSupportRadius())
+                        {
+                           normalizationConstant += smoothingKernel_.kernelFunction(dirVector);
+                        }
+                     } // else: do not include this direction in smoothing
+                  }
+                  else
+                  {
+                     smoothedFillLevel += smoothingKernel_.kernelFunction(dirVector) *
+                                          fillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(0));
+
+                     if (dirVector.length() < smoothingKernel_.getSupportRadius())
+                     {
+                        normalizationConstant += smoothingKernel_.kernelFunction(dirVector);
+                     }
+                  }
+               }
+            }
+         }
+         else
+         {
+            if constexpr (Stencil_T::D == uint_t(3))
+            {
+               for (int k = -int_c(kernelSize); k <= int_c(kernelSize); ++k)
+               {
+                  for (int j = -int_c(kernelSize); j <= int_c(kernelSize); ++j)
+                  {
+                     for (int i = -int_c(kernelSize); i <= int_c(kernelSize); ++i)
+                     {
+                        const Vector3< real_t > dirVector(real_c(i), real_c(j), real_c(k));
+
+                        if (isPartOfMaskSet(flagFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(k)),
+                                            obstacleFlagMask))
+                        {
+                           if (includeObstacleNeighbors_)
+                           {
+                              // in solid cells, use values from smoothed fill field (instead of regular fill field)
+                              // that have been set by ObstacleFillLevelSweep
+                              smoothedFillLevel +=
+                                 smoothingKernel_.kernelFunction(dirVector) *
+                                 smoothFillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(k));
+
+                              if (dirVector.length() < smoothingKernel_.getSupportRadius())
+                              {
+                                 normalizationConstant += smoothingKernel_.kernelFunction(dirVector);
+                              }
+                           } // else: do not include this direction in smoothing
+                        }
+                        else
+                        {
+                           smoothedFillLevel += smoothingKernel_.kernelFunction(dirVector) *
+                                                fillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(k));
+
+                           if (dirVector.length() < smoothingKernel_.getSupportRadius())
+                           {
+                              normalizationConstant += smoothingKernel_.kernelFunction(dirVector);
+                           }
+                        }
+                     }
+                  }
+               }
+            }
+         }
+
+         smoothedFillLevel /= normalizationConstant;
+         *smoothFillFieldIt = smoothedFillLevel;
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/SurfaceGeometryHandler.h b/src/lbm_generated/free_surface/surface_geometry/SurfaceGeometryHandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..db78bf3c0cf3b98f311290dfb8cad0a411eb9373
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/SurfaceGeometryHandler.h
@@ -0,0 +1,225 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SurfaceGeometryHandler.h
+//! \ingroup surface_geometry
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Handles the surface geometry (normal and curvature computation) by creating fields and adding sweeps.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/StringUtility.h"
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/AddToStorage.h"
+#include "field/FlagField.h"
+
+#include "lbm_generated/blockforest/SimpleCommunication.h"
+#include "lbm_generated/free_surface/BlockStateDetectorSweep.h"
+#include "lbm_generated/free_surface/FlagInfo.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <type_traits>
+#include <vector>
+
+#include "CurvatureModel.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Handles the surface geometry (normal and curvature computation) by creating fields and adding sweeps.
+ **********************************************************************************************************************/
+template< typename StorageSpecification_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, typename FlagInfo_T >
+class SurfaceGeometryHandler
+{
+ protected:
+   using vector_t = Vector3<real_t>;
+
+   // explicitly use either D2Q9 or D3Q27 here, as the geometry operations require (or are most accurate with) the full
+   // neighborhood;
+   using Stencil_T       = typename std::conditional< StorageSpecification_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+   using Communication_T = lbm_generated::SimpleCommunication< Stencil_T >;
+   using StateSweep      = BlockStateDetectorSweep< FlagField_T >; // used in friend classes
+
+ public:
+   SurfaceGeometryHandler(const std::shared_ptr< StructuredBlockForest >& blockForest,
+                          FlagInfo_T& flagInfo,
+                          const BlockDataID& flagFieldID,
+                          const BlockDataID& fillFieldID, const std::string& curvatureModel, bool computeCurvature,
+                          bool enableWetting, real_t contactAngleInDegrees)
+      : blockForest_(blockForest), flagInfo_(flagInfo), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID),
+        curvatureModel_(curvatureModel), computeCurvature_(computeCurvature), enableWetting_(enableWetting),
+        contactAngle_(ContactAngle(contactAngleInDegrees))
+   {
+      curvatureFieldID_      = field::addToStorage< ScalarField_T >(blockForest_, "Curvature field", real_c(0), field::fzyx, uint_c(1));
+      normalFieldID_         = field::addToStorage< VectorField_T >(blockForest_, "Normal field", real_c(0), field::fzyx, uint_c(1));
+      obstacleNormalFieldID_ = field::addToStorage< VectorField_T >(blockForest_, "Obstacle normal field", real_c(0), field::fzyx, uint_c(1));
+
+      obstacleFlagIDSet_ = flagInfo_.getObstacleIDSet();
+
+      if (StorageSpecification_T::Stencil::D == uint_t(2))
+      {
+         WALBERLA_LOG_INFO_ON_ROOT(
+            "IMPORTANT REMARK: You are using a D2Q9 stencil in SurfaceGeometryHandler. Be aware that the "
+            "results might slightly differ when compared to a D3Q19 stencil and periodicity in the third direction. "
+            "This is caused by the smoothing of the fill level field, where the additional directions in the D3Q27 add "
+            "additional weights to the smoothing kernel. Therefore, the resulting smoothed fill level will be "
+            "different.")
+      }
+   }
+
+   ConstBlockDataID getConstCurvatureFieldID() const { return curvatureFieldID_; }
+   ConstBlockDataID getConstNormalFieldID() const { return normalFieldID_; }
+   ConstBlockDataID getConstObstNormalFieldID() const { return obstacleNormalFieldID_; }
+
+   BlockDataID getCurvatureFieldID() const { return curvatureFieldID_; }
+   BlockDataID getNormalFieldID() const { return normalFieldID_; }
+   BlockDataID getObstNormalFieldID() const { return obstacleNormalFieldID_; }
+   FlagInfo_T getFlagInfo() const {return flagInfo_;}
+   Vector3< bool > isObstacleInGlobalGhostLayer() const
+   {
+      Vector3< bool > isObstacleInGlobalGhostLayer(false, false, false);
+
+      for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+      {
+         const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID_);
+
+         const CellInterval domainCellBB = blockForest_->getDomainCellBB();
+
+         // disable OpenMP such that loop termination works correctly
+         WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, {
+            // get cell in global coordinates
+            Cell globalCell = Cell(x, y, z);
+            blockForest_->transformBlockLocalToGlobalCell(globalCell, *blockIt);
+
+            // check if the current cell is located in a global ghost layer
+            const bool isCellInGlobalGhostLayerX =
+               globalCell[0] < domainCellBB.xMin() || globalCell[0] > domainCellBB.xMax();
+
+            const bool isCellInGlobalGhostLayerY =
+               globalCell[1] < domainCellBB.yMin() || globalCell[1] > domainCellBB.yMax();
+
+            const bool isCellInGlobalGhostLayerZ =
+               globalCell[2] < domainCellBB.zMin() || globalCell[2] > domainCellBB.zMax();
+
+            // skip corners, as they do not influence periodic communication
+            if ((isCellInGlobalGhostLayerX && (isCellInGlobalGhostLayerY || isCellInGlobalGhostLayerZ)) ||
+                (isCellInGlobalGhostLayerY && isCellInGlobalGhostLayerZ))
+            {
+               continue;
+            }
+
+            if (!isObstacleInGlobalGhostLayer[0] && isCellInGlobalGhostLayerX &&
+                isPartOfMaskSet(flagField->get(x, y, z), flagField->getMask(flagInfo_.getObstacleIDSet())))
+            {
+               isObstacleInGlobalGhostLayer[0] = true;
+            }
+
+            if (!isObstacleInGlobalGhostLayer[1] && isCellInGlobalGhostLayerY &&
+                isPartOfMaskSet(flagField->get(x, y, z), flagField->getMask(flagInfo_.getObstacleIDSet())))
+            {
+               isObstacleInGlobalGhostLayer[1] = true;
+            }
+
+            if (!isObstacleInGlobalGhostLayer[2] && isCellInGlobalGhostLayerZ &&
+                isPartOfMaskSet(flagField->get(x, y, z), flagField->getMask(flagInfo_.getObstacleIDSet())))
+            {
+               isObstacleInGlobalGhostLayer[2] = true;
+            }
+
+            if (isObstacleInGlobalGhostLayer[0] && isObstacleInGlobalGhostLayer[1] && isObstacleInGlobalGhostLayer[2])
+            {
+               break; // there is no need to check other cells on this block
+            }
+         }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP
+      }
+
+      mpi::allReduceInplace(isObstacleInGlobalGhostLayer, mpi::LOGICAL_OR);
+
+      return isObstacleInGlobalGhostLayer;
+   };
+
+   void addSweeps(SweepTimeloop& timeloop) const
+   {
+      auto blockStateUpdate = StateSweep(blockForest_, flagInfo_, flagFieldID_);
+
+      if (!string_icompare(curvatureModel_, "FiniteDifferenceMethod"))
+      {
+         curvature_model::FiniteDifferenceMethod< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >
+            model;
+         model.addSweeps(timeloop, *this);
+      }
+      else
+      {
+         if (!string_icompare(curvatureModel_, "LocalTriangulation"))
+         {
+            curvature_model::LocalTriangulation< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T, VectorField_T, FlagInfo_T >
+               model;
+            model.addSweeps(timeloop, *this);
+         }
+         else
+         {
+            if (!string_icompare(curvatureModel_, "SimpleFiniteDifferenceMethod"))
+            {
+               curvature_model::SimpleFiniteDifferenceMethod< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                              VectorField_T, FlagInfo_T >
+                  model;
+               model.addSweeps(timeloop, *this);
+            }
+            else { WALBERLA_ABORT("The specified curvature model is unknown.") }
+         }
+      }
+   }
+
+ protected:
+   std::shared_ptr< StructuredBlockForest > blockForest_; // used by friend classes
+
+   FlagInfo_T& flagInfo_;
+
+   BlockDataID flagFieldID_;
+   ConstBlockDataID fillFieldID_;
+
+   BlockDataID curvatureFieldID_;
+   BlockDataID normalFieldID_;
+   BlockDataID obstacleNormalFieldID_; // mean normal in obstacle cells required for e.g. artificial curvature contact
+                                       // model (dissertation of S. Donath, 2011, section 6.5.3.2)
+
+   Set< FlagUID > obstacleFlagIDSet_; // used by friend classes (see CurvatureModel.impl.h)
+
+   std::string curvatureModel_;
+   bool computeCurvature_;     // allows to not compute curvature (just normal) when e.g. the surface tension is 0
+   bool enableWetting_;        // used by friend classes
+   ContactAngle contactAngle_; // used by friend classes
+
+   friend class curvature_model::FiniteDifferenceMethod< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                         VectorField_T, FlagInfo_T >;
+   friend class curvature_model::LocalTriangulation< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                     VectorField_T, FlagInfo_T >;
+   friend class curvature_model::SimpleFiniteDifferenceMethod< Stencil_T, StorageSpecification_T, FlagField_T, ScalarField_T,
+                                                               VectorField_T, FlagInfo_T >;
+}; // class SurfaceGeometryHandler
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/Utility.cpp b/src/lbm_generated/free_surface/surface_geometry/Utility.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08711fd8b85584a963e3c5197110fe426aedceac
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/Utility.cpp
@@ -0,0 +1,547 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file Utility.cpp
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Helper functions for surface geometry computations.
+//
+//======================================================================================================================
+
+#include "Utility.h"
+
+#include "core/math/Constants.h"
+#include "core/math/Matrix3.h"
+#include "core/math/Vector3.h"
+
+#include <cmath>
+#include <vector>
+
+#include "ContactAngle.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+bool computeArtificalWallPoint(const Vector3< real_t >& globalInterfacePointLocation,
+                               const Vector3< real_t >& globalCellCoordinate, const Vector3< real_t >& normal,
+                               const Vector3< real_t >& wallNormal, const Vector3< real_t >& obstacleNormal,
+                               const ContactAngle& contactAngle, Vector3< real_t >& artificialWallPointCoord,
+                               Vector3< real_t >& artificialWallNormal)
+{
+   // get local interface point location (location of interface point inside cell with origin (0.5,0.5,0.5))
+   const Vector3< real_t > interfacePointLocation = globalInterfacePointLocation - globalCellCoordinate;
+
+   // check whether the interface plane intersects one of the cell's edges; exit this function if it does not intersect
+   // any edge in at least one direction (see dissertation of S. Donath, 2011, section 6.4.5.4)
+   if (wallNormal[0] < real_c(0) &&
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)))
+   {
+      return false;
+   }
+
+   if (wallNormal[0] > real_c(0) &&
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(1)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)))
+   {
+      return false;
+   }
+
+   if (wallNormal[1] < real_c(0) &&
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)))
+   {
+      return false;
+   }
+
+   if (wallNormal[1] > real_c(0) &&
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(1)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)))
+   {
+      return false;
+   }
+
+   if (wallNormal[2] < real_c(0) &&
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)))
+   {
+      return false;
+   }
+
+   if (wallNormal[2] > real_c(0) &&
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(1)),
+                                Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)) &&
+
+       (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(1)),
+                                Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal,
+                                interfacePointLocation) < real_c(0)))
+   {
+      return false;
+   }
+
+   // line 1 in Algorithm 6.2 in dissertation of S. Donath, 2011
+   real_t cosAlpha = dot(normal, obstacleNormal);
+
+   // determine sin(alpha) via orthogonal projections to compute alpha in the correct quadrant
+   const Vector3< real_t > projector = normal - cosAlpha * obstacleNormal;
+   const Vector3< real_t > baseVec   = cross(obstacleNormal, normal);
+   const real_t sinAlpha             = projector * cross(baseVec, obstacleNormal).getNormalized();
+
+   // compute alpha (angle between surface plane and wall)
+   real_t alpha;
+   if (sinAlpha >= real_c(0)) { alpha = real_c(std::acos(cosAlpha)); }
+   else { alpha = real_c(2) * math::pi - real_c(std::acos(cosAlpha)); }
+
+   // line 2 in Algorithm 6.2 in dissertation of S. Donath, 2011
+   const real_t theta = contactAngle.getInRadians();
+   const real_t delta = theta - alpha;
+
+   // determine distance from interface point to wall plane
+   const real_t wallDistance = dot(fabs(interfacePointLocation), wallNormal);
+
+   // line 3-4 in Algorithm 6.2 in dissertation of S. Donath, 2011
+   // correct contact angle is already reached
+   if (realIsEqual(delta, real_c(0), real_c(1e-14)))
+   {
+      // IMPORTANT: the following approach is in contrast to the dissertation of S. Donath, 2011
+      // - Dissertation: only the intersection of the interface surface with the wall is computed; it is known that this
+      // could in rare cases lead to unstable configurations
+      // - Here: extrapolate (extend) the wall point such that the triangle is considered valid during curvature
+      // computation;
+      // This has been adapted from the old waLBerla source code. While delta is considered to be zero, there is
+      // still a division by delta below. This has not been found problematic when using double precision, however it
+      // leads to invalid values in single precision. Therefore, the following macro exits the function prematurely when
+      // using single precision and returns false such that the wall point will not be used.
+#ifndef WALBERLA_DOUBLE_ACCURACY
+      return false;
+#endif
+
+      // determine the direction in which the artifical wall point is to be expected
+      // expression "dot(wallNormal,normal)*wallNormal" is orthogonal projection of normal on wallNormal
+      // (wallNormal has already been normalized => no division by vector length required)
+      const Vector3< real_t > targetDir = (normal - dot(wallNormal, normal) * wallNormal).getNormalized();
+
+      const real_t sinTheta = contactAngle.getSin();
+
+      // distance of interface point to wall in direction of wallNormal
+      real_t wallPointDistance = wallDistance / sinTheta; // d_WP in dissertation of S. Donath, 2011
+
+      // wall point must not be too close or far away from interface point too avoid degenerated triangles
+      real_t virtWallDistance;
+      if (wallPointDistance < real_c(0.8))
+      {
+         // extend distance with a heuristically chosen tolerance such that the point is not thrown away when checking
+         // for degenerated triangles in the curvature computation
+         wallPointDistance = real_c(0.801);
+         virtWallDistance  = wallPointDistance * sinTheta; // d_W'
+      }
+      else
+      {
+         if (wallPointDistance > real_c(1.8))
+         {
+            // reduce distance with heuristically chosen tolerance
+            wallPointDistance = real_c(1.799);
+            virtWallDistance  = wallPointDistance * sinTheta; // d_W'
+         }
+         else
+         {
+            virtWallDistance = wallDistance; // d_W'
+         }
+      }
+
+      const real_t virtPointDistance = virtWallDistance / sinTheta; // d_WP'
+
+      // compute point by shifting virtWallDistance along wallNormal starting from globalInterfacePointLocation
+      // virtWallProjection is given in global coordinates
+      const Vector3< real_t > virtWallProjection = globalInterfacePointLocation - virtWallDistance * wallNormal;
+
+      const real_t cosTheta = contactAngle.getCos();
+
+      // compute artificial wall point by starting from virtWallProjection and shifting "virtPointDistance*cosTheta" in
+      // targetDir (vritualWallPointCoord is r_W in dissertation of S. Donath, 2011)
+      artificialWallPointCoord = virtWallProjection + virtPointDistance * cosTheta * targetDir;
+
+      // radius r of the artificial circle "virtPointDistance*0.5/(sin(0.5)*delta)"
+      // midpoint M of the artificial circle "normal*r+globalInterfacePointLocation"
+      // normal of the virtual wall point "M-artificialWallPointCoord"
+      artificialWallNormal = normal * virtPointDistance * real_c(0.5) / (std::sin(real_c(0.5) * delta)) +
+                             globalInterfacePointLocation - artificialWallPointCoord;
+      artificialWallNormal = artificialWallNormal.getNormalized();
+
+      return true;
+   }
+   else
+   {
+      // compute base angles of triangle; line 6 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      const real_t beta = real_c(0.5) * (math::pi - real_c(std::fabs(delta)));
+
+      real_t gamma;
+      // line 7 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      if (theta < alpha) { gamma = beta - theta; }
+      else { gamma = beta + theta - math::pi; }
+
+      const real_t wallPointDistance =
+         wallDistance / real_c(std::cos(std::fabs(gamma))); // d_WP in dissertation of S. Donath, 2011
+
+      // line 9 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      // division by zero not possible as delta==0 is caught above
+      real_t radius = real_c(0.5) * wallPointDistance / std::sin(real_c(0.5) * real_c(std::fabs(delta)));
+
+      // check wallPointDistance for being in a valid range (to avoid degenerated triangles)
+      real_t artificialWallPointDistance = wallPointDistance; // d'_WP in dissertation of S. Donath, 2011
+
+      if (wallPointDistance < real_c(0.8))
+      {
+         // extend distance with a heuristically chosen tolerance such that the point is not thrown away when checking
+         // for degenerated triangles in the curvature computation
+         artificialWallPointDistance = real_c(0.801);
+
+         // if extended distance exceeds circle diameter, assume delta=90 degrees
+         if (artificialWallPointDistance > real_c(2) * radius)
+         {
+            radius = artificialWallPointDistance * math::one_div_root_two;
+         }
+      }
+      else
+      {
+         // reduce distance with heuristically chosen tolerance
+         if (wallPointDistance > real_c(1.8)) { artificialWallPointDistance = real_c(1.799); }
+      }
+
+      // line 17 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      const real_t artificialDelta =
+         real_c(2) * real_c(std::asin(real_c(0.5) * artificialWallPointDistance /
+                                      real_c(radius))); // delta' in dissertation of S. Donath, 2011
+
+      // line 18 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      Vector3< real_t > rotVec = cross(normal, obstacleNormal);
+
+      // change direction of rotation axis for delta>0; this is in contrast to Algorithm 6.2 in dissertation of Stefan
+      // Donath, 2011 but was found to be necessary as otherwise the artificialWallNormal points in the wrong direction
+      if (delta > real_c(0)) { rotVec *= real_c(-1); }
+
+      // line 19 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      const Matrix3< real_t > rotMat(rotVec, artificialDelta);
+
+      // line 20 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      artificialWallNormal = rotMat * normal;
+
+      // line 21 in Algorithm 6.2 in dissertation of S. Donath, 2011
+      if (theta < alpha)
+      {
+         artificialWallPointCoord = globalInterfacePointLocation + radius * (normal - artificialWallNormal);
+      }
+      else { artificialWallPointCoord = globalInterfacePointLocation - radius * (normal - artificialWallNormal); }
+
+      return true;
+   }
+}
+
+Vector3< real_t > getInterfacePoint(const Vector3< real_t >& normal, real_t fillLevel)
+{
+   // exploit symmetries of cubic cell to simplify the algorithm, i.e., restrict the fill level to the interval
+   // [0,0.5] (see dissertation of T. Pohl, 2008, p. 22f)
+   bool fillMirrored = false;
+   if (fillLevel >= real_c(0.5))
+   {
+      fillMirrored = true;
+      fillLevel    = real_c(1) - fillLevel;
+   }
+
+   // sort normal components such that nx, ny, nz >= 0 and nx <= ny <= nz to simplify the algorithm
+   Vector3< real_t > normalSorted = fabs(normal);
+   if (normalSorted[0] > normalSorted[1])
+   {
+      // swap nx and ny
+      real_t tmp      = normalSorted[0];
+      normalSorted[0] = normalSorted[1];
+      normalSorted[1] = tmp;
+   }
+
+   if (normalSorted[1] > normalSorted[2])
+   {
+      // swap ny and nz
+      real_t tmp      = normalSorted[1];
+      normalSorted[1] = normalSorted[2];
+      normalSorted[2] = tmp;
+
+      if (normalSorted[0] > normalSorted[1])
+      {
+         // swap nx and ny
+         tmp             = normalSorted[0];
+         normalSorted[0] = normalSorted[1];
+         normalSorted[1] = tmp;
+      }
+   }
+
+   // minimal and maximal plane offset chosen as in the dissertation of T. Pohl, 2008, p. 22
+   real_t offsetMin = real_c(-0.866025); // sqrt(3/4), lowest possible value
+   real_t offsetMax = real_c(0);
+
+   // find correct interface position by bisection (Algorithm 2.1, p. 22 in dissertation of T. Pohl, 2008)
+   const uint_t numBisections =
+      uint_c(10); // number of bisections, =10 in dissertation of T. Pohl, 2008 (heuristically chosen)
+
+   for (uint_t i = uint_c(0); i <= numBisections; ++i)
+   {
+      const real_t offsetTmp    = real_c(0.5) * (offsetMin + offsetMax);
+      const real_t newFillLevel = computeCellFluidVolume(normalSorted, offsetTmp);
+
+      // volume is too small, reduce upper bound
+      if (newFillLevel > fillLevel) { offsetMax = offsetTmp; }
+
+      // volume is too large, reduce lower bound
+      else { offsetMin = offsetTmp; }
+   }
+   real_t offset = real_c(0.5) * (offsetMin + offsetMax);
+
+   if (fillMirrored) { offset *= real_c(-1); }
+   const Vector3< real_t > interfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal;
+
+   return interfacePoint;
+}
+
+real_t getCellEdgeIntersection(const Vector3< real_t >& edgePoint, const Vector3< real_t >& edgeDirection,
+                               const Vector3< real_t >& normal, const Vector3< real_t >& surfacePoint)
+{
+   // #ifndef BELOW_CELL
+   // #   define BELOW_CELL (-10)
+   // #endif
+
+#ifndef ABOVE_CELL
+#   define ABOVE_CELL (-20)
+#endif
+   // mathematical description:
+   // surface plane in coordinate form: x * normal = surfacePoint * normal
+   // cell edge line: edgePoint + lambda * edgeDirection
+   // => point of intersection: lambda = (interfacePoint * normal - edgePoint * normal) / edgeDirection * normal
+
+   // compute angle between normal and cell edge
+   real_t cosAngle = dot(edgeDirection, normal);
+
+   real_t intersection = real_c(0);
+
+   // intersection exists only if angle is not 90°, i.e., (intersection != 0)
+   if (std::fabs(cosAngle) >= real_c(1e-14))
+   {
+      intersection = ((surfacePoint - edgePoint) * normal) / cosAngle;
+
+      //      // intersection is below cell
+      //      if (intersection < real_c(0)) { intersection = real_c(BELOW_CELL); }
+
+      // intersection is above cell
+      if (intersection > real_c(1)) { intersection = real_c(ABOVE_CELL); }
+   }
+   else // no intersection if angle is 90° (intersection == 0)
+   {
+      intersection = real_c(-1);
+   }
+
+   return intersection;
+}
+
+real_t computeCellFluidVolume(const Vector3< real_t >& normal, real_t offset)
+{
+   const Vector3< real_t > interfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal;
+
+   real_t volume = real_c(0);
+
+   // stores points of intersection with cell edges; points are shifted along normal such that the points lay
+   // on one plane and surface area can be calculated
+   std::vector< Vector3< real_t > > points;
+
+   // SW: south west, EB: east bottom, etc.
+   real_t iSW = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                        Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint);
+
+   // if no intersection with edge SW, volume is zero
+   if (iSW > real_c(0) || realIsIdentical(iSW, ABOVE_CELL))
+   {
+      real_t iSE = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+                                           Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint);
+      real_t iNW = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+                                           Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint);
+      real_t iNE = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)),
+                                           Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint);
+
+      // simple case: all four vertices are included in fluid domain (see Figure 2.12, p. 24 in dissertation of Thomas
+      // Pohl, 2008)
+      if (iNE > real_c(0)) { volume = real_c(0.25) * (iSW + iSE + iNW + iNE); }
+      else
+      {
+         if (iSE >= real_c(0))
+         {
+            real_t iEB =
+               getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)),
+                                       Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, interfacePoint);
+
+            // shift intersection points along normal and store points
+            points.push_back(Vector3< real_t >(real_c(1), real_c(0), real_c(iSE)) - interfacePoint);
+            points.push_back(Vector3< real_t >(real_c(1), real_c(iEB), real_c(0)) - interfacePoint);
+
+            volume += iSE * iEB;
+         }
+         else
+         {
+            real_t iSB =
+               getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                       Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, interfacePoint);
+
+            points.push_back(Vector3< real_t >(real_c(iSB), real_c(0), real_c(0)) - interfacePoint);
+         }
+
+         if (iNW >= real_c(0))
+         {
+            real_t iNB =
+               getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)),
+                                       Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, interfacePoint);
+
+            points.push_back(Vector3< real_t >(real_c(iNB), real_c(1), real_c(0)) - interfacePoint);
+            points.push_back(Vector3< real_t >(real_c(0), real_c(1), real_c(iNW)) - interfacePoint);
+
+            volume += iNB * iNW;
+         }
+         else
+         {
+            real_t iWB =
+               getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)),
+                                       Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, interfacePoint);
+
+            points.push_back(Vector3< real_t >(real_c(0), real_c(iWB), real_c(0)) - interfacePoint);
+         }
+
+         real_t iWT =
+            getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+                                    Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, interfacePoint);
+         if (iWT >= real_c(0))
+         {
+            real_t iST =
+               getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)),
+                                       Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, interfacePoint);
+
+            points.push_back(Vector3< real_t >(real_c(0), real_c(iWT), real_c(1)) - interfacePoint);
+            points.push_back(Vector3< real_t >(real_c(iST), real_c(0), real_c(1)) - interfacePoint);
+
+            volume += iWT * iST;
+         }
+         else { points.push_back(Vector3< real_t >(real_c(0), real_c(0), real_c(iSW)) - interfacePoint); }
+
+         Vector3< real_t > vectorProduct(real_c(0));
+         real_t area = real_c(0);
+         size_t j    = points.size() - 1;
+
+         // compute the vector product, the length of which gives the surface area of the corresponding
+         // parallelogram;
+         for (size_t i = 0; i != points.size(); ++i)
+         {
+            vectorProduct[0] = points[i][1] * points[j][2] - points[i][2] * points[j][1];
+            vectorProduct[1] = points[i][2] * points[j][0] - points[i][0] * points[j][2];
+            vectorProduct[2] = points[i][0] * points[j][1] - points[i][1] * points[j][0];
+
+            // area of the triangle is obtained through division by 2; this is done below in the volume
+            // calculation (where the division is then by 6 instead of by 3)
+            area += vectorProduct.length();
+            j = i;
+         }
+
+         // compute and sum the volumes of each resulting pyramid: V = area * height * 1/3
+         volume += area * (normal * interfacePoint);
+         volume /= real_c(6.0); // division by 6 since the area was not divided by 2 above
+      }
+   }
+   else // no intersection with edge SW, volume is zero
+   {
+      volume = real_c(0);
+   }
+
+   return volume;
+}
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/free_surface/surface_geometry/Utility.h b/src/lbm_generated/free_surface/surface_geometry/Utility.h
new file mode 100644
index 0000000000000000000000000000000000000000..ffcf48bbde8a84d4e8409a6e5d596d60f6fec175
--- /dev/null
+++ b/src/lbm_generated/free_surface/surface_geometry/Utility.h
@@ -0,0 +1,68 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file Utility.h
+//! \ingroup surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Helper functions for surface geometry computations.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "ContactAngle.h"
+
+namespace walberla
+{
+namespace free_surface_generated
+{
+/***********************************************************************************************************************
+ * Compute the point p that lays on the plane of the interface surface (see Algorithm 2.1 in dissertation of Thomas
+ * Pohl, 2008). p = (0.5, 0.5, 0.5) + offset * normal.
+ **********************************************************************************************************************/
+Vector3< real_t > getInterfacePoint(const Vector3< real_t >& normal, real_t fillLevel);
+
+/***********************************************************************************************************************
+ * Compute the intersection point of a surface (defined by normal and surfacePoint) with an edge of a cell.
+ **********************************************************************************************************************/
+real_t getCellEdgeIntersection(const Vector3< real_t >& edgePoint, const Vector3< real_t >& edgeDirection,
+                               const Vector3< real_t >& normal, const Vector3< real_t >& surfacePoint);
+
+/***********************************************************************************************************************
+ * Compute the fluid volume within an interface cell with respect to
+ * - the interface normal
+ * - the interface surface offset.
+ *
+ * see dissertation of T. Pohl, 2008, section 2.5.3, p. 23-26.
+ **********************************************************************************************************************/
+real_t computeCellFluidVolume(const Vector3< real_t >& normal, real_t offset);
+
+/***********************************************************************************************************************
+ * Compute an artificial wall point according to the artifical curvature wetting model from the dissertation of Stefan
+ * Donath, 2011. The artificial wall point and the artificial normal can be used to alter the curvature computation with
+ * local triangulation. The interface curvature is changed such that the correct laplace pressure with respect to the
+ * contact angle is assumed near solid cells.
+ *
+ * see dissertation of T. Pohl, 2008, section 6.3.3
+ **********************************************************************************************************************/
+bool computeArtificalWallPoint(const Vector3< real_t >& globalInterfacePointLocation,
+                               const Vector3< real_t >& globalCellCoordinate, const Vector3< real_t >& normal,
+                               const Vector3< real_t >& wallNormal, const Vector3< real_t >& obstacleNormal,
+                               const ContactAngle& contactAngle, Vector3< real_t >& artificialWallPointCoord,
+                               Vector3< real_t >& artificialWallNormal);
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm_generated/macroscopics/CMakeLists.txt b/src/lbm_generated/macroscopics/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..96b2ebfc7f5392ab2e61e80196db5569b94582ad
--- /dev/null
+++ b/src/lbm_generated/macroscopics/CMakeLists.txt
@@ -0,0 +1,5 @@
+target_sources( lbm_generated
+        PRIVATE
+        Equilibrium.h
+        DensityAndMomentumDensity.h
+        )
diff --git a/src/lbm_generated/macroscopics/DensityAndMomentumDensity.h b/src/lbm_generated/macroscopics/DensityAndMomentumDensity.h
new file mode 100644
index 0000000000000000000000000000000000000000..d615e4529b960acca6ccfd02bb08973507e347f4
--- /dev/null
+++ b/src/lbm_generated/macroscopics/DensityAndMomentumDensity.h
@@ -0,0 +1,62 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file DensityAndMomentumDensity.h
+//! \ingroup lbm_generated
+//! \author Markus Holzer<markus.holzer@fau.de>
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/math/Vector3.h"
+
+#include <type_traits>
+
+namespace walberla::lbm_generated {
+
+template< typename StorageSpecification_T, typename PdfField_T >
+real_t getDensityAndMomentumDensity( Vector3< real_t > & momentumDensity, const PdfField_T & pdf,
+                                    const cell_idx_t x, const cell_idx_t y, const cell_idx_t z )
+{
+   const auto & xyz0 = pdf(x,y,z,0);
+
+   auto d = StorageSpecification_T::Stencil::begin();
+
+   const auto & firstPdf = pdf.getF( &xyz0, d.toIdx() );
+
+   momentumDensity[0] = firstPdf * real_c(d.cx());
+   momentumDensity[1] = firstPdf * real_c(d.cy());
+   momentumDensity[2] = firstPdf * real_c(d.cz());
+   real_t rho =  firstPdf + ( ( StorageSpecification_T::zeroCenteredPDFs ) ? real_t(0.0) : real_t(1.0) );
+
+   ++d;
+
+   while( d != StorageSpecification_T::Stencil::end() )
+   {
+      const auto & pdfValue = pdf.getF( &xyz0, d.toIdx() );
+
+      momentumDensity[0] += pdfValue * real_c(d.cx());
+      momentumDensity[1] += pdfValue * real_c(d.cy());
+      momentumDensity[2] += pdfValue * real_c(d.cz());
+      rho                += pdfValue;
+
+      ++d;
+   }
+
+   return rho;
+}
+} // namespace walberla::lbm_generated
diff --git a/src/lbm_generated/macroscopics/Equilibrium.h b/src/lbm_generated/macroscopics/Equilibrium.h
new file mode 100644
index 0000000000000000000000000000000000000000..c651b67a2aa61cd14435e092cc01530c85d63feb
--- /dev/null
+++ b/src/lbm_generated/macroscopics/Equilibrium.h
@@ -0,0 +1,305 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla 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.
+//
+//  waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file Equilibrium.h
+//! \ingroup lbm_generated
+//! \author Markus Holzer<markus.holzer@fau.de>
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/math/Vector3.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+#include "stencil/Directions.h"
+
+#include <type_traits>
+namespace walberla::lbm_generated
+{
+
+//////////////////////////////////////////
+// set equilibrium distribution (x,y,z) //
+//////////////////////////////////////////
+
+
+template< typename StorageSpecification_T >
+struct Equilibrium
+{
+   template< typename FieldPtrOrIterator >
+   static void set(FieldPtrOrIterator& it, const Vector3< real_t >& velocity = Vector3< real_t >(real_t(0.0)),
+                   const real_t rho = real_t(1.0))
+   {
+      it[0] = rho * -0.33333333333333331 * (velocity[0] * velocity[0]) +
+              rho * -0.33333333333333331 * (velocity[1] * velocity[1]) +
+              rho * -0.33333333333333331 * (velocity[2] * velocity[2]) + rho * 0.33333333333333331;
+      it[1] = rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+              rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+              rho * 0.16666666666666666 * velocity[1] + rho * 0.16666666666666666 * (velocity[1] * velocity[1]);
+      it[2] = rho * -0.16666666666666666 * velocity[1] + rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+              rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+              rho * 0.16666666666666666 * (velocity[1] * velocity[1]);
+      it[3] = rho * -0.16666666666666666 * velocity[0] + rho * -0.16666666666666666 * (velocity[1] * velocity[1]) +
+              rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+              rho * 0.16666666666666666 * (velocity[0] * velocity[0]);
+      it[4] = rho * -0.16666666666666666 * (velocity[1] * velocity[1]) +
+              rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+              rho * 0.16666666666666666 * velocity[0] + rho * 0.16666666666666666 * (velocity[0] * velocity[0]);
+      it[5] = rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+              rho * -0.16666666666666666 * (velocity[1] * velocity[1]) + rho * 0.055555555555555552 +
+              rho * 0.16666666666666666 * velocity[2] + rho * 0.16666666666666666 * (velocity[2] * velocity[2]);
+      it[6] = rho * -0.16666666666666666 * velocity[2] + rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+              rho * -0.16666666666666666 * (velocity[1] * velocity[1]) + rho * 0.055555555555555552 +
+              rho * 0.16666666666666666 * (velocity[2] * velocity[2]);
+      it[7] = rho * -0.083333333333333329 * velocity[0] + rho * -0.25 * velocity[0] * velocity[1] +
+              rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+              rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+              rho * 0.083333333333333329 * (velocity[1] * velocity[1]);
+      it[8] = rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+              rho * 0.083333333333333329 * velocity[1] + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+              rho * 0.083333333333333329 * (velocity[1] * velocity[1]) + rho * 0.25 * velocity[0] * velocity[1];
+      it[9] = rho * -0.083333333333333329 * velocity[0] + rho * -0.083333333333333329 * velocity[1] +
+              rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+              rho * 0.083333333333333329 * (velocity[1] * velocity[1]) + rho * 0.25 * velocity[0] * velocity[1];
+      it[10] = rho * -0.083333333333333329 * velocity[1] + rho * -0.25 * velocity[0] * velocity[1] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+               rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+               rho * 0.083333333333333329 * (velocity[1] * velocity[1]);
+      it[11] = rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+               rho * 0.083333333333333329 * velocity[2] + rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[1] * velocity[2];
+      it[12] = rho * -0.083333333333333329 * velocity[1] + rho * -0.25 * velocity[1] * velocity[2] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[2] +
+               rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      it[13] = rho * -0.083333333333333329 * velocity[0] + rho * -0.25 * velocity[0] * velocity[2] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[2] +
+               rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      it[14] = rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+               rho * 0.083333333333333329 * velocity[2] + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[0] * velocity[2];
+      it[15] = rho * -0.083333333333333329 * velocity[2] + rho * -0.25 * velocity[1] * velocity[2] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+               rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      it[16] = rho * -0.083333333333333329 * velocity[1] + rho * -0.083333333333333329 * velocity[2] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[1] * velocity[2];
+      it[17] = rho * -0.083333333333333329 * velocity[0] + rho * -0.083333333333333329 * velocity[2] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[0] * velocity[2];
+      it[18] = rho * -0.083333333333333329 * velocity[2] + rho * -0.25 * velocity[0] * velocity[2] +
+               rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+               rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+               rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+   }
+
+   template< typename PdfField_T >
+   static void set(PdfField_T& pdf, const cell_idx_t x, const cell_idx_t y, const cell_idx_t z,
+                   const Vector3< real_t >& velocity = Vector3< real_t >(real_t(0.0)), const real_t rho = real_t(1.0))
+   {
+      pdf(x, y, z, 0) = rho * -0.33333333333333331 * (velocity[0] * velocity[0]) +
+                        rho * -0.33333333333333331 * (velocity[1] * velocity[1]) +
+                        rho * -0.33333333333333331 * (velocity[2] * velocity[2]) + rho * 0.33333333333333331;
+      pdf(x, y, z, 1) = rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                        rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                        rho * 0.16666666666666666 * velocity[1] +
+                        rho * 0.16666666666666666 * (velocity[1] * velocity[1]);
+      pdf(x, y, z, 2) = rho * -0.16666666666666666 * velocity[1] +
+                        rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                        rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                        rho * 0.16666666666666666 * (velocity[1] * velocity[1]);
+      pdf(x, y, z, 3) = rho * -0.16666666666666666 * velocity[0] +
+                        rho * -0.16666666666666666 * (velocity[1] * velocity[1]) +
+                        rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                        rho * 0.16666666666666666 * (velocity[0] * velocity[0]);
+      pdf(x, y, z, 4) = rho * -0.16666666666666666 * (velocity[1] * velocity[1]) +
+                        rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                        rho * 0.16666666666666666 * velocity[0] +
+                        rho * 0.16666666666666666 * (velocity[0] * velocity[0]);
+      pdf(x, y, z, 5) = rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                        rho * -0.16666666666666666 * (velocity[1] * velocity[1]) + rho * 0.055555555555555552 +
+                        rho * 0.16666666666666666 * velocity[2] +
+                        rho * 0.16666666666666666 * (velocity[2] * velocity[2]);
+      pdf(x, y, z, 6) = rho * -0.16666666666666666 * velocity[2] +
+                        rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                        rho * -0.16666666666666666 * (velocity[1] * velocity[1]) + rho * 0.055555555555555552 +
+                        rho * 0.16666666666666666 * (velocity[2] * velocity[2]);
+      pdf(x, y, z, 7) = rho * -0.083333333333333329 * velocity[0] + rho * -0.25 * velocity[0] * velocity[1] +
+                        rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+                        rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                        rho * 0.083333333333333329 * (velocity[1] * velocity[1]);
+      pdf(x, y, z, 8) =
+         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+         rho * 0.083333333333333329 * velocity[1] + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+         rho * 0.083333333333333329 * (velocity[1] * velocity[1]) + rho * 0.25 * velocity[0] * velocity[1];
+      pdf(x, y, z, 9) = rho * -0.083333333333333329 * velocity[0] + rho * -0.083333333333333329 * velocity[1] +
+                        rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                        rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                        rho * 0.25 * velocity[0] * velocity[1];
+      pdf(x, y, z, 10) = rho * -0.083333333333333329 * velocity[1] + rho * -0.25 * velocity[0] * velocity[1] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+                         rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                         rho * 0.083333333333333329 * (velocity[1] * velocity[1]);
+      pdf(x, y, z, 11) =
+         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+         rho * 0.083333333333333329 * velocity[2] + rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+         rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[1] * velocity[2];
+      pdf(x, y, z, 12) = rho * -0.083333333333333329 * velocity[1] + rho * -0.25 * velocity[1] * velocity[2] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[2] +
+                         rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                         rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      pdf(x, y, z, 13) = rho * -0.083333333333333329 * velocity[0] + rho * -0.25 * velocity[0] * velocity[2] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[2] +
+                         rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                         rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      pdf(x, y, z, 14) =
+         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+         rho * 0.083333333333333329 * velocity[2] + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+         rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[0] * velocity[2];
+      pdf(x, y, z, 15) = rho * -0.083333333333333329 * velocity[2] + rho * -0.25 * velocity[1] * velocity[2] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+                         rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                         rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      pdf(x, y, z, 16) = rho * -0.083333333333333329 * velocity[1] + rho * -0.083333333333333329 * velocity[2] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                         rho * 0.083333333333333329 * (velocity[2] * velocity[2]) +
+                         rho * 0.25 * velocity[1] * velocity[2];
+      pdf(x, y, z, 17) = rho * -0.083333333333333329 * velocity[0] + rho * -0.083333333333333329 * velocity[2] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                         rho * 0.083333333333333329 * (velocity[2] * velocity[2]) +
+                         rho * 0.25 * velocity[0] * velocity[2];
+      pdf(x, y, z, 18) = rho * -0.083333333333333329 * velocity[2] + rho * -0.25 * velocity[0] * velocity[2] +
+                         rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+                         rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                         rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+   }
+
+   static real_t get(const stencil::Direction direction,
+                     const Vector3< real_t >& velocity = Vector3< real_t >(real_t(0.0)), const real_t rho = real_t(1.0))
+   {
+      switch (direction)
+      {
+      case 0: {
+         return rho * -0.33333333333333331 * (velocity[0] * velocity[0]) +
+                rho * -0.33333333333333331 * (velocity[1] * velocity[1]) +
+                rho * -0.33333333333333331 * (velocity[2] * velocity[2]) + rho * 0.33333333333333331;
+      }
+      case 1: {
+         return rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                rho * 0.16666666666666666 * velocity[1] + rho * 0.16666666666666666 * (velocity[1] * velocity[1]);
+      }
+      case 2: {
+         return rho * -0.16666666666666666 * velocity[1] + rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                rho * 0.16666666666666666 * (velocity[1] * velocity[1]);
+      }
+      case 3: {
+         return rho * -0.16666666666666666 * velocity[0] + rho * -0.16666666666666666 * (velocity[1] * velocity[1]) +
+                rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                rho * 0.16666666666666666 * (velocity[0] * velocity[0]);
+      }
+      case 4: {
+         return rho * -0.16666666666666666 * (velocity[1] * velocity[1]) +
+                rho * -0.16666666666666666 * (velocity[2] * velocity[2]) + rho * 0.055555555555555552 +
+                rho * 0.16666666666666666 * velocity[0] + rho * 0.16666666666666666 * (velocity[0] * velocity[0]);
+      }
+      case 5: {
+         return rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                rho * -0.16666666666666666 * (velocity[1] * velocity[1]) + rho * 0.055555555555555552 +
+                rho * 0.16666666666666666 * velocity[2] + rho * 0.16666666666666666 * (velocity[2] * velocity[2]);
+      }
+      case 6: {
+         return rho * -0.16666666666666666 * velocity[2] + rho * -0.16666666666666666 * (velocity[0] * velocity[0]) +
+                rho * -0.16666666666666666 * (velocity[1] * velocity[1]) + rho * 0.055555555555555552 +
+                rho * 0.16666666666666666 * (velocity[2] * velocity[2]);
+      }
+      case 7: {
+         return rho * -0.083333333333333329 * velocity[0] + rho * -0.25 * velocity[0] * velocity[1] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+                rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[1] * velocity[1]);
+      }
+      case 8: {
+         return rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+                rho * 0.083333333333333329 * velocity[1] + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[1] * velocity[1]) + rho * 0.25 * velocity[0] * velocity[1];
+      }
+      case 9: {
+         return rho * -0.083333333333333329 * velocity[0] + rho * -0.083333333333333329 * velocity[1] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[1] * velocity[1]) + rho * 0.25 * velocity[0] * velocity[1];
+      }
+      case 10: {
+         return rho * -0.083333333333333329 * velocity[1] + rho * -0.25 * velocity[0] * velocity[1] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+                rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[1] * velocity[1]);
+      }
+      case 11: {
+         return rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+                rho * 0.083333333333333329 * velocity[2] + rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[1] * velocity[2];
+      }
+      case 12: {
+         return rho * -0.083333333333333329 * velocity[1] + rho * -0.25 * velocity[1] * velocity[2] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[2] +
+                rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      }
+      case 13: {
+         return rho * -0.083333333333333329 * velocity[0] + rho * -0.25 * velocity[0] * velocity[2] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[2] +
+                rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      }
+      case 14: {
+         return rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+                rho * 0.083333333333333329 * velocity[2] + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[0] * velocity[2];
+      }
+      case 15: {
+         return rho * -0.083333333333333329 * velocity[2] + rho * -0.25 * velocity[1] * velocity[2] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[1] +
+                rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      }
+      case 16: {
+         return rho * -0.083333333333333329 * velocity[1] + rho * -0.083333333333333329 * velocity[2] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[1] * velocity[1]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[1] * velocity[2];
+      }
+      case 17: {
+         return rho * -0.083333333333333329 * velocity[0] + rho * -0.083333333333333329 * velocity[2] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]) + rho * 0.25 * velocity[0] * velocity[2];
+      }
+      case 18: {
+         return rho * -0.083333333333333329 * velocity[2] + rho * -0.25 * velocity[0] * velocity[2] +
+                rho * 0.027777777777777776 + rho * 0.083333333333333329 * velocity[0] +
+                rho * 0.083333333333333329 * (velocity[0] * velocity[0]) +
+                rho * 0.083333333333333329 * (velocity[2] * velocity[2]);
+      }
+      default:
+         WALBERLA_ABORT("Invalid Stencil direction");
+      }
+   }
+};
+
+} // namespace walberla::lbm_generated
\ No newline at end of file