From 6525cbb1c47afd72079a86eeb75fa7c63e02d46a Mon Sep 17 00:00:00 2001
From: Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
Date: Fri, 8 Jul 2022 16:09:02 +0200
Subject: [PATCH] Add free-surface module

---
 .../ComplexGeometry/ComplexGeometry.cpp       |    2 +-
 apps/showcases/CMakeLists.txt                 |    2 +
 .../FreeSurface/BubblyPoiseuille.cpp          |  369 ++++++
 .../FreeSurface/BubblyPoiseuille.prm          |  108 ++
 apps/showcases/FreeSurface/CMakeLists.txt     |   48 +
 apps/showcases/FreeSurface/CapillaryWave.cpp  |  473 +++++++
 apps/showcases/FreeSurface/CapillaryWave.prm  |  107 ++
 .../FreeSurface/DamBreakCylindrical.cpp       |  606 +++++++++
 .../FreeSurface/DamBreakCylindrical.prm       |  111 ++
 .../FreeSurface/DamBreakRectangular.cpp       |  570 ++++++++
 .../FreeSurface/DamBreakRectangular.prm       |  111 ++
 apps/showcases/FreeSurface/DropImpact.cpp     |  367 ++++++
 apps/showcases/FreeSurface/DropImpact.prm     |  110 ++
 apps/showcases/FreeSurface/DropWetting.cpp    |  421 ++++++
 apps/showcases/FreeSurface/DropWetting.prm    |  108 ++
 apps/showcases/FreeSurface/GravityWave.cpp    |  579 +++++++++
 apps/showcases/FreeSurface/GravityWave.prm    |  107 ++
 .../FreeSurface/GravityWaveCodegen.cpp        |  580 +++++++++
 .../GravityWaveLatticeModelGeneration.py      |   34 +
 apps/showcases/FreeSurface/MovingDrop.cpp     |  325 +++++
 apps/showcases/FreeSurface/MovingDrop.prm     |  108 ++
 apps/showcases/FreeSurface/RisingBubble.cpp   |  465 +++++++
 apps/showcases/FreeSurface/RisingBubble.prm   |  108 ++
 apps/showcases/FreeSurface/TaylorBubble.cpp   |  494 +++++++
 apps/showcases/FreeSurface/TaylorBubble.prm   |  110 ++
 src/core/StringUtility.h                      |   40 +-
 src/core/StringUtility.impl.h                 |  115 +-
 src/core/cell/Cell.h                          |   16 +-
 src/core/stringToNum.h                        |   14 +-
 src/lbm/CMakeLists.txt                        |    4 +-
 .../blockforest/communication/CMakeLists.txt  |    5 +
 .../communication/SimpleCommunication.h       |  170 +++
 .../communication/UpdateSecondGhostLayer.h    |  144 ++
 src/lbm/boundary/SimpleExtrapolationOutflow.h |  114 ++
 src/lbm/boundary/all.h                        |    1 +
 .../free_surface/BlockStateDetectorSweep.h    |  111 ++
 src/lbm/free_surface/CMakeLists.txt           |   19 +
 src/lbm/free_surface/FlagDefinitions.h        |   51 +
 src/lbm/free_surface/FlagInfo.h               |  215 +++
 src/lbm/free_surface/FlagInfo.impl.h          |  199 +++
 src/lbm/free_surface/InitFunctions.h          |  229 ++++
 src/lbm/free_surface/InterfaceFromFillLevel.h |   78 ++
 src/lbm/free_surface/LoadBalancing.h          |  345 +++++
 src/lbm/free_surface/MaxVelocityComputer.h    |  110 ++
 src/lbm/free_surface/SurfaceMeshWriter.h      |  176 +++
 src/lbm/free_surface/TotalMassComputer.h      |  144 ++
 src/lbm/free_surface/VtkWriter.h              |  173 +++
 src/lbm/free_surface/boundary/CMakeLists.txt  |    6 +
 .../boundary/FreeSurfaceBoundaryHandling.h    |  188 +++
 .../FreeSurfaceBoundaryHandling.impl.h        |  554 ++++++++
 .../boundary/SimplePressureWithFreeSurface.h  |  150 +++
 src/lbm/free_surface/bubble_model/Bubble.h    |  171 +++
 .../bubble_model/BubbleDefinitions.h          |   42 +
 .../bubble_model/BubbleDistanceAdaptor.h      |   76 ++
 .../bubble_model/BubbleIDFieldPackInfo.h      |  147 +++
 .../free_surface/bubble_model/BubbleModel.h   |  240 ++++
 .../bubble_model/BubbleModel.impl.h           |  692 ++++++++++
 .../bubble_model/BubbleModelFromConfig.h      |   75 ++
 .../bubble_model/BubbleModelFromConfig.impl.h |   91 ++
 .../free_surface/bubble_model/CMakeLists.txt  |   24 +
 .../DisjoiningPressureBubbleModel.h           |  142 ++
 .../DisjoiningPressureBubbleModel.impl.h      |   83 ++
 .../bubble_model/DistanceInfo.cpp             |  112 ++
 .../free_surface/bubble_model/DistanceInfo.h  |  100 ++
 src/lbm/free_surface/bubble_model/FloodFill.h |  104 ++
 .../bubble_model/FloodFill.impl.h             |  216 +++
 src/lbm/free_surface/bubble_model/Geometry.h  |   87 ++
 .../free_surface/bubble_model/Geometry.impl.h |  219 ++++
 .../bubble_model/MergeInformation.cpp         |  337 +++++
 .../bubble_model/MergeInformation.h           |  102 ++
 .../bubble_model/NewBubbleCommunication.cpp   |  232 ++++
 .../bubble_model/NewBubbleCommunication.h     |  126 ++
 .../bubble_model/RegionalFloodFill.h          |  254 ++++
 src/lbm/free_surface/dynamics/CMakeLists.txt  |   17 +
 .../dynamics/CellConversionSweep.h            |  315 +++++
 .../dynamics/ConversionFlagsResetSweep.h      |   70 +
 .../dynamics/ExcessMassDistributionModel.h    |  214 +++
 .../dynamics/ExcessMassDistributionSweep.h    |  212 +++
 .../ExcessMassDistributionSweep.impl.h        |  594 +++++++++
 .../dynamics/ForceWeightingSweep.h            |   96 ++
 .../dynamics/PdfReconstructionModel.h         |  173 +++
 .../free_surface/dynamics/PdfRefillingModel.h |  147 +++
 .../free_surface/dynamics/PdfRefillingSweep.h |  447 +++++++
 .../dynamics/PdfRefillingSweep.impl.h         |  718 ++++++++++
 .../dynamics/StreamReconstructAdvectSweep.h   |  202 +++
 .../dynamics/SurfaceDynamicsHandler.h         |  441 +++++++
 .../dynamics/functionality/AdvectMass.h       |  305 +++++
 .../dynamics/functionality/CMakeLists.txt     |    8 +
 .../FindInterfaceCellConversion.h             |  255 ++++
 .../functionality/GetLaplacePressure.h        |   61 +
 .../functionality/GetOredNeighborhood.h       |   62 +
 .../ReconstructInterfaceCellABB.h             |  442 +++++++
 .../surface_geometry/CMakeLists.txt           |   22 +
 .../surface_geometry/ContactAngle.h           |   54 +
 .../surface_geometry/CurvatureModel.h         |   90 ++
 .../surface_geometry/CurvatureModel.impl.h    |  269 ++++
 .../surface_geometry/CurvatureSweep.h         |  211 +++
 .../surface_geometry/CurvatureSweep.impl.h    |  518 ++++++++
 .../surface_geometry/DetectWettingSweep.h     |  334 +++++
 .../ExtrapolateNormalsSweep.h                 |   71 +
 .../ExtrapolateNormalsSweep.impl.h            |   70 +
 .../surface_geometry/NormalSweep.h            |  109 ++
 .../surface_geometry/NormalSweep.impl.h       |  456 +++++++
 .../surface_geometry/ObstacleFillLevelSweep.h |   91 ++
 .../ObstacleFillLevelSweep.impl.h             |   88 ++
 .../surface_geometry/ObstacleNormalSweep.h    |   98 ++
 .../ObstacleNormalSweep.impl.h                |  147 +++
 .../surface_geometry/SmoothingSweep.h         |  113 ++
 .../surface_geometry/SmoothingSweep.impl.h    |  159 +++
 .../surface_geometry/SurfaceGeometryHandler.h |  168 +++
 .../free_surface/surface_geometry/Utility.cpp |  547 ++++++++
 .../free_surface/surface_geometry/Utility.h   |   68 +
 tests/lbm/CMakeLists.txt                      |  144 +-
 tests/lbm/free_surface/LoadBalancingTest.cpp  |  192 +++
 .../bubble_model/BubbleBodyMover.h            |   71 +
 .../bubble_model/BubbleBodyMover.impl.h       |   99 ++
 .../bubble_model/BubbleInitializationTest.cpp |  154 +++
 .../bubble_model/BubbleModelTester.h          |   96 ++
 .../bubble_model/BubbleModelTester.impl.h     |  160 +++
 .../bubble_model/MergeAndSplitTest.cpp        |  230 ++++
 .../MergeAndSplitTestConnected.png            |  Bin 0 -> 1681 bytes
 .../MergeAndSplitTestUnconnected.png          |  Bin 0 -> 1649 bytes
 .../bubble_model/MergeInformationTest.cpp     |  231 ++++
 .../bubble_model/MovingSpheresTest.cpp        |  173 +++
 .../bubble_model/RegionalFloodFillTest.cpp    |   88 ++
 .../bubble_model/SplitDetectionTest.cpp       |  152 +++
 .../free_surface/dynamics/AdvectionTest.cpp   |  220 ++++
 .../dynamics/CellConversionTest.cpp           |  280 ++++
 .../lbm/free_surface/dynamics/CodegenTest.cpp |  227 ++++
 .../ExcessMassDistributionFallbackTest.cpp    |  360 +++++
 .../ExcessMassDistributionParallelTest.cpp    | 1154 +++++++++++++++++
 .../ExcessMassDistributionParallelTest.ods    |  Bin 0 -> 16650 bytes
 .../lbm/free_surface/dynamics/InflowTest.cpp  |  281 ++++
 .../LatticeModelGenerationFreeSurface.py      |   34 +
 .../PdfReconstructionFreeSlipTest.cpp         |  201 +++
 .../dynamics/PdfReconstructionTest.cpp        |  606 +++++++++
 .../dynamics/PdfRefillingTest.cpp             | 1123 ++++++++++++++++
 .../dynamics/PdfRefillingTest.ods             |  Bin 0 -> 37960 bytes
 .../dynamics/WettingConversionTest.cpp        |  247 ++++
 .../surface_geometry/CellFluidVolumeTest.cpp  |   74 ++
 .../surface_geometry/CurvatureOfSineTest.cpp  |  545 ++++++++
 .../CurvatureOfSphereTest.cpp                 |  373 ++++++
 .../surface_geometry/DetectWettingTest.cpp    |  294 +++++
 .../GetInterfacePointTest.cpp                 |   76 ++
 .../NormalsEquivalenceTest.cpp                |  213 +++
 .../surface_geometry/NormalsNearSolidTest.cpp |  178 +++
 .../surface_geometry/NormalsOfSineTest.cpp    |  470 +++++++
 .../surface_geometry/NormalsOfSphereTest.cpp  |  363 ++++++
 .../ObstacleFillLevelTest.cpp                 |  283 ++++
 .../surface_geometry/ObstacleNormalsTest.cpp  |  186 +++
 .../surface_geometry/WettingCurvatureTest.cpp |  341 +++++
 151 files changed, 31781 insertions(+), 66 deletions(-)
 create mode 100644 apps/showcases/FreeSurface/BubblyPoiseuille.cpp
 create mode 100644 apps/showcases/FreeSurface/BubblyPoiseuille.prm
 create mode 100644 apps/showcases/FreeSurface/CMakeLists.txt
 create mode 100644 apps/showcases/FreeSurface/CapillaryWave.cpp
 create mode 100644 apps/showcases/FreeSurface/CapillaryWave.prm
 create mode 100644 apps/showcases/FreeSurface/DamBreakCylindrical.cpp
 create mode 100644 apps/showcases/FreeSurface/DamBreakCylindrical.prm
 create mode 100644 apps/showcases/FreeSurface/DamBreakRectangular.cpp
 create mode 100644 apps/showcases/FreeSurface/DamBreakRectangular.prm
 create mode 100644 apps/showcases/FreeSurface/DropImpact.cpp
 create mode 100644 apps/showcases/FreeSurface/DropImpact.prm
 create mode 100644 apps/showcases/FreeSurface/DropWetting.cpp
 create mode 100644 apps/showcases/FreeSurface/DropWetting.prm
 create mode 100644 apps/showcases/FreeSurface/GravityWave.cpp
 create mode 100644 apps/showcases/FreeSurface/GravityWave.prm
 create mode 100644 apps/showcases/FreeSurface/GravityWaveCodegen.cpp
 create mode 100644 apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py
 create mode 100644 apps/showcases/FreeSurface/MovingDrop.cpp
 create mode 100644 apps/showcases/FreeSurface/MovingDrop.prm
 create mode 100644 apps/showcases/FreeSurface/RisingBubble.cpp
 create mode 100644 apps/showcases/FreeSurface/RisingBubble.prm
 create mode 100644 apps/showcases/FreeSurface/TaylorBubble.cpp
 create mode 100644 apps/showcases/FreeSurface/TaylorBubble.prm
 create mode 100644 src/lbm/blockforest/communication/CMakeLists.txt
 create mode 100644 src/lbm/blockforest/communication/SimpleCommunication.h
 create mode 100644 src/lbm/blockforest/communication/UpdateSecondGhostLayer.h
 create mode 100644 src/lbm/boundary/SimpleExtrapolationOutflow.h
 create mode 100644 src/lbm/free_surface/BlockStateDetectorSweep.h
 create mode 100644 src/lbm/free_surface/CMakeLists.txt
 create mode 100644 src/lbm/free_surface/FlagDefinitions.h
 create mode 100644 src/lbm/free_surface/FlagInfo.h
 create mode 100644 src/lbm/free_surface/FlagInfo.impl.h
 create mode 100644 src/lbm/free_surface/InitFunctions.h
 create mode 100644 src/lbm/free_surface/InterfaceFromFillLevel.h
 create mode 100644 src/lbm/free_surface/LoadBalancing.h
 create mode 100644 src/lbm/free_surface/MaxVelocityComputer.h
 create mode 100644 src/lbm/free_surface/SurfaceMeshWriter.h
 create mode 100644 src/lbm/free_surface/TotalMassComputer.h
 create mode 100644 src/lbm/free_surface/VtkWriter.h
 create mode 100644 src/lbm/free_surface/boundary/CMakeLists.txt
 create mode 100644 src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h
 create mode 100644 src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h
 create mode 100644 src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h
 create mode 100644 src/lbm/free_surface/bubble_model/Bubble.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleDefinitions.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleModel.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleModel.impl.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h
 create mode 100644 src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h
 create mode 100644 src/lbm/free_surface/bubble_model/CMakeLists.txt
 create mode 100644 src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h
 create mode 100644 src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h
 create mode 100644 src/lbm/free_surface/bubble_model/DistanceInfo.cpp
 create mode 100644 src/lbm/free_surface/bubble_model/DistanceInfo.h
 create mode 100644 src/lbm/free_surface/bubble_model/FloodFill.h
 create mode 100644 src/lbm/free_surface/bubble_model/FloodFill.impl.h
 create mode 100644 src/lbm/free_surface/bubble_model/Geometry.h
 create mode 100644 src/lbm/free_surface/bubble_model/Geometry.impl.h
 create mode 100644 src/lbm/free_surface/bubble_model/MergeInformation.cpp
 create mode 100644 src/lbm/free_surface/bubble_model/MergeInformation.h
 create mode 100644 src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp
 create mode 100644 src/lbm/free_surface/bubble_model/NewBubbleCommunication.h
 create mode 100644 src/lbm/free_surface/bubble_model/RegionalFloodFill.h
 create mode 100644 src/lbm/free_surface/dynamics/CMakeLists.txt
 create mode 100644 src/lbm/free_surface/dynamics/CellConversionSweep.h
 create mode 100644 src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h
 create mode 100644 src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h
 create mode 100644 src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h
 create mode 100644 src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
 create mode 100644 src/lbm/free_surface/dynamics/ForceWeightingSweep.h
 create mode 100644 src/lbm/free_surface/dynamics/PdfReconstructionModel.h
 create mode 100644 src/lbm/free_surface/dynamics/PdfRefillingModel.h
 create mode 100644 src/lbm/free_surface/dynamics/PdfRefillingSweep.h
 create mode 100644 src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h
 create mode 100644 src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h
 create mode 100644 src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h
 create mode 100644 src/lbm/free_surface/dynamics/functionality/AdvectMass.h
 create mode 100644 src/lbm/free_surface/dynamics/functionality/CMakeLists.txt
 create mode 100644 src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h
 create mode 100644 src/lbm/free_surface/dynamics/functionality/GetLaplacePressure.h
 create mode 100644 src/lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h
 create mode 100644 src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h
 create mode 100644 src/lbm/free_surface/surface_geometry/CMakeLists.txt
 create mode 100644 src/lbm/free_surface/surface_geometry/ContactAngle.h
 create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureModel.h
 create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/DetectWettingSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/NormalSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/NormalSweep.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/SmoothingSweep.h
 create mode 100644 src/lbm/free_surface/surface_geometry/SmoothingSweep.impl.h
 create mode 100644 src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h
 create mode 100644 src/lbm/free_surface/surface_geometry/Utility.cpp
 create mode 100644 src/lbm/free_surface/surface_geometry/Utility.h
 create mode 100644 tests/lbm/free_surface/LoadBalancingTest.cpp
 create mode 100644 tests/lbm/free_surface/bubble_model/BubbleBodyMover.h
 create mode 100644 tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h
 create mode 100644 tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp
 create mode 100644 tests/lbm/free_surface/bubble_model/BubbleModelTester.h
 create mode 100644 tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h
 create mode 100644 tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp
 create mode 100644 tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png
 create mode 100644 tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png
 create mode 100644 tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp
 create mode 100644 tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp
 create mode 100644 tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp
 create mode 100644 tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/AdvectionTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/CellConversionTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/CodegenTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods
 create mode 100644 tests/lbm/free_surface/dynamics/InflowTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py
 create mode 100644 tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp
 create mode 100644 tests/lbm/free_surface/dynamics/PdfRefillingTest.ods
 create mode 100644 tests/lbm/free_surface/dynamics/WettingConversionTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp
 create mode 100644 tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp

diff --git a/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp b/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp
index a9cca407c..0f4366447 100644
--- a/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp
+++ b/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp
@@ -239,7 +239,7 @@ int main( int argc, char * argv[] )
                                                                                                              flagFieldId, fluidFlagUID ) ),
                                   "LBM stability check" );
 
-   timeloop.addFuncAfterTimeStep( perfLogger, "PerformanceLogger" );
+   timeloop.addFuncAfterTimeStep( perfLogger, "Evaluator: performance logging" );
 
 
    // add VTK output to time loop
diff --git a/apps/showcases/CMakeLists.txt b/apps/showcases/CMakeLists.txt
index 6330a83bd..c6f0534cc 100644
--- a/apps/showcases/CMakeLists.txt
+++ b/apps/showcases/CMakeLists.txt
@@ -1,6 +1,7 @@
 add_subdirectory( BidisperseFluidizedBed )
 add_subdirectory( CombinedResolvedUnresolved )
 add_subdirectory( FluidizedBed )
+add_subdirectory( FreeSurface )
 add_subdirectory( HeatConduction )
 add_subdirectory( LightRisingParticleInFluidAMR )
 add_subdirectory( Mixer )
@@ -12,3 +13,4 @@ endif()
 if ( WALBERLA_BUILD_WITH_CODEGEN AND NOT WALBERLA_BUILD_WITH_OPENMP)
    add_subdirectory( PorousMedia )
 endif()
+
diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.cpp b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp
new file mode 100644
index 000000000..00ae3e9f4
--- /dev/null
+++ b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp
@@ -0,0 +1,369 @@
+//======================================================================================================================
+//
+//  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 BubblyPoiseuille.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates a plane Poiseuille flow with randomly distributed bubbles in the flow.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/math/Random.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/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"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DropInPool
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   const auto domainParameters              = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t channelWidth                = domainParameters.getParameter< real_t >("channelWidth");
+   const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor");
+   const real_t bubbleDiameter    = domainParameters.getParameter< real_t >("bubbleDiameterFactor") * channelWidth;
+   const real_t gasVolumeFraction = domainParameters.getParameter< real_t >("gasVolumeFraction");
+
+   // define domain size
+   Vector3< uint_t > domainSize = domainSizeFactor * channelWidth;
+   domainSize[0]                = uint_c(domainSizeFactor[0] * channelWidth);
+   domainSize[1]                = uint_c(domainSizeFactor[1] * channelWidth);
+   domainSize[2]                = uint_c(domainSizeFactor[2] * channelWidth);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(channelWidth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(gasVolumeFraction);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter);
+
+   Vector3< uint_t > realDomainSize;
+   realDomainSize[0] = cellsPerBlock[0] * numBlocks[0];
+   realDomainSize[1] = cellsPerBlock[1] * numBlocks[1];
+   realDomainSize[2] = cellsPerBlock[2] * numBlocks[2];
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize);
+
+   if (domainSize[0] != realDomainSize[0] && periodicity[0])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in x-direction can not be obtained with the number of blocks you specified.")
+   }
+   if (domainSize[1] != realDomainSize[1] && periodicity[1])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in y-direction can not be obtained with the number of blocks you specified.")
+   }
+   if (domainSize[2] != realDomainSize[2] && periodicity[2])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in z-direction can not be obtained with the number of blocks you specified.")
+   }
+
+   // read physics parameters from parameter file
+   const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const real_t reynoldsNumber  = physicsParameters.getParameter< real_t >("reynoldsNumber");
+   const real_t mortonNumber    = physicsParameters.getParameter< real_t >("mortonNumber");
+   const real_t relaxationRate  = physicsParameters.getParameter< real_t >("relaxationRate");
+
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t forceX = real_c(8) * viscosity * viscosity * reynoldsNumber / real_c(std::pow(channelWidth, 3));
+   const Vector3< real_t > force = Vector3< real_t >(forceX, real_c(0), real_c(0));
+
+   const real_t analMaxVelocity = viscosity * reynoldsNumber / channelWidth;
+
+   const real_t surfaceTension = real_c(std::pow(forceX * real_c(std::pow(viscosity, 4)) / mortonNumber,
+                                                 real_c(1) / real_c(3))); // formula only valid for rho=1
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(mortonNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(analMaxVelocity);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel,
+                                                            Vector3< real_t >(real_c(0)), real_c(1), 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(1.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();
+
+   // initialize parabolic Poiseuille profile
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, {
+         // get global coordinate (with respect to whole simulation domain) of the currently processed cell
+         Vector3< real_t > globalCellCoordinate = blockForest->getBlockLocalCellCenter(*blockIt, pdfFieldIt.cell());
+
+         const real_t height = real_c(realDomainSize[2]);
+
+         const real_t velocityX =
+            forceX * real_c(0.5) / viscosity * globalCellCoordinate[2] * (height - globalCellCoordinate[2]);
+
+         pdfField->setDensityAndVelocity(pdfFieldIt.cell(), Vector3< real_t >(velocityX, real_c(0), real_c(0)),
+                                         real_c(1));
+      }); // WALBERLA_FOR_ALL_CELLS
+   }
+
+   const real_t bubbleRadius          = bubbleDiameter * real_c(0.5);
+   const real_t domainVolume          = real_c(realDomainSize[0] * realDomainSize[1] * realDomainSize[2]);
+   const real_t gasVolume             = gasVolumeFraction * domainVolume;
+   const real_t bubbleVolume          = real_c(4) / real_c(3) * real_c(math::pi) * real_c(std::pow(bubbleRadius, 3));
+   const uint_t numBubbles            = uint_c(gasVolume / bubbleVolume);
+   const real_t realGasVolumeFraction = real_c(numBubbles) * real_c(bubbleVolume) / domainVolume;
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBubbles);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realGasVolumeFraction);
+
+   // randomly place bubbles
+   for (uint_t bubble = uint_c(0); bubble != numBubbles; ++bubble)
+   {
+      walberla::math::seedRandomGenerator(std::mt19937::result_type(bubble));
+      const Vector3< real_t > bubbleCenter =
+         Vector3< real_t >(math::realRandom(bubbleRadius, real_c(realDomainSize[0]) - bubbleRadius),
+                           math::realRandom(bubbleRadius, real_c(realDomainSize[1]) - bubbleRadius),
+                           math::realRandom(bubbleRadius, real_c(realDomainSize[2]) - bubbleRadius));
+
+      const geometry::Sphere sphereBubble(bubbleCenter, bubbleRadius);
+      bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true);
+   }
+
+   // initialize 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      if (t % uint_c(real_c(timesteps / 100)) == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t);
+      }
+      timeloop.singleStep(timingPool, true);
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace DropInPool
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DropInPool::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.prm b/apps/showcases/FreeSurface/BubblyPoiseuille.prm
new file mode 100644
index 000000000..c6e7d413c
--- /dev/null
+++ b/apps/showcases/FreeSurface/BubblyPoiseuille.prm
@@ -0,0 +1,108 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 20, 20, 20 >;
+   periodicity                   < 1, 1, 0 >;
+   loadBalancingFrequency        0;
+   printLoadBalancingStatistics  false;
+}
+
+DomainParameters
+{
+   channelWidth            100;
+   domainSizeFactor        < 2, 1, 1 >;   // values multiplied with channelWidth
+   bubbleDiameterFactor    0.1;           // value multiplied with channelWidth
+   gasVolumeFraction       0.1;
+}
+
+PhysicsParameters
+{
+   reynoldsNumber    10000;
+   mortonNumber      1e-5;
+   relaxationRate    1.989;
+   enableWetting     false;
+   contactAngle      0;       // only used if enableWetting=true
+   timesteps         10000;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   enableBubbleModel             true;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 5000;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   //Border { direction N;  walldistance -1; NoSlip{} }
+   //Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   Border { direction T;  walldistance -1; NoSlip{} }
+   Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 2170;
+   baseFolder mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency 2170;
+      ghostLayers 0;
+      baseFolder vtk-out;
+      samplingResolution 1;
+
+      writers
+      {
+         velocity;
+         density;
+         //pdf;
+         flag;
+         fill_level;
+         force;
+         curvature;
+         normal;
+         obstacle_normal;
+         mapped_flag;
+        }
+
+        inclusion_filters
+        {
+            // only include liquid and interface cells in VTK output
+            //liquidInterfaceFilter;
+        }
+
+        before_functions
+        {
+            //ghost_layer_synchronization; // only needed if writing the ghost layer
+        }
+
+   }
+   domain_decomposition
+   {
+      writeFrequency 0;
+      baseFolder vtk-out;
+      outputDomainDecomposition true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/CMakeLists.txt b/apps/showcases/FreeSurface/CMakeLists.txt
new file mode 100644
index 000000000..0e6edd5b2
--- /dev/null
+++ b/apps/showcases/FreeSurface/CMakeLists.txt
@@ -0,0 +1,48 @@
+waLBerla_link_files_to_builddir( *.prm )
+
+waLBerla_add_executable(NAME    BubblyPoiseuille
+                        FILES   BubblyPoiseuille.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    CapillaryWave
+                        FILES   CapillaryWave.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    DamBreakCylindrical
+                        FILES   DamBreakCylindrical.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    DamBreakRectangular
+                        FILES   DamBreakRectangular.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    DropImpact
+                        FILES   DropImpact.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    DropWetting
+                        FILES   DropWetting.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    GravityWave
+                        FILES   GravityWave.cpp
+                        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_add_executable(NAME    GravityWaveCodegen
+                           FILES   GravityWaveCodegen.cpp
+                           DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk
+                                   GravityWaveLatticeModelGeneration)
+endif()
+
+waLBerla_add_executable(NAME    RisingBubble
+                        FILES   RisingBubble.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    TaylorBubble
+                        FILES   TaylorBubble.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
diff --git a/apps/showcases/FreeSurface/CapillaryWave.cpp b/apps/showcases/FreeSurface/CapillaryWave.cpp
new file mode 100644
index 000000000..d36003bc0
--- /dev/null
+++ b/apps/showcases/FreeSurface/CapillaryWave.cpp
@@ -0,0 +1,473 @@
+//======================================================================================================================
+//
+//  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 CapillaryWave.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates a standing wave purely governed by surface tension forces, i.e., without any body forces
+// such as gravity
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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/dynamics/SurfaceDynamicsHandler.h"
+#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm/free_surface/surface_geometry/Utility.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace CapillaryWave
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D2Q9< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+// write each entry in "vector" to line in a file; columns are separated by tabs
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, const std::string& filename);
+
+// function describing the global initialization profile
+inline real_t initializationProfile(real_t x, real_t amplitude, real_t offset, real_t wavelength)
+{
+   return amplitude * std::cos(x / wavelength * real_c(2) * math::pi + math::pi) + offset;
+}
+
+// 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& 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),
+        domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition),
+        frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      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();
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         *surfaceYPosition_ = real_c(0);
+
+         CellInterval globalSearchInterval(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0), globalXCoordinate_,
+                                           cell_idx_c(domainSize_[1]), cell_idx_c(0));
+
+         if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval))
+         {
+            // transform specified global x-coordinate into block local coordinate
+            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 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))))
+               {
+                  const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0));
+
+                  // transform local y-coordinate to global coordinate
+                  Cell localResultCell = localEvalCell;
+                  localResultCell[1]   = y;
+                  blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt);
+                  *surfaceYPosition_ = real_c(localResultCell[1]) + fillLevel;
+
+                  break;
+               }
+            }
+         }
+      }
+      // communicate result among all processes
+      mpi::allReduceInplace< real_t >(*surfaceYPosition_, mpi::MAX);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   ConstBlockDataID fillFieldID_;
+   Vector3< uint_t > domainSize_;
+   cell_idx_t globalXCoordinate_;
+   std::shared_ptr< real_t > surfaceYPosition_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class SurfaceYPositionEvaluator
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // get domain parameters from parameter file
+   auto domainParameters         = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const uint_t domainWidth      = domainParameters.getParameter< uint_t >("domainWidth");
+   const real_t liquidDepth      = domainParameters.getParameter< real_t >("liquidDepth");
+   const real_t initialAmplitude = domainParameters.getParameter< real_t >("initialAmplitude");
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = domainWidth;
+   domainSize[1] = uint_c(liquidDepth * real_c(2));
+   domainSize[2] = uint_c(1);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(liquidDepth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialAmplitude);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics);
+
+   // get physics parameters from parameter file
+   auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   const real_t relaxationRate           = physicsParameters.getParameter< real_t >("relaxationRate");
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber");
+   const real_t waveNumber     = real_c(2) * math::pi / real_c(domainSize[0]);
+   const real_t waveFrequency  = reynoldsNumber * viscosity / real_c(domainSize[0]) / initialAmplitude;
+
+   // sum of both phases' densities is implicitly neglected here (would be factor of 1)
+   const real_t surfaceTension = waveFrequency * waveFrequency / std::pow(waveNumber, real_c(3));
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   const Vector3< real_t > force = Vector3< real_t >(real_c(0));
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, 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();
+
+   // 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
+
+   const uint_t numTotalPoints = (fillLevelInitSamples + uint_c(1)) * (fillLevelInitSamples + uint_c(1));
+   const real_t stepsize       = real_c(1) / real_c(fillLevelInitSamples);
+
+   // initialize sine profile such that there is exactly one period; every length is normalized with domainSize[0]
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // cell in block-local coordinates
+         const Cell localCell = fillFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = fillFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // Monte-Carlo like estimation of the fill level:
+         // create uniformly-distributed sample points in each cell and count the number of points below the sine
+         // profile; this fraction of points is used as the fill level to initialize the profile
+         uint_t numPointsBelow = uint_c(0);
+
+         for (uint_t xSample = uint_c(0); xSample <= fillLevelInitSamples; ++xSample)
+         {
+            // value of the sine-function
+            const real_t functionValue = initializationProfile(real_c(globalCell[0]) + real_c(xSample) * stepsize,
+                                                               initialAmplitude, liquidDepth, real_c(domainSize[0]));
+
+            for (uint_t ySample = uint_c(0); ySample <= fillLevelInitSamples; ++ySample)
+            {
+               const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize;
+               // with operator <, a fill level of 1 can not be reached when the line is equal to the cell's top border;
+               // with operator <=, a fill level of 0 can not be reached when the line is equal to the cell's bottom
+               // border
+               if (yPoint < functionValue) { ++numPointsBelow; }
+            }
+         }
+
+         // fill level is fraction of points below sine profile
+         fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints);
+      }) // 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_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)); }
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   // get fields created by surface geometry handler
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold);
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   // add load balancing
+   LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer(
+      blockForest, communication, pdfCommunication, bubbleModel, 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(0.5) * real_c(domainSize[0])), evaluationFrequency, surfaceYPosition);
+   timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      WALBERLA_ROOT_SECTION()
+      {
+         // non-dimensionalize time and surface position
+         const real_t tNonDimensional        = real_c(t) * waveFrequency;
+         const real_t positionNonDimensional = (*surfaceYPosition - liquidDepth) / initialAmplitude;
+
+         const std::vector< real_t > resultVector{ tNonDimensional, positionNonDimensional };
+         if (t % evaluationFrequency == uint_c(0))
+         {
+            WALBERLA_LOG_DEVEL("time step = " << t);
+            WALBERLA_LOG_DEVEL("\t\ttNonDimensional = " << tNonDimensional
+                                                        << "\n\t\tpositionNonDimensional = " << positionNonDimensional);
+            writeVectorToFile(resultVector, filename);
+         }
+      }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   for (const auto i : vector)
+   {
+      file << "\t" << i;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+} // namespace CapillaryWave
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::CapillaryWave::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/CapillaryWave.prm b/apps/showcases/FreeSurface/CapillaryWave.prm
new file mode 100644
index 000000000..8d172cadd
--- /dev/null
+++ b/apps/showcases/FreeSurface/CapillaryWave.prm
@@ -0,0 +1,107 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 25, 25, 1 >;
+   periodicity                   < 1, 0, 1 >;
+   loadBalancingFrequency        50;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   domainWidth       50; // equivalent to wavelength
+   liquidDepth       25;
+   initialAmplitude  0.5;
+}
+
+PhysicsParameters
+{
+   reynoldsNumber    10;
+   relaxationRate    1.8;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         10000;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 5000;
+   evaluationFrequency     100;
+   filename                capillary-wave.txt;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   Border { direction N;  walldistance -1; NoSlip{} }
+   Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   //Border { direction T;  walldistance -1; NoSlip{} }
+   //Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 100;
+   baseFolder     mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency       100;
+      ghostLayers          0;
+      baseFolder           vtk-out;
+      samplingResolution   1;
+
+      writers
+      {
+         fill_level;
+         mapped_flag;
+         velocity;
+         density;
+         //curvature;
+         //normal;
+         //obstacle_normal;
+         //pdf;
+         //flag;
+         //force;
+      }
+
+      inclusion_filters
+      {
+         //liquidInterfaceFilter; // only include liquid and interface cells in VTK output
+      }
+
+      before_functions
+      {
+         //ghost_layer_synchronization; // only needed if writing the ghost layer
+      }
+   }
+   domain_decomposition
+   {
+      writeFrequency             100;
+      baseFolder                 vtk-out;
+      outputDomainDecomposition  true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DamBreakCylindrical.cpp b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp
new file mode 100644
index 000000000..129cda8de
--- /dev/null
+++ b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp
@@ -0,0 +1,606 @@
+//======================================================================================================================
+//
+//  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 DamBreakCylindrical.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates the collapse of a cylindrical liquid column in 3D. Reference experiments are available from
+// Martin, Moyce (1952), doi:10.1098/rsta.1952.0006
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/math/DistributedSample.h"
+#include "core/math/Sample.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/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 "stencil/D3Q27.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DamBreakCylindrical
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRTField< ScalarField_T >;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+template< typename T >
+void writeNumberVector(const std::vector< T >& numberVector, const uint_t& timestep, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   file << timestep;
+   for (const auto number : numberVector)
+   {
+      file << "\t" << number;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+// get statistical Sample of column width, i.e., radius of the liquid front at the bottom layer (y=0); the center point
+// of the cylindrical column is assumed to be constant throughout the simulations
+// IMPORTANT REMARK: This implementation is not very efficient, as it gathers a slice of the whole flag field on a
+// single process to perform the evaluation.
+template< typename FreeSurfaceBoundaryHandling_T >
+class columnRadiusEvaluator
+{
+ public:
+   columnRadiusEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest,
+                         const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                         const Vector3< uint_t >& domainSize, const Vector3< real_t >& initialOrigin, uint_t interval,
+                         const std::shared_ptr< math::Sample >& columnRadiusSample)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize),
+        initialOrigin_(initialOrigin), columnRadiusSample_(columnRadiusSample), interval_(interval),
+        executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0)) { return; }
+
+      // gather a slice of the flag field on rank 0 (WARNING: simple, but inefficient)
+      std::shared_ptr< FlagField_T > flagFieldGathered = nullptr;
+      WALBERLA_ROOT_SECTION()
+      {
+         flagFieldGathered = std::make_shared< FlagField_T >(domainSize_[0], uint_c(1), domainSize_[2], uint_c(0));
+      }
+      field::gatherSlice< FlagField_T, FlagField_T >(
+         *flagFieldGathered, blockForest, freeSurfaceBoundaryHandling->getFlagFieldID(), 1, cell_idx_c(0), 0);
+
+      WALBERLA_ROOT_SECTION()
+      {
+         columnRadiusSample_->clear();
+
+         const Cell startCell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0));
+
+         const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo =
+            freeSurfaceBoundaryHandling->getFlagInfo();
+         WALBERLA_CHECK(flagInfo.isGas(flagFieldGathered->get(startCell)),
+                        "The \"startCell\" in columnRadiusEvaluator's flood fill algorithm must be a gas cell.");
+
+         getRadiusFromFloodFill(flagFieldGathered, startCell, *columnRadiusSample_);
+      }
+   }
+
+   void getRadiusFromFloodFill(const std::shared_ptr< FlagField_T >& flagFieldGathered, const Cell& startCell,
+                               math::Sample& columnRadiusSample)
+   {
+      WALBERLA_CHECK_EQUAL(startCell.y(), cell_idx_c(0),
+                           "columnRadiusEvaluator is meant to be search at the domain's bottom layer only (at y=0).");
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      std::queue< Cell > cellQueue;
+      cellQueue.push(startCell);
+
+      while (!cellQueue.empty())
+      {
+         Cell cell = cellQueue.front();
+         cellQueue.pop();
+
+         if (flagInfo.isGas(flagFieldGathered->get(cell)))
+         {
+            // remove flag such that cell is not detected as gas when searching from neighboring cells
+            flagFieldGathered->get(cell) = flag_t(0);
+
+            for (auto dir = stencil::D3Q27::beginNoCenter(); dir != stencil::D3Q27::end(); ++dir)
+            {
+               // only search at y=0
+               if (dir.cy() != 0) { continue; }
+
+               const Cell neighborCell = Cell(cell.x() + dir.cx(), cell.y() + dir.cy(), cell.z() + dir.cz());
+
+               // make sure that the algorithm stays within the field dimensions
+               if (neighborCell.x() >= cell_idx_c(0) && neighborCell.x() < cell_idx_c(flagFieldGathered->xSize()) &&
+                   neighborCell.y() >= cell_idx_c(0) && neighborCell.y() < cell_idx_c(flagFieldGathered->ySize()) &&
+                   neighborCell.z() >= cell_idx_c(0) && neighborCell.z() < cell_idx_c(flagFieldGathered->zSize()))
+               {
+                  // add neighboring cell to queue
+                  cellQueue.push(neighborCell);
+               }
+            }
+         }
+         else
+         {
+            if (flagInfo.isInterface(flagFieldGathered->get(cell)))
+            {
+               // get center point of this cell; directly use y=0 here
+               const Vector3< real_t > cellCenter(real_c(cell.x()) + real_c(0.5), real_c(0),
+                                                  real_c(cell.z()) + real_c(0.5));
+
+               const real_t radius = (initialOrigin_ - cellCenter).length();
+
+               columnRadiusSample.castToRealAndInsert(radius);
+            } // else: cell is neither gas, nor interface => do nothing
+         }
+      }
+   }
+
+ private:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   Vector3< uint_t > domainSize_;
+   Vector3< real_t > initialOrigin_;
+   std::shared_ptr< math::Sample > columnRadiusSample_;
+
+   uint_t interval_;
+   uint_t executionCounter_;
+}; // class columnRadiusEvaluator
+
+// get height of residual liquid column, i.e., height of liquid at initial center
+template< typename FreeSurfaceBoundaryHandling_T >
+class ColumnHeightEvaluator
+{
+ public:
+   ColumnHeightEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                         const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                         const Vector3< uint_t >& domainSize, const Vector3< real_t >& initialOrigin, uint_t interval,
+                         const std::shared_ptr< cell_idx_t >& currentColumnHeight)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize),
+        initialOrigin_(initialOrigin), currentColumnHeight_(currentColumnHeight), interval_(interval),
+        executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0)) { return; }
+
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      *currentColumnHeight_ = cell_idx_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         cell_idx_t maxColumnHeight = cell_idx_c(0);
+         bool isInterfaceFound      = false;
+
+         const CellInterval globalSearchInterval(cell_idx_c(initialOrigin_[0]), cell_idx_c(0),
+                                                 cell_idx_c(initialOrigin_[2]), cell_idx_c(initialOrigin_[0]),
+                                                 cell_idx_c(domainSize_[1]), cell_idx_c(initialOrigin_[2]));
+
+         if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval))
+         {
+            CellInterval localSearchInterval = globalSearchInterval;
+
+            // get intersection of globalSearchInterval and this block's bounding box (both in global coordinates)
+            localSearchInterval.intersect(blockForest->getBlockCellBB(*blockIt));
+
+            blockForest->transformGlobalToBlockLocalCellInterval(localSearchInterval, *blockIt);
+
+            const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+
+            for (auto c = localSearchInterval.begin(); c != localSearchInterval.end(); ++c)
+            {
+               if (flagInfo.isInterface(flagField->get(*c)))
+               {
+                  if (c->y() >= maxColumnHeight)
+                  {
+                     maxColumnHeight  = c->y();
+                     isInterfaceFound = true;
+                  }
+               }
+            }
+
+            if (isInterfaceFound)
+            {
+               // transform local y-coordinate to global coordinate
+               Cell localResultCell = Cell(cell_idx_c(0), maxColumnHeight, cell_idx_c(0));
+               blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt);
+               if (localResultCell[1] > *currentColumnHeight_) { *currentColumnHeight_ = localResultCell[1]; }
+            }
+         }
+      }
+      mpi::allReduceInplace< cell_idx_t >(*currentColumnHeight_, mpi::MAX);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   Vector3< uint_t > domainSize_;
+   Vector3< real_t > initialOrigin_;
+   std::shared_ptr< cell_idx_t > currentColumnHeight_;
+
+   uint_t interval_;
+   uint_t executionCounter_;
+}; // class ColumnHeightEvaluator
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   auto domainParameters     = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t columnRadius = domainParameters.getParameter< real_t >("columnRadius");
+   const real_t columnRatio  = domainParameters.getParameter< real_t >("columnRatio");
+   const real_t columnHeight = columnRadius * columnRatio;
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = uint_c(real_c(12) * columnRadius);
+   domainSize[1] = uint_c(real_c(2) * columnHeight);
+   domainSize[2] = domainSize[0];
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnRadius);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnHeight);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnRatio);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics);
+
+   // read physics parameters from parameter file
+   auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   // Galilei number: Ga = density * force * L^3 / kinematicViscosity^2
+   const real_t galileiNumber = physicsParameters.getParameter< real_t >("galileiNumber");
+
+   // Bond (Eötvös) number: Bo = (density_liquid - density_gas) * force * L^2 / surfaceTension
+   const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber");
+
+   const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate");
+   const real_t viscosity      = (real_c(1) / relaxationRate - real_c(0.5)) / real_c(3);
+   const real_t forceY         = galileiNumber * viscosity * viscosity / real_c(std::pow(columnRadius, real_c(3)));
+   const Vector3< real_t > force(real_c(0), -forceY, real_c(0));
+   const real_t surfaceTension = std::abs(forceY) * columnRadius * columnRadius / bondNumber;
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(galileiNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+   const real_t smagorinskyConstant          = modelParameters.getParameter< real_t >("smagorinskyConstant");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(smagorinskyConstant);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add relaxationRate field (initialized with relaxationRate from parameter file)
+   const BlockDataID relaxationRateFieldID = field::addToStorage< ScalarField_T >(
+      blockForest, "Relaxation rate field", relaxationRate, field::fzyx, uint_c(1));
+
+   const CollisionModel_T collisionModel = lbm::collision_model::SRTField< ScalarField_T >(relaxationRateFieldID);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel =
+      LatticeModel_T(collisionModel, lbm::force_model::GuoField< VectorField_T >(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, 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 geometry::Cylinder cylinderColumn(
+      Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), real_c(-1), real_c(0.5) * real_c(domainSize[2])),
+      Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), columnHeight, real_c(0.5) * real_c(domainSize[2])),
+      columnRadius);
+   bubble_model::addBodyToFillLevelField< geometry::Cylinder >(*blockForest, fillFieldID, cylinderColumn, false);
+
+   // initialize 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+      bubbleModelDerived->setAtmosphere(
+         Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1));
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, columnHeight);
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold, relaxationRateFieldID, smagorinskyConstant);
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   // add load balancing
+   LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer(
+      blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // add sweep for evaluating the column height at the origin
+   const std::shared_ptr< cell_idx_t > currentColumnHeight = std::make_shared< cell_idx_t >(columnHeight);
+   const ColumnHeightEvaluator< FreeSurfaceBoundaryHandling_T > heightEvaluator(
+      blockForest, freeSurfaceBoundaryHandling, domainSize,
+      Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), real_c(0), real_c(0.5) * real_c(domainSize[2])),
+      evaluationFrequency, currentColumnHeight);
+   timeloop.addFuncAfterTimeStep(heightEvaluator, "Evaluator: column height");
+
+   // add sweep for evaluating the column width (distance of front to origin)
+   const std::shared_ptr< math::Sample > columnRadiusSample = std::make_shared< math::Sample >();
+   const columnRadiusEvaluator< FreeSurfaceBoundaryHandling_T > columnRadiusEvaluator(
+      blockForest, freeSurfaceBoundaryHandling, domainSize,
+      Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), real_c(0), real_c(0.5) * real_c(domainSize[2])),
+      evaluationFrequency, columnRadiusSample);
+   timeloop.addFuncAfterTimeStep(columnRadiusEvaluator, "Evaluator: radius");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      if (t % evaluationFrequency == uint_c(0))
+      {
+         // set initial values
+         real_t T              = real_c(0);
+         real_t Z_mean         = real_c(1);
+         real_t Z_max          = real_c(1);
+         real_t Z_min          = real_c(1);
+         real_t Z_stdDeviation = real_c(0);
+         real_t H              = real_c(1);
+
+         if (!columnRadiusSample->empty())
+         {
+            // compute dimensionless quantities as defined in paper from Martin and Moyce (1952)
+            T              = real_c(t) * std::sqrt(columnRatio * std::abs(force[1]) / columnRadius);
+            Z_mean         = columnRadiusSample->mean() / columnRadius;
+            Z_max          = columnRadiusSample->max() / columnRadius;
+            Z_min          = columnRadiusSample->min() / columnRadius;
+            Z_stdDeviation = columnRadiusSample->stdDeviation() / columnRadius;
+            H              = real_c(*currentColumnHeight) / columnHeight;
+         }
+
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step =" << t);
+         WALBERLA_LOG_DEVEL_ON_ROOT("\t\tT = " << T << "\n\t\tZ_mean = " << Z_mean << "\n\t\tZ_max = " << Z_max
+                                               << "\n\t\tZ_min = " << Z_min
+                                               << "\n\t\tZ_stdDeviation = " << Z_stdDeviation << "\n\t\tH = " << H);
+
+         WALBERLA_ROOT_SECTION()
+         {
+            const std::vector< real_t > resultVector{ T, Z_mean, Z_max, Z_min, Z_stdDeviation, H };
+            writeNumberVector(resultVector, t, filename);
+         }
+
+         mpi::broadcastObject(Z_max, 0);
+
+         // simulation is considered converged
+         if (Z_max >= real_c(domainSize[0]) * real_c(0.5) / columnRadius - real_c(0.5))
+         {
+            WALBERLA_LOG_DEVEL_ON_ROOT("Liquid has reached domain borders.");
+            break;
+         }
+      }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace DamBreakCylindrical
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DamBreakCylindrical::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DamBreakCylindrical.prm b/apps/showcases/FreeSurface/DamBreakCylindrical.prm
new file mode 100644
index 000000000..de81d688e
--- /dev/null
+++ b/apps/showcases/FreeSurface/DamBreakCylindrical.prm
@@ -0,0 +1,111 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 10, 5, 10 >;
+   periodicity                   < 1, 0, 1 >;
+   loadBalancingFrequency        248;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   columnRadius 25;  // initial radius of the liquid column; "a" in Martin & Moyce's paper (10.1098/rsta.1952.0006)
+   columnRatio 1;    // ratio between columnHeight and columnRadius; "n^2" in Martin & Moyce's paper
+}
+
+PhysicsParameters
+{
+   galileiNumber     1831123817;
+   bondNumber        445;
+   relaxationRate    1.9995;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         2480;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+
+   smagorinskyConstant           0.1;
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 2480;
+   evaluationFrequency     24;
+   filename                rot-breaking-dam.txt;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1;  FreeSlip{} }
+   //Border { direction E;  walldistance -1;  FreeSlip{} }
+
+   // Y
+   Border { direction N;  walldistance -1;  FreeSlip{} }
+   Border { direction S;  walldistance -1;  FreeSlip{} }
+
+   // Z
+   //Border { direction T;  walldistance -1; FreeSlip{} }
+   //Border { direction B;  walldistance -1; FreeSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 248;
+   baseFolder mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency 248;
+      ghostLayers 0;
+      baseFolder vtk-out;
+      samplingResolution 1;
+
+      writers
+      {
+         velocity;
+         density;
+         //pdf;
+         flag;
+         fill_level;
+         force;
+         curvature;
+         normal;
+         obstacle_normal;
+         mapped_flag;
+        }
+
+        inclusion_filters
+        {
+            // only include liquid and interface cells in VTK output
+            //liquidInterfaceFilter;
+        }
+
+        before_functions
+        {
+            //ghost_layer_synchronization; // only needed if writing the ghost layer
+        }
+
+   }
+   domain_decomposition
+   {
+      writeFrequency 248;
+      baseFolder vtk-out;
+      outputDomainDecomposition true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DamBreakRectangular.cpp b/apps/showcases/FreeSurface/DamBreakRectangular.cpp
new file mode 100644
index 000000000..e3a4a5e4e
--- /dev/null
+++ b/apps/showcases/FreeSurface/DamBreakRectangular.cpp
@@ -0,0 +1,570 @@
+//======================================================================================================================
+//
+//  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 DamBreakRectangular.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates the collapse of a rectangular liquid column in 2D. Reference experiments are available from
+// Martin, Moyce (1952), doi:10.1098/rsta.1952.0006
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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/D2Q9.h"
+#include "lbm/lattice_model/D3Q19.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DamBreakRectangular
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRTField< ScalarField_T >;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D2Q9< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+template< typename T >
+void writeNumberVector(const std::vector< T >& numberVector, const uint_t& timestep, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   file << timestep;
+   for (const auto number : numberVector)
+   {
+      file << "\t" << number;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+// get height of residual liquid column, i.e., height of liquid at x=0
+template< typename FreeSurfaceBoundaryHandling_T >
+class ColumnHeightEvaluator
+{
+ public:
+   ColumnHeightEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                         const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                         const Vector3< uint_t >& domainSize, uint_t interval,
+                         const std::shared_ptr< cell_idx_t >& currentColumnHeight)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize),
+        currentColumnHeight_(currentColumnHeight), interval_(interval), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0)) { return; }
+
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      *currentColumnHeight_ = cell_idx_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         cell_idx_t maxColumnHeight = cell_idx_c(0);
+         bool isInterfaceFound      = false;
+
+         const CellInterval globalSearchInterval(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0), cell_idx_c(0),
+                                                 cell_idx_c(domainSize_[1]), cell_idx_c(0));
+
+         if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval))
+         {
+            CellInterval localSearchInterval = globalSearchInterval;
+
+            // get intersection of globalSearchInterval and this block's bounding box (both in global coordinates)
+            localSearchInterval.intersect(blockForest->getBlockCellBB(*blockIt));
+
+            blockForest->transformGlobalToBlockLocalCellInterval(localSearchInterval, *blockIt);
+
+            const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+
+            for (auto c = localSearchInterval.begin(); c != localSearchInterval.end(); ++c)
+            {
+               if (flagInfo.isInterface(flagField->get(*c)))
+               {
+                  if (c->y() >= maxColumnHeight)
+                  {
+                     maxColumnHeight  = c->y();
+                     isInterfaceFound = true;
+                  }
+               }
+            }
+
+            if (isInterfaceFound)
+            {
+               // transform local y-coordinate to global coordinate
+               Cell localResultCell = Cell(cell_idx_c(0), maxColumnHeight, cell_idx_c(0));
+               blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt);
+               if (localResultCell[1] > *currentColumnHeight_) { *currentColumnHeight_ = localResultCell[1]; }
+            }
+         }
+      }
+      mpi::allReduceInplace< cell_idx_t >(*currentColumnHeight_, mpi::MAX);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   Vector3< uint_t > domainSize_;
+   std::shared_ptr< cell_idx_t > currentColumnHeight_;
+
+   uint_t interval_;
+   uint_t executionCounter_;
+}; // class ColumnHeightEvaluator
+
+// get width of liquid column (distance of wave front to origin, at bottom of the domain)
+template< typename FreeSurfaceBoundaryHandling_T >
+class ColumnWidthEvaluator
+{
+ public:
+   ColumnWidthEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                        const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                        const Vector3< uint_t >& domainSize, uint_t interval,
+                        const std::shared_ptr< cell_idx_t >& currentColumnWidth)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize),
+        currentColumnWidth_(currentColumnWidth), interval_(interval), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0)) { return; }
+
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      *currentColumnWidth_ = cell_idx_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         cell_idx_t maxColumnWidth = cell_idx_c(0);
+         bool isInterfaceFound     = false;
+
+         // only search in an interval with a height of 10 cells to avoid detecting droplets
+         const CellInterval globalSearchInterval(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0),
+                                                 cell_idx_c(domainSize_[0]), cell_idx_c(0), cell_idx_c(0));
+
+         if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval))
+         {
+            CellInterval localSearchInterval = globalSearchInterval;
+
+            // get intersection of globalSearchInterval and this block's bounding box (both in global coordinates)
+            localSearchInterval.intersect(blockForest->getBlockCellBB(*blockIt));
+
+            blockForest->transformGlobalToBlockLocalCellInterval(localSearchInterval, *blockIt);
+
+            const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+
+            for (auto c = localSearchInterval.begin(); c != localSearchInterval.end(); ++c)
+            {
+               if (flagInfo.isInterface(flagField->get(*c)))
+               {
+                  if (c->x() >= maxColumnWidth)
+                  {
+                     maxColumnWidth   = c->x();
+                     isInterfaceFound = true;
+                  }
+               }
+            }
+
+            if (isInterfaceFound)
+            {
+               // transform local x-coordinate to global coordinate
+               Cell localResultCell = Cell(maxColumnWidth, cell_idx_c(0), cell_idx_c(0));
+               blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt);
+               if (localResultCell[0] > *currentColumnWidth_) { *currentColumnWidth_ = localResultCell[0]; }
+            }
+         }
+      }
+      mpi::allReduceInplace< cell_idx_t >(*currentColumnWidth_, mpi::MAX);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   Vector3< uint_t > domainSize_;
+   std::shared_ptr< cell_idx_t > currentColumnWidth_;
+
+   uint_t interval_;
+   uint_t executionCounter_;
+}; // class ColumnWidthEvaluator
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   auto domainParameters     = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t columnWidth  = domainParameters.getParameter< real_t >("columnWidth");
+   const real_t columnRatio  = domainParameters.getParameter< real_t >("columnRatio");
+   const real_t columnHeight = columnWidth * columnRatio;
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = uint_c(real_c(15) * columnWidth);
+   domainSize[1] = uint_c(real_c(2) * columnHeight);
+   domainSize[2] = uint_c(1);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnWidth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnHeight);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnRatio);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics);
+
+   // read physics parameters from parameter file
+   auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   // Galilei number: Ga = density * force * L^3 / kinematicViscosity^2
+   const real_t galileiNumber = physicsParameters.getParameter< real_t >("galileiNumber");
+
+   // Bond (Eötvös) number: Bo = (density_liquid - density_gas) * force * L^2 / surfaceTension
+   const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber");
+
+   const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate");
+   const real_t viscosity      = (real_c(1) / relaxationRate - real_c(0.5)) / real_c(3);
+   const real_t forceY         = galileiNumber * viscosity * viscosity / real_c(std::pow(columnWidth, real_c(3)));
+   const Vector3< real_t > force(real_c(0), -forceY, real_c(0));
+   const real_t surfaceTension = std::abs(forceY) * columnWidth * columnWidth / bondNumber;
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(galileiNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+   const real_t smagorinskyConstant          = modelParameters.getParameter< real_t >("smagorinskyConstant");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(smagorinskyConstant);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add relaxationRate field (initialized with relaxationRate from parameter file)
+   const BlockDataID relaxationRateFieldID = field::addToStorage< ScalarField_T >(
+      blockForest, "Relaxation rate field", relaxationRate, field::fzyx, uint_c(1));
+
+   const CollisionModel_T collisionModel = lbm::collision_model::SRTField< ScalarField_T >(relaxationRateFieldID);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, 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();
+
+   // initialize rectangular column of liquid
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      // cell of the liquid column's outermost corner in block-local coordinates
+      Cell localColumnCornerCell =
+         Cell(cell_idx_c(std::floor(columnWidth)), cell_idx_c(std::floor(columnHeight)), cell_idx_c(0));
+
+      blockForest->transformGlobalToBlockLocalCell(localColumnCornerCell, *blockIt);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // liquid cells
+         if (fillFieldIt.x() < localColumnCornerCell[0] && fillFieldIt.y() < localColumnCornerCell[1])
+         {
+            *fillFieldIt = real_c(1);
+         }
+
+         // interface cells at side
+         if (fillFieldIt.x() == localColumnCornerCell[0] && fillFieldIt.y() < localColumnCornerCell[1])
+         {
+            *fillFieldIt = columnWidth - std::floor(columnWidth);
+         }
+
+         // interface cells at top
+         if (fillFieldIt.y() == localColumnCornerCell[1] && fillFieldIt.x() < localColumnCornerCell[0])
+         {
+            *fillFieldIt = columnHeight - std::floor(columnHeight);
+         }
+
+         // interface cell in corner
+         if (fillFieldIt.x() == localColumnCornerCell[0] && fillFieldIt.y() == localColumnCornerCell[1])
+         {
+            *fillFieldIt =
+               real_c(0.5) * ((columnWidth - std::floor(columnWidth)) + (columnHeight - std::floor(columnHeight)));
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // initialize 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_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)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, columnHeight);
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold, relaxationRateFieldID, smagorinskyConstant);
+
+   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),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // add sweep for evaluating the column height at the origin
+   const std::shared_ptr< cell_idx_t > currentColumnHeight = std::make_shared< cell_idx_t >(columnHeight);
+   const ColumnHeightEvaluator< FreeSurfaceBoundaryHandling_T > heightEvaluator(
+      blockForest, freeSurfaceBoundaryHandling, domainSize, evaluationFrequency, currentColumnHeight);
+   timeloop.addFuncAfterTimeStep(heightEvaluator, "Evaluator: column height");
+
+   // add sweep for evaluating the column width (distance of front to origin)
+   const std::shared_ptr< cell_idx_t > currentColumnWidth = std::make_shared< cell_idx_t >(columnWidth);
+   const ColumnWidthEvaluator< FreeSurfaceBoundaryHandling_T > widthEvaluator(
+      blockForest, freeSurfaceBoundaryHandling, domainSize, evaluationFrequency, currentColumnWidth);
+   timeloop.addFuncAfterTimeStep(widthEvaluator, "Evaluator: column width");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      if (t % evaluationFrequency == uint_c(0))
+      {
+         // compute dimensionless quantities as defined in paper from Martin and Moyce (1952)
+         const real_t T = real_c(t) * std::sqrt(columnRatio * std::abs(force[1]) / columnWidth);
+         const real_t Z = real_c(*currentColumnWidth) / columnWidth;
+         const real_t H = real_c(*currentColumnHeight) / columnHeight;
+         const std::vector< real_t > resultVector{ T, Z, H };
+
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step =" << t);
+         WALBERLA_LOG_DEVEL_ON_ROOT("\t\tT = " << T << "\n\t\tZ = " << Z << "\n\t\tH = " << H);
+         WALBERLA_ROOT_SECTION() { writeNumberVector(resultVector, t, filename); }
+
+         // simulation is considered converged
+         if (Z >= real_c(domainSize[0]) / columnWidth - real_c(0.5))
+         {
+            WALBERLA_LOG_DEVEL_ON_ROOT("Liquid has reached opposite wall.");
+            break;
+         }
+      }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace DamBreakRectangular
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DamBreakRectangular::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DamBreakRectangular.prm b/apps/showcases/FreeSurface/DamBreakRectangular.prm
new file mode 100644
index 000000000..34fdca4b0
--- /dev/null
+++ b/apps/showcases/FreeSurface/DamBreakRectangular.prm
@@ -0,0 +1,111 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 50, 50, 1 >;
+   periodicity                   < 0, 0, 1 >;
+   loadBalancingFrequency        991;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   columnWidth 50;   // initial width of the liquid column; "a" in Martin & Moyce's paper (10.1098/rsta.1952.0006)
+   columnRatio 2;    // ratio between columnHeight and columnWidth; "n^2" in Martin & Moyce's paper
+}
+
+PhysicsParameters
+{
+   galileiNumber     1831123817;
+   bondNumber        445;
+   relaxationRate    1.9995;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         9910;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+
+   smagorinskyConstant           0.1;
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 25000;
+   evaluationFrequency     99;
+   filename                breaking-dam.txt;
+}
+
+BoundaryParameters
+{
+   // X
+   Border { direction W;  walldistance -1;  FreeSlip{} }
+   Border { direction E;  walldistance -1;  FreeSlip{} }
+
+   // Y
+   Border { direction N;  walldistance -1;  FreeSlip{} }
+   Border { direction S;  walldistance -1;  FreeSlip{} }
+
+   // Z
+   //Border { direction T;  walldistance -1; FreeSlip{} }
+   //Border { direction B;  walldistance -1; FreeSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 0;
+   baseFolder mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency 991;
+      ghostLayers 0;
+      baseFolder vtk-out;
+      samplingResolution 1;
+
+      writers
+      {
+         velocity;
+         density;
+         //pdf;
+         flag;
+         fill_level;
+         force;
+         curvature;
+         normal;
+         obstacle_normal;
+         mapped_flag;
+        }
+
+        inclusion_filters
+        {
+            // only include liquid and interface cells in VTK output
+            //liquidInterfaceFilter;
+        }
+
+        before_functions
+        {
+            //ghost_layer_synchronization; // only needed if writing the ghost layer
+        }
+
+   }
+   domain_decomposition
+   {
+      writeFrequency 991;
+      baseFolder vtk-out;
+      outputDomainDecomposition true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DropImpact.cpp b/apps/showcases/FreeSurface/DropImpact.cpp
new file mode 100644
index 000000000..143885a05
--- /dev/null
+++ b/apps/showcases/FreeSurface/DropImpact.cpp
@@ -0,0 +1,367 @@
+//======================================================================================================================
+//
+//  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 DropImpact.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates the impact of a droplet into a pool of liquid. Reference experiments are available from
+// - Wang, Chen (2000), doi:10.1063/1.1287511 (vertical impact)
+// - Reijers, Liu, Lohse, Gelderblom (2019), url: http://arxiv.org/abs/1903.08978 (oblique impact)
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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/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"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DropImpact
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   const auto domainParameters              = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t dropDiameter                = domainParameters.getParameter< real_t >("dropDiameter");
+   const Vector3< real_t > dropCenterFactor = domainParameters.getParameter< Vector3< real_t > >("dropCenterFactor");
+   const real_t poolHeightFactor            = domainParameters.getParameter< real_t >("poolHeightFactor");
+   const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor");
+
+   // define domain size
+   Vector3< uint_t > domainSize = domainSizeFactor * dropDiameter;
+   domainSize[0]                = uint_c(domainSizeFactor[0] * dropDiameter);
+   domainSize[1]                = uint_c(domainSizeFactor[1] * dropDiameter);
+   domainSize[2]                = uint_c(domainSizeFactor[2] * dropDiameter);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropCenterFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(poolHeightFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+
+   Vector3< uint_t > realDomainSize;
+   realDomainSize[0] = cellsPerBlock[0] * numBlocks[0];
+   realDomainSize[1] = cellsPerBlock[1] * numBlocks[1];
+   realDomainSize[2] = cellsPerBlock[2] * numBlocks[2];
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize);
+
+   if (domainSize[0] != realDomainSize[0] && periodicity[0])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in x-direction can not be obtained with the number of blocks you specified.")
+   }
+   if (domainSize[1] != realDomainSize[1] && periodicity[1])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in y-direction can not be obtained with the number of blocks you specified.")
+   }
+   // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size
+   int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]);
+   if (boundaryThicknessZ < 0)
+   {
+      WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.")
+   }
+
+   // read physics parameters from parameter file
+   const auto physicsParameters   = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const real_t bondNumber        = physicsParameters.getParameter< real_t >("bondNumber");
+   const real_t weberNumber       = physicsParameters.getParameter< real_t >("weberNumber");
+   const real_t ohnesorgeNumber   = physicsParameters.getParameter< real_t >("ohnesorgeNumber");
+   const real_t relaxationRate    = physicsParameters.getParameter< real_t >("relaxationRate");
+   const real_t impactAngleDegree = physicsParameters.getParameter< real_t >("impactAngleDegree");
+
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t surfaceTension             = real_c(std::pow(viscosity / ohnesorgeNumber, 2)) / dropDiameter;
+   const real_t gravitationalAccelerationZ = bondNumber * surfaceTension / (dropDiameter * dropDiameter);
+   const real_t impactVelocityMagnitude    = real_c(std::pow(weberNumber * surfaceTension / dropDiameter, 0.5));
+
+   const Vector3< real_t > force(real_c(0), real_c(0), -gravitationalAccelerationZ);
+
+   const real_t impactVelocityY =
+      impactVelocityMagnitude * real_c(std::sin(impactAngleDegree * math::pi / real_c(180)));
+   const real_t impactVelocityZ =
+      impactVelocityMagnitude * real_c(std::cos(impactAngleDegree * math::pi / real_c(180)));
+
+   const Vector3< real_t > impactVelocity(real_c(0), impactVelocityY, -impactVelocityZ);
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(impactVelocity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, impactVelocity, real_c(1), 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 real_t poolHeight = poolHeightFactor * dropDiameter;
+
+   // initialize a pool of liquid at the bottom of the domain in z-direction
+   const AABB poolAABB(real_c(0), real_c(0), real_c(0), real_c(domainSize[0]), real_c(domainSize[1]), poolHeight);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+      PdfField_T* const pdfField     = blockIt->getData< PdfField_T >(pdfFieldID);
+
+      // determine the cells that are relevant for a block (still in global coordinates)
+      const CellInterval relevantCellBB = blockForest->getCellBBFromAABB(poolAABB.getIntersection(blockIt->getAABB()));
+
+      // transform the global coordinates of relevant cells to block local coordinates
+      CellInterval blockLocalCellBB;
+      blockForest->transformGlobalToBlockLocalCellInterval(blockLocalCellBB, *blockIt, relevantCellBB);
+
+      WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ(blockLocalCellBB, {
+         fillField->get(x, y, z) = real_c(1);
+         pdfField->setDensityAndVelocity(x, y, z, Vector3< real_t >(real_c(0)), real_c(1));
+      }) // WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ
+   }
+
+   const Vector3< real_t > dropCenter = dropCenterFactor * dropDiameter;
+
+   const geometry::Sphere sphereDrop(dropCenter, dropDiameter * real_c(0.5));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false);
+
+   // initialize 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+      bubbleModelDerived->setAtmosphere(
+         Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1));
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, poolHeight);
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      if (t % uint_c(real_c(timesteps / 100)) == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t);
+      }
+      timeloop.singleStep(timingPool, true);
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace DropImpact
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DropImpact::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DropImpact.prm b/apps/showcases/FreeSurface/DropImpact.prm
new file mode 100644
index 000000000..07afacce7
--- /dev/null
+++ b/apps/showcases/FreeSurface/DropImpact.prm
@@ -0,0 +1,110 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 10, 10, 10 >;
+   periodicity                   < 1, 1, 0 >;
+   loadBalancingFrequency        372;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   dropDiameter      20;
+   dropCenterFactor  < 2, 2, 1 >;      // values multiplied with dropDiameter
+   poolHeightFactor  0.5;              // value multiplied with dropDiameter
+   domainSizeFactor  < 10, 10, 5 >;    // values multiplied with dropDiameter
+}
+
+PhysicsParameters
+{
+   bondNumber        3.18;
+   weberNumber       2010;
+   ohnesorgeNumber   0.0384;
+   relaxationRate    1.989;
+   impactAngleDegree 0;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         6000;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 3000;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   //Border { direction N;  walldistance -1; NoSlip{} }
+   //Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   Border { direction T;  walldistance -1; NoSlip{} }
+   Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 50;
+   baseFolder mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency 1000;
+      ghostLayers 0;
+      baseFolder vtk-out;
+      samplingResolution 1;
+
+      writers
+      {
+         velocity;
+         density;
+         //pdf;
+         flag;
+         fill_level;
+         force;
+         curvature;
+         normal;
+         obstacle_normal;
+         mapped_flag;
+        }
+
+        inclusion_filters
+        {
+            // only include liquid and interface cells in VTK output
+            //liquidInterfaceFilter;
+        }
+
+        before_functions
+        {
+            //ghost_layer_synchronization; // only needed if writing the ghost layer
+        }
+
+   }
+   domain_decomposition
+   {
+      writeFrequency 372;
+      baseFolder vtk-out;
+      outputDomainDecomposition true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DropWetting.cpp b/apps/showcases/FreeSurface/DropWetting.cpp
new file mode 100644
index 000000000..20445dead
--- /dev/null
+++ b/apps/showcases/FreeSurface/DropWetting.cpp
@@ -0,0 +1,421 @@
+//======================================================================================================================
+//
+//  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 DropWetting.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates a droplet on a plane with specified target contact angle. The resulting drop height can be
+// compared to an analytical model, as done in e.g.:
+// dissertation of S. Bogner (2017), section 4.4.3.2 , urn:nbn:de:bvb:29-opus4-87191
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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/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"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DropWetting
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+// compute the L2 norm of the relative difference between the current and the previous time-averaged quantities
+template< typename FreeSurfaceBoundaryHandling_T >
+class DropHeightEvaluator
+{
+ public:
+   DropHeightEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                       const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& boundaryHandlingID,
+                       const ConstBlockDataID& fillFieldID, const std::shared_ptr< real_t >& dropHeight,
+                       uint_t interval)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(boundaryHandlingID), fillFieldID_(fillFieldID),
+        dropHeight_(dropHeight), interval_(interval), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0)) { return; }
+
+      // compute the height of the spherical drop cap
+      computeDropHeight();
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   ConstBlockDataID fillFieldID_;
+
+   std::shared_ptr< real_t > dropHeight_;
+
+   uint_t interval_;
+
+   uint_t executionCounter_; // number of times operator() has been called
+
+   void computeDropHeight()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      *dropHeight_ = real_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         real_t maxDropHeight = real_c(0);
+
+         const FlagField_T* const flagField   = blockIt->template getData< const FlagField_T >(flagFieldID);
+         const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_);
+
+         WALBERLA_FOR_ALL_CELLS(
+            flagFieldIt, flagField, fillFieldIt, fillField,
+
+            if (flagInfo.isInterface(flagFieldIt)) {
+               Cell globalCell = flagFieldIt.cell();
+               blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt);
+
+               real_t currentDropHeight = real_c(globalCell.z()) + *fillFieldIt;
+               maxDropHeight            = currentDropHeight > maxDropHeight ? currentDropHeight : maxDropHeight;
+            }) // WALBERLA_FOR_ALL_CELLS
+
+         if (maxDropHeight > *dropHeight_) { *dropHeight_ = maxDropHeight; }
+      }
+      // get maximum dropHeight among all processes
+      mpi::allReduceInplace< real_t >(*dropHeight_, mpi::MAX);
+
+      WALBERLA_LOG_DEVEL_VAR_ON_ROOT(*dropHeight_);
+   }
+}; // class DropHeightEvaluator
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   const auto domainParameters              = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t dropDiameter                = domainParameters.getParameter< real_t >("dropDiameter");
+   const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor");
+
+   // define domain size
+   Vector3< uint_t > domainSize = domainSizeFactor * dropDiameter;
+   domainSize[0]                = uint_c(domainSizeFactor[0] * dropDiameter);
+   domainSize[1]                = uint_c(domainSizeFactor[1] * dropDiameter);
+   domainSize[2]                = uint_c(domainSizeFactor[2] * dropDiameter);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+
+   Vector3< uint_t > realDomainSize;
+   realDomainSize[0] = cellsPerBlock[0] * numBlocks[0];
+   realDomainSize[1] = cellsPerBlock[1] * numBlocks[1];
+   realDomainSize[2] = cellsPerBlock[2] * numBlocks[2];
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize);
+
+   if (domainSize[0] != realDomainSize[0] && periodicity[0])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in x-direction can not be obtained with the number of blocks you specified.")
+   }
+   if (domainSize[1] != realDomainSize[1] && periodicity[1])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in y-direction can not be obtained with the number of blocks you specified.")
+   }
+   // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size
+   int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]);
+   if (boundaryThicknessZ < 0)
+   {
+      WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.")
+   }
+
+   // read physics parameters from parameter file
+   const auto physicsParameters  = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const real_t relaxationRate   = physicsParameters.getParameter< real_t >("relaxationRate");
+   const real_t surfaceTension   = physicsParameters.getParameter< real_t >("surfaceTension");
+   const Vector3< real_t > force = physicsParameters.getParameter< Vector3< real_t > >("force");
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const real_t convergenceThreshold    = evaluationParameters.getParameter< real_t >("convergenceThreshold");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(convergenceThreshold);
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, 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();
+
+   // add spherical drop to fill level field
+   const geometry::Sphere sphereDrop(Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]),
+                                                       real_c(0.5) * real_c(domainSize[1]),
+                                                       real_c(0.5) * real_c(dropDiameter)),
+                                     real_c(dropDiameter) * real_c(0.5));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false);
+
+   // initialize 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+      bubbleModelDerived->setAtmosphere(
+         Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1));
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, real_c(0.5) * dropDiameter);
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold);
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   // add load balancing
+   LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer(
+      blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // evaluate height of the drop
+   const std::shared_ptr< real_t > dropHeight = std::make_shared< real_t >(real_c(0));
+   const DropHeightEvaluator< FreeSurfaceBoundaryHandling_T > dropHeightEvaluator(
+      blockForest, freeSurfaceBoundaryHandling, fillFieldID, dropHeight, evaluationFrequency);
+   timeloop.addFuncAfterTimeStep(dropHeightEvaluator, "Evaluator: drop height");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(1), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   real_t formerDropHeight = real_c(0);
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      // check convergence
+      if (t % evaluationFrequency == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t)
+         WALBERLA_LOG_DEVEL_ON_ROOT("\t\tdrop height = " << *dropHeight)
+         if (std::abs(formerDropHeight - *dropHeight) / *dropHeight < convergenceThreshold)
+         {
+            WALBERLA_LOG_DEVEL_ON_ROOT("Final converged drop height=" << *dropHeight);
+            return EXIT_SUCCESS;
+         }
+         formerDropHeight = *dropHeight;
+      }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   WALBERLA_LOG_DEVEL_ON_ROOT("Final non-converged drop height=" << *dropHeight);
+
+   return EXIT_SUCCESS;
+}
+} // namespace DropWetting
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DropWetting::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/DropWetting.prm b/apps/showcases/FreeSurface/DropWetting.prm
new file mode 100644
index 000000000..bdfbe4e1e
--- /dev/null
+++ b/apps/showcases/FreeSurface/DropWetting.prm
@@ -0,0 +1,108 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 10, 10, 10 >;
+   periodicity                   < 1, 1, 0 >;
+   loadBalancingFrequency        100;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   dropDiameter      20;
+   domainSizeFactor  < 3, 3, 2 >;    // values multiplied with dropDiameter
+}
+
+PhysicsParameters
+{
+   relaxationRate 1.96;
+   surfaceTension 8.37e-3;
+   force <0, 0, -1.14e-7>;
+   enableWetting true;
+   contactAngle 45; // only used if enableWetting=true
+   timesteps       10001;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 1000;
+   evaluationFrequency 200;
+   convergenceThreshold 1e-5;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   //Border { direction N;  walldistance -1; NoSlip{} }
+   //Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   Border { direction T;  walldistance -1; NoSlip{} }
+   Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 100;
+   baseFolder mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency 100;
+      ghostLayers 0;
+      baseFolder vtk-out;
+      samplingResolution 1;
+
+      writers
+      {
+         velocity;
+         density;
+         //pdf;
+         flag;
+         fill_level;
+         force;
+         curvature;
+         normal;
+         obstacle_normal;
+         mapped_flag;
+        }
+
+        inclusion_filters
+        {
+            // only include liquid and interface cells in VTK output
+            //liquidInterfaceFilter;
+        }
+
+        before_functions
+        {
+            //ghost_layer_synchronization; // only needed if writing the ghost layer
+        }
+
+   }
+   domain_decomposition
+   {
+      writeFrequency 100;
+      baseFolder vtk-out;
+      outputDomainDecomposition true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/GravityWave.cpp b/apps/showcases/FreeSurface/GravityWave.cpp
new file mode 100644
index 000000000..6faca3c70
--- /dev/null
+++ b/apps/showcases/FreeSurface/GravityWave.cpp
@@ -0,0 +1,579 @@
+//======================================================================================================================
+//
+//  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 GravityWave.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates a standing wave purely governed by gravity, i.e., without surface tension forces.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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/dynamics/SurfaceDynamicsHandler.h"
+#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm/free_surface/surface_geometry/Utility.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace GravityWave
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D2Q9< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+// write each entry in "vector" to line in a file; columns are separated by tabs
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, const std::string& filename);
+
+// function describing the initialization profile (in global coordinates)
+inline real_t initializationProfile(real_t x, real_t amplitude, real_t offset, real_t wavelength)
+{
+   return amplitude * std::cos(x / wavelength * real_c(2) * math::pi + math::pi) + offset;
+}
+
+// evaluate the symmetry of the fill level field along the y-axis located at the center in x-direction
+// IMPORTANT REMARK: This implementation is very inefficient, as it gathers the field on a single process to perform the
+// evaluation.
+template< typename ScalarField_T >
+class SymmetryXEvaluator
+{
+ public:
+   SymmetryXEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID,
+                      const Vector3< uint_t >& domainSize, uint_t interval,
+                      const std::shared_ptr< real_t >& symmetryNorm)
+      : blockForest_(blockForest), fillFieldID_(fillFieldID), domainSize_(domainSize), interval_(interval),
+        symmetryNorm_(symmetryNorm), executionCounter_(uint_c(0))
+   {
+      auto blockForestPtr = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForestPtr);
+   }
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; }
+
+      real_t fillLevelSum2      = real_c(0); // sum of each cell's squared fill level
+      real_t deltaFillLevelSum2 = real_c(0); // sum of each cell's fill level difference to its symmetrical counterpart
+
+      // gather the fill level field on rank 0 (WARNING: simple, but very inefficient)
+      std::shared_ptr< ScalarField_T > fillFieldGathered = nullptr;
+      WALBERLA_ROOT_SECTION()
+      {
+         fillFieldGathered =
+            std::make_shared< ScalarField_T >(domainSize_[0], domainSize_[1], domainSize_[2], uint_c(0));
+      }
+      field::gather< ScalarField_T, ScalarField_T >(*fillFieldGathered, blockForest, fillFieldID_);
+
+      WALBERLA_ROOT_SECTION()
+      {
+         // get field's center-coordinate in x-direction
+         uint_t fieldXCenter = fillFieldGathered->xSize() / uint_c(2);
+
+         WALBERLA_FOR_ALL_CELLS_XYZ(fillFieldGathered, {
+            // skip cells in the right half of the field in x-direction, as they are treated
+            // as mirrored cells later
+            if (x >= cell_idx_c(fieldXCenter)) { continue; }
+
+            // get this cell's x-distance to the field's center
+            uint_t cellDistXCenter = uint_c(std::abs(int_c(fieldXCenter) - int_c(x)));
+
+            // get x-coordinate of (mirrored) cell in the right half of the field
+            cell_idx_t fieldRightX = cell_idx_c(fieldXCenter + cellDistXCenter);
+            if (fillFieldGathered->xSize() % 2 == uint_c(0))
+            {
+               fieldRightX -= cell_idx_c(1); // if xSize is even, the blocks on the right must be shifted by -1
+            }
+
+            // get fill level
+            const real_t fillLevel   = fillFieldGathered->get(x, y, z);
+            real_t fillLevelMirrored = real_c(0);
+            fillLevelMirrored        = fillFieldGathered->get(fieldRightX, y, z);
+
+            fillLevelSum2 += fillLevel * fillLevel;
+
+            const real_t deltaFill = fillLevel - fillLevelMirrored;
+            deltaFillLevelSum2 += deltaFill * deltaFill;
+         }) // WALBERLA_FOR_ALL_CELLS_XYZ
+      }
+
+      // communicate values among all processes
+      mpi::allReduceInplace< real_t >(fillLevelSum2, mpi::SUM);
+      mpi::allReduceInplace< real_t >(deltaFillLevelSum2, mpi::SUM);
+
+      // compute L2 norm evaluate symmetry
+      *symmetryNorm_ = real_c(std::pow(deltaFillLevelSum2 / fillLevelSum2, real_c(0.5)));
+   }
+
+ private:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+   ConstBlockDataID fillFieldID_;
+   Vector3< uint_t > domainSize_;
+   uint_t interval_;
+   std::shared_ptr< real_t > symmetryNorm_;
+   uint_t executionCounter_;
+}; // 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& 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),
+        domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition),
+        frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      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)
+      {
+         real_t maxSurfaceYPosition = real_c(0);
+
+         CellInterval globalSearchInterval(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0), globalXCoordinate_,
+                                           cell_idx_c(domainSize_[1]), cell_idx_c(0));
+
+         if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval))
+         {
+            // transform specified global x-coordinate into block local coordinate
+            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 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))))
+               {
+                  const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0));
+
+                  // transform local y-coordinate to global coordinate
+                  Cell localResultCell = localEvalCell;
+                  localResultCell[1]   = y;
+                  blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt);
+                  maxSurfaceYPosition = real_c(localResultCell[1]) + fillLevel;
+
+                  break;
+               }
+            }
+         }
+
+         if (maxSurfaceYPosition > *surfaceYPosition_) { *surfaceYPosition_ = maxSurfaceYPosition; }
+      }
+      // communicate result among all processes
+      mpi::allReduceInplace< real_t >(*surfaceYPosition_, mpi::MAX);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   ConstBlockDataID fillFieldID_;
+   Vector3< uint_t > domainSize_;
+   cell_idx_t globalXCoordinate_;
+   std::shared_ptr< real_t > surfaceYPosition_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class SurfaceYPositionEvaluator
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // get domain parameters from parameter file
+   auto domainParameters         = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const uint_t domainWidth      = domainParameters.getParameter< uint_t >("domainWidth");
+   const real_t liquidDepth      = domainParameters.getParameter< real_t >("liquidDepth");
+   const real_t initialAmplitude = domainParameters.getParameter< real_t >("initialAmplitude");
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = domainWidth;
+   domainSize[1] = uint_c(liquidDepth * real_c(2));
+   domainSize[2] = uint_c(1);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(liquidDepth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialAmplitude);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics);
+
+   // get physics parameters from parameter file
+   auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   const real_t relaxationRate           = physicsParameters.getParameter< real_t >("relaxationRate");
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber");
+   const real_t waveNumber     = real_c(2) * math::pi / real_c(domainSize[0]);
+   const real_t waveFrequency  = reynoldsNumber * viscosity / real_c(domainSize[0]) / initialAmplitude;
+   const real_t forceY         = -(waveFrequency * waveFrequency) / waveNumber / std::tanh(waveNumber * liquidDepth);
+   const Vector3< real_t > force(real_c(0), forceY, real_c(0));
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, 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();
+
+   // 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
+
+   const uint_t numTotalPoints = (fillLevelInitSamples + uint_c(1)) * (fillLevelInitSamples + uint_c(1));
+   const real_t stepsize       = real_c(1) / real_c(fillLevelInitSamples);
+
+   // initialize sine profile such that there is exactly one period in the domain, i.e., with wavelength=domainSize[0];
+   // every length is normalized with domainSize[0]
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // cell in block-local coordinates
+         const Cell localCell = fillFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = fillFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // Monte-Carlo like estimation of the fill level:
+         // create uniformly-distributed sample points in each cell and count the number of points below the sine
+         // profile; this fraction of points is used as the fill level to initialize the profile
+         uint_t numPointsBelow = uint_c(0);
+
+         for (uint_t xSample = uint_c(0); xSample <= fillLevelInitSamples; ++xSample)
+         {
+            // value of the sine-function
+            const real_t functionValue = initializationProfile(real_c(globalCell[0]) + real_c(xSample) * stepsize,
+                                                               initialAmplitude, liquidDepth, real_c(domainSize[0]));
+
+            for (uint_t ySample = uint_c(0); ySample <= fillLevelInitSamples; ++ySample)
+            {
+               const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize;
+               // with operator <, a fill level of 1 can not be reached when the line is equal to the cell's top border;
+               // with operator <=, a fill level of 0 can not be reached when the line is equal to the cell's bottom
+               // border
+               if (yPoint < functionValue) { ++numPointsBelow; }
+            }
+         }
+
+         // fill level is fraction of points below sine profile
+         fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints);
+      }) // 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 called only after every solid flag has been set; otherwise, the boundary handling
+   // might not detect solid flags correctly
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_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)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, liquidDepth);
+
+   // set density in non-liquid or non-interface cells to one (after initializing with hydrostatic pressure)
+   setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   const real_t surfaceTension = real_c(0);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with zero surface
+   // tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   // get fields created by surface geometry handler
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      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)),
+      evaluationFrequency, surfaceYPosition);
+   timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position");
+
+   // add sweep for evaluating the symmetry of the fill level field in x-direction
+   const std::shared_ptr< real_t > symmetryNorm = std::make_shared< real_t >(real_c(0));
+   const SymmetryXEvaluator< ScalarField_T > symmetryEvaluator(blockForest, fillFieldID, domainSize,
+                                                               evaluationFrequency, symmetryNorm);
+   timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   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");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      WALBERLA_ROOT_SECTION()
+      {
+         // non-dimensionalize time and surface position
+         const real_t tNonDimensional        = real_c(t) * waveFrequency;
+         const real_t positionNonDimensional = (*surfaceYPosition - liquidDepth) / initialAmplitude;
+
+         const std::vector< real_t > resultVector{ tNonDimensional, positionNonDimensional, *symmetryNorm };
+         if (t % evaluationFrequency == uint_c(0))
+         {
+            WALBERLA_LOG_DEVEL("time step = " << t);
+            WALBERLA_LOG_DEVEL("\t\ttNonDimensional = " << tNonDimensional
+                                                        << "\n\t\tpositionNonDimensional = " << positionNonDimensional
+                                                        << "\n\t\tsymmetryNorm = " << *symmetryNorm);
+            writeVectorToFile(resultVector, filename);
+         }
+      }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   for (const auto i : vector)
+   {
+      file << "\t" << i;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+} // namespace GravityWave
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::GravityWave::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/GravityWave.prm b/apps/showcases/FreeSurface/GravityWave.prm
new file mode 100644
index 000000000..363a59c84
--- /dev/null
+++ b/apps/showcases/FreeSurface/GravityWave.prm
@@ -0,0 +1,107 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 25, 25, 1 >;
+   periodicity                   < 1, 0, 1 >;
+   loadBalancingFrequency        0;
+   printLoadBalancingStatistics  false;
+}
+
+DomainParameters
+{
+   domainWidth       200; // equivalent to wavelength
+   liquidDepth       100;
+   initialAmplitude  2;
+}
+
+PhysicsParameters
+{
+   reynoldsNumber    10;
+   relaxationRate    1.8;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         86400;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 43200;
+   evaluationFrequency     864;
+   filename                gravity-wave.txt;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   Border { direction N;  walldistance -1; NoSlip{} }
+   Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   //Border { direction T;  walldistance -1; NoSlip{} }
+   //Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 864;
+   baseFolder     mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency       864;
+      ghostLayers          0;
+      baseFolder           vtk-out;
+      samplingResolution   1;
+
+      writers
+      {
+         fill_level;
+         mapped_flag;
+         velocity;
+         density;
+         //curvature;
+         //normal;
+         //obstacle_normal;
+         //pdf;
+         //flag;
+         //force;
+      }
+
+      inclusion_filters
+      {
+         //liquidInterfaceFilter; // only include liquid and interface cells in VTK output
+      }
+
+      before_functions
+      {
+         //ghost_layer_synchronization; // only needed if writing the ghost layer
+      }
+   }
+   domain_decomposition
+   {
+      writeFrequency             0;
+      baseFolder                 vtk-out;
+      outputDomainDecomposition  true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
new file mode 100644
index 000000000..a6b41aea0
--- /dev/null
+++ b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
@@ -0,0 +1,580 @@
+//======================================================================================================================
+//
+//  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 GravityWaveCodegen.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates a standing wave purely governed by gravity, i.e., without surface tension forces. The
+// implementation uses an LBM kernel generated with lbmpy.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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 "GravityWaveLatticeModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace GravityWaveCodegen
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+// write each entry in "vector" to line in a file; columns are separated by tabs
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, const std::string& filename);
+
+// function describing the global initialization profile
+inline real_t initializationProfile(real_t x, real_t amplitude, real_t offset, real_t wavelength)
+{
+   return amplitude * std::cos(x / wavelength * real_c(2) * math::pi + math::pi) + offset;
+}
+
+// evaluate the symmetry of the fill level field along the y-axis located at the center in x-direction
+// IMPORTANT REMARK: This implementation is very inefficient, as it gathers the field on a single process to perform the
+// evaluation.
+template< typename ScalarField_T >
+class SymmetryXEvaluator
+{
+ public:
+   SymmetryXEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID,
+                      const Vector3< uint_t >& domainSize, uint_t interval,
+                      const std::shared_ptr< real_t >& symmetryNorm)
+      : blockForest_(blockForest), fillFieldID_(fillFieldID), domainSize_(domainSize), interval_(interval),
+        symmetryNorm_(symmetryNorm), executionCounter_(uint_c(0))
+   {
+      auto blockForestPtr = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForestPtr);
+   }
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; }
+
+      real_t fillLevelSum2      = real_c(0); // sum of each cell's squared fill level
+      real_t deltaFillLevelSum2 = real_c(0); // sum of each cell's fill level difference to its symmetrical counterpart
+
+      // gather the fill level field on rank 0 (WARNING: simple, but very inefficient)
+      std::shared_ptr< ScalarField_T > fillFieldGathered = nullptr;
+      WALBERLA_ROOT_SECTION()
+      {
+         fillFieldGathered =
+            std::make_shared< ScalarField_T >(domainSize_[0], domainSize_[1], domainSize_[2], uint_c(0));
+      }
+      field::gather< ScalarField_T, ScalarField_T >(*fillFieldGathered, blockForest, fillFieldID_);
+
+      WALBERLA_ROOT_SECTION()
+      {
+         // get field's center-coordinate in x-direction
+         uint_t fieldXCenter = fillFieldGathered->xSize() / uint_c(2);
+
+         WALBERLA_FOR_ALL_CELLS_XYZ(fillFieldGathered, {
+            // skip cells in the right half of the field in x-direction, as they are treated
+            // as mirrored cells later
+            if (x >= cell_idx_c(fieldXCenter)) { continue; }
+
+            // get this cell's x-distance to the field's center
+            uint_t cellDistXCenter = uint_c(std::abs(int_c(fieldXCenter) - int_c(x)));
+
+            // get x-coordinate of (mirrored) cell in the right half of the field
+            cell_idx_t fieldRightX = cell_idx_c(fieldXCenter + cellDistXCenter);
+            if (fillFieldGathered->xSize() % 2 == uint_c(0))
+            {
+               fieldRightX -= cell_idx_c(1); // if xSize is even, the blocks on the right must be shifted by -1
+            }
+
+            // get fill level
+            const real_t fillLevel   = fillFieldGathered->get(x, y, z);
+            real_t fillLevelMirrored = real_c(0);
+            fillLevelMirrored        = fillFieldGathered->get(fieldRightX, y, z);
+
+            fillLevelSum2 += fillLevel * fillLevel;
+
+            const real_t deltaFill = fillLevel - fillLevelMirrored;
+            deltaFillLevelSum2 += deltaFill * deltaFill;
+         }) // WALBERLA_FOR_ALL_CELLS_XYZ
+      }
+
+      // communicate values among all processes
+      mpi::allReduceInplace< real_t >(fillLevelSum2, mpi::SUM);
+      mpi::allReduceInplace< real_t >(deltaFillLevelSum2, mpi::SUM);
+
+      // compute L2 norm evaluate symmetry
+      *symmetryNorm_ = real_c(std::pow(deltaFillLevelSum2 / fillLevelSum2, real_c(0.5)));
+   }
+
+ private:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+   ConstBlockDataID fillFieldID_;
+   Vector3< uint_t > domainSize_;
+   uint_t interval_;
+   std::shared_ptr< real_t > symmetryNorm_;
+   uint_t executionCounter_;
+}; // 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& 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),
+        domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition),
+        frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      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)
+      {
+         real_t maxSurfaceYPosition = real_c(0);
+
+         CellInterval globalSearchInterval(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0), globalXCoordinate_,
+                                           cell_idx_c(domainSize_[1]), cell_idx_c(0));
+
+         if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval))
+         {
+            // transform specified global x-coordinate into block local coordinate
+            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 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))))
+               {
+                  const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0));
+
+                  // transform local y-coordinate to global coordinate
+                  Cell localResultCell = localEvalCell;
+                  localResultCell[1]   = y;
+                  blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt);
+                  maxSurfaceYPosition = real_c(localResultCell[1]) + fillLevel;
+
+                  break;
+               }
+            }
+         }
+
+         if (maxSurfaceYPosition > *surfaceYPosition_) { *surfaceYPosition_ = maxSurfaceYPosition; }
+      }
+      // communicate result among all processes
+      mpi::allReduceInplace< real_t >(*surfaceYPosition_, mpi::MAX);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   ConstBlockDataID fillFieldID_;
+   Vector3< uint_t > domainSize_;
+   cell_idx_t globalXCoordinate_;
+   std::shared_ptr< real_t > surfaceYPosition_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class SurfaceYPositionEvaluator
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // get domain parameters from parameter file
+   auto domainParameters         = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const uint_t domainWidth      = domainParameters.getParameter< uint_t >("domainWidth");
+   const real_t liquidDepth      = domainParameters.getParameter< real_t >("liquidDepth");
+   const real_t initialAmplitude = domainParameters.getParameter< real_t >("initialAmplitude");
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = domainWidth;
+   domainSize[1] = uint_c(liquidDepth * real_c(2));
+   domainSize[2] = uint_c(1);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(liquidDepth);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialAmplitude);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics);
+
+   // get physics parameters from parameter file
+   auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate");
+   const real_t viscosity      = real_c(1) / real_c(3) * (real_c(1) / relaxationRate - real_c(0.5));
+
+   const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber");
+   const real_t waveNumber     = real_c(2) * math::pi / real_c(domainSize[0]);
+   const real_t waveFrequency  = reynoldsNumber * viscosity / real_c(domainSize[0]) / initialAmplitude;
+   const real_t forceY         = -(waveFrequency * waveFrequency) / waveNumber / std::tanh(waveNumber * liquidDepth);
+   const Vector3< real_t > force(real_c(0), forceY, real_c(0));
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(force[0], force[1], force[2], relaxationRate);
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, 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();
+
+   // 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
+
+   const uint_t numTotalPoints = (fillLevelInitSamples + uint_c(1)) * (fillLevelInitSamples + uint_c(1));
+   const real_t stepsize       = real_c(1) / real_c(fillLevelInitSamples);
+
+   // initialize sine profile such that there is exactly one period in the domain, i.e., with wavelength=domainSize[0];
+   // every length is normalized with domainSize[0]
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // cell in block-local coordinates
+         const Cell localCell = fillFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = fillFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // Monte-Carlo like estimation of the fill level:
+         // create uniformly-distributed sample points in each cell and count the number of points below the sine
+         // profile; this fraction of points is used as the fill level to initialize the profile
+         uint_t numPointsBelow = uint_c(0);
+
+         for (uint_t xSample = uint_c(0); xSample <= fillLevelInitSamples; ++xSample)
+         {
+            // value of the sine-function
+            const real_t functionValue = initializationProfile(real_c(globalCell[0]) + real_c(xSample) * stepsize,
+                                                               initialAmplitude, liquidDepth, real_c(domainSize[0]));
+
+            for (uint_t ySample = uint_c(0); ySample <= fillLevelInitSamples; ++ySample)
+            {
+               const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize;
+               // with operator <, a fill level of 1 can not be reached when the line is equal to the cell's top border;
+               // with operator <=, a fill level of 0 can not be reached when the line is equal to the cell's bottom
+               // border
+               if (yPoint < functionValue) { ++numPointsBelow; }
+            }
+         }
+
+         // fill level is fraction of points below sine profile
+         fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints);
+      }) // 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_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)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, liquidDepth);
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   const real_t surfaceTension = real_c(0);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with no surface
+   // tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   // get fields created by surface geometry handler
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      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)),
+      evaluationFrequency, surfaceYPosition);
+   timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position");
+
+   // add sweep for evaluating the symmetry of the fill level field in x-direction
+   const std::shared_ptr< real_t > symmetryNorm = std::make_shared< real_t >(real_c(0));
+   const SymmetryXEvaluator< ScalarField_T > symmetryEvaluator(blockForest, fillFieldID, domainSize,
+                                                               evaluationFrequency, symmetryNorm);
+   timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   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");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      WALBERLA_ROOT_SECTION()
+      {
+         // non-dimensionalize time and surface position
+         const real_t tNonDimensional        = real_c(t) * waveFrequency;
+         const real_t positionNonDimensional = (*surfaceYPosition - liquidDepth) / initialAmplitude;
+
+         const std::vector< real_t > resultVector{ tNonDimensional, positionNonDimensional, *symmetryNorm };
+         if (t % evaluationFrequency == uint_c(0))
+         {
+            WALBERLA_LOG_DEVEL("time step = " << t);
+            WALBERLA_LOG_DEVEL("\t\ttNonDimensional = " << tNonDimensional
+                                                        << "\n\t\tpositionNonDimensional = " << positionNonDimensional
+                                                        << "\n\t\tsymmetryNorm = " << *symmetryNorm);
+            writeVectorToFile(resultVector, filename);
+         }
+      }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   for (const auto i : vector)
+   {
+      file << "\t" << i;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+} // namespace GravityWaveCodegen
+} // 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
diff --git a/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py b/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py
new file mode 100644
index 000000000..22f474fcf
--- /dev/null
+++ b/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py
@@ -0,0 +1,34 @@
+import sympy as sp
+
+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
+
+# general parameters
+stencil = LBStencil(Stencil.D3Q19)
+omega = sp.Symbol('omega')
+force = sp.symbols('force_:3')
+layout = 'fzyx'
+
+# method definition
+lbm_config = LBMConfig(stencil=stencil,
+                       method=Method.SRT,
+                       relaxation_rate=omega,
+                       compressible=True,
+                       force=force,
+                       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)
+
+with CodeGeneration() as ctx:
+    generate_lattice_model(ctx, "GravityWaveLatticeModel", collision_rule, field_layout=layout)
diff --git a/apps/showcases/FreeSurface/MovingDrop.cpp b/apps/showcases/FreeSurface/MovingDrop.cpp
new file mode 100644
index 000000000..478ee193c
--- /dev/null
+++ b/apps/showcases/FreeSurface/MovingDrop.cpp
@@ -0,0 +1,325 @@
+//======================================================================================================================
+//
+//  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 MovingDrop.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief This showcase simulates a moving drop through a periodic domain.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.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/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"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DropInPool
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   const auto domainParameters              = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t dropDiameter                = domainParameters.getParameter< real_t >("dropDiameter");
+   const Vector3< real_t > dropCenterFactor = domainParameters.getParameter< Vector3< real_t > >("dropCenterFactor");
+   const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor");
+
+   // define domain size
+   Vector3< uint_t > domainSize = domainSizeFactor * dropDiameter;
+   domainSize[0]                = uint_c(domainSizeFactor[0] * dropDiameter);
+   domainSize[1]                = uint_c(domainSizeFactor[1] * dropDiameter);
+   domainSize[2]                = uint_c(domainSizeFactor[2] * dropDiameter);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropCenterFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+
+   Vector3< uint_t > realDomainSize;
+   realDomainSize[0] = cellsPerBlock[0] * numBlocks[0];
+   realDomainSize[1] = cellsPerBlock[1] * numBlocks[1];
+   realDomainSize[2] = cellsPerBlock[2] * numBlocks[2];
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize);
+
+   if (domainSize[0] != realDomainSize[0] && periodicity[0])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in x-direction can not be obtained with the number of blocks you specified.")
+   }
+   if (domainSize[1] != realDomainSize[1] && periodicity[1])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in y-direction can not be obtained with the number of blocks you specified.")
+   }
+   if (domainSize[2] != realDomainSize[2] && periodicity[2])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in z-direction can not be obtained with the number of blocks you specified.")
+   }
+
+   // read physics parameters from parameter file
+   const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const real_t weberNumber     = physicsParameters.getParameter< real_t >("weberNumber");
+   const real_t ohnesorgeNumber = physicsParameters.getParameter< real_t >("ohnesorgeNumber");
+   const real_t relaxationRate  = physicsParameters.getParameter< real_t >("relaxationRate");
+
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t surfaceTension   = real_c(std::pow(viscosity / ohnesorgeNumber, 2)) / dropDiameter;
+   const real_t initialVelocityX = real_c(std::pow(weberNumber * surfaceTension / dropDiameter, 0.5));
+
+   const Vector3< real_t > force(real_c(0));
+
+   const Vector3< real_t > initialVelocity(initialVelocityX, real_c(0), real_c(0));
+
+   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+
+   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialVelocity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add pdf field
+   const BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, initialVelocity, real_c(1), 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 Vector3< real_t > dropCenter = dropCenterFactor * dropDiameter;
+
+   const geometry::Sphere sphereDrop(dropCenter, dropDiameter * real_c(0.5));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false);
+
+   // initialize 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+      bubbleModelDerived->setAtmosphere(
+         Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1));
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   const ConstBlockDataID normalFieldID    = geometryHandler.getConstNormalFieldID();
+
+   geometryHandler.addSweeps(timeloop);
+
+   // add boundary handling for standard boundaries and free surface boundaries
+   const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add logging for computational performance
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      if (t % uint_c(100) == uint_c(0)) { WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep=" << t); }
+      timeloop.singleStep(timingPool, true);
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace DropInPool
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DropInPool::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/MovingDrop.prm b/apps/showcases/FreeSurface/MovingDrop.prm
new file mode 100644
index 000000000..184963db1
--- /dev/null
+++ b/apps/showcases/FreeSurface/MovingDrop.prm
@@ -0,0 +1,108 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 10, 10, 10 >;
+   periodicity                   < 1, 1, 1 >;
+   loadBalancingFrequency        0;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   dropDiameter      50;
+   dropCenterFactor  < 1, 1, 1 >;    // values multiplied with dropDiameter
+   poolHeightFactor  0;                // value multiplied with dropDiameter
+   domainSizeFactor  < 2, 2, 4 >;   // values multiplied with dropDiameter
+}
+
+PhysicsParameters
+{
+   weberNumber       2010;
+   ohnesorgeNumber   0.0384;
+   relaxationRate    1.989;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         1000000;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 10000;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   //Border { direction N;  walldistance -1; NoSlip{} }
+   //Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   //Border { direction T;  walldistance -1; NoSlip{} }
+   //Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 10000;
+   baseFolder mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency 10000;
+      ghostLayers 0;
+      baseFolder vtk-out;
+      samplingResolution 1;
+
+      writers
+      {
+         velocity;
+         density;
+         //pdf;
+         flag;
+         fill_level;
+         force;
+         curvature;
+         normal;
+         obstacle_normal;
+         mapped_flag;
+        }
+
+        inclusion_filters
+        {
+            // only include liquid and interface cells in VTK output
+            //liquidInterfaceFilter;
+        }
+
+        before_functions
+        {
+            //ghost_layer_synchronization; // only needed if writing the ghost layer
+        }
+
+   }
+   domain_decomposition
+   {
+      writeFrequency 0;
+      baseFolder vtk-out;
+      outputDomainDecomposition true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/RisingBubble.cpp b/apps/showcases/FreeSurface/RisingBubble.cpp
new file mode 100644
index 000000000..3dabdf5da
--- /dev/null
+++ b/apps/showcases/FreeSurface/RisingBubble.cpp
@@ -0,0 +1,465 @@
+//======================================================================================================================
+//
+//  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 RisingBubble.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This showcase simulates a rising gas bubble in a stationary liquid. Reference experiments are available from
+// Bhaga, Weber (1981), doi:10.1017/S002211208100311X
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/StringUtility.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"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace RisingBubble
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+// write each entry in "vector" to line in a file; the first column contains the current timestep; all values are
+// separated by tabs
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename);
+
+// IMPORTANT REMARK: this does not work when multiple bubbles are in the system (e.g. after splitting)
+template< typename FreeSurfaceBoundaryHandling_T >
+class CenterOfMassComputer
+{
+ public:
+   CenterOfMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                        const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                        uint_t frequency, const std::shared_ptr< Vector3< real_t > >& centerOfMass)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling),
+        centerOfMass_(centerOfMass), frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      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)) { return; }
+
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      *centerOfMass_   = Vector3< real_t >(real_c(0));
+      Cell cellSum     = Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0));
+      uint_t cellCount = uint_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+
+         WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, {
+            if (flagInfo.isGas(flagFieldIt))
+            {
+               Cell globalCell = flagFieldIt.cell();
+               // transform local cell to global coordinates
+               blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt);
+               cellSum += globalCell;
+               ++cellCount;
+            }
+         }) // WALBERLA_FOR_ALL_CELLS
+      }
+
+      mpi::allReduceInplace< uint_t >(cellCount, mpi::SUM);
+      mpi::allReduceInplace< cell_idx_t >(cellSum[0], mpi::SUM);
+      mpi::allReduceInplace< cell_idx_t >(cellSum[1], mpi::SUM);
+      mpi::allReduceInplace< cell_idx_t >(cellSum[2], mpi::SUM);
+
+      (*centerOfMass_)[0] = real_c(cellSum[0]) / real_c(cellCount);
+      (*centerOfMass_)[1] = real_c(cellSum[1]) / real_c(cellCount);
+      (*centerOfMass_)[2] = real_c(cellSum[2]) / real_c(cellCount);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   std::shared_ptr< Vector3< real_t > > centerOfMass_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class CenterOfMassComputer
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // get domain parameters from parameter file
+   const auto domainParameters              = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const uint_t bubbleDiameter              = domainParameters.getParameter< uint_t >("bubbleDiameter");
+   const Vector3< real_t > bubblePosition   = domainParameters.getParameter< Vector3< real_t > >("bubblePosition");
+   const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor");
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = uint_c(domainSizeFactor[0] * real_c(bubbleDiameter));
+   domainSize[1] = uint_c(domainSizeFactor[1] * real_c(bubbleDiameter));
+   domainSize[2] = uint_c(domainSizeFactor[2] * real_c(bubbleDiameter));
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubblePosition);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+
+   Vector3< uint_t > realDomainSize;
+   realDomainSize[0] = cellsPerBlock[0] * numBlocks[0];
+   realDomainSize[1] = cellsPerBlock[1] * numBlocks[1];
+   realDomainSize[2] = cellsPerBlock[2] * numBlocks[2];
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize);
+
+   if (domainSize[0] != realDomainSize[0] && periodicity[0])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in x-direction can not be obtained with the numer of blocks you specified.")
+   }
+   if (domainSize[1] != realDomainSize[1] && periodicity[1])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in y-direction can not be obtained with the numer of blocks you specified.")
+   }
+   // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size
+   int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]);
+   if (boundaryThicknessZ < 0)
+   {
+      WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.")
+   }
+
+   // read physics parameters from parameter file
+   const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const real_t bondNumber      = physicsParameters.getParameter< real_t >("bondNumber");
+   const real_t mortonNumber    = physicsParameters.getParameter< real_t >("mortonNumber");
+   const real_t relaxationRate  = physicsParameters.getParameter< real_t >("relaxationRate");
+   const bool enableWetting     = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle    = physicsParameters.getParameter< real_t >("contactAngle");
+   const uint_t timesteps       = physicsParameters.getParameter< uint_t >("timesteps");
+
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t surfaceTension = real_c(
+      std::pow(bondNumber * std::pow(viscosity, 4) / mortonNumber / real_c(bubbleDiameter * bubbleDiameter), 0.5));
+
+   const real_t gravitationalAccelerationZ =
+      mortonNumber * real_c(std::pow(surfaceTension, 3)) / real_c(std::pow(viscosity, 4));
+
+   const Vector3< real_t > force(real_c(0), real_c(0), -gravitationalAccelerationZ);
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(mortonNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add various fields
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2));
+
+   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 geometry::Sphere sphereBubble(Vector3< real_t >(bubblePosition[0] * real_c(bubbleDiameter),
+                                                         bubblePosition[1] * real_c(bubbleDiameter),
+                                                         bubblePosition[2] * real_c(bubbleDiameter)),
+                                       real_c(bubbleDiameter) * real_c(0.5));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true);
+
+   // add boundary conditions from config file
+   const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters");
+   freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters);
+   for (int i = -1; i != boundaryThicknessZ; ++i)
+   {
+      freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::T, cell_idx_c(i));
+   }
+
+   // 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, real_c(domainSize[2]) * real_c(0.5));
+
+   // set density in non-liquid or non-interface cells to 1 (to be done after initializing with hydrostatic pressure)
+   setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   // get bubble's center of mass position
+   const std::shared_ptr< Vector3< real_t > > centerOfMass =
+      std::make_shared< Vector3< real_t > >(Vector3< real_t >(real_c(0)));
+   const CenterOfMassComputer< FreeSurfaceBoundaryHandling_T > centerOfMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, evaluationFrequency, centerOfMass);
+   timeloop.addFuncAfterTimeStep(centerOfMassComputer, "Evaluator: center of mass");
+
+   // add computation of total mass
+   const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, evaluationFrequency, totalMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(1), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add performance logging
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   const real_t stoppingHeight = real_c(domainSize[2] - bubbleDiameter);
+
+   uint_t timestepOld                = uint_c(0);
+   Vector3< real_t > centerOfMassOld = Vector3< real_t >(real_c(0));
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      // only evaluate if center of mass has moved "significantly"
+      if ((*centerOfMass)[2] > (centerOfMassOld[2] + real_c(1)) && t > uint_c(0))
+      {
+         WALBERLA_ROOT_SECTION()
+         {
+            real_t riseVelocity    = ((*centerOfMass)[2] - centerOfMassOld[2]) / real_c(t - timestepOld);
+            const real_t dragForce = real_c(4) / real_c(3) * gravitationalAccelerationZ * real_c(bubbleDiameter) /
+                                     (riseVelocity * riseVelocity);
+
+            WALBERLA_LOG_DEVEL("time step = " << t);
+            WALBERLA_LOG_DEVEL("\t\tcenterOfMass = " << *centerOfMass << "\n\t\triseVelocity = " << riseVelocity
+                                                     << "\n\t\tdragForce = " << dragForce);
+            WALBERLA_LOG_DEVEL("\t\ttotalMass = " << *totalMass);
+
+            const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce };
+
+            writeVectorToFile(resultVector, t, filename);
+         }
+
+         timestepOld     = t;
+         centerOfMassOld = *centerOfMass;
+      }
+
+      // stop simulation before bubble hits the top wall
+      if ((*centerOfMass)[2] > stoppingHeight) { break; }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+   return EXIT_SUCCESS;
+}
+
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   file << timestep;
+   for (const auto i : vector)
+   {
+      file << "\t" << i;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+} // namespace RisingBubble
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::RisingBubble::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/RisingBubble.prm b/apps/showcases/FreeSurface/RisingBubble.prm
new file mode 100644
index 000000000..5d483426b
--- /dev/null
+++ b/apps/showcases/FreeSurface/RisingBubble.prm
@@ -0,0 +1,108 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 16, 16, 16 >;
+   periodicity                   < 1, 1, 0 >;
+   loadBalancingFrequency        445;
+   printLoadBalancingStatistics  true;
+}
+
+DomainParameters
+{
+   bubbleDiameter    16;
+   bubblePosition    < 4, 4, 1 >;  // initial bubble position (values multiplied with bubbleDiameter)
+   domainSizeFactor  < 8, 8, 20 >; // values multiplied with bubbleDiameter
+}
+
+PhysicsParameters
+{
+   bondNumber        115;
+   mortonNumber      4.63e-3;
+   relaxationRate    1.95;
+   enableWetting     false;
+   contactAngle      0; // only used if enableWetting=true
+   timesteps         8900;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             true;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 4450;
+   evaluationFrequency     44;
+   filename                rising-bubble.txt;
+}
+
+BoundaryParameters
+{
+   // X
+   //Border { direction W;  walldistance -1; NoSlip{} }
+   //Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   //Border { direction N;  walldistance -1; NoSlip{} }
+   //Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   Border { direction T;  walldistance -1; NoSlip{} }
+   Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 445;
+   baseFolder     mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency       445;
+      ghostLayers          0;
+      baseFolder           vtk-out;
+      samplingResolution   1;
+
+      writers
+      {
+         fill_level;
+         mapped_flag;
+         velocity;
+         density;
+         //curvature;
+         //normal;
+         //obstacle_normal;
+         //pdf;
+         //flag;
+         //force;
+      }
+
+      inclusion_filters
+      {
+         //liquidInterfaceFilter; // only include liquid and interface cells in VTK output
+      }
+
+      before_functions
+      {
+         //ghost_layer_synchronization; // only needed if writing the ghost layer
+      }
+   }
+   domain_decomposition
+   {
+      writeFrequency             445;
+      baseFolder                 vtk-out;
+      outputDomainDecomposition  true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/TaylorBubble.cpp b/apps/showcases/FreeSurface/TaylorBubble.cpp
new file mode 100644
index 000000000..f32cfb982
--- /dev/null
+++ b/apps/showcases/FreeSurface/TaylorBubble.cpp
@@ -0,0 +1,494 @@
+//======================================================================================================================
+//
+//  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 TaylorBubble.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief This showcase simulates a rising gas bubble in a cylindrical tube.
+//
+// This showcase simulates an elongated gas bubble in a cylindrical tube. Reference experiments are available from
+// Bugg, Saad (2002), doi:10.1016/S0301-9322(02)00002-2
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/StringUtility.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"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace TaylorBubble
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using CollisionModel_T      = lbm::collision_model::SRT;
+using ForceModel_T          = lbm::force_model::GuoField< VectorField_T >;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >;
+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 flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+// write each entry in "vector" to line in a file; the first column contains the current timestep; all values are
+// separated by tabs
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename);
+
+// IMPORTANT REMARK: this does not work when multiple bubbles are in the system (e.g. after splitting)
+template< typename FreeSurfaceBoundaryHandling_T >
+class CenterOfMassComputer
+{
+ public:
+   CenterOfMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                        const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                        uint_t interval, const std::shared_ptr< Vector3< real_t > >& centerOfMass)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling),
+        centerOfMass_(centerOfMass), interval_(interval), executionCounter_(uint_c(0))
+   {}
+
+   void operator()()
+   {
+      auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling);
+
+      ++executionCounter_;
+
+      // only evaluate in given intervals
+      if (executionCounter_ % interval_ != uint_c(0)) { return; }
+
+      const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+      const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+      *centerOfMass_   = Vector3< real_t >(real_c(0));
+      Cell cellSum     = Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0));
+      uint_t cellCount = uint_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+
+         WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, {
+            if (flagInfo.isGas(flagFieldIt))
+            {
+               Cell globalCell = flagFieldIt.cell();
+               // transform local cell to global coordinates
+               blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt);
+               cellSum += globalCell;
+               ++cellCount;
+            }
+         }) // WALBERLA_FOR_ALL_CELLS
+      }
+
+      mpi::allReduceInplace< uint_t >(cellCount, mpi::SUM);
+      mpi::allReduceInplace< cell_idx_t >(cellSum[0], mpi::SUM);
+      mpi::allReduceInplace< cell_idx_t >(cellSum[1], mpi::SUM);
+      mpi::allReduceInplace< cell_idx_t >(cellSum[2], mpi::SUM);
+
+      (*centerOfMass_)[0] = real_c(cellSum[0]) / real_c(cellCount);
+      (*centerOfMass_)[1] = real_c(cellSum[1]) / real_c(cellCount);
+      (*centerOfMass_)[2] = real_c(cellSum[2]) / real_c(cellCount);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   std::shared_ptr< Vector3< real_t > > centerOfMass_;
+
+   uint_t interval_;
+   uint_t executionCounter_;
+}; // class CenterOfMassComputer
+
+int main(int argc, char** argv)
+{
+   Environment walberlaEnv(argc, argv);
+
+   if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") }
+
+   // print content of parameter file
+   WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config());
+
+   // get block forest parameters from parameter file
+   auto blockForestParameters              = walberlaEnv.config()->getOneBlock("BlockForestParameters");
+   const Vector3< uint_t > cellsPerBlock   = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock");
+   const Vector3< bool > periodicity       = blockForestParameters.getParameter< Vector3< bool > >("periodicity");
+   const uint_t loadBalancingFrequency     = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency");
+   const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics");
+
+   // read domain parameters from parameter file
+   const auto domainParameters              = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const real_t tubeDiameter                = domainParameters.getParameter< real_t >("tubeDiameter");
+   const real_t bubbleDiameter              = domainParameters.getParameter< real_t >("bubbleDiameter");
+   const real_t bubbleHeight                = domainParameters.getParameter< real_t >("bubbleHeight");
+   const Vector3< real_t > bubbleBottomEnd  = domainParameters.getParameter< Vector3< real_t > >("bubbleBottomEnd");
+   const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor");
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = uint_c(domainSizeFactor[0] * tubeDiameter);
+   domainSize[1] = uint_c(domainSizeFactor[1] * tubeDiameter);
+   domainSize[2] = uint_c(domainSizeFactor[2] * tubeDiameter);
+
+   // compute number of blocks as defined by domainSize and cellsPerBlock
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0])));
+   numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1])));
+   numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2])));
+
+   // get number of (MPI) processes
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(tubeDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleHeight);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleBottomEnd);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock);
+
+   Vector3< uint_t > realDomainSize;
+   realDomainSize[0] = cellsPerBlock[0] * numBlocks[0];
+   realDomainSize[1] = cellsPerBlock[1] * numBlocks[1];
+   realDomainSize[2] = cellsPerBlock[2] * numBlocks[2];
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize);
+
+   if (domainSize[0] != realDomainSize[0] && periodicity[0])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in x-direction can not be obtained with the numer of blocks you specified.")
+   }
+   if (domainSize[1] != realDomainSize[1] && periodicity[1])
+   {
+      WALBERLA_ABORT(
+         "The specified domain size in y-direction can not be obtained with the numer of blocks you specified.")
+   }
+   // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size
+   int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]);
+   if (boundaryThicknessZ < 0)
+   {
+      WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.")
+   }
+
+   // read physics parameters from parameter file
+   const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const real_t bondNumber      = physicsParameters.getParameter< real_t >("bondNumber");
+   const real_t mortonNumber    = physicsParameters.getParameter< real_t >("mortonNumber");
+   const real_t relaxationRate  = physicsParameters.getParameter< real_t >("relaxationRate");
+   const bool enableWetting     = physicsParameters.getParameter< bool >("enableWetting");
+   const real_t contactAngle    = physicsParameters.getParameter< real_t >("contactAngle");
+   const uint_t timesteps       = physicsParameters.getParameter< uint_t >("timesteps");
+
+   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
+   const real_t viscosity                = collisionModel.viscosity();
+
+   const real_t surfaceTension =
+      real_c(std::pow(bondNumber * std::pow(viscosity, 4) / mortonNumber / real_c(tubeDiameter * tubeDiameter), 0.5));
+
+   const real_t gravitationalAccelerationZ =
+      mortonNumber * real_c(std::pow(surfaceTension, 3)) / real_c(std::pow(viscosity, 4));
+
+   const Vector3< real_t > force = Vector3< real_t >(real_c(0), real_c(0), -gravitationalAccelerationZ);
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(mortonNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension);
+
+   // read model parameters from parameter file
+   const auto modelParameters               = walberlaEnv.config()->getOneBlock("ModelParameters");
+   const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel");
+   const std::string pdfRefillingModel      = modelParameters.getParameter< std::string >("pdfRefillingModel");
+   const std::string excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   const bool enableForceWeighting           = modelParameters.getParameter< bool >("enableForceWeighting");
+   const bool useSimpleMassExchange          = modelParameters.getParameter< bool >("useSimpleMassExchange");
+   const real_t cellConversionThreshold      = modelParameters.getParameter< real_t >("cellConversionThreshold");
+   const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold");
+   const bool enableBubbleModel              = modelParameters.getParameter< bool >("enableBubbleModel");
+   const bool enableBubbleSplits             = modelParameters.getParameter< bool >("enableBubbleSplits");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits);
+
+   // read evaluation parameters from parameter file
+   const auto evaluationParameters      = walberlaEnv.config()->getOneBlock("EvaluationParameters");
+   const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency");
+   const uint_t evaluationFrequency     = evaluationParameters.getParameter< uint_t >("evaluationFrequency");
+   const std::string filename           = evaluationParameters.getParameter< std::string >("filename");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+   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);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID));
+
+   // add various fields
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2));
+
+   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 Vector3< real_t > bubbleTopEnd =
+      Vector3< real_t >(bubbleBottomEnd[0], bubbleBottomEnd[1], bubbleBottomEnd[2] + bubbleHeight);
+   const geometry::Cylinder cylinderBubble(bubbleBottomEnd * tubeDiameter, bubbleTopEnd * tubeDiameter,
+                                           bubbleDiameter * tubeDiameter * real_c(0.5));
+
+   bubble_model::addBodyToFillLevelField< geometry::Cylinder >(*blockForest, fillFieldID, cylinderBubble, true);
+
+   // add boundary conditions from config file
+   const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters");
+   freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters);
+   for (int i = -1; i != boundaryThicknessZ; ++i)
+   {
+      freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::T, cell_idx_c(i));
+   }
+
+   // initialize cylindrical domain walls
+   const Vector3< real_t > domainCylinderBottomEnd =
+      Vector3< real_t >(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), real_c(0));
+   const Vector3< real_t > domainCylinderTopEnd = Vector3< real_t >(
+      real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), real_c(domainSize[2]));
+   const geometry::Cylinder cylinderTube(domainCylinderBottomEnd, domainCylinderTopEnd, tubeDiameter * real_c(0.5));
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, {
+         Cell globalCell = flagFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt);
+         const Vector3< real_t > globalPoint = blockForest->getCellCenter(globalCell);
+
+         if (!geometry::contains(cylinderTube, globalPoint))
+         {
+            freeSurfaceBoundaryHandling->setNoSlipInCell(globalCell);
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // 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();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // add bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr;
+   if (enableBubbleModel)
+   {
+      const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived =
+         std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits);
+      bubbleModelDerived->initFromFillLevelField(fillFieldID);
+
+      bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived);
+   }
+   else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); }
+
+   // initialize hydrostatic pressure
+   initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, real_c(domainSize[2]) * real_c(0.5));
+
+   // 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);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   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 > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, 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),
+      loadBalancingFrequency, printLoadBalancingStatistics);
+   timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
+
+   const std::shared_ptr< Vector3< real_t > > centerOfMass =
+      std::make_shared< Vector3< real_t > >(Vector3< real_t >(real_c(0)));
+   const CenterOfMassComputer< FreeSurfaceBoundaryHandling_T > centerOfMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, evaluationFrequency, centerOfMass);
+   timeloop.addFuncAfterTimeStep(centerOfMassComputer, "Evaluator: center of mass");
+
+   const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, evaluationFrequency, totalMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID,
+      geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
+      geometryHandler.getObstNormalFieldID());
+
+   // add triangle mesh output of free surface
+   SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter(
+      blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(1), walberlaEnv.config());
+   surfaceMeshWriter(); // write initial mesh
+   timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh");
+
+   // add performance logging
+   const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs,
+                                                          performanceLogFrequency);
+   timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging");
+
+   WcTimingPool timingPool;
+
+   const real_t stoppingHeight = real_c(domainSize[2]) - bubbleHeight * tubeDiameter;
+
+   uint_t timestepOld                = uint_c(0);
+   Vector3< real_t > centerOfMassOld = Vector3< real_t >(real_c(0));
+
+   for (uint_t t = uint_c(0); t != timesteps; ++t)
+   {
+      timeloop.singleStep(timingPool, true);
+
+      // only evaluate if center of mass has moved "significantly"
+      if ((*centerOfMass)[2] > (centerOfMassOld[2] + real_c(1)) && t > uint_c(0))
+      {
+         WALBERLA_ROOT_SECTION()
+         {
+            real_t riseVelocity    = ((*centerOfMass)[2] - centerOfMassOld[2]) / real_c(t - timestepOld);
+            const real_t dragForce = real_c(4) / real_c(3) * gravitationalAccelerationZ * real_c(bubbleDiameter) /
+                                     (riseVelocity * riseVelocity);
+
+            WALBERLA_LOG_DEVEL("time step = " << t);
+            WALBERLA_LOG_DEVEL("\t\tcenterOfMass = " << *centerOfMass << "\n\t\triseVelocity = " << riseVelocity
+                                                     << "\n\t\tdragForce = " << dragForce);
+            WALBERLA_LOG_DEVEL("\t\ttotalMass = " << *totalMass);
+
+            const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce };
+
+            writeVectorToFile(resultVector, t, filename);
+         }
+
+         timestepOld     = t;
+         centerOfMassOld = *centerOfMass;
+      }
+
+
+      // stop simulation before bubble hits the top wall
+      if ((*centerOfMass)[2] > stoppingHeight) { break; }
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+   return EXIT_SUCCESS;
+}
+
+template< typename T >
+void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename)
+{
+   std::fstream file;
+   file.open(filename, std::fstream::app);
+
+   file << timestep;
+   for (const auto i : vector)
+   {
+      file << "\t" << i;
+   }
+
+   file << "\n";
+   file.close();
+}
+
+} // namespace TaylorBubble
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::TaylorBubble::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/TaylorBubble.prm b/apps/showcases/FreeSurface/TaylorBubble.prm
new file mode 100644
index 000000000..a2b289aee
--- /dev/null
+++ b/apps/showcases/FreeSurface/TaylorBubble.prm
@@ -0,0 +1,110 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 16, 16, 20 >;
+   periodicity                   < 0, 0, 0 >;
+   loadBalancingFrequency        0;
+   printLoadBalancingStatistics  false;
+}
+
+DomainParameters
+{
+   tubeDiameter      32;
+   bubbleDiameter    0.75;             // value multiplied with tubeDiameter
+   bubbleHeight      3;                // value multiplied with tubeDiameter
+   bubbleBottomEnd   < 0.5, 0.5, 1 >;  // initial cylindrical bubble's bottom end center (values multiplied with tubeDiameter)
+   domainSizeFactor  < 1, 1, 10 >;     // values multiplied with tubeDiameter
+}
+
+PhysicsParameters
+{
+   bondNumber        100;
+   mortonNumber      0.015;
+   relaxationRate    1.8;
+   enableWetting     false;
+   contactAngle      60; // only used if enableWetting=true
+   timesteps         12240;
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   pdfRefillingModel             EquilibriumRefilling;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   enableForceWeighting          false;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             true;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
+}
+
+EvaluationParameters
+{
+   performanceLogFrequency 1224;
+   evaluationFrequency     61;
+   filename                taylor-bubble.txt;
+}
+
+BoundaryParameters
+{
+   // X
+   Border { direction W;  walldistance -1; NoSlip{} }
+   Border { direction E;  walldistance -1; NoSlip{} }
+
+   // Y
+   Border { direction N;  walldistance -1; NoSlip{} }
+   Border { direction S;  walldistance -1; NoSlip{} }
+
+   // Z
+   Border { direction T;  walldistance -1; NoSlip{} }
+   Border { direction B;  walldistance -1; NoSlip{} }
+}
+
+MeshOutputParameters
+{
+   writeFrequency 612;
+   baseFolder     mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency       612;
+      ghostLayers          0;
+      baseFolder           vtk-out;
+      samplingResolution   1;
+
+      writers
+      {
+         fill_level;
+         mapped_flag;
+         velocity;
+         density;
+         curvature;
+         normal;
+         obstacle_normal;
+         //pdf;
+         //flag;
+         //force;
+      }
+
+      inclusion_filters
+      {
+         //liquidInterfaceFilter; // only include liquid and interface cells in VTK output
+      }
+
+      before_functions
+      {
+         //ghost_layer_synchronization; // only needed if writing the ghost layer
+      }
+   }
+   domain_decomposition
+   {
+      writeFrequency             0;
+      baseFolder                 vtk-out;
+      outputDomainDecomposition  true;
+   }
+}
\ No newline at end of file
diff --git a/src/core/StringUtility.h b/src/core/StringUtility.h
index 780cfb9de..9ac7590fa 100644
--- a/src/core/StringUtility.h
+++ b/src/core/StringUtility.h
@@ -30,50 +30,60 @@ namespace walberla
 {
 
 // Convert (in place) every character in string to uppercase.
-inline void string_to_upper(std::string &s);
+inline void string_to_upper(std::string& s);
 
 // Convert (copy) every character in string to uppercase.
-inline std::string string_to_upper_copy(const std::string &s);
+inline std::string string_to_upper_copy(const std::string& s);
 
 // Convert (in place) every character in string to lowercase.
-inline void string_to_lower(std::string &s);
+inline void string_to_lower(std::string& s);
 
 // Convert (in place) every character in string to lowercase.
-inline std::string string_to_lower_copy(const std::string &s);
+inline std::string string_to_lower_copy(const std::string& s);
 
 // Remove (in place) all whitespaces at the beginning of a string.
-inline void string_trim_left(std::string &s);
+inline void string_trim_left(std::string& s);
 
 // Remove (in place) all whitespaces at the end of a string.
-inline void string_trim_right(std::string &s);
+inline void string_trim_right(std::string& s);
 
 // Remove (in place) all whitespaces at the beginning and at the end of a string.
-inline void string_trim(std::string &s);
+inline void string_trim(std::string& s);
 
 // Remove (copy) all whitespaces at the beginning of a string.
-inline std::string string_trim_left_copy(const std::string &s);
+inline std::string string_trim_left_copy(const std::string& s);
 
 // Remove (copy) all whitespaces at the end of a string.
-inline std::string string_trim_right_copy(const std::string &s);
+inline std::string string_trim_right_copy(const std::string& s);
 
 // Remove (copy) all whitespaces at the beginning and at the end of a string.
-inline std::string string_trim_copy(const std::string &s);
+inline std::string string_trim_copy(const std::string& s);
 
 // Split a string at the given delimiters into a vector of substrings.
 // E.g. specify std::string(" |,") in order to split at characters ' ' and ','.
-inline std::vector<std::string> string_split(std::string s, const std::string &delimiters);
+inline std::vector< std::string > string_split(std::string s, const std::string& delimiters);
 
 // Replace (in place) all occurrences of substring "old" with substring "new".
-inline void string_replace_all(std::string &s, const std::string &oldSubstr, const std::string &newSubstr);
+inline void string_replace_all(std::string& s, const std::string& oldSubstr, const std::string& newSubstr);
 
 // Replace (copy) all occurrences of substring "old" with substring "new".
-inline std::string string_replace_all_copy(const std::string &s, const std::string &oldSubstr, const std::string &newSubstr);
+inline std::string string_replace_all_copy(const std::string& s, const std::string& oldSubstr,
+                                           const std::string& newSubstr);
 
 // Check whether a string ends with a certain substring.
-inline bool string_ends_with(const std::string &s, const std::string &substr);
+inline bool string_ends_with(const std::string& s, const std::string& substr);
 
 // Case-insensitive std::string::compare.
-inline int string_icompare(const std::string &s1, const std::string &s2);
+inline int string_icompare(const std::string& s1, const std::string& s2);
+
+// Convert a floating point number to string with a specified number of decimal places.
+template< typename T >
+inline std::string to_string_with_precision(T number, uint_t decimalPlaces);
+
+// Convert a floating point number to string with only the relevant number of decimal places, i.e., zeros at the end are
+// cut off.
+template< typename T >
+inline std::string to_string_only_relevant_digits(T number);
 
 } // namespace walberla
 
diff --git a/src/core/StringUtility.impl.h b/src/core/StringUtility.impl.h
index 730bf5928..02b5f3937 100644
--- a/src/core/StringUtility.impl.h
+++ b/src/core/StringUtility.impl.h
@@ -31,61 +31,76 @@
 namespace walberla
 {
 // Convert (in place) every character in string to uppercase.
-inline void string_to_upper(std::string &s) {
-   std::transform(s.begin(), s.end(), s.begin(), [](char c){ return static_cast<char>(std::toupper(static_cast<unsigned char>(c))); });
+inline void string_to_upper(std::string& s)
+{
+   std::transform(s.begin(), s.end(), s.begin(),
+                  [](char c) { return static_cast< char >(std::toupper(static_cast< unsigned char >(c))); });
 }
 
 // Convert (copy) every character in string to uppercase.
-inline std::string string_to_upper_copy(const std::string &s) {
+inline std::string string_to_upper_copy(const std::string& s)
+{
    std::string result = s;
    string_to_upper(result);
    return result;
 }
 
 // Convert (in place) every character in string to lowercase.
-inline void string_to_lower(std::string &s) {
-   std::transform(s.begin(), s.end(), s.begin(), [](char c){ return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); });
+inline void string_to_lower(std::string& s)
+{
+   std::transform(s.begin(), s.end(), s.begin(),
+                  [](char c) { return static_cast< char >(std::tolower(static_cast< unsigned char >(c))); });
 }
 
 // Convert (copy) every character in string to lowercase.
-inline std::string string_to_lower_copy(const std::string &s) {
+inline std::string string_to_lower_copy(const std::string& s)
+{
    std::string result = s;
    string_to_lower(result);
    return result;
 }
 
 // Remove (in place) all whitespaces at the beginning of a string.
-inline void string_trim_left(std::string &s) {
-   s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](char c) { return ! std::isspace(static_cast<unsigned char>(c)); }));
+inline void string_trim_left(std::string& s)
+{
+   s.erase(s.begin(),
+           std::find_if(s.begin(), s.end(), [](char c) { return !std::isspace(static_cast< unsigned char >(c)); }));
 }
 
 // Remove (in place) all whitespaces at the end of a string.
-inline void string_trim_right(std::string &s) {
-   s.erase(std::find_if(s.rbegin(), s.rend(), [](char c) { return ! std::isspace(static_cast<unsigned char>(c)); }).base(), s.end());
+inline void string_trim_right(std::string& s)
+{
+   s.erase(
+      std::find_if(s.rbegin(), s.rend(), [](char c) { return !std::isspace(static_cast< unsigned char >(c)); }).base(),
+      s.end());
 }
 
 // Remove (in place) all whitespaces at the beginning and at the end of a string.
-inline void string_trim(std::string &s) {
+inline void string_trim(std::string& s)
+{
    string_trim_left(s);
    string_trim_right(s);
 }
 
 // Remove (copy) all whitespaces at the beginning of a string.
-inline std::string string_trim_left_copy(const std::string &s) {
+inline std::string string_trim_left_copy(const std::string& s)
+{
    std::string result = s;
    string_trim_left(result);
    return result;
 }
 
 // Remove (copy) all whitespaces at the end of a string.
-inline std::string string_trim_right_copy(const std::string &s) {
+inline std::string string_trim_right_copy(const std::string& s)
+{
    std::string result = s;
    string_trim_left(result);
    return result;
 }
 
 // Remove (copy) all whitespaces at the beginning and at the end of a string.
-inline std::string string_trim_copy(const std::string &s) {
+inline std::string string_trim_copy(const std::string& s)
+{
    std::string result = s;
    string_trim_left(result);
    return result;
@@ -93,17 +108,22 @@ inline std::string string_trim_copy(const std::string &s) {
 
 // Split a string at the given delimiters into a vector of substrings.
 // E.g. specify std::string(" ,") in order to split at characters ' ' and ','.
-inline std::vector<std::string> string_split(std::string s, const std::string &delimiters) {
-   std::vector<std::string> substrings;
+inline std::vector< std::string > string_split(std::string s, const std::string& delimiters)
+{
+   std::vector< std::string > substrings;
 
-   auto sub_begin = s.begin();   // iterator to the begin and end of a substring
-   auto sub_end = sub_begin;
+   auto sub_begin = s.begin(); // iterator to the begin and end of a substring
+   auto sub_end   = sub_begin;
 
-   for (auto it = s.begin(); it != s.end(); ++it) {
-      for (auto d : delimiters) {
-         if (*it == d) {   // current character in s is a delimiter
+   for (auto it = s.begin(); it != s.end(); ++it)
+   {
+      for (auto d : delimiters)
+      {
+         if (*it == d)
+         { // current character in s is a delimiter
             sub_end = it;
-            if (sub_begin < sub_end) { // make sure that the substring is not empty
+            if (sub_begin < sub_end)
+            { // make sure that the substring is not empty
                substrings.emplace_back(sub_begin, sub_end);
             }
             sub_begin = ++sub_end;
@@ -113,38 +133,73 @@ inline std::vector<std::string> string_split(std::string s, const std::string &d
    }
 
    // add substring from last delimiter to the end of s
-   if (sub_begin < s.end()) {
-      substrings.emplace_back(sub_begin, s.end());
-   }
+   if (sub_begin < s.end()) { substrings.emplace_back(sub_begin, s.end()); }
 
    return substrings;
 }
 
 // Replace (in place) all occurrences of substring "old" with substring "new".
-inline void string_replace_all(std::string &s, const std::string &oldSubstr, const std::string &newSubstr) {
+inline void string_replace_all(std::string& s, const std::string& oldSubstr, const std::string& newSubstr)
+{
    // loop written to avoid infinite-loops when newSubstr contains oldSubstr
-   for (size_t pos = s.find(oldSubstr); pos != std::string::npos;) {
+   for (size_t pos = s.find(oldSubstr); pos != std::string::npos;)
+   {
       s.replace(pos, oldSubstr.length(), newSubstr);
       pos = s.find(oldSubstr, pos + newSubstr.length());
    }
 }
 
 // Replace (copy) all occurrences of substring "old" with substring "new".
-inline std::string string_replace_all_copy(const std::string &s, const std::string &oldSubstr, const std::string &newSubstr) {
+inline std::string string_replace_all_copy(const std::string& s, const std::string& oldSubstr,
+                                           const std::string& newSubstr)
+{
    std::string result = s;
    string_replace_all(result, oldSubstr, newSubstr);
    return result;
 }
 
 // Check whether a string ends with a certain substring.
-inline bool string_ends_with(const std::string &s, const std::string &substr) {
+inline bool string_ends_with(const std::string& s, const std::string& substr)
+{
    return s.rfind(substr) == (s.length() - substr.length());
 }
 
 // Case-insensitive wrapper for std::string::compare (return values as for std::string::compare).
-inline int string_icompare(const std::string &s1, const std::string &s2) {
+inline int string_icompare(const std::string& s1, const std::string& s2)
+{
    // function std::string::compare returns 0 in case of equality => invert result to obtain expected bool behavior
    return string_to_lower_copy(s1).compare(string_to_lower_copy(s2));
 }
 
+// Convert a floating point number to string with a specified number of decimal places.
+template< typename T >
+inline std::string to_string_with_precision(T number, uint_t decimalPlaces)
+{
+   return std::to_string(number).substr(0, std::to_string(number).find(".") + decimalPlaces + 1);
+}
+
+// Convert a floating point number to string with only the relevant number of decimal places, i.e., zeros at the end are
+// cut off.
+template< typename T >
+inline std::string to_string_only_relevant_digits(T number)
+{
+   if (std::numeric_limits< T >::is_integer) { return std::to_string(number); }
+
+   // get integer part of number
+   std::string integerPart = std::to_string(number).substr(0, std::to_string(number).find(".") + 1);
+
+   // get fractional part of number
+   std::string fractionalPart = std::to_string(number).substr(std::to_string(number).find(".") + 1);
+
+   // remove any 0 at the end of the number's fractional part
+   fractionalPart = fractionalPart.substr(0, fractionalPart.find_last_not_of('0') + 1);
+
+   // remove "." of integerPart if fractionalPart is empty
+   if (fractionalPart.empty()) {
+      integerPart.pop_back();
+   }
+
+   return integerPart + fractionalPart;
+}
+
 } // namespace walberla
diff --git a/src/core/cell/Cell.h b/src/core/cell/Cell.h
index 63a650dbd..8f41297b7 100644
--- a/src/core/cell/Cell.h
+++ b/src/core/cell/Cell.h
@@ -1,15 +1,15 @@
 //======================================================================================================================
 //
-//  This file is part of waLBerla. waLBerla is free software: you can 
+//  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 
+//  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 
+//
+//  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/>.
 //
@@ -24,6 +24,7 @@
 
 #include "core/DataTypes.h"
 #include "core/debug/Debug.h"
+#include "core/math/Vector3.h"
 #include "core/mpi/BufferSizeTrait.h"
 #include "core/mpi/RecvBuffer.h"
 #include "core/mpi/SendBuffer.h"
@@ -51,6 +52,7 @@ public:
    inline Cell( const cell_idx_t _x, const cell_idx_t _y, const cell_idx_t _z ) { cell[0] = _x; cell[1] = _y; cell[2] = _z; }
  //inline Cell( const int        _x, const int        _y, const int        _z );
    inline Cell( const uint_t     _x, const uint_t     _y, const uint_t     _z );
+   inline Cell( const Vector3<cell_idx_t>& vec ){ cell[0] = vec[0]; cell[1] = vec[1]; cell[2] = vec[2]; };
    //@}
 
    /*! \name Arithmetic operators */
diff --git a/src/core/stringToNum.h b/src/core/stringToNum.h
index d031b53d3..10c897956 100644
--- a/src/core/stringToNum.h
+++ b/src/core/stringToNum.h
@@ -1,15 +1,15 @@
 //======================================================================================================================
 //
-//  This file is part of waLBerla. waLBerla is free software: you can 
+//  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 
+//  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 
+//
+//  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/>.
 //
diff --git a/src/lbm/CMakeLists.txt b/src/lbm/CMakeLists.txt
index 511aafa84..829e0505b 100644
--- a/src/lbm/CMakeLists.txt
+++ b/src/lbm/CMakeLists.txt
@@ -1,6 +1,6 @@
 ###################################################################################################
 #
-# Module lbm 
+# Module lbm
 #
 ###################################################################################################
 
@@ -33,6 +33,7 @@ add_subdirectory( blockforest )
 add_subdirectory( sweeps )
 add_subdirectory( communication )
 add_subdirectory( field )
+add_subdirectory( free_surface )
 add_subdirectory( refinement )
 add_subdirectory( gui )
 add_subdirectory( boundary )
@@ -47,4 +48,3 @@ add_subdirectory( inplace_streaming )
 
 ###################################################################################################
 
- 
\ No newline at end of file
diff --git a/src/lbm/blockforest/communication/CMakeLists.txt b/src/lbm/blockforest/communication/CMakeLists.txt
new file mode 100644
index 000000000..a39e2f187
--- /dev/null
+++ b/src/lbm/blockforest/communication/CMakeLists.txt
@@ -0,0 +1,5 @@
+target_sources( lbm
+        PRIVATE
+        SimpleCommunication.h
+        UpdateSecondGhostLayer.h
+        )
diff --git a/src/lbm/blockforest/communication/SimpleCommunication.h b/src/lbm/blockforest/communication/SimpleCommunication.h
new file mode 100644
index 000000000..42bffe263
--- /dev/null
+++ b/src/lbm/blockforest/communication/SimpleCommunication.h
@@ -0,0 +1,170 @@
+//======================================================================================================================
+//
+//  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"
+
+#include "lbm/communication/PdfFieldPackInfo.h"
+
+namespace walberla
+{
+namespace blockforest
+{
+using communication::UniformBufferedScheme;
+
+template< typename Stencil_T >
+class SimpleCommunication : public communication::UniformBufferedScheme< Stencil_T >
+{
+   using RealScalarField_T = GhostLayerField< real_t, 1 >;
+   using VectorField_T     = GhostLayerField< Vector3< real_t >, 1 >;
+   using PdfField_T        = GhostLayerField< real_t, Stencil_T::Size >;
+   using UintScalarField_T = GhostLayerField< uint_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());
+
+      using field::communication::PackInfo;
+
+      if (firstBlock.isDataClassOrSubclassOf< PdfField_T >(fieldId))
+      {
+         this->addPackInfo(make_shared< PackInfo< PdfField_T > >(fieldId));
+      }
+      else
+      {
+         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< UintScalarField_T >(fieldId))
+                        {
+                           this->addPackInfo(make_shared< PackInfo< UintScalarField_T > >(fieldId));
+                        }
+                        else { WALBERLA_ABORT("Problem with UID"); }
+                     }
+                  }
+               }
+            }
+         }
+      }
+
+      return *this;
+   }
+
+ protected:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+}; // class SimpleCommunication
+
+} // namespace blockforest
+} // namespace walberla
diff --git a/src/lbm/blockforest/communication/UpdateSecondGhostLayer.h b/src/lbm/blockforest/communication/UpdateSecondGhostLayer.h
new file mode 100644
index 000000000..1e9c69ed1
--- /dev/null
+++ b/src/lbm/blockforest/communication/UpdateSecondGhostLayer.h
@@ -0,0 +1,144 @@
+//======================================================================================================================
+//
+//  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
+{
+namespace blockforest
+{
+/********************************************************************************************************************
+ * 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 blockforest
+} // namespace walberla
diff --git a/src/lbm/boundary/SimpleExtrapolationOutflow.h b/src/lbm/boundary/SimpleExtrapolationOutflow.h
new file mode 100644
index 000000000..46c43aa70
--- /dev/null
+++ b/src/lbm/boundary/SimpleExtrapolationOutflow.h
@@ -0,0 +1,114 @@
+//======================================================================================================================
+//
+//  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 SimpleExtrapolationOutflow.h
+//! \ingroup lbm
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Outflow boundary condition based on extrapolation. See equation F.1 in Geier et al., 2005,
+//!        doi: 10.1016/j.camwa.2015.05.001
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "boundary/Boundary.h"
+
+#include "core/DataTypes.h"
+#include "core/cell/CellInterval.h"
+#include "core/config/Config.h"
+#include "core/debug/Debug.h"
+
+#include "lbm/field/PdfField.h"
+
+#include "stencil/Directions.h"
+
+#include <vector>
+
+namespace walberla
+{
+namespace lbm
+{
+template< typename LatticeModel_T, typename FlagField_T >
+class SimpleExtrapolationOutflow : public Boundary< typename FlagField_T::flag_t >
+{
+ protected:
+   using PDFField = PdfField< LatticeModel_T >;
+   using Stencil  = typename LatticeModel_T::Stencil;
+   using flag_t   = typename FlagField_T::flag_t;
+
+ public:
+   static const bool threadsafe = true;
+
+   static shared_ptr< BoundaryConfiguration > createConfiguration(const Config::BlockHandle& /*config*/)
+   {
+      return make_shared< BoundaryConfiguration >();
+   }
+
+   SimpleExtrapolationOutflow(const BoundaryUID& boundaryUID, const FlagUID& uid, PDFField* const pdfField)
+      : Boundary< flag_t >(boundaryUID), uid_(uid), pdfField_(pdfField)
+   {
+      WALBERLA_ASSERT_NOT_NULLPTR(pdfField_);
+   }
+
+   void pushFlags(std::vector< FlagUID >& uids) const { uids.push_back(uid_); }
+
+   void beforeBoundaryTreatment() const {}
+   void afterBoundaryTreatment() const {}
+
+   template< typename Buffer_T >
+   void packCell(Buffer_T&, const cell_idx_t, const cell_idx_t, const cell_idx_t) const
+   {}
+
+   template< typename Buffer_T >
+   void registerCell(Buffer_T&, const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t)
+   {}
+   void registerCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t, const BoundaryConfiguration&)
+   {}
+
+   void registerCells(const flag_t, const CellInterval&, const BoundaryConfiguration&) {}
+   template< typename CellIterator >
+   void registerCells(const flag_t, const CellIterator&, const CellIterator&, const BoundaryConfiguration&)
+   {}
+
+   void unregisterCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t) const {}
+
+#ifndef NDEBUG
+   inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir,
+                              const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask)
+#else
+   inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir,
+                              const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t /*mask*/)
+#endif
+   {
+      WALBERLA_ASSERT_EQUAL(nx, x + cell_idx_c(stencil::cx[dir]));
+      WALBERLA_ASSERT_EQUAL(ny, y + cell_idx_c(stencil::cy[dir]));
+      WALBERLA_ASSERT_EQUAL(nz, z + cell_idx_c(stencil::cz[dir]));
+      WALBERLA_ASSERT_UNEQUAL(mask & this->mask_, numeric_cast< flag_t >(0));
+
+      // only true if "this->mask_" only contains one single flag, which is the case for the current implementation of
+      // this boundary condition (Outlet)
+      WALBERLA_ASSERT_EQUAL(mask & this->mask_, this->mask_);
+
+      // equation F.1 in Geier et al. 2015
+      pdfField_->get(nx, ny, nz, Stencil::invDirIdx(dir)) = pdfField_->get(x, y, z, Stencil::invDirIdx(dir));
+   }
+
+ private:
+   const FlagUID uid_;
+   PDFField* const pdfField_;
+}; // class Outlet
+
+} // namespace lbm
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/boundary/all.h b/src/lbm/boundary/all.h
index 31709c60e..11f7698b3 100644
--- a/src/lbm/boundary/all.h
+++ b/src/lbm/boundary/all.h
@@ -34,6 +34,7 @@
 #include "ParserUBB.h"
 #include "Pressure.h"
 #include "SimpleDiffusionDirichlet.h"
+#include "SimpleExtrapolationOutflow.h"
 #include "SimplePAB.h"
 #include "SimplePressure.h"
 #include "SimpleUBB.h"
diff --git a/src/lbm/free_surface/BlockStateDetectorSweep.h b/src/lbm/free_surface/BlockStateDetectorSweep.h
new file mode 100644
index 000000000..f2ef50bd1
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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/free_surface/CMakeLists.txt b/src/lbm/free_surface/CMakeLists.txt
new file mode 100644
index 000000000..54b500251
--- /dev/null
+++ b/src/lbm/free_surface/CMakeLists.txt
@@ -0,0 +1,19 @@
+target_sources( lbm
+        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( boundary )
+add_subdirectory( bubble_model )
+add_subdirectory( dynamics )
+add_subdirectory( surface_geometry )
diff --git a/src/lbm/free_surface/FlagDefinitions.h b/src/lbm/free_surface/FlagDefinitions.h
new file mode 100644
index 000000000..4dd93303a
--- /dev/null
+++ b/src/lbm/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
+{
+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/free_surface/FlagInfo.h b/src/lbm/free_surface/FlagInfo.h
new file mode 100644
index 000000000..e332cf8dc
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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/free_surface/FlagInfo.impl.h b/src/lbm/free_surface/FlagInfo.impl.h
new file mode 100644
index 000000000..574001ea4
--- /dev/null
+++ b/src/lbm/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
+{
+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/free_surface/InitFunctions.h b/src/lbm/free_surface/InitFunctions.h
new file mode 100644
index 000000000..b98f55d54
--- /dev/null
+++ b/src/lbm/free_surface/InitFunctions.h
@@ -0,0 +1,229 @@
+//======================================================================================================================
+//
+//  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
+{
+/***********************************************************************************************************************
+ * 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 (*fillFieldIt <= real_c(0))
+            {
+               // 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 PdfField_T >
+void initHydrostaticPressure(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                             const BlockDataID& pdfFieldID, 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)
+   {
+      PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID);
+
+      CellInterval local = pdfField->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
+         const real_t rho =
+            real_c(1) + real_c(3) * forceComponent * (real_c(coordinate) + real_c(0.5) - std::ceil(fluidHeight));
+
+         const Vector3< real_t > velocity = pdfField->getVelocity(*cellIt);
+
+         pdfField->setDensityAndVelocity(*cellIt, velocity, rho);
+      }
+   }
+}
+
+/***********************************************************************************************************************
+ * Set density in non-liquid and non-interface cells to 1.
+ **********************************************************************************************************************/
+template< typename FlagField_T, typename PdfField_T >
+void setDensityInNonFluidCellsToOne(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
+                                    const FlagInfo< FlagField_T >& flagInfo, const ConstBlockDataID& flagFieldID,
+                                    const BlockDataID& pdfFieldID)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      PdfField_T* const pdfField         = blockIt->getData< PdfField_T >(pdfFieldID);
+      const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+         if (!flagInfo.isLiquid(*flagFieldIt) && !flagInfo.isInterface(*flagFieldIt))
+         {
+            // set density in gas cells to 1
+            pdfField->setDensityAndVelocity(pdfFieldIt.cell(), Vector3< real_t >(real_c(0)), real_c(1));
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+}
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/InterfaceFromFillLevel.h b/src/lbm/free_surface/InterfaceFromFillLevel.h
new file mode 100644
index 000000000..ef113e327
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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 (fillLevel <= real_c(0.0)) { 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/free_surface/LoadBalancing.h b/src/lbm/free_surface/LoadBalancing.h
new file mode 100644
index 000000000..c9df54478
--- /dev/null
+++ b/src/lbm/free_surface/LoadBalancing.h
@@ -0,0 +1,345 @@
+//======================================================================================================================
+//
+//  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/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/blockforest/communication/SimpleCommunication.h"
+#include "lbm/free_surface/BlockStateDetectorSweep.h"
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+#include <algorithm>
+#include <numeric>
+
+namespace walberla
+{
+namespace free_surface
+{
+
+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 LatticeModelStencil_T >
+class LoadBalancer
+{
+ public:
+   LoadBalancer(const std::shared_ptr< StructuredBlockForest >& blockForestPtr,
+                const blockforest::SimpleCommunication< CommunicationStencil_T >& communication,
+                const blockforest::SimpleCommunication< LatticeModelStencil_T >& pdfCommunication,
+                const std::shared_ptr< bubble_model::BubbleModelBase >& bubbleModel, uint_t blockWeightFullFreeSurface,
+                uint_t blockWeightOnlyLBM, uint_t blockWeightOnlyGasAndBoundary, uint_t frequency,
+                bool printStatistics = false)
+      : blockForest_(blockForestPtr), communication_(communication), pdfCommunication_(pdfCommunication),
+        bubbleModel_(bubbleModel), 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_();
+            bubbleModel_->update();
+
+            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_;
+   blockforest::SimpleCommunication< CommunicationStencil_T > communication_;
+   blockforest::SimpleCommunication< LatticeModelStencil_T > pdfCommunication_;
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel_;
+
+   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/free_surface/MaxVelocityComputer.h b/src/lbm/free_surface/MaxVelocityComputer.h
new file mode 100644
index 000000000..a3e2b0cb2
--- /dev/null
+++ b/src/lbm/free_surface/MaxVelocityComputer.h
@@ -0,0 +1,110 @@
+//======================================================================================================================
+//
+//  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 in the system.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+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();
+
+      Vector3< real_t > maxVelocity = Vector3< real_t >(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:maxVelocity[0]) reduction(max:maxVelocity[1]) reduction(max:maxVelocity[2]),
+            {
+            if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt))
+            {
+               const Vector3< real_t > velocity = pdfField->getVelocity(pdfFieldIt.cell());
+
+               if (velocity[0] > maxVelocity[0]) { maxVelocity[0] = velocity[0]; }
+               if (velocity[1] > maxVelocity[1]) { maxVelocity[1] = velocity[1]; }
+               if (velocity[2] > maxVelocity[2]) { maxVelocity[2] = velocity[2]; }
+            }
+            }) // WALBERLA_FOR_ALL_CELLS_OMP
+      }
+
+      mpi::allReduceInplace< real_t >(maxVelocity[0], mpi::MAX);
+      mpi::allReduceInplace< real_t >(maxVelocity[1], mpi::MAX);
+      mpi::allReduceInplace< real_t >(maxVelocity[2], 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/free_surface/SurfaceMeshWriter.h b/src/lbm/free_surface/SurfaceMeshWriter.h
new file mode 100644
index 000000000..334f9d714
--- /dev/null
+++ b/src/lbm/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
+{
+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/free_surface/TotalMassComputer.h b/src/lbm/free_surface/TotalMassComputer.h
new file mode 100644
index 000000000..192ec8755
--- /dev/null
+++ b/src/lbm/free_surface/TotalMassComputer.h
@@ -0,0 +1,144 @@
+//======================================================================================================================
+//
+//  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/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"
+
+namespace walberla
+{
+namespace free_surface
+{
+template< typename FreeSurfaceBoundaryHandling_T, typename PdfField_T, typename FlagField_T, typename ScalarField_T >
+class TotalMassComputer
+{
+ public:
+   TotalMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                     const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                     const ConstBlockDataID& pdfFieldID, const ConstBlockDataID& fillFieldID, uint_t frequency,
+                     const std::shared_ptr< real_t >& totalMass)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID),
+        fillFieldID_(fillFieldID), totalMass_(totalMass), frequency_(frequency), executionCounter_(uint_c(0))
+   {}
+
+   TotalMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                     const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                     const ConstBlockDataID& pdfFieldID, const ConstBlockDataID& fillFieldID,
+                     const ConstBlockDataID& excessMassFieldID, uint_t frequency,
+                     const std::shared_ptr< real_t >& totalMass)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID),
+        fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass), 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)) { computeMass(blockForest, freeSurfaceBoundaryHandling); }
+      else
+      {
+         // only evaluate in given frequencies
+         if (executionCounter_ % frequency_ == uint_c(0)) { computeMass(blockForest, freeSurfaceBoundaryHandling); }
+      }
+
+      ++executionCounter_;
+   }
+
+   void computeMass(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 mass = 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))
+               {
+                  const real_t density = pdfField->getDensity(pdfFieldIt.cell());
+                  mass += *fillFieldIt * density + *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))
+               {
+                  const real_t density = pdfField->getDensity(pdfFieldIt.cell());
+                  mass += *fillFieldIt * density;
+               }
+            }) // WALBERLA_FOR_ALL_CELLS_OMP
+         }
+      }
+
+      mpi::allReduceInplace< real_t >(mass, mpi::SUM);
+
+      *totalMass_ = mass;
+   };
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+
+   const ConstBlockDataID pdfFieldID_;
+   const ConstBlockDataID fillFieldID_;
+   const ConstBlockDataID excessMassFieldID_ = ConstBlockDataID();
+
+   std::shared_ptr< real_t > totalMass_;
+
+   uint_t frequency_;
+   uint_t executionCounter_;
+}; // class TotalMassComputer
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/VtkWriter.h b/src/lbm/free_surface/VtkWriter.h
new file mode 100644
index 000000000..36f056740
--- /dev/null
+++ b/src/lbm/free_surface/VtkWriter.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 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 "lbm/field/Adaptors.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "vtk/Initialization.h"
+
+#include "FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * Add VTK output to time loop that includes all relevant free surface information. It must be configured via
+ * config-file.
+ **********************************************************************************************************************/
+template< typename LatticeModel_T, typename FreeSurfaceBoundaryHandling_T, typename PdfField_T, typename FlagField_T,
+          typename ScalarField_T, typename VectorField_T >
+void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, SweepTimeloop& timeloop,
+                  const std::weak_ptr< Config >& configPtr,
+                  const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo, const BlockDataID& pdfFieldID,
+                  const BlockDataID& flagFieldID, const BlockDataID& fillFieldID, const BlockDataID& forceFieldID,
+                  const BlockDataID& curvatureFieldID, const BlockDataID& normalFieldID,
+                  const BlockDataID& obstacleNormalFieldID)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   const auto config = configPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(config);
+
+   // add various adaptors (for simplified access to macroscopic quantities)
+   const BlockDataID densityAdaptorID = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::Density >(
+      blockForest, pdfFieldID, "DensityAdaptor");
+   const BlockDataID velocityAdaptorID =
+      field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::VelocityVector >(blockForest, pdfFieldID,
+                                                                                        "VelocityVectorAdaptor");
+   // 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;
+
+      // add fields to VTK output
+      writers.push_back(std::make_shared< VTKWriter< typename lbm::Adaptor< LatticeModel_T >::VelocityVector > >(
+         velocityAdaptorID, "velocity"));
+      writers.push_back(std::make_shared< VTKWriter< typename lbm::Adaptor< LatticeModel_T >::Density > >(
+         densityAdaptorID, "density"));
+      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"));
+      writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(forceFieldID, "force"));
+
+      // map flagIDs to integer values
+      const auto flagMapper =
+         std::make_shared< field::FlagFieldMapping< FlagField_T, walberla::uint_t > >(flagFieldID, "mapped_flag");
+      flagMapper->addMapping(flagIDs::liquidFlagID, uint_c(1));
+      flagMapper->addMapping(flagIDs::interfaceFlagID, uint_c(2));
+      flagMapper->addMapping(flagIDs::gasFlagID, uint_c(3));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, uint_c(4));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, uint_c(6));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, uint_c(6));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, uint_c(7));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, uint_c(8));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, uint_c(9));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, uint_c(10));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::simpleExtrapolationOutflowFlagID, uint_c(11));
+
+      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));
+
+      beforeFuncs["ghost_layer_synchronization"] = preVTKComm;
+   };
+
+   // 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);
+   }
+
+   // only enable the zerosetter (see below) if the non-liquid and non-interface cells are not excluded anyway
+   bool enableZeroSetter            = true;
+   const auto vtkConfigBlock        = config->getOneBlock("VTK");
+   const auto fluidFieldConfigBlock = vtkConfigBlock.getBlock("fluid_field");
+   if (fluidFieldConfigBlock)
+   {
+      auto inclusionFiltersConfigBlock = fluidFieldConfigBlock.getBlock("inclusion_filters");
+
+      if (inclusionFiltersConfigBlock.isDefined("liquidInterfaceFilter"))
+      {
+         // liquidInterfaceFilter is defined which limits VTK-output to only liquid and interface cells
+         enableZeroSetter = false;
+      }
+   }
+
+   // 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
+   if (enableZeroSetter)
+   {
+      const auto function = [&](IBlock* block) {
+         using namespace free_surface;
+         PdfField_T* const pdfField         = block->getData< PdfField_T >(pdfFieldID);
+         const FlagField_T* const flagField = block->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 >(0), real_c(1.0));
+            }
+         }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+      };
+      timeloop.add() << Sweep(function, "VTK: zero-setting");
+   }
+}
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/boundary/CMakeLists.txt b/src/lbm/free_surface/boundary/CMakeLists.txt
new file mode 100644
index 000000000..9b3f1195a
--- /dev/null
+++ b/src/lbm/free_surface/boundary/CMakeLists.txt
@@ -0,0 +1,6 @@
+target_sources( lbm
+        PRIVATE
+        FreeSurfaceBoundaryHandling.h
+        FreeSurfaceBoundaryHandling.impl.h
+        SimplePressureWithFreeSurface.h
+        )
diff --git a/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h
new file mode 100644
index 000000000..f21e971ce
--- /dev/null
+++ b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h
@@ -0,0 +1,188 @@
+//======================================================================================================================
+//
+//  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 Boundary.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Boundary handling for the free surface LBM module.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "boundary/BoundaryHandling.h"
+
+#include "field/GhostLayerField.h"
+
+#include "geometry/initializer/InitializationManager.h"
+#include "geometry/initializer/OverlapFieldFromBody.h"
+
+#include "lbm/boundary/all.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/InitFunctions.h"
+#include "lbm/free_surface/InterfaceFromFillLevel.h"
+#include "lbm/free_surface/boundary/SimplePressureWithFreeSurface.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * Boundary handling for the free surface LBM extension.
+ **********************************************************************************************************************/
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+class FreeSurfaceBoundaryHandling
+{
+ public:
+   using flag_t    = typename FlagField_T::value_type;
+   using Stencil_T = typename LatticeModel_T::Stencil;
+   using CommunicationStencil_T =
+      typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+   // boundary
+   using NoSlip_T                     = lbm::NoSlip< LatticeModel_T, flag_t >;
+   using FreeSlip_T                   = lbm::FreeSlip< LatticeModel_T, FlagField_T >;
+   using UBB_T                        = lbm::UBB< LatticeModel_T, flag_t >;
+   using Pressure_T                   = SimplePressureWithFreeSurface< LatticeModel_T, FlagField_T >;
+   using Outlet_T                     = lbm::Outlet< LatticeModel_T, FlagField_T, 4, 3 >;
+   using SimpleExtrapolationOutflow_T = lbm::SimpleExtrapolationOutflow< LatticeModel_T, FlagField_T >;
+   using UBB_Inflow_T =
+      lbm::UBB< LatticeModel_T, flag_t >; // creates interface cells in the direction of the prescribed velocity, i.e.,
+                                          // represents an inflow boundary condition
+
+   // handling type
+   using BoundaryHandling_T =
+      BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, UBB_T, UBB_Inflow_T, Pressure_T, Pressure_T, Outlet_T,
+                        SimpleExtrapolationOutflow_T,
+                        FreeSlip_T >; // 2 pressure boundaries with different densities, e.g., inflow-outflow
+
+   using FlagInfo_T = FlagInfo< FlagField_T >;
+
+   // constructor
+   FreeSurfaceBoundaryHandling(const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID,
+                               BlockDataID fillLevelID);
+
+   // initialize fluid field from config file using (quotes indicate the string to be used in the file):
+   // - "CellInterval" (see src/geometry/initializer/BoundaryFromCellInterval.h)
+   // - "Border" (see src/geometry/initializer/BoundaryFromDomainBorder.h)
+   // - "Image" (see src/geometry/initializer/BoundaryFromImage.h)
+   // - "Body" (see src/geometry/initializer/OverlapFieldFromBody.h)
+   inline void initFromConfig(const Config::BlockHandle& block);
+
+   // initialize free surface object from geometric body (see src/geometry/initializer/OverlapFieldFromBody.h)
+   template< typename Body_T >
+   inline void addFreeSurfaceObject(const Body_T& body, bool addOrSubtract = false);
+
+   // clear and initialize flags in every cell according to the fill level and initialize fill level in boundaries (with
+   // value 1) and obstacles such that the bubble model does not detect obstacles as gas cells
+   void initFlagsFromFillLevel();
+
+   inline void setNoSlipAtBorder(stencil::Direction d, cell_idx_t wallDistance = cell_idx_c(0));
+   inline void setNoSlipAtAllBorders(cell_idx_t wallDistance = cell_idx_c(0));
+   void setNoSlipInCell(const Cell& globalCell);
+
+   inline void setFreeSlipAtBorder(stencil::Direction d, cell_idx_t wallDistance = cell_idx_c(0));
+   inline void setFreeSlipAtAllBorders(cell_idx_t wallDistance = cell_idx_c(0));
+   void setFreeSlipInCell(const Cell& globalCell);
+
+   void setUBBInCell(const Cell& globalCell, const Vector3< real_t >& velocity);
+
+   // UBB that generates interface cells to resemble an inflow boundary
+   void setInflowInCell(const Cell& globalCell, const Vector3< real_t >& velocity);
+
+   inline void setPressure(real_t density);
+   void setPressureOutflow(real_t density);
+   void setBodyForce(const Vector3< real_t >& bodyForce);
+
+   void enableBubbleOutflow(BubbleModelBase* bubbleModel);
+
+   // checks if an obstacle cell is located in an outermost ghost layer (corners are explicitly ignored, as they do not
+   // influence periodic communication)
+   Vector3< bool > isObstacleInGlobalGhostLayer();
+
+   // flag management
+   const FlagInfo< FlagField_T >& getFlagInfo() const { return flagInfo_; }
+
+   // flag IDs
+   static const field::FlagUID noSlipFlagID;
+   static const field::FlagUID ubbFlagID;
+   static const field::FlagUID ubbInflowFlagID;
+   static const field::FlagUID pressureFlagID;
+   static const field::FlagUID pressureOutflowFlagID;
+   static const field::FlagUID outletFlagID;
+   static const field::FlagUID simpleExtrapolationOutflowFlagID;
+   static const field::FlagUID freeSlipFlagID;
+
+   // boundary IDs
+   static const BoundaryUID noSlipBoundaryID;
+   static const BoundaryUID ubbBoundaryID;
+   static const BoundaryUID ubbInflowBoundaryID;
+   static const BoundaryUID pressureBoundaryID;
+   static const BoundaryUID pressureOutflowBoundaryID;
+   static const BoundaryUID outletBoundaryID;
+   static const BoundaryUID simpleExtrapolationOutflowBoundaryID;
+   static const BoundaryUID freeSlipBoundaryID;
+
+   inline BlockDataID getHandlingID() const { return handlingID_; }
+   inline BlockDataID getPdfFieldID() const { return pdfFieldID_; }
+   inline BlockDataID getFillFieldID() const { return fillFieldID_; }
+   inline BlockDataID getFlagFieldID() const { return flagFieldID_; }
+
+   // executes standard waLBerla boundary handling
+   class ExecuteBoundaryHandling
+   {
+    public:
+      ExecuteBoundaryHandling(const BlockDataID& collection) : handlingID_(collection) {}
+      void operator()(IBlock* const block) const
+      {
+         BoundaryHandling_T* const handling = block->getData< BoundaryHandling_T >(handlingID_);
+         // reset "near boundary" flags
+         handling->refresh();
+         (*handling)();
+      }
+
+    protected:
+      BlockDataID handlingID_;
+   }; // class ExecuteBoundaryHandling
+
+   ExecuteBoundaryHandling getBoundarySweep() const { return ExecuteBoundaryHandling(getHandlingID()); }
+
+ private:
+   FlagInfo< FlagField_T > flagInfo_;
+
+   // register standard waLBerla initializers
+   geometry::initializer::InitializationManager getInitManager();
+
+   std::shared_ptr< StructuredBlockForest > blockForest_;
+
+   BlockDataID flagFieldID_;
+   BlockDataID pdfFieldID_;
+   BlockDataID fillFieldID_;
+
+   BlockDataID handlingID_;
+
+   blockforest::communication::UniformBufferedScheme< CommunicationStencil_T > comm_;
+}; // class FreeSurfaceBoundaryHandling
+
+} // namespace free_surface
+} // namespace walberla
+
+#include "FreeSurfaceBoundaryHandling.impl.h"
diff --git a/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h
new file mode 100644
index 000000000..e5fde552f
--- /dev/null
+++ b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h
@@ -0,0 +1,554 @@
+//======================================================================================================================
+//
+//  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 FreeSurfaceBoundaryHandling.impl.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Boundary handling for the free surface LBM module.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/AddToStorage.h"
+#include "field/FlagField.h"
+#include "field/communication/PackInfo.h"
+
+#include "geometry/initializer/BoundaryFromCellInterval.h"
+#include "geometry/initializer/BoundaryFromDomainBorder.h"
+#include "geometry/initializer/BoundaryFromImage.h"
+#include "geometry/structured/GrayScaleImage.h"
+
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/InterfaceFromFillLevel.h"
+#include "lbm/lattice_model/CollisionModel.h"
+
+#include "FreeSurfaceBoundaryHandling.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace internal
+{
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+class BoundaryBlockDataHandling
+   : public domain_decomposition::BlockDataHandling<
+        typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::BoundaryHandling_T >
+{
+ public:
+   using BoundaryHandling_T =
+      typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T,
+                                            ScalarField_T >::BoundaryHandling_T; // handling as defined in
+                                                                                 // FreeSurfaceBoundaryHandling.h
+
+   BoundaryBlockDataHandling(const FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >* boundary)
+      : boundary_(boundary)
+   {}
+
+   // initialize standard waLBerla boundary handling
+   BoundaryHandling_T* initialize(IBlock* const block)
+   {
+      using B      = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+      using flag_t = typename B::flag_t;
+
+      // get fields
+      FlagField_T* const flagField           = block->getData< FlagField_T >(boundary_->getFlagFieldID());
+      typename B::PdfField_T* const pdfField = block->getData< typename B::PdfField_T >(boundary_->getPdfFieldID());
+
+      auto interfaceFlag = flag_t(flagField->getFlag(flagIDs::interfaceFlagID));
+      auto liquidFlag    = flag_t(flagField->getFlag(flagIDs::liquidFlagID));
+
+      // domainMask is used to identify liquid and interface cells
+      auto domainMask = flag_t(liquidFlag | interfaceFlag);
+      WALBERLA_ASSERT(domainMask != 0);
+
+      // initialize boundary conditions
+      typename B::UBB_T ubb(B::ubbBoundaryID, B::ubbFlagID, pdfField, flagField);
+      typename B::UBB_Inflow_T ubbInflow(B::ubbInflowBoundaryID, B::ubbInflowFlagID, pdfField, flagField);
+      typename B::NoSlip_T noslip(B::noSlipBoundaryID, B::noSlipFlagID, pdfField);
+      typename B::Pressure_T pressure(B::pressureBoundaryID, B::pressureFlagID, block, pdfField, flagField,
+                                      interfaceFlag, real_c(1.0));
+      typename B::Pressure_T pressureOutflow(B::pressureOutflowBoundaryID, B::pressureOutflowFlagID, block, pdfField,
+                                             flagField, interfaceFlag, real_c(1.0));
+      typename B::Outlet_T outlet(B::outletBoundaryID, B::outletFlagID, pdfField, flagField, domainMask);
+      typename B::SimpleExtrapolationOutflow_T simpleExtrapolationOutflow(
+         B::simpleExtrapolationOutflowBoundaryID, B::simpleExtrapolationOutflowFlagID, pdfField);
+      typename B::FreeSlip_T freeSlip(B::freeSlipBoundaryID, B::freeSlipFlagID, pdfField, flagField, domainMask);
+
+      return new BoundaryHandling_T("Boundary Handling", flagField, domainMask, noslip, ubb, ubbInflow, pressure,
+                                    pressureOutflow, outlet, simpleExtrapolationOutflow, freeSlip);
+   }
+
+   void serialize(IBlock* const block, const BlockDataID& id, mpi::SendBuffer& buffer)
+   {
+      BoundaryHandling_T* const boundaryHandlingPtr = block->getData< BoundaryHandling_T >(id);
+      CellInterval everyCell                        = boundaryHandlingPtr->getFlagField()->xyzSizeWithGhostLayer();
+      boundaryHandlingPtr->pack(buffer, everyCell, true);
+   }
+
+   BoundaryHandling_T* deserialize(IBlock* const block) { return initialize(block); }
+
+   void deserialize(IBlock* const block, const BlockDataID& id, mpi::RecvBuffer& buffer)
+   {
+      BoundaryHandling_T* const boundaryHandlingPtr = block->getData< BoundaryHandling_T >(id);
+      CellInterval everyCell                        = boundaryHandlingPtr->getFlagField()->xyzSizeWithGhostLayer();
+      boundaryHandlingPtr->unpack(buffer, everyCell, true);
+   }
+
+ private:
+   const FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >* boundary_;
+}; // class BoundaryBlockDataHandling
+
+// helper function wrapper for adding the flag field to the block storage; since the input parameter for an
+// initialization function in field::addFlagFieldToStorage() is a std::function<void(FlagField_T*,IBlock* const)>, we
+// need a function wrapper that has both these input parameters; as FlagInfo< FlagField_T >::registerFlags() does not
+// have both of these input parameters, a function wrapper with both input parameters is created and the second input
+// parameter is simply ignored inside the function wrapper
+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);
+}
+
+} // namespace internal
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FreeSurfaceBoundaryHandling(
+   const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID, BlockDataID fillLevelID)
+   : blockForest_(blockForest), pdfFieldID_(pdfFieldID), fillFieldID_(fillLevelID), comm_(blockForest)
+{
+   // initialize obstacleIDs
+   Set< FlagUID > obstacleIDs;
+   obstacleIDs += noSlipFlagID;
+   obstacleIDs += ubbFlagID;
+   obstacleIDs += ubbInflowFlagID;
+   obstacleIDs += pressureFlagID;
+   obstacleIDs += pressureOutflowFlagID;
+   obstacleIDs += freeSlipFlagID;
+   obstacleIDs += outletFlagID;
+   obstacleIDs += simpleExtrapolationOutflowFlagID;
+
+   // initialize outflowIDs
+   Set< FlagUID > outflowIDs;
+   outflowIDs += pressureOutflowFlagID;
+   outflowIDs += outletFlagID;
+   outflowIDs += simpleExtrapolationOutflowFlagID;
+
+   // initialize outflowIDs
+   Set< FlagUID > inflowIDs;
+   inflowIDs += ubbInflowFlagID;
+
+   // initialize freeSlipIDs
+   Set< FlagUID > freeSlipIDs;
+   freeSlipIDs += freeSlipFlagID;
+
+   // create callable function wrapper with input arguments 1 and 2 unset, whereas arguments 3, 4 and 5 are set to be
+   // obstacleIDs, outflowIDs, and inflowIDs, respectively; this is necessary for field::addFlagFieldToStorage()
+   auto ffInitFunc = std::bind(internal::flagFieldInitFunction< FlagField_T >, std::placeholders::_1,
+                               std::placeholders::_2, obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs);
+
+   // IMPORTANT REMARK: The flag field needs two ghost layers because of function advectMass(). There, the flags of all
+   // D3Q* neighbors are determined for each cell, including cells in the first ghost layer. Therefore, all D3Q*
+   // neighbors of the first ghost layer must be accessible. This requires a second ghost layer.
+   flagFieldID_ = field::addFlagFieldToStorage< FlagField_T >(blockForest, "Flags", uint_c(2), true, ffInitFunc);
+
+   // create FlagInfo
+   flagInfo_ = FlagInfo< FlagField_T >(obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs);
+   WALBERLA_ASSERT(flagInfo_.isConsistentAcrossBlocksAndProcesses(blockForest, flagFieldID_));
+
+   // add boundary handling to blockForest
+   handlingID_ = blockForest_->addBlockData(
+      std::make_shared< internal::BoundaryBlockDataHandling< LatticeModel_T, FlagField_T, ScalarField_T > >(this),
+      "Boundary Handling");
+
+   // create communication object with fill level field, since fill levels determine the flags during the simulation
+   comm_.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID_));
+}
+
+// define IDs (static const variables)
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::noSlipFlagID = field::FlagUID("NoSlip");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbFlagID = field::FlagUID("UBB");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowFlagID =
+   field::FlagUID("UBB_Inflow");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureFlagID =
+   field::FlagUID("Pressure");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureOutflowFlagID =
+   field::FlagUID("PressureOutflow");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::outletFlagID = field::FlagUID("Outlet");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::simpleExtrapolationOutflowFlagID =
+      field::FlagUID("SimpleExtrapolationOutflow");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::freeSlipFlagID =
+   field::FlagUID("FreeSlip");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::noSlipBoundaryID = BoundaryUID("NoSlip");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbBoundaryID = BoundaryUID("UBB");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowBoundaryID =
+   BoundaryUID("UBB_Inflow");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureBoundaryID =
+   BoundaryUID("Pressure");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureOutflowBoundaryID =
+   BoundaryUID("PressureOutflow");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::outletBoundaryID = BoundaryUID("Outlet");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::simpleExtrapolationOutflowBoundaryID =
+      BoundaryUID("SimpleExtrapolationOutflow");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::freeSlipBoundaryID =
+   BoundaryUID("FreeSlip");
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+geometry::initializer::InitializationManager
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::getInitManager()
+{
+   using namespace geometry::initializer;
+
+   InitializationManager initManager(blockForest_->getBlockStorage());
+
+   // define initializers
+   auto cellIntvInit = std::make_shared< BoundaryFromCellInterval< BoundaryHandling_T > >(*blockForest_, handlingID_);
+   auto borderInit   = std::make_shared< BoundaryFromDomainBorder< BoundaryHandling_T > >(*blockForest_, handlingID_);
+   auto imgInit =
+      std::make_shared< BoundaryFromImage< BoundaryHandling_T, geometry::GrayScaleImage > >(*blockForest_, handlingID_);
+   auto bodyInit = std::make_shared< OverlapFieldFromBody >(*blockForest_, fillFieldID_);
+
+   // register initializers
+   initManager.registerInitializer("CellInterval", cellIntvInit);
+   initManager.registerInitializer("Border", borderInit);
+   initManager.registerInitializer("Image", imgInit);
+   initManager.registerInitializer("Body", bodyInit);
+
+   return initManager;
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::initFromConfig(
+   const Config::BlockHandle& configBlock)
+{
+   // initialize from config file
+   getInitManager().init(configBlock);
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+template< typename Body_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::addFreeSurfaceObject(const Body_T& body,
+                                                                                                     bool addOrSubtract)
+{
+   geometry::initializer::OverlapFieldFromBody(*blockForest_, fillFieldID_).init(body, addOrSubtract);
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setNoSlipAtBorder(
+   stencil::Direction d, cell_idx_t wallDistance)
+{
+   geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_);
+   init.init(noSlipFlagID, d, wallDistance);
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setNoSlipAtAllBorders(
+   cell_idx_t wallDistance)
+{
+   geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_);
+   init.initAllBorders(noSlipFlagID, wallDistance);
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setNoSlipInCell(const Cell& globalCell)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+
+      // transform cell in global coordinates to cell in block local coordinates
+      Cell blockLocalCell;
+      blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell);
+
+      handling->forceBoundary(noSlipFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2]);
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setFreeSlipAtBorder(
+   stencil::Direction d, cell_idx_t wallDistance)
+{
+   geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_);
+   init.init(freeSlipFlagID, d, wallDistance);
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setFreeSlipAtAllBorders(
+   cell_idx_t wallDistance)
+{
+   geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_);
+   init.initAllBorders(freeSlipFlagID, wallDistance);
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setFreeSlipInCell(
+   const Cell& globalCell)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+
+      // transform cell in global coordinates to cell in block local coordinates
+      Cell blockLocalCell;
+      blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell);
+
+      handling->forceBoundary(freeSlipFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2]);
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setPressure(real_t density)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+      Pressure_T& pressure =
+         handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureFlagID));
+      pressure.setLatticeDensity(density);
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setUBBInCell(
+   const Cell& globalCell, const Vector3< real_t >& velocity)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+
+      typename UBB_Inflow_T::Velocity ubbVel(velocity);
+
+      // transform cell in global coordinates to cell in block-local coordinates
+      Cell blockLocalCell;
+      blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell);
+
+      // get block cell bounding box to check if cell is contained in block
+      CellInterval blockCellBB = blockForest_->getBlockCellBB(*blockIt);
+
+      // flag field has two ghost layers so blockCellBB is actually larger than returned; this is relevant for setups
+      // where the UBB is set in a ghost layer cell
+      blockCellBB.expand(cell_idx_c(2));
+
+      if (blockCellBB.contains(globalCell))
+      {
+         handling->forceBoundary(ubbFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2], ubbVel);
+      }
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setInflowInCell(
+   const Cell& globalCell, const Vector3< real_t >& velocity)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+
+      typename UBB_Inflow_T::Velocity ubbVel(velocity);
+
+      // transform cell in global coordinates to cell in block-local coordinates
+      Cell blockLocalCell;
+      blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell);
+
+      // get block cell bounding box to check if cell is contained in block
+      CellInterval blockCellBB = blockForest_->getBlockCellBB(*blockIt);
+
+      // flag field has two ghost layers so blockCellBB is actually larger than returned; this is relevant for setups
+      // where the UBB is set in a ghost layer cell
+      blockCellBB.expand(cell_idx_c(2));
+
+      if (blockCellBB.contains(globalCell))
+      {
+         handling->forceBoundary(ubbInflowFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2], ubbVel);
+      }
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setPressureOutflow(real_t density)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+      Pressure_T& pressure =
+         handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureOutflowFlagID));
+      pressure.setLatticeDensity(density);
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::enableBubbleOutflow(
+   BubbleModelBase* bubbleModel)
+{
+   for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt)
+   {
+      BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_);
+
+      // get pressure from boundary handling
+      Pressure_T& pressure =
+         handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureFlagID));
+      Pressure_T& pressureOutflow =
+         handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureOutflowFlagID));
+
+      // set pressure in bubble model
+      pressure.setBubbleModel(bubbleModel);
+      pressureOutflow.setBubbleModel(bubbleModel);
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+Vector3< bool >
+   FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::isObstacleInGlobalGhostLayer()
+{
+   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[0], mpi::LOGICAL_OR);
+   mpi::allReduceInplace(isObstacleInGlobalGhostLayer[1], mpi::LOGICAL_OR);
+   mpi::allReduceInplace(isObstacleInGlobalGhostLayer[2], mpi::LOGICAL_OR);
+
+   return isObstacleInGlobalGhostLayer;
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T >
+void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::initFlagsFromFillLevel()
+{
+   const Vector3< bool > isObstacleInGlobalGhostLayer = this->isObstacleInGlobalGhostLayer();
+
+   WALBERLA_ROOT_SECTION()
+   {
+      if ((blockForest_->isXPeriodic() && isObstacleInGlobalGhostLayer[0]) ||
+          (blockForest_->isYPeriodic() && isObstacleInGlobalGhostLayer[1]) ||
+          (blockForest_->isZPeriodic() && isObstacleInGlobalGhostLayer[2]))
+      {
+         WALBERLA_LOG_WARNING_ON_ROOT(
+            "WARNING: An obstacle cell is located in a global outermost ghost layer in a periodic "
+            "direction. Be aware that due to periodicity, this obstacle cell will be "
+            "overwritten during communication.");
+      }
+   }
+
+   // communicate fill level (neighborhood is used in initialization)
+   comm_();
+
+   // initialize fill level in boundaries (with value 1), i.e., obstacles such that the bubble model does not detect
+   // obstacles as gas cells
+   free_surface::initFillLevelsInBoundaries< BoundaryHandling_T, typename LatticeModel_T::Stencil, ScalarField_T >(
+      blockForest_, handlingID_, fillFieldID_);
+
+   // clear and initialize flags in every cell according to the fill level
+   free_surface::initFlagsFromFillLevels< BoundaryHandling_T, typename LatticeModel_T::Stencil, FlagField_T,
+                                          const ScalarField_T >(blockForest_, flagInfo_, handlingID_, fillFieldID_);
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h b/src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h
new file mode 100644
index 000000000..534d965f9
--- /dev/null
+++ b/src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h
@@ -0,0 +1,150 @@
+//======================================================================================================================
+//
+//  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 SimplePressureWithFreeSurface.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christian Godenschwager
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief SimplePressure boundary condition for the free surface LBM.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "boundary/Boundary.h"
+
+#include "core/DataTypes.h"
+#include "core/cell/CellInterval.h"
+#include "core/config/Config.h"
+#include "core/debug/Debug.h"
+#include "core/math/Vector3.h"
+
+#include "field/FlagField.h"
+
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+#include "lbm/lattice_model/EquilibriumDistribution.h"
+#include "lbm/lattice_model/ForceModel.h"
+
+#include "stencil/Directions.h"
+
+#include <vector>
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * SimplePressure boundary condition for the free surface LBM. The implementation is almost identical to the general
+ * lbm/boundary/SimplePressure.h boundary condition, however, the boundary pressure (density) is also set in the bubble
+ * model.
+ **********************************************************************************************************************/
+template< typename LatticeModel_T, typename FlagField_T >
+class SimplePressureWithFreeSurface : public boundary::Boundary< typename FlagField_T::flag_t >
+{
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+   using Stencil_T  = typename LatticeModel_T::Stencil;
+   using flag_t     = typename FlagField_T::flag_t;
+
+ public:
+   static const bool threadsafe = true;
+
+   static std::shared_ptr< BoundaryConfiguration > createConfiguration(const Config::BlockHandle& /*config*/)
+   {
+      return std::make_shared< BoundaryConfiguration >();
+   }
+
+   SimplePressureWithFreeSurface(const BoundaryUID& boundaryUID, const FlagUID& uid, IBlock* block,
+                                 PdfField_T* const pdfField, FlagField_T* flagField, flag_t interfaceFlag,
+                                 const real_t latticeDensity)
+      : Boundary< flag_t >(boundaryUID), uid_(uid), block_(block), pdfs_(pdfField), flagField_(flagField),
+        interfaceFlag_(interfaceFlag), bubbleModel_(nullptr), latticeDensity_(latticeDensity)
+   {
+      WALBERLA_ASSERT_NOT_NULLPTR(pdfs_);
+      WALBERLA_ASSERT_NOT_NULLPTR(flagField);
+   }
+
+   void pushFlags(std::vector< FlagUID >& uids) const { uids.push_back(uid_); }
+
+   void beforeBoundaryTreatment() const {}
+   void afterBoundaryTreatment() const {}
+
+   template< typename Buffer_T >
+   void packCell(Buffer_T&, const cell_idx_t, const cell_idx_t, const cell_idx_t) const
+   {}
+
+   template< typename Buffer_T >
+   void registerCell(Buffer_T&, const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t)
+   {}
+
+   void registerCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t, const BoundaryConfiguration&)
+   {}
+   void registerCells(const flag_t, const CellInterval&, const BoundaryConfiguration&) const {}
+   template< typename CellIterator >
+   void registerCells(const flag_t, const CellIterator&, const CellIterator&, const BoundaryConfiguration&) const
+   {}
+
+   void unregisterCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t) const {}
+
+   void setLatticeDensity(real_t newLatticeDensity) { latticeDensity_ = newLatticeDensity; }
+
+   void setBubbleModel(BubbleModelBase* bubbleModel) { bubbleModel_ = bubbleModel; }
+
+#ifndef NDEBUG
+   inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir,
+                              const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask)
+#else
+   inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir,
+                              const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t /*mask*/)
+#endif
+   {
+      WALBERLA_ASSERT_EQUAL(nx, x + cell_idx_c(stencil::cx[dir]));
+      WALBERLA_ASSERT_EQUAL(ny, y + cell_idx_c(stencil::cy[dir]));
+      WALBERLA_ASSERT_EQUAL(nz, z + cell_idx_c(stencil::cz[dir]));
+
+      WALBERLA_ASSERT_UNEQUAL((mask & this->mask_), numeric_cast< flag_t >(0));
+      WALBERLA_ASSERT_EQUAL((mask & this->mask_),
+                            this->mask_); // only true if "this->mask_" only contains one single flag, which is the case
+                                          // for the current implementation of this boundary condition (SimplePressure)
+      Vector3< real_t > u = pdfs_->getVelocity(x, y, z);
+
+      // set density in bubble model according to pressure boundary condition
+      if (bubbleModel_ && flagField_->isFlagSet(x, y, z, interfaceFlag_))
+      {
+         bubbleModel_->setDensity(block_, Cell(x, y, z), latticeDensity_);
+      }
+
+      // result will be streamed to (x,y,z, stencil::inverseDir[d]) during sweep
+      pdfs_->get(nx, ny, nz, Stencil_T::invDirIdx(dir)) =
+         -pdfs_->get(x, y, z, Stencil_T::idx[dir]) // anti-bounce-back
+         + real_c(2) * lbm::EquilibriumDistribution< LatticeModel_T >::getSymmetricPart(
+                          dir, u, latticeDensity_); // pressure term
+   }
+
+ protected:
+   FlagUID uid_;
+
+   IBlock* block_;
+   PdfField_T* pdfs_;
+   FlagField_T* flagField_;
+   flag_t interfaceFlag_;
+   BubbleModelBase* bubbleModel_;
+
+   real_t latticeDensity_;
+};
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/Bubble.h b/src/lbm/free_surface/bubble_model/Bubble.h
new file mode 100644
index 000000000..bb3b9a074
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/Bubble.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 Bubble.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Describes a bubble as gas volume via volume and density.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/debug/Debug.h"
+#include "core/mpi/MPIWrapper.h"
+#include "core/mpi/RecvBuffer.h"
+#include "core/mpi/SendBuffer.h"
+
+#include <iostream>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+// forward declarations of friend classes
+template< typename Stencil_T >
+class BubbleModel;
+
+class MergeInformation;
+
+/***********************************************************************************************************************
+ * Describes a bubble as gas volume via volume and density. A bubble can be located on multiple blocks, i.e., processes.
+ **********************************************************************************************************************/
+class Bubble
+{
+ public:
+   explicit Bubble(real_t initVolume)
+      : initVolume_(initVolume), currentVolume_(initVolume), volumeDiff_(real_c(0)), rho_(real_c(1.0))
+   {}
+
+   Bubble(real_t initVolume, real_t density)
+      : initVolume_(initVolume), currentVolume_(density * initVolume), volumeDiff_(real_c(0)), rho_(density)
+   {}
+
+   // dummy constructor with meaningless default values
+   Bubble() : initVolume_(real_c(-1.0)), currentVolume_(real_c(-1.0)), volumeDiff_(real_c(0)), rho_(real_c(-1.0)) {}
+
+   real_t getInitVolume() const { return initVolume_; }
+   real_t getCurrentVolume() const { return currentVolume_; }
+   real_t getDensity() const { return rho_; }
+
+   bool hasConstantDensity() const { return hasConstantDensity_; }
+
+   void setConstantDensity(real_t density = real_c(1.0))
+   {
+      hasConstantDensity_ = true;
+      rho_                = density;
+   }
+
+   // update the bubble volume change
+   void updateVolumeDiff(real_t diff) { volumeDiff_ += diff; }
+
+   void setDensity(real_t rho)
+   {
+      initVolume_ = rho * currentVolume_;
+      updateDensity();
+   }
+
+ private:
+   template< typename Stencil_T >
+   friend class BubbleModel;
+
+   friend class MergeInformation;
+
+   // merge bubbles by adding volumes, and update density (see dissertation of S. Bogner, 2017, section 4.3)
+   void merge(const Bubble& other)
+   {
+      initVolume_ += other.initVolume_;
+      currentVolume_ += other.currentVolume_;
+      updateDensity();
+   }
+
+   // update bubble volume and density using the change in the bubble's volume (see dissertation of S. Bogner, 2017,
+   // section 4.3)
+   void applyVolumeDiff(real_t diff)
+   {
+      WALBERLA_ASSERT(volumeDiff_ <= real_c(0.0));
+      currentVolume_ += diff;
+      updateDensity();
+   }
+
+   // return and reset the bubble's volume change
+   real_t getAndResetVolumeDiff()
+   {
+      real_t ret  = volumeDiff_;
+      volumeDiff_ = real_c(0);
+      return ret;
+   }
+
+   // update bubble density (see dissertation of S. Bogner, 2017, section 4.3)
+   void updateDensity()
+   {
+      if (hasConstantDensity_) return;
+      rho_ = initVolume_ / currentVolume_;
+   }
+
+   real_t initVolume_;
+   real_t currentVolume_;
+   real_t volumeDiff_; // bubble's volume change (caused by interface fill level or movement)
+   real_t rho_;
+
+   bool hasConstantDensity_{ false };
+
+   // function for packing a bubble into SendBuffer
+   friend mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const Bubble& b);
+
+   // function for unpacking a bubble from RecvBuffer
+   friend mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, Bubble& b);
+}; // class Bubble
+
+inline mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const Bubble& b)
+{
+   return buf << b.initVolume_ << b.currentVolume_;
+}
+
+inline mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, Bubble& b)
+{
+   buf >> b.initVolume_ >> b.currentVolume_;
+   b.updateDensity();
+   return buf;
+}
+
+inline std::ostream& operator<<(std::ostream& os, const Bubble& b)
+{
+   os << "Bubble (" << b.getInitVolume() << "," << b.getCurrentVolume() << "," << b.getDensity() << ")";
+
+   return os;
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+namespace walberla
+{
+namespace mpi
+{
+template<> // value type
+struct BufferSizeTrait< free_surface::bubble_model::Bubble >
+{
+   static const bool constantSize = true;
+   // 2 * real_t since the buffers above are filled with initVolume_ and currentVolume_
+   static const uint_t size = 2 * sizeof(real_t) + mpi::BUFFER_DEBUG_OVERHEAD;
+};
+} // namespace mpi
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/BubbleDefinitions.h b/src/lbm/free_surface/bubble_model/BubbleDefinitions.h
new file mode 100644
index 000000000..b67397be1
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleDefinitions.h
@@ -0,0 +1,42 @@
+//======================================================================================================================
+//
+//  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 BubbleDefinitions.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Type definitions for the bubble_model.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/GhostLayerField.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+using BubbleID                   = uint32_t;
+const uint32_t INVALID_BUBBLE_ID = uint32_t(-1);
+
+using BubbleField_T = GhostLayerField< BubbleID, 1 >;
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h b/src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h
new file mode 100644
index 000000000..8f6a24a68
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h
@@ -0,0 +1,76 @@
+//======================================================================================================================
+//
+//  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 BubbleDistanceAdaptor.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Get the distance to a certain bubble ID.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/adaptors/GhostLayerFieldAdaptor.h"
+
+#include "DisjoiningPressureBubbleModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Get the distance to a certain bubble ID. The search region is limited by maxDistance.
+ **********************************************************************************************************************/
+class BubbleDistanceAdaptionFunction
+{
+ public:
+   using basefield_t        = DisjoiningPressureBubbleModel::DistanceField;
+   using basefield_iterator = basefield_t::const_base_iterator;
+   using value_type         = real_t;
+
+   static const uint_t F_SIZE = 1u;
+
+   BubbleDistanceAdaptionFunction(BubbleID ownBubbleID, real_t maxDistance)
+      : bubbleID_(ownBubbleID), maxDistance_(maxDistance)
+   {
+      WALBERLA_ASSERT_GREATER(maxDistance_, real_c(1.0));
+   }
+
+   value_type operator()(const basefield_t& baseField, cell_idx_t x, cell_idx_t y, cell_idx_t z,
+                         cell_idx_t /*f*/ = 0) const
+   {
+      WALBERLA_ASSERT_GREATER(maxDistance_, real_c(1.0));
+      return baseField.get(x, y, z).getDistanceToNearestBubble(bubbleID_, maxDistance_);
+   }
+
+   value_type operator()(const basefield_iterator& it) const
+   {
+      WALBERLA_ASSERT_GREATER(maxDistance_, real_c(1.0));
+      return it->getDistanceToNearestBubble(bubbleID_, maxDistance_);
+   }
+
+ private:
+   BubbleID bubbleID_;
+   real_t maxDistance_;
+}; // class BubbleDistanceAdaptionFunction
+
+using BubbleDistanceAdaptor = field::GhostLayerFieldAdaptor< BubbleDistanceAdaptionFunction, 0 >;
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h b/src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h
new file mode 100644
index 000000000..05b0f0b23
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.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 BubbleIDFieldPackInfo.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Pack/unpack information for a field containing bubble IDs.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/communication/PackInfo.h"
+
+#include "stencil/D3Q27.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Pack/unpack information for a field containing bubble IDs.
+ *
+ * The bubble ID field requires a special pack info, since whenever the ghost layers are updated, the bubble model has
+ * to look for possible bubble merges.
+ *
+ * This could also be implemented with a regular FieldPackInfo and an immediate loop over the ghost layer only. However,
+ * it is more efficient by directly looking for bubble merges while unpacking the ghost layer, since the elements
+ * do not have to be loaded twice.
+ ***********************************************************************************************************************/
+template< typename Stencil_T >
+class BubbleIDFieldPackInfo : public field::communication::PackInfo< GhostLayerField< BubbleID, 1 > >
+{
+ public:
+   using CommunicationStencil_T =
+      typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+
+   using field_t = GhostLayerField< BubbleID, 1 >;
+
+   BubbleIDFieldPackInfo(const BlockDataID& bdId, MergeInformation* mergeInfo)
+      : field::communication::PackInfo< field_t >(bdId), mergeInfo_(mergeInfo)
+   {}
+
+   bool threadsafeReceiving() const override { return false; }
+
+   void unpackData(IBlock* receiver, stencil::Direction dir, mpi::RecvBuffer& buffer) override
+   {
+      field_t* const field = receiver->getData< field_t >(this->bdId_);
+      WALBERLA_ASSERT_NOT_NULLPTR(field);
+
+#ifndef NDEBUG
+      uint_t xSize;
+      uint_t ySize;
+      uint_t zSize;
+      buffer >> xSize >> ySize >> zSize;
+      WALBERLA_ASSERT_EQUAL(xSize, field->xSize());
+      WALBERLA_ASSERT_EQUAL(ySize, field->ySize());
+      WALBERLA_ASSERT_EQUAL(zSize, field->zSize());
+#endif
+
+      for (auto fieldIt = field->beginGhostLayerOnly(dir); fieldIt != field->end(); ++fieldIt)
+      {
+         // update ghost layer with received values
+         buffer >> *fieldIt;
+
+         // look for bubble merges with bubbles from other blocks, i.e., analyze the just received ghost layer
+         lookForMerges(fieldIt, dir, field);
+      }
+   }
+
+   // communicate bubble IDs locally (between blocks on the same process) and immediately check for bubble merges
+   void communicateLocal(const IBlock* sender, IBlock* receiver, stencil::Direction dir) override
+   {
+      // get sender and receiver fields
+      const field_t* const senderField = sender->getData< const field_t >(this->bdId_);
+      field_t* const receiverField     = receiver->getData< field_t >(this->bdId_);
+
+      WALBERLA_ASSERT_EQUAL(senderField->xSize(), receiverField->xSize());
+      WALBERLA_ASSERT_EQUAL(senderField->ySize(), receiverField->ySize());
+      WALBERLA_ASSERT_EQUAL(senderField->zSize(), receiverField->zSize());
+
+      auto srcIt = senderField->beginSliceBeforeGhostLayer(dir); // iterates only over last slice before ghost layer
+      auto dstIt = receiverField->beginGhostLayerOnly(stencil::inverseDir[dir]); // iterates only over ghost layer
+
+      while (srcIt != senderField->end())
+      {
+         // fill receiver's ghost layer with values from the sender's outermost inner layer
+         *dstIt = *srcIt;
+
+         // look for bubble merges with bubbles from other blocks, i.e., analyze the just received ghost layer
+         lookForMerges(dstIt, stencil::inverseDir[dir], receiverField);
+
+         ++srcIt;
+         ++dstIt;
+      }
+
+      WALBERLA_ASSERT(srcIt == senderField->end() && dstIt == receiverField->end());
+   }
+
+ protected:
+   // looks for bubble merges with bubbles from other blocks from the view of a ghost layer; argument "iterator i"
+   // should iterate ghost layer only
+   void lookForMerges(const field_t::iterator& i, stencil::Direction dir, const field_t* field)
+   {
+      using namespace stencil;
+
+      // only iterate the relevant neighborhood, for example:
+      // in ghost layer at "W", check all neighbors in directions containing "E"
+      // in ghost layer at "NE", check all neighbors in directions containing "SW" (=> SW, TSW and BSW)
+      // in ghost layer at "TNE", only check the neighbor in direction "BSW"
+      for (uint_t d = uint_c(0); d < uint_c(CommunicationStencil_T::d_per_d_length[inverseDir[dir]]); ++d)
+      {
+         const Direction neighborDirection = CommunicationStencil_T::d_per_d[inverseDir[dir]][d];
+         auto neighborVal                  = i.neighbor(neighborDirection);
+
+         // merge only occurs if bubble IDs from both blocks are valid and different
+         if (neighborVal != INVALID_BUBBLE_ID && *i != INVALID_BUBBLE_ID && neighborVal != *i)
+         {
+            Cell neighborCell = i.cell() + Cell(cx[neighborDirection], cy[neighborDirection], cz[neighborDirection]);
+
+            // make sure that the neighboring cell is not in a different ghost layer but part of the domain
+            if (field->isInInnerPart(neighborCell)) { mergeInfo_->registerMerge(*i, neighborVal); }
+         }
+      }
+   }
+
+   MergeInformation* mergeInfo_;
+}; // class BubbleIDFieldPackInfo
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/BubbleModel.h b/src/lbm/free_surface/bubble_model/BubbleModel.h
new file mode 100644
index 000000000..a22d540ea
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleModel.h
@@ -0,0 +1,240 @@
+//======================================================================================================================
+//
+//  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 BubbleModel.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief System for tracking pressure/density in gas volumes.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "blockforest/StructuredBlockForest.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/math/Vector3.h"
+
+#include "field/GhostLayerField.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q27.h"
+
+#include "Bubble.h"
+#include "BubbleDefinitions.h"
+#include "FloodFill.h"
+#include "MergeInformation.h"
+#include "NewBubbleCommunication.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+class BubbleModelBase
+{
+ public:
+   virtual ~BubbleModelBase() = default;
+
+   virtual real_t getDensity(IBlock* block, const Cell& cell) const       = 0;
+   virtual void setDensity(IBlock* block, const Cell& cell, real_t value) = 0;
+   virtual void setDensityOfAllBubbles(real_t val)                        = 0;
+
+   // call updateVolumeDiff() with fillLevelDifference
+   virtual void reportFillLevelChange(IBlock* block, const Cell& cell, real_t fillLevelDifference) = 0;
+   virtual void reportLiquidToInterfaceConversion(IBlock* block, const Cell& cell)                 = 0;
+   virtual void reportInterfaceToLiquidConversion(IBlock* block, const Cell& cell)                 = 0;
+
+   virtual void update() = 0;
+}; // class BubbleModelBase
+
+/***********************************************************************************************************************
+ * Implementation for setups in which no bubble model is required. Reports always a constant pressure
+ **********************************************************************************************************************/
+class BubbleModelConstantPressure : public BubbleModelBase
+{
+ public:
+   BubbleModelConstantPressure(real_t constantLatticeDensity) : constantLatticeDensity_(constantLatticeDensity) {}
+   ~BubbleModelConstantPressure() override = default;
+
+   real_t getDensity(IBlock*, const Cell&) const override { return constantLatticeDensity_; }
+   void setDensity(IBlock*, const Cell&, real_t) override {}
+   void setDensityOfAllBubbles(real_t val) override { constantLatticeDensity_ = val; }
+
+   void reportFillLevelChange(IBlock*, const Cell&, real_t) override{};
+   void reportLiquidToInterfaceConversion(IBlock*, const Cell&) override{};
+   void reportInterfaceToLiquidConversion(IBlock*, const Cell&) override{};
+
+   void update() override {}
+
+ private:
+   real_t constantLatticeDensity_;
+}; // class BubbleModelConstantPressure
+
+/***********************************************************************************************************************
+ * System for tracking pressure/density in gas volumes.
+ *
+ * The pure volume of fluid code calculates how mass/fluid moves across the domain. As input it needs the gas density or
+ * pressure. The density is equal in the same bubble. To track the pressure, individual gas volumes have to be tracked.
+ * The bubbles can split or merge, can possibly range across multiple blocks and across multiple processes. The handling
+ * of this is implemented in this class.
+ **********************************************************************************************************************/
+template< typename Stencil_T >
+class BubbleModel : public BubbleModelBase
+{
+ public:
+   BubbleModel(const std::shared_ptr< StructuredBlockForest >& blockStorage, bool enableBubbleSplits);
+   ~BubbleModel() override = default;
+
+   // initialize bubble model from fill level field; bubble model is cleared and bubbles are created in cells with fill
+   // level less than 1
+   void initFromFillLevelField(const ConstBlockDataID& fillField);
+
+   void setDensityOfAllBubbles(real_t rho) override;
+
+   // mark the specified (gas) cell for belonging to the atmosphere bubble with constant pressure; the atmosphere's
+   // bubble ID is set to the highest ID that was found on any of the processes at cells that belong to the atmosphere;
+   // WARNING: This function must be called on all processes for the same cells, even if the cells are not located on
+   // the current block.
+   void setAtmosphere(const Cell& cellInGlobalCoordinates, real_t constantRho = real_c(1.0));
+
+   // accessing
+   real_t getDensity(IBlock* block, const Cell& cell) const override
+   {
+      // get the bubble containing cell
+      const Bubble* bubble = getBubble(block, cell);
+      WALBERLA_ASSERT_NOT_NULLPTR(bubble, "Cell " << cell << " does not belong to a bubble.");
+
+      return bubble->getDensity();
+   }
+
+   void setDensity(IBlock* block, const Cell& cell, real_t value) override
+   {
+      // get the bubble containing cell
+      Bubble* bubble = getBubble(block, cell);
+      WALBERLA_ASSERT_NOT_NULLPTR(bubble, "Cell " << cell << " does not belong to a bubble.");
+
+      bubble->setDensity(value);
+   }
+
+   const BubbleID& getBubbleID(IBlock* block, const Cell& cell) const;
+   BubbleID& getBubbleID(IBlock* block, const Cell& cell);
+
+   // cell and fill level update
+   void reportFillLevelChange(IBlock* block, const Cell& cell, real_t fillLevelDifference) override;
+
+   // assign a bubble ID (from the first found neighboring cell) to the newly created interface cell; if multiple bubble
+   // IDs are found in neighborhood, register a merge
+   void reportLiquidToInterfaceConversion(IBlock* block, const Cell& cell) override;
+
+   // invalidate the bubble ID of the converted liquid cell and check for bubble splits
+   void reportInterfaceToLiquidConversion(IBlock* block, const Cell& cell) override;
+
+   ConstBlockDataID getBubbleFieldID() const { return bubbleFieldID_; }
+
+   // combine information about a bubble
+   struct BubbleInfo
+   {
+      BubbleInfo() : nrOfCells(uint_c(0)) {}
+      Vector3< real_t > centerOfMass;
+      uint_t nrOfCells;
+      Bubble* bubble;
+   };
+
+   // compute bubbleInfo for each bubble and (MPI) reduce it on root
+   std::vector< BubbleInfo > computeBubbleStats();
+
+   // write bubbleInfo to terminal on root process
+   void logBubbleStatsOnRoot();
+
+   // update the bubble model:
+   // - communicate bubble ID field
+   // - merge and split bubbles (involves global MPI communications)
+   void update() override;
+
+ protected:
+   const Bubble* getBubble(IBlock* block, const Cell& cell) const;
+   Bubble* getBubble(IBlock* block, const Cell& cell);
+
+   const std::vector< Bubble >& getBubbles() const { return bubbles_; };
+
+   // check a 3x3x3 neighborhood whether a bubble could have split; calling is function is relatively inexpensive and
+   // can be used as indicator whether the more expensive extendedSplitCheck() makes sense
+   static bool checkForSplit(BubbleField_T* bf, const Cell& cell, BubbleID prevBubbleID);
+
+   // check "neighborhood" cells in each direction around "cell" to ensure that a bubble has really split
+   static bool extendedSplitCheck(BubbleField_T* bf, const Cell& cell, BubbleID oldBubbleID,
+                                  cell_idx_t neighborhood = 2);
+
+   using StencilForSplit_T =
+      typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+
+   // split bubbles, assign new global bubble IDs, and handle potential bubble merges resulting from splitting (involves
+   // global MPI communication)
+   void handleSplits();
+
+   // delete potentially splitting bubbles and create new ones from them; new bubbles are added to newBubbleCom
+   void markAndCreateSplittedBubbles(NewBubbleCommunication& newBubbleComm, const std::vector< bool >& splitIndicator);
+
+   // in a 3x3x3 neighborhood, find all directions that are connected to startDir (by having same bubbleID) using a
+   // flood fill algorithm; flood fill is more efficient than simply iterating over all neighbors since the latter would
+   // require lots of extra logic
+   static uint32_t mapNeighborhood(BubbleField_T* bf, stencil::Direction startDir, const Cell& cell, BubbleID bubbleID);
+
+   // block storage to access bubbleField and fillField
+   std::shared_ptr< StructuredBlockStorage > blockStorage_;
+
+   // field that stores every cell's bubble ID; if a cell does not belong to any bubble, its ID is set to
+   // INVALID_BUBBLE_ID; this field is managed by the BubbleModel and should not be passed outside
+   BlockDataID bubbleFieldID_;
+
+   // vector that stores all bubbles; it is kept synchronized across all processes
+   std::vector< Bubble > bubbles_;
+
+   // helper class to manage bubble merges
+   MergeInformation mergeInformation_;
+
+   // communication scheme for the bubble field
+   blockforest::communication::UniformBufferedScheme< Stencil_T > bubbleFieldCommunication_;
+
+   // store split information, i.e., hints for splitting; store only hints since merges have to be processed first
+   struct SplitHint
+   {
+      SplitHint(IBlock* _block, const Cell& _cell) : block(_block), cell(_cell) {}
+      IBlock* block;
+      Cell cell;
+   };
+
+   // vector with outstanding splits that (still) need to be processed
+   std::vector< SplitHint > splitsToProcess_;
+
+   std::shared_ptr< FloodFillInterface > floodFill_;
+
+   // disable splits to decrease computational costs
+   bool enableBubbleSplits_;
+
+   inline BubbleField_T* getBubbleField(IBlock* block) const { return block->getData< BubbleField_T >(bubbleFieldID_); }
+
+}; // class BubbleModel
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+using walberla::free_surface::bubble_model::BubbleModelBase;
+
+#include "BubbleModel.impl.h"
diff --git a/src/lbm/free_surface/bubble_model/BubbleModel.impl.h b/src/lbm/free_surface/bubble_model/BubbleModel.impl.h
new file mode 100644
index 000000000..7ee915c2a
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleModel.impl.h
@@ -0,0 +1,692 @@
+//======================================================================================================================
+//
+//  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 BubbleModel.impl.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief System for tracking pressure/density in gas volumes.
+//
+//======================================================================================================================
+
+#include "field/AddToStorage.h"
+
+#include "lbm/free_surface/InterfaceFromFillLevel.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q27.h"
+
+#include "BubbleIDFieldPackInfo.h"
+#include "BubbleModel.h"
+#include "RegionalFloodFill.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Stencil_T >
+BubbleModel< Stencil_T >::BubbleModel(const std::shared_ptr< StructuredBlockForest >& blockStorage,
+                                      bool enableBubbleSplits)
+   : blockStorage_(blockStorage), bubbleFieldID_(field::addToStorage< BubbleField_T >(
+                                     blockStorage, "BubbleIDs", BubbleID(INVALID_BUBBLE_ID), field::fzyx, uint_c(1))),
+     bubbleFieldCommunication_(blockStorage), enableBubbleSplits_(enableBubbleSplits)
+{
+   bubbleFieldCommunication_.addPackInfo(
+      std::make_shared< BubbleIDFieldPackInfo< Stencil_T > >(bubbleFieldID_, &mergeInformation_));
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::initFromFillLevelField(const ConstBlockDataID& fillFieldID)
+{
+   // mark regions belonging to the same bubble
+   floodFill_ = std::make_shared< FloodFillUsingFillLevel< Stencil_T > >(fillFieldID);
+
+   bubbles_.clear();
+
+   NewBubbleCommunication newBubbleComm;
+
+   // start numbering the bubbles from 0
+   BubbleID firstNewBubbleID = 0;
+   BubbleID nextID           = firstNewBubbleID;
+
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      // get fields
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+      BubbleField_T* const bubbleField     = blockIt->getData< BubbleField_T >(bubbleFieldID_);
+
+      // initialize bubbleField with invalid IDs
+      bubbleField->set(INVALID_BUBBLE_ID);
+
+      auto bubbleIt    = bubbleField->begin();
+      auto fillFieldIt = fillField->begin();
+
+      while (bubbleIt != bubbleField->end())
+      {
+         // only consider cells
+         // - that are either gas or interface
+         // - for which no bubble ID is set yet (each call to floodFill_->run() sets new bubbleIDs to cells)
+         if ((*fillFieldIt < real_c(1) || isInterfaceFromFillLevel< Stencil_T >(*fillField, bubbleIt.cell())) &&
+             *bubbleIt == INVALID_BUBBLE_ID)
+         {
+            real_t volume;
+            uint_t nrOfCells;
+
+            // set (block local, preliminary) bubble IDs in the bubble field
+            floodFill_->run(*blockIt, bubbleFieldID_, bubbleIt.cell(), nextID++, volume, nrOfCells);
+
+            // create new bubble with preliminary bubbleIDs; the final global bubbleIDs are set in communicateAndApply()
+            newBubbleComm.createBubble(Bubble(volume));
+         }
+
+         ++bubbleIt;
+         ++fillFieldIt;
+      }
+   }
+
+   // set global bubble IDs
+   newBubbleComm.communicateAndApply(bubbles_, *blockStorage_, bubbleFieldID_);
+
+   // clear merge information, i.e., make sure that no merge is already registered
+   mergeInformation_.resizeAndClear(bubbles_.size());
+
+   // communicate bubble field IDs
+   bubbleFieldCommunication_();
+
+   // communicate bubble merges
+   mergeInformation_.communicateMerges();
+
+   if (mergeInformation_.hasMerges())
+   {
+      // merge bubbles (add bubble volumes and delete/rename bubble IDs)
+      mergeInformation_.mergeAndReorderBubbleVector(bubbles_);
+
+      // rename bubble IDs on all blocks
+      for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+         mergeInformation_.renameOnBubbleField(blockIt->getData< BubbleField_T >(bubbleFieldID_));
+   }
+
+   // clear merge information after bubbles have been merged
+   mergeInformation_.resizeAndClear(bubbles_.size());
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::setDensityOfAllBubbles(real_t rho)
+{
+   for (auto it = bubbles_.begin(); it != bubbles_.end(); ++it)
+   {
+      if (!it->hasConstantDensity()) { it->setDensity(rho); }
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::setAtmosphere(const Cell& cellInGlobalCoordinates, real_t rho)
+{
+   // temporarily set the atmosphere's bubble ID to invalid
+   BubbleID atmosphereBubbbleID = INVALID_BUBBLE_ID;
+
+   // get the cell's block (or nullptr if the block is not on this process)
+   IBlock* blockWithAtmosphereBubble = blockStorage_->getBlock(cellInGlobalCoordinates);
+
+   // set atmosphere bubble ID to this cell's bubble ID
+   if (blockWithAtmosphereBubble) // else: block does not exist locally
+   {
+      Cell localCell;
+      blockStorage_->transformGlobalToBlockLocalCell(localCell, *blockWithAtmosphereBubble, cellInGlobalCoordinates);
+
+      const BubbleField_T* const bf = blockWithAtmosphereBubble->getData< const BubbleField_T >(bubbleFieldID_);
+
+      // get this cell's bubble ID
+      atmosphereBubbbleID = bf->get(localCell);
+
+      // cell must be a gas cell; therefore, a valid bubble ID must be set
+      WALBERLA_ASSERT_UNEQUAL(atmosphereBubbbleID, INVALID_BUBBLE_ID);
+   }
+
+   // variable for (MPI) reducing the bubble ID; value of -1 is set in order to ignore this bubble in maximum reduction
+   int reducedBubbleID = atmosphereBubbbleID != INVALID_BUBBLE_ID ? int_c(atmosphereBubbbleID) : -1;
+
+   // get the highest of all processes' bubble IDs
+   WALBERLA_MPI_SECTION()
+   {
+      MPI_Allreduce(MPI_IN_PLACE, &reducedBubbleID, 1, MPITrait< int >::type(), MPI_MAX, MPI_COMM_WORLD);
+   }
+
+   if (reducedBubbleID < 0)
+   {
+      WALBERLA_LOG_WARNING_ON_ROOT("Could not set atmosphere in the non-gas cell " << cellInGlobalCoordinates);
+   }
+   else
+   {
+      // set atmosphere bubble ID to highest of all processes' bubble IDs
+      atmosphereBubbbleID = BubbleID(reducedBubbleID);
+      bubbles_[atmosphereBubbbleID].setConstantDensity(rho);
+   }
+}
+
+template< typename Stencil_T >
+const Bubble* BubbleModel< Stencil_T >::getBubble(IBlock* blockIt, const Cell& cell) const
+{
+   const BubbleField_T* bf = getBubbleField(blockIt);
+   const BubbleID id       = bf->get(cell);
+
+   return (id == INVALID_BUBBLE_ID) ? nullptr : &bubbles_[id];
+}
+
+template< typename Stencil_T >
+Bubble* BubbleModel< Stencil_T >::getBubble(IBlock* blockIt, const Cell& cell)
+{
+   const BubbleField_T* bf = getBubbleField(blockIt);
+   const BubbleID id       = bf->get(cell);
+
+   return (id == INVALID_BUBBLE_ID) ? nullptr : &bubbles_[id];
+}
+
+template< typename Stencil_T >
+const BubbleID& BubbleModel< Stencil_T >::getBubbleID(IBlock* blockIt, const Cell& cell) const
+{
+   const BubbleField_T* bf = getBubbleField(blockIt);
+   return bf->get(cell);
+}
+
+template< typename Stencil_T >
+BubbleID& BubbleModel< Stencil_T >::getBubbleID(IBlock* blockIt, const Cell& cell)
+{
+   BubbleField_T* bf = getBubbleField(blockIt);
+   return bf->get(cell);
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::reportFillLevelChange(IBlock* blockIt, const Cell& cell, real_t fillLevelDifference)
+{
+   Bubble* b = getBubble(blockIt, cell);
+   WALBERLA_ASSERT_NOT_NULLPTR(b,
+                               "Reporting fill level change in cell " << cell << " where no bubble ID is registered.");
+
+   // update the bubble volume change; fillLevelDifference is negated because variable is:
+   // - positive if fill level increased => bubble volume has to decrease
+   // - negative if fill level decreased => bubble volume has to increase
+   if (b) { b->updateVolumeDiff(-fillLevelDifference); }
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::reportLiquidToInterfaceConversion(IBlock* blockIt, const Cell& cell)
+{
+   // get bubble field
+   BubbleField_T* bf = getBubbleField(blockIt);
+
+   // get this cell's bubble ID
+   BubbleID& thisCellID = bf->get(cell);
+
+   // this cell is converted from liquid to interface; liquid cells have no bubble ID such that this cell can not have
+   // a bubble ID, yet
+   WALBERLA_ASSERT_EQUAL(thisCellID, INVALID_BUBBLE_ID);
+
+   // iterate neighborhood and assign the first found bubble ID to this new interface cell
+   using SearchStencil_T = typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+   for (auto d = SearchStencil_T::beginNoCenter(); d != SearchStencil_T::end(); ++d)
+   {
+      // get bubble ID of neighboring cell
+      BubbleID neighborID = bf->get(cell[0] + d.cx(), cell[1] + d.cy(), cell[2] + d.cz());
+      if (neighborID != INVALID_BUBBLE_ID)
+      {
+         // assign the first found neighbor's bubble ID to this cell
+         if (thisCellID == INVALID_BUBBLE_ID) { thisCellID = neighborID; }
+         else
+         {
+            // if multiple different bubble IDs are in neighborhood, trigger merging
+            if (thisCellID != neighborID) { mergeInformation_.registerMerge(thisCellID, neighborID); }
+         }
+      }
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::reportInterfaceToLiquidConversion(IBlock* blockIt, const Cell& cell)
+{
+   // get bubble field
+   BubbleField_T* bf = getBubbleField(blockIt);
+
+   // get this cell's bubble ID
+   BubbleID oldBubbleID = bf->get(cell);
+
+   // this cell is converted from interface to liquid; the interface cell must already have a valid bubble ID
+   WALBERLA_ASSERT_UNEQUAL(oldBubbleID, INVALID_BUBBLE_ID);
+
+   // invalidate the converted cell's bubble ID (liquid cells must not have a bubble ID)
+   bf->get(cell) = INVALID_BUBBLE_ID;
+
+   if (enableBubbleSplits_)
+   {
+      // check a 3x3x3 neighborhood whether a bubble could have split
+      if (checkForSplit(bf, cell, oldBubbleID))
+      {
+         WALBERLA_LOG_INFO("Possible bubble split detected due to conversion in cell " << cell << ".");
+
+         // check a larger neighborhood to ensure that the bubble has really split
+         if (extendedSplitCheck(bf, cell, oldBubbleID, cell_idx_c(3)))
+         {
+            WALBERLA_LOG_INFO("Extended split check confirmed split.");
+            // register this bubble split
+            splitsToProcess_.emplace_back(blockIt, cell);
+         }
+         else { WALBERLA_LOG_INFO("Extended split check ruled out split."); }
+      }
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::update()
+{
+   // communicate field with bubble IDs
+   bubbleFieldCommunication_();
+
+   // vector for (MPI) reducing each bubble's volume change and indicators for whether merges and splits occurred
+   static std::vector< real_t > reduceVector;
+
+   // indicators that are (MPI) reduced to identify merges and splits
+   real_t mergeIndicator = mergeInformation_.hasMerges() ? real_c(1) : real_c(0);
+   real_t splitIndicator = splitsToProcess_.empty() ? real_c(0) : real_c(1);
+
+   // extend the vector for storing the merge and split indicator
+   reduceVector.resize(bubbles_.size() + 2);
+
+   uint_t i = uint_c(0);
+   for (; i < bubbles_.size(); ++i)
+   {
+      // get each bubble's volume change
+      reduceVector[i] = bubbles_[i].getAndResetVolumeDiff();
+   }
+
+   // append the indicators at the end of reduceVector
+   reduceVector[i++] = mergeIndicator;
+   reduceVector[i++] = splitIndicator;
+   WALBERLA_ASSERT_EQUAL(i, reduceVector.size()); // make sure that indexing is correct
+
+   WALBERLA_MPI_SECTION()
+   {
+      // globally (MPI) reduce each bubble's volume change, the number of merges, and the number of splits
+      MPI_Allreduce(MPI_IN_PLACE, &reduceVector[0], int_c(reduceVector.size()), MPITrait< real_t >::type(), MPI_SUM,
+                    MPI_COMM_WORLD);
+   }
+
+   uint_t j = uint_c(0);
+   for (; j < bubbles_.size(); ++j)
+   {
+      // update each bubble's volume and density
+      bubbles_[j].applyVolumeDiff(reduceVector[j]);
+   }
+
+   // check for merges and splits
+   bool mergeHappened = (reduceVector[j++] > real_c(0));
+   bool splitHappened = (reduceVector[j++] > real_c(0));
+   WALBERLA_ASSERT_EQUAL(j, reduceVector.size()); // make sure that indexing is correct
+
+   // treat bubble merges
+   if (mergeHappened)
+   {
+      WALBERLA_ROOT_SECTION()
+      {
+         // std::stringstream ss;
+         // mergeInformation_.print(ss);
+         // WALBERLA_LOG_INFO("Merge detected, " << ss.str());
+         WALBERLA_LOG_INFO("Merge detected");
+      }
+
+      // globally communicate bubble merges and rename bubble IDs accordingly
+      mergeInformation_.communicateMerges();
+
+      // merge bubbles
+      mergeInformation_.mergeAndReorderBubbleVector(bubbles_);
+
+      // update, i.e., rename bubble IDs in the bubble ID field
+      for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+      {
+         mergeInformation_.renameOnBubbleField(getBubbleField(&(*blockIt)));
+      }
+   }
+
+   // treat bubble splits
+   if (splitHappened) { handleSplits(); }
+
+   mergeInformation_.resizeAndClear(bubbles_.size());
+   splitsToProcess_.clear();
+}
+
+template< typename Stencil_T >
+bool BubbleModel< Stencil_T >::checkForSplit(BubbleField_T* bf, const Cell& cell, BubbleID previousBubbleID)
+{
+   using namespace stencil;
+
+   // Variable neighborHoodInfo has bits set in each connected neighboring direction where the cell belongs to bubble
+   // previousBubbleID (=ID of the bubble that got "lost" by converting cell from interface to liquid). The
+   // neighborHoodInfo is built via flood fill exactly once from a starting direction (e.g., first direction in which
+   // previousBubbleID was found). Thus, the connected neighborhood is built for only one direction and cells that
+   // are not connected to this starting direction are not part of the connected neighborhood. A split occurs, i.e., is
+   // detected when previousBubbleID is found in an unconnected region.
+   // Example:
+   // previousBubbleID is 1, the cells in the center were converted from bubble ID=1 to liquid (ID=f)
+   //   1 1 1
+   //   f f f
+   //   1 1 1
+   // The first direction in which previousBubbleID is set shall be N (north). The connected neighborhood is then built
+   // (c=connected, n=not connected):
+   //   c c c
+   //   n n n
+   //   n n n
+   // At neighbor N, neighbors NW and NE are found: no split is detected since we are in the connected neighborhood.
+   // Then, neighbor S is found. Since S is not in the (first) connected neighborhood, a split is detected.
+   uint32_t neighborHoodInfo = uint32_c(0);
+
+   for (auto d = StencilForSplit_T::beginNoCenter(); d != StencilForSplit_T::end(); ++d)
+   {
+      // get the bubble ID of the neighboring cell
+      BubbleID neighborID = bf->getNeighbor(cell, *d);
+
+      // neighboring bubble is a different one (or no bubble) than this cell's bubble
+      if (neighborID != previousBubbleID) { continue; }
+      // => from here: bubbles are the same, i.e., neighborID == previousBubbleID
+
+      if (neighborHoodInfo > uint32_c(0)) // the neighborhood map has already been created
+      {
+         // "connected" bit is set in this direction, i.e., the neighbor is connected
+         if (neighborHoodInfo & dirToBinary[*d]) { continue; }
+         else // "connected" bit is not set in this direction, i.e., the neighbor is not connected
+         {
+            // since neighborID == previousBubbleID but neighbor is not connected, a split had to occur
+            return true;
+         }
+      }
+      else // the neighborhood map has not been created, yet
+      {
+         // create connected neighborhood starting from direction d
+         neighborHoodInfo = mapNeighborhood(bf, *d, cell, neighborID);
+      }
+   }
+   return false;
+}
+
+template< typename Stencil_T >
+bool BubbleModel< Stencil_T >::extendedSplitCheck(BubbleField_T* bf, const Cell& cell, BubbleID previousBubbleID,
+                                                  cell_idx_t neighborhood)
+{
+   // RegionalFloodFill is used to find connected regions in a larger (>3x3x3) neighborhood
+   RegionalFloodFill< BubbleID, StencilForSplit_T >* neighborHoodInfo = nullptr;
+
+   for (auto d = StencilForSplit_T::beginNoCenter(); d != StencilForSplit_T::end(); ++d)
+   {
+      // get the bubble ID of the neighboring cell
+      BubbleID neighborID = bf->getNeighbor(cell, *d);
+
+      // neighboring bubble is a different one (or no bubble) than this cell's bubble
+      if (neighborID != previousBubbleID) { continue; }
+      // => from here: bubbles are the same, i.e., neighborID == previousBubbleID
+
+      if (neighborHoodInfo) // the neighborhood map has already been created
+      {
+         if (neighborHoodInfo->connected(*d)) { continue; }
+         else // bubble is not connected in direction d and a split occurred
+         {
+            delete neighborHoodInfo;
+            return true;
+         }
+      }
+      else // the neighborhood map has not been created, yet
+      {
+         // create connected neighborhood starting from direction d
+         neighborHoodInfo =
+            new RegionalFloodFill< BubbleID, StencilForSplit_T >(bf, cell, *d, neighborID, neighborhood);
+      }
+   }
+
+   delete neighborHoodInfo;
+   return false;
+}
+
+template< typename Stencil_T >
+uint32_t BubbleModel< Stencil_T >::mapNeighborhood(BubbleField_T* bf, stencil::Direction startDir, const Cell& cell,
+                                                   BubbleID bubbleID)
+{
+   using namespace stencil;
+
+   uint32_t result = uint32_c(0);
+
+   // use stack to store directions that still need to be searched
+   std::vector< Direction > stack;
+   stack.push_back(startDir);
+
+   while (!stack.empty())
+   {
+      // next search direction is the last entry in stack
+      Direction d = stack.back();
+
+      // remove the current search direction from stack
+      stack.pop_back();
+
+      WALBERLA_ASSERT(d != C); // do not search in center direction
+      WALBERLA_ASSERT(bf->get(cell[0] + cx[d], cell[1] + cy[d], cell[2] + cz[d]) ==
+                      bubbleID); // cell must belong to the same bubble
+
+      // add this direction to result, i.e., to the "connected neighborhood" using bitwise OR
+      result |= dirToBinary[d];
+
+      // in direction d, iterate over d's neighboring directions i, i.e., iterate over a (at maximum) 3x3x3 neighborhood
+      // from the viewpoint of cell
+      for (uint_t i = uint_c(1); i < StencilForSplit_T::dir_neighbors_length[d]; ++i)
+      {
+         // transform direction d's neighbor in direction i to a direction from the viewpoint of cell, e.g., d=N, i=W =>
+         // nDir=NW
+         Direction nDir = StencilForSplit_T::dir_neighbors[d][i];
+
+         // cell in direction nDir belongs to the bubble and is not already in result
+         if (bf->get(cell[0] + cx[nDir], cell[1] + cy[nDir], cell[2] + cz[nDir]) == bubbleID &&
+             (result & dirToBinary[nDir]) == 0)
+         {
+            // add nDir to stack such that it gets added to result and used as start cell in the next iteration;
+            // the connected bit will never be set for directions that are not connected to startDir
+            stack.push_back(nDir);
+         }
+      }
+   }
+
+   return result;
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::handleSplits()
+{
+   // splitIndicator is set to true for bubbles for which a split was registered; this can not be done earlier since
+   // bubble IDs may have changed during merging
+   std::vector< bool > splitIndicator(bubbles_.size());
+
+   // process all registered splits
+   for (auto i = splitsToProcess_.begin(); i != splitsToProcess_.end(); ++i)
+   {
+      BubbleField_T* bubbleField = getBubbleField(i->block);
+
+      const Cell& c = i->cell;
+
+      // cell c was transformed from interface to liquid and has no valid bubble ID anymore; mark remaining gas cells
+      // with valid bubble IDs for being split
+      for (auto d = StencilForSplit_T::begin(); d != StencilForSplit_T::end(); ++d)
+      {
+         BubbleID id = bubbleField->get(c.x() + d.cx(), c.y() + d.cy(), c.z() + d.cz());
+
+         // mark bubble's cell for splitting in splitIndicator
+         if (id != INVALID_BUBBLE_ID) { splitIndicator[id] = true; }
+      }
+   }
+
+   // communicate (MPI reduce) splitIndicator among all processes
+   allReduceInplace(splitIndicator, mpi::BITWISE_OR);
+
+   // create communication for new bubbles
+   NewBubbleCommunication newBubbleComm(bubbles_.size());
+
+   // treat split and create new (splitted) bubbles with new IDs
+   markAndCreateSplittedBubbles(newBubbleComm, splitIndicator);
+
+   // communicate all bubbles and assign new global bubble IDs
+   newBubbleComm.communicateAndApply(bubbles_, splitIndicator, *blockStorage_, bubbleFieldID_);
+
+   // clear merge information
+   mergeInformation_.resizeAndClear(bubbles_.size());
+
+   // communicate bubble field
+   bubbleFieldCommunication_();
+
+   // communicate merges
+   mergeInformation_.communicateMerges();
+
+   // merge new splitted bubbles (merges can occur at the block border after the bubbleField was communicated;
+   // bubbleFieldCommunication is linked to mergeInformation_ to report the merges there)
+   if (mergeInformation_.hasMerges())
+   {
+      // merge bubbles
+      mergeInformation_.mergeAndReorderBubbleVector(bubbles_);
+      for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+      {
+         // assign new bubble IDs
+         mergeInformation_.renameOnBubbleField(blockIt->getData< BubbleField_T >(bubbleFieldID_));
+      }
+   }
+
+   // clear merge information
+   mergeInformation_.resizeAndClear(bubbles_.size());
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::markAndCreateSplittedBubbles(NewBubbleCommunication& newBubbleComm,
+                                                            const std::vector< bool >& splitIndicator)
+{
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      BubbleField_T* bubbleField = getBubbleField(&(*blockIt));
+
+      // loop over the whole domain and start a flood fill at positions where bubbles are marked in splitIndicator
+      for (auto bubbleIt = bubbleField->begin(); bubbleIt != bubbleField->end(); ++bubbleIt)
+      {
+         // skip cells that
+         // - do not belong to a bubble
+         // - belong to a bubble that might have been created during this function call (i.e., this bubble ID is not yet
+         // known to the vector spliIndicator)
+         // - belong to a bubble for which no split was detected
+         if (*bubbleIt == INVALID_BUBBLE_ID || *bubbleIt >= splitIndicator.size() || !splitIndicator[*bubbleIt])
+         {
+            continue;
+         }
+
+         const Cell& curCell       = bubbleIt.cell();
+         real_t densityOfOldBubble = getDensity(&(*blockIt), curCell);
+         real_t volume;
+         uint_t nrOfCells;
+
+         // mark the whole region of the bubble (to which this cells belongs) in bubbleField
+         floodFill_->run(*blockIt, bubbleFieldID_, curCell, newBubbleComm.nextFreeBubbleID(), volume, nrOfCells);
+
+         // create new bubble
+         newBubbleComm.createBubble(Bubble(volume, densityOfOldBubble));
+      }
+   }
+}
+
+template< typename Stencil_T >
+std::vector< typename BubbleModel< Stencil_T >::BubbleInfo > BubbleModel< Stencil_T >::computeBubbleStats()
+{
+   std::vector< BubbleInfo > bubbleStats;
+   bubbleStats.assign(bubbles_.size(), BubbleInfo());
+
+   // iterate all bubbles on each block
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      const BubbleField_T* bubbleField = getBubbleField(&(*blockIt));
+      WALBERLA_FOR_ALL_CELLS(bubbleFieldIt, bubbleField, {
+         if (*bubbleFieldIt == INVALID_BUBBLE_ID) { continue; }
+
+         Vector3< real_t > cellCenter;
+         Cell globalCell;
+         blockStorage_->transformBlockLocalToGlobalCell(globalCell, *blockIt, bubbleFieldIt.cell());
+         blockStorage_->getCellCenter(cellCenter[0], cellCenter[1], cellCenter[2], globalCell);
+
+         // center of mass of this bubble on this block
+         bubbleStats[*bubbleFieldIt].centerOfMass += cellCenter;
+
+         // bubble's number of cells
+         bubbleStats[*bubbleFieldIt].nrOfCells++;
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // store bubble information in reduceVec; reducing a vector of real_t is significantly easier than reducing
+   // bubbleStats (vector of bubbleInfo)
+   std::vector< real_t > reduceVec;
+   for (auto statsIter = bubbleStats.begin(); statsIter != bubbleStats.end(); ++statsIter)
+   {
+      reduceVec.push_back(statsIter->centerOfMass[0]);
+      reduceVec.push_back(statsIter->centerOfMass[1]);
+      reduceVec.push_back(statsIter->centerOfMass[2]);
+      reduceVec.push_back(real_c(statsIter->nrOfCells));
+   }
+
+   // (MPI) reduce bubble information on root
+   mpi::reduceInplace(reduceVec, mpi::SUM, 0, MPI_COMM_WORLD);
+
+   WALBERLA_ROOT_SECTION()
+   {
+      uint_t idx = uint_c(0);
+      uint_t i   = uint_c(0);
+      for (auto statsIter = bubbleStats.begin(); statsIter != bubbleStats.end(); ++statsIter)
+      {
+         statsIter->centerOfMass[0] = reduceVec[idx++];
+         statsIter->centerOfMass[1] = reduceVec[idx++];
+         statsIter->centerOfMass[2] = reduceVec[idx++];
+         statsIter->nrOfCells       = uint_c(reduceVec[idx++]);
+
+         // compute the bubble's global center of mass
+         statsIter->centerOfMass /= real_c(statsIter->nrOfCells);
+
+         // store the bubble ID
+         statsIter->bubble = &(bubbles_[i]);
+         ++i;
+      }
+      WALBERLA_ASSERT_EQUAL(idx, reduceVec.size());
+
+      return bubbleStats;
+   }
+   else // else belongs to macro WALBERLA_ROOT_SECTION()
+   {
+      return std::vector< BubbleInfo >();
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModel< Stencil_T >::logBubbleStatsOnRoot()
+{
+   std::vector< BubbleInfo > bubbleStats = computeBubbleStats();
+
+   WALBERLA_ROOT_SECTION()
+   {
+      WALBERLA_LOG_RESULT("Bubble Status:");
+      for (auto it = bubbleStats.begin(); it != bubbleStats.end(); ++it)
+      {
+         WALBERLA_LOG_RESULT("\tPosition:" << it->centerOfMass << "  #Cells: " << it->nrOfCells);
+      }
+   }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h
new file mode 100644
index 000000000..dfd86821b
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h
@@ -0,0 +1,75 @@
+//======================================================================================================================
+//
+//  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 BubbleModelFromConfig.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Create and initialize BubbleModel with information from a config object.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/config/Config.h"
+
+#include "BubbleModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+* Create and initialize BubbleModel with information from a config object.
+*
+* Example configuration block:
+* \verbatim
+  {
+      // For scenarios where no splits can occur - expensive split detection can be switched off - defaults to False
+      enableBubbleSplits False;
+
+      // optional atmosphere block(s) : Atmosphere bubbles are bubbles with constant pressure
+      atmosphere {
+         position  < 1.5, 175.5, 1 >;   // a point inside the bubble that should becomes atmosphere
+         density     1.1;           // the value of constant pressure. Default value: 1.0
+      }
+
+      // optional disjoining pressure
+      // Disjoining pressure model holds bubbles apart that are close to each other
+      DisjoiningPressure
+      {
+         maxDisjoiningPressure 0.1; // maximum value of disjoining pressure - defaults to 0.2
+         maxDistance 4;             // for bubbles with distance greater 'maxDistance' cells
+                                    // there is no disjoining pressure - defaults to 10 cells
+      }
+   }
+   \endverbatim
+*
+*
+* The fill level field must be fully initialized.
+* If the handle of the config block returns nullptr, the bubble model is created with default values.
+***********************************************************************************************************************/
+template< typename Stencil_T >
+std::shared_ptr< BubbleModelBase > createFromConfig(const std::shared_ptr< StructuredBlockForest >& blockStorage,
+                                                    ConstBlockDataID fillFieldID,
+                                                    const Config::BlockHandle& configBlock = Config::BlockHandle());
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "BubbleModelFromConfig.impl.h"
\ No newline at end of file
diff --git a/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h
new file mode 100644
index 000000000..4ffa219cb
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.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 BubbleModelFromConfig.impl.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Create and initialize BubbleModel with information from a config object.
+//
+//======================================================================================================================
+
+#include "BubbleModelFromConfig.h"
+#include "DisjoiningPressureBubbleModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Stencil_T >
+std::shared_ptr< BubbleModelBase > createFromConfig(const std::shared_ptr< StructuredBlockForest >& blockForest,
+                                                    ConstBlockDataID fillFieldID,
+                                                    const Config::BlockHandle& configBlock)
+{
+   if (!configBlock)
+   {
+      auto bubbleModel = std::make_shared< BubbleModel< Stencil_T > >(blockForest);
+      bubbleModel->initFromFillLevelField(fillFieldID);
+      return bubbleModel;
+   }
+
+   bool enableBubbleSplits = configBlock.getParameter< bool >("enableBubbleSplits", false);
+
+   std::shared_ptr< BubbleModel< Stencil_T > > bubbleModel;
+
+   real_t constantLatticeDensity = configBlock.getParameter< real_t >("constantLatticeDensity", real_c(-1));
+   if (constantLatticeDensity > 0) { return std::make_shared< BubbleModelConstantPressure >(constantLatticeDensity); }
+
+   auto disjoiningPressureCfg = configBlock.getBlock("DisjoiningPressure");
+   if (disjoiningPressureCfg)
+   {
+      const real_t maxDistance = disjoiningPressureCfg.getParameter< real_t >(
+         "maxBubbleDistance"); // d_range in the paper from Koerner et al., 2005
+      const real_t disjoiningPressureConstant = disjoiningPressureCfg.getParameter< real_t >(
+         "disjoiningPressureConstant"); // c_pi in the paper from Koerner et al., 2005
+      const uint_t distFieldUpdateInter = disjoiningPressureCfg.getParameter< uint_t >("distanceFieldUpdateInterval");
+
+      bubbleModel = std::make_shared< DisjoiningPressureBubbleModel< Stencil_T > >(
+         blockForest, maxDistance, disjoiningPressureConstant, enableBubbleSplits, distFieldUpdateInter);
+
+      WALBERLA_LOG_PROGRESS_ON_ROOT("Using Disjoining Pressure BubbleModel");
+   }
+   else
+   {
+      bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, enableBubbleSplits);
+      WALBERLA_LOG_PROGRESS_ON_ROOT("Using Standard BubbleModel");
+   }
+
+   bubbleModel->initFromFillLevelField(fillFieldID);
+
+   // get all atmosphere blocks
+   std::vector< Config::BlockHandle > atmosphereBlocks;
+   configBlock.getBlocks("atmosphere", atmosphereBlocks);
+   for (auto it = atmosphereBlocks.begin(); it != atmosphereBlocks.end(); ++it)
+   {
+      Vector3< real_t > position = it->getParameter< Vector3< real_t > >("position");
+      real_t density             = it->getParameter< real_t >("density", 1.0);
+      Cell cell;
+      blockForest->getCell(cell, position[0], position[1], position[2]);
+      bubbleModel->setAtmosphere(cell, density);
+   }
+
+   return bubbleModel;
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/CMakeLists.txt b/src/lbm/free_surface/bubble_model/CMakeLists.txt
new file mode 100644
index 000000000..712930e38
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/CMakeLists.txt
@@ -0,0 +1,24 @@
+target_sources( lbm
+        PRIVATE
+        Bubble.h
+        BubbleDefinitions.h
+        BubbleDistanceAdaptor.h
+        BubbleIDFieldPackInfo.h
+        BubbleModel.h
+        BubbleModel.impl.h
+        BubbleModelFromConfig.h
+        BubbleModelFromConfig.impl.h
+        DisjoiningPressureBubbleModel.h
+        DisjoiningPressureBubbleModel.impl.h
+        DistanceInfo.cpp
+        DistanceInfo.h
+        FloodFill.h
+        FloodFill.impl.h
+        Geometry.h
+        Geometry.impl.h
+        MergeInformation.cpp
+        MergeInformation.h
+        NewBubbleCommunication.cpp
+        NewBubbleCommunication.h
+        RegionalFloodFill.h
+        )
diff --git a/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h
new file mode 100644
index 000000000..278536fee
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h
@@ -0,0 +1,142 @@
+//======================================================================================================================
+//
+//  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 DisjoiningPressureBubbleModel.h
+//! \ingroup bubble_model
+//! \author Daniela Anderl
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Bubble Model with additional pressure term (disjoining pressure).
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/GhostLayerField.h"
+
+#include <cmath>
+
+#include "BubbleModel.h"
+#include "DistanceInfo.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Bubble Model with additional pressure term (disjoining pressure) which depends on distance to the nearest bubble. The
+ * disjoining pressure is computed as in the paper from Koerner et al., 2005, where variable
+ * - "disjPressConst_" is called "c_pi"
+ * - "maxDistance_" is called "d_range"
+ * - "dist" is called "d_int".
+ *
+ * Be aware that the value of the first two variables are phenomenological, i.e., they have to be chosen according to
+ * comparisons with experiments. The current default values are more or less randomly chosen.
+ **********************************************************************************************************************/
+template< typename Stencil_T >
+class DisjoiningPressureBubbleModel : public BubbleModel< Stencil_T >
+{
+ public:
+   using DistanceField_T = GhostLayerField< DistanceInfo, 1 >;
+
+   explicit DisjoiningPressureBubbleModel(const std::shared_ptr< StructuredBlockForest >& blockStorage,
+                                          const real_t maxDistance    = real_c(7),
+                                          const real_t disjPressConst = real_c(0.05), bool enableBubbleSplits = true,
+                                          uint_t distanceFieldUpdateInterval = uint_c(1));
+
+   ~DisjoiningPressureBubbleModel() override = default;
+
+   // compute the gas density with disjoining pressure according to the paper from Koerner et al., 2005
+   real_t getDensity(IBlock* block, const Cell& cell) const override
+   {
+      // get bubble field and bubble ID
+      const BubbleField_T* bf = BubbleModel< Stencil_T >::getBubbleField(block);
+      const BubbleID id       = bf->get(cell);
+
+      WALBERLA_ASSERT_UNEQUAL(id, INVALID_BUBBLE_ID, "Cell " << cell << " does not contain a bubble.");
+
+      const real_t gasDensity = BubbleModel< Stencil_T >::bubbles_[id].getDensity();
+
+      // cache a pointer to the block and to the distance field since block->getData<DistanceField>() is expensive
+      static IBlock* blockCache         = nullptr;
+      static DistanceField_T* distField = nullptr;
+
+      // update cache
+      if (block != blockCache)
+      {
+         blockCache = block;
+         distField  = block->getData< DistanceField_T >(distanceFieldSrcID_);
+      }
+
+      const DistanceInfo& distanceInfo = distField->get(cell);
+
+      // get the distance to the nearest neighboring interface cell from a different bubble
+      const real_t dist = distanceInfo.getDistanceToNearestBubble(id, maxDistance_);
+
+      real_t disjoiningPressure = real_c(0.0);
+
+      // computation of disjoining pressure according to the paper from Koerner et al., 2005 where variable
+      // - "disjPressConst_" is called "c_pi"
+      // - "maxDistance_" is called "d_range"
+      // - "dist" is called "d_int"
+      if (dist > maxDistance_)
+      {
+         // disjoining pressure is zero for bubbles outside of range "maxDistance"
+         disjoiningPressure = real_c(0);
+      }
+      else
+      {
+         // disjoining pressure as Koerner et al., 2005
+         disjoiningPressure = disjPressConst_ * real_c(std::fabs(maxDistance_ - dist));
+      }
+
+      return gasDensity - disjoiningPressure;
+   }
+
+   // update the bubble model
+   void update() override
+   {
+      static uint_t step = uint_c(0);
+
+      // update the distance field
+      if (step % distanceFieldUpdateInterval_ == 0) updateDistanceField();
+
+      // update regular bubble model
+      BubbleModel< Stencil_T >::update();
+
+      ++step;
+   }
+
+   ConstBlockDataID getDistanceFieldID() { return distanceFieldSrcID_; }
+
+ protected:
+   void updateDistanceField();
+
+   real_t maxDistance_;    // d_range in the paper from Koerner et al., 2005
+   real_t disjPressConst_; // c_pi in the paper from Koerner et al., 2005
+
+   BlockDataID distanceFieldSrcID_;
+   BlockDataID distanceFieldDstID_;
+
+   uint_t distanceFieldUpdateInterval_;
+}; // class DisjoiningPressureBubbleModel
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "DisjoiningPressureBubbleModel.impl.h"
diff --git a/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h
new file mode 100644
index 000000000..6f8e42218
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h
@@ -0,0 +1,83 @@
+//======================================================================================================================
+//
+//  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 DisjoiningPressureBubbleModel.impl.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Bubble Model with additional pressure term (disjoining pressure).
+//
+//======================================================================================================================
+
+#include "field/AddToStorage.h"
+#include "field/communication/PackInfo.h"
+
+#include "DisjoiningPressureBubbleModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Stencil_T >
+DisjoiningPressureBubbleModel< Stencil_T >::DisjoiningPressureBubbleModel(
+   const std::shared_ptr< StructuredBlockForest >& blockStorage, real_t maxDistance, real_t disjPressConst,
+   bool enableBubbleSplits, uint_t distanceFieldUpdateInterval)
+   : BubbleModel< Stencil_T >(blockStorage, enableBubbleSplits), maxDistance_(maxDistance),
+     disjPressConst_(disjPressConst), distanceFieldSrcID_(field::addToStorage< DistanceField_T >(
+                                         blockStorage, "BubbleDistance Src", DistanceInfo(), field::fzyx, uint_c(1))),
+     distanceFieldDstID_(field::addToStorage< DistanceField_T >(blockStorage, "BubbleDistance Dst", DistanceInfo(),
+                                                                field::fzyx, uint_c(1))),
+     distanceFieldUpdateInterval_(distanceFieldUpdateInterval)
+{
+   BubbleModel< Stencil_T >::bubbleFieldCommunication_.addPackInfo(
+      std::make_shared< field::communication::PackInfo< DistanceField_T > >(distanceFieldSrcID_));
+}
+
+template< typename Stencil_T >
+void DisjoiningPressureBubbleModel< Stencil_T >::updateDistanceField()
+{
+   for (auto blockIt = BubbleModel< Stencil_T >::blockStorage_->begin();
+        blockIt != BubbleModel< Stencil_T >::blockStorage_->end(); ++blockIt)
+   {
+      // get fields
+      const BubbleField_T* const bubbleField =
+         blockIt->template getData< const BubbleField_T >(BubbleModel< Stencil_T >::getBubbleFieldID());
+      DistanceField_T* const distSrcField = blockIt->template getData< DistanceField_T >(distanceFieldSrcID_);
+      DistanceField_T* const distDstField = blockIt->template getData< DistanceField_T >(distanceFieldDstID_);
+
+      WALBERLA_FOR_ALL_CELLS(distDstIt, distDstField, distSrcIt, distSrcField, bubbleIt, bubbleField, {
+         distDstIt->clear();
+
+         // if current cell is part of a bubble, set the distance to zero
+         if (*bubbleIt != INVALID_BUBBLE_ID) { distDstIt->setToZero(*bubbleIt); }
+
+         // loop over src field neighborhood and combine them into this cell
+         for (auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d)
+         {
+            // sum distance with already known distances from neighborhood
+            distDstIt->combine(distSrcIt.neighbor(*d), d.length(), maxDistance_);
+         }
+      }); // WALBERLA_FOR_ALL_CELLS
+
+      // swap pointers to distance fields
+      distSrcField->swapDataPointers(*distDstField);
+   }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/DistanceInfo.cpp b/src/lbm/free_surface/bubble_model/DistanceInfo.cpp
new file mode 100644
index 000000000..db90cae4c
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/DistanceInfo.cpp
@@ -0,0 +1,112 @@
+//======================================================================================================================
+//
+//  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 DistanceInfo.cpp
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute the distances between bubbles.
+//
+//======================================================================================================================
+
+#include "DistanceInfo.h"
+
+#include "core/mpi/BufferDataTypeExtensions.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const DistanceInfo& di)
+{
+   buf << di.bubbleInfos_;
+   return buf;
+}
+
+mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, DistanceInfo& di)
+{
+   di.clear();
+   buf >> di.bubbleInfos_;
+   return buf;
+}
+
+real_t DistanceInfo::getDistanceToNearestBubble(const BubbleID& ownID, real_t maxDistance) const
+{
+   // limit search region to maxDistance
+   real_t minDistance = maxDistance;
+
+   // find the minimum distance
+   for (auto it = bubbleInfos_.begin(); it != bubbleInfos_.end(); it++)
+   {
+      if (minDistance > it->second && it->first != ownID) { minDistance = it->second; }
+   }
+
+   return minDistance;
+}
+
+void DistanceInfo::combine(const DistanceInfo& other, real_t linkDistance, real_t maxDistance)
+{
+   // sum of another cell's distance to the nearest bubble and linkDistance
+   real_t sumDistance = real_c(0);
+
+   // loop over the "bubbleInfos" (std::map) of another cell
+   for (auto it = other.bubbleInfos_.begin(); it != other.bubbleInfos_.end(); it++)
+   {
+      // the other cell's bubble is not in contained in this cell's bubbleInfos, yet
+      if (bubbleInfos_.find(it->first) == bubbleInfos_.end())
+      {
+         sumDistance = it->second + linkDistance;
+
+         // add an entry for the other cell's nearest bubble, if the summed distance is below maxDistance
+         if (sumDistance < maxDistance) { bubbleInfos_[it->first] = sumDistance; }
+      }
+      else // the other cell's bubble is contained in this cell's bubbleInfos
+      {
+         sumDistance   = it->second + linkDistance;
+         real_t& entry = bubbleInfos_[it->first];
+
+         // update the distance for this bubble if it is less than the currently known distance
+         if (entry > sumDistance) { entry = sumDistance; }
+      }
+   }
+}
+
+void DistanceInfo::setToZero(bubble_model::BubbleID id)
+{
+   // "bubbleInfos" is of type std::map, if no entry with key "id" exists, a new entry is created
+   bubbleInfos_[id] = real_c(0.0);
+}
+
+void DistanceInfo::removeZeroDistanceFromLiquid()
+{
+   for (auto it = bubbleInfos_.begin(); it != bubbleInfos_.end(); it++)
+      if (it->second <= real_c(0.0)) { bubbleInfos_.erase(it); }
+}
+
+bool DistanceInfo::operator==(DistanceInfo& other) { return (this->bubbleInfos_ == other.bubbleInfos_); }
+
+void DistanceInfo::print(std::ostream& os) const
+{
+   for (auto it = bubbleInfos_.begin(); it != bubbleInfos_.end(); it++)
+   {
+      os << "( " << it->first << "," << it->second << ")\n";
+   }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/DistanceInfo.h b/src/lbm/free_surface/bubble_model/DistanceInfo.h
new file mode 100644
index 000000000..72efc7ca9
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/DistanceInfo.h
@@ -0,0 +1,100 @@
+//======================================================================================================================
+//
+//  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 DistanceInfo.h
+//! \ingroup bubble_model
+//! \author Daniela Anderl
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute the distances between bubbles.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/mpi/BufferDataTypeExtensions.h"
+
+#include "field/GhostLayerField.h"
+
+#include <map>
+
+#include "BubbleDefinitions.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Compute the distances from other bubbles in a diffusive manner.
+ *
+ * Each bubble spreads its distance until a defined maximum distance is reached. Cells that are within this
+ * maximum distance from one ore more bubbles know the distance from itself to each of these bubbles (or the single
+ * bubble).
+ *
+ **********************************************************************************************************************/
+class DistanceInfo
+{
+ public:
+   DistanceInfo() = default;
+
+   // get the distance to the bubble that is closest to bubble "ownID" within the range of maxDistance
+   real_t getDistanceToNearestBubble(const BubbleID& ownID, real_t maxDistance) const;
+
+   // combine "bubbleInfos" (std::map) of the current cell with "bubbleInfos" of another cell; the distance between
+   // these two cells is linkDistance:
+   // - if the other cell's bubble is not yet registered, insert it and calculate the distance
+   // - if the other cell's bubble is already registered, update the distance
+   void combine(const DistanceInfo& other, real_t linkDistance, real_t maxDistance);
+
+   // set the distance to bubble "id" to zero; create an entry for this bubble, if non yet exists
+   void setToZero(bubble_model::BubbleID id);
+
+   // remove any entry in "bubbleInfos" with distance <= 0
+   void removeZeroDistanceFromLiquid();
+
+   bool operator==(DistanceInfo& other);
+
+   void clear() { bubbleInfos_.clear(); }
+
+   // print the content of bubbleInfos
+   void print(std::ostream& os) const;
+
+ protected:
+   friend mpi::SendBuffer& operator<<(mpi::SendBuffer&, const DistanceInfo&);
+   friend mpi::RecvBuffer& operator>>(mpi::RecvBuffer&, DistanceInfo&);
+
+   std::map< bubble_model::BubbleID, real_t > bubbleInfos_;
+}; // class DistanceInfo
+
+mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const DistanceInfo& di);
+mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, DistanceInfo& di);
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+namespace walberla
+{
+namespace mpi
+{
+template<>
+struct BufferSizeTrait< free_surface::bubble_model::DistanceInfo >
+{
+   static const bool constantSize = false;
+};
+} // namespace mpi
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/FloodFill.h b/src/lbm/free_surface/bubble_model/FloodFill.h
new file mode 100644
index 000000000..b23d4d774
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/FloodFill.h
@@ -0,0 +1,104 @@
+//======================================================================================================================
+//
+//  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 FloodFill.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Flood fill algorithm to identify connected gas volumes as bubbles.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/cell/Cell.h"
+
+#include <queue>
+
+#include "BubbleDefinitions.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Use the flood fill (also called seed fill) algorithm to identify connected gas volumes as bubbles and mark the whole
+ * region belonging to the bubble with a common bubble ID in bubbleField.
+ **********************************************************************************************************************/
+class FloodFillInterface
+{
+ public:
+   virtual ~FloodFillInterface() = default;
+
+   /********************************************************************************************************************
+    * Marks the region of a bubble in the BubbleField
+    *
+    * bubbleField   Field where the bubble is marked. BubbleField must not contain entries equal to newBubbleID.
+    * startCell     Cell that belongs to bubble. It is used as starting point for flood fill algorithm.
+    * newBubbleID   An integer, greater than zero. This value is used as the bubble marker (ID) in the bubble field.
+    * volume        Volume of the gas phase, i.e., the sum of (1-fillLevel) for all cells inside the bubble.
+    * nrOfCells     The number of cells that belong to this bubble.
+    ********************************************************************************************************************/
+   virtual void run(IBlock& block, BlockDataID bubbleIDField, const Cell& startCell, BubbleID newBubbleID,
+                    real_t& volume, uint_t& nrOfCells) = 0;
+}; // class FloodFillInterface
+
+/***********************************************************************************************************************
+ * Use only fill level to identify bubbles.
+ * Problem: isInterfaceFromFillLevel() has to be called to identify interface cells; this is expensive and requires
+ * information from neighboring cells.
+ **********************************************************************************************************************/
+template< typename Stencil_T >
+class FloodFillUsingFillLevel : public FloodFillInterface
+{
+ public:
+   FloodFillUsingFillLevel(ConstBlockDataID fillFieldID) : fillFieldID_(fillFieldID) {}
+   ~FloodFillUsingFillLevel() override = default;
+
+   void run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell, BubbleID newBubbleID, real_t& volume,
+            uint_t& nrOfCells) override;
+
+ private:
+   ConstBlockDataID fillFieldID_;
+}; // FloodFillUsingFillLevel
+
+/***********************************************************************************************************************
+ * Use flag field to identify interface cells which should be faster than using only the fill level.
+ **********************************************************************************************************************/
+template< typename FlagField_T >
+class FloodFillUsingFlagField : public FloodFillInterface
+{
+ public:
+   FloodFillUsingFlagField(ConstBlockDataID fillFieldID, ConstBlockDataID flagFieldID)
+      : fillFieldID_(fillFieldID), flagFieldID_(flagFieldID)
+   {}
+
+   ~FloodFillUsingFlagField() override = default;
+
+   void run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell, BubbleID newBubbleID, real_t& volume,
+            uint_t& nrOfCells) override;
+
+ private:
+   ConstBlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+}; // FloodFillUsingFlagField
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "FloodFill.impl.h"
\ No newline at end of file
diff --git a/src/lbm/free_surface/bubble_model/FloodFill.impl.h b/src/lbm/free_surface/bubble_model/FloodFill.impl.h
new file mode 100644
index 000000000..96f1c0361
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/FloodFill.impl.h
@@ -0,0 +1,216 @@
+//======================================================================================================================
+//
+//  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 FloodFill.impl.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Flood fill algorithm to identify connected gas volumes as bubbles.
+//
+//======================================================================================================================
+
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/InterfaceFromFillLevel.h"
+
+#include "FloodFill.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+// Sets newBubbleID in all cells that belong to the same bubble as startCell. Only uses the fillField but needs to
+// call isInterfaceFromFillLevel() to identify interface cells. This is expensive and requires information from
+// neighboring cells.
+template< typename Stencil_T >
+void FloodFillUsingFillLevel< Stencil_T >::run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell,
+                                               BubbleID newBubbleID, real_t& volume, uint_t& nrOfCells)
+{
+   // get fields
+   BubbleField_T* const bubbleField     = block.getData< BubbleField_T >(bubbleFieldID);
+   const ScalarField_T* const fillField = block.getData< const ScalarField_T >(fillFieldID_);
+
+   WALBERLA_ASSERT_EQUAL(fillField->xyzSize(), bubbleField->xyzSize());
+
+   volume    = real_c(0);
+   nrOfCells = uint_c(0);
+
+   std::queue< Cell > cellQueue;
+   cellQueue.push(startCell);
+
+   std::vector< bool > addedLast;
+
+   using namespace stencil;
+   const int dirs[4]    = { N, S, T, B };
+   const uint_t numDirs = uint_c(4);
+
+   CellInterval fieldSizeInterval = fillField->xyzSize();
+
+   while (!cellQueue.empty())
+   {
+      // process first cell in queue
+      Cell cell = cellQueue.front();
+
+      // go to the beginning of the x-line, i.e., find the minimum x-coordinate that still belongs to this bubble
+      while (cell.x() > cell_idx_c(0) &&
+             bubbleField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()) != newBubbleID &&
+             (fillField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()) < real_c(1.0) ||
+              isInterfaceFromFillLevel< Stencil_T >(*fillField, cell.x() - cell_idx_c(1), cell.y(), cell.z())))
+      {
+         --cell.x();
+      }
+
+      // vector stores whether a cell in a direction from dirs was added already to make sure that for the whole x-line,
+      // only one neighboring cell per direction is added to the cellQueue
+      addedLast.assign(numDirs, false);
+
+      // loop to the end of the x-line and mark any cell that belongs to this bubble
+      while (cell.x() < cell_idx_c(fillField->xSize()) && bubbleField->get(cell) != newBubbleID &&
+             (fillField->get(cell) < real_c(1) || isInterfaceFromFillLevel< Stencil_T >(*fillField, cell)))
+      {
+         // set bubble ID to this cell
+         bubbleField->get(cell) = newBubbleID;
+         volume += real_c(1.0) - fillField->get(cell);
+         nrOfCells++;
+
+         // iterate over all directions in dirs to check which cells in y- and z-direction should be processed next,
+         // i.e., which cells should be added to cellQueue
+         for (uint_t i = uint_c(0); i < numDirs; ++i)
+         {
+            Cell neighborCell(cell.x(), cell.y() + cy[dirs[i]], cell.z() + cz[dirs[i]]);
+
+            // neighboring cell is not inside the field
+            if (!fieldSizeInterval.contains(neighborCell)) { continue; }
+
+            // neighboring cell is part of the bubble
+            if ((fillField->get(neighborCell) < real_c(1) ||
+                 isInterfaceFromFillLevel< Stencil_T >(*fillField, neighborCell)) &&
+                bubbleField->get(neighborCell) != newBubbleID)
+            {
+               // make sure that for the whole x-line, only one neighboring cell per direction is added to the cellQueue
+               if (!addedLast[i])
+               {
+                  addedLast[i] = true;
+
+                  // add neighboring cell to queue such that it serves as starting cell in one of the next iterations
+                  cellQueue.push(neighborCell);
+               }
+            }
+            else { addedLast[i] = false; }
+         }
+
+         ++cell.x();
+      }
+
+      // remove the processed cell from cellQueue
+      cellQueue.pop();
+   }
+}
+
+// Sets newBubbleID in all cells that belong to the same bubble as startCell. Uses the fillField and the flagField
+// and is therefore less expensive than the approach that only uses the fillField.
+template< typename FlagField_T >
+void FloodFillUsingFlagField< FlagField_T >::run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell,
+                                                 BubbleID newBubbleID, real_t& volume, uint_t& nrOfCells)
+{
+   // get fields
+   BubbleField_T* const bubbleField     = block.getData< BubbleField_T >(bubbleFieldID);
+   const ScalarField_T* const fillField = block.getData< const ScalarField_T >(fillFieldID_);
+   const FlagField_T* const flagField   = block.getData< const FlagField_T >(flagFieldID_);
+
+   WALBERLA_ASSERT_EQUAL(fillField->xyzSize(), bubbleField->xyzSize());
+   WALBERLA_ASSERT_EQUAL(fillField->xyzSize(), flagField->xyzSize());
+
+   using flag_t = typename FlagField_T::flag_t;
+   // gasInterfaceFlagMask masks interface and gas cells (using bitwise OR)
+   flag_t gasInterfaceFlagMask = flag_t(0);
+   gasInterfaceFlagMask        = flag_t(gasInterfaceFlagMask | flagField->getFlag(flagIDs::interfaceFlagID));
+   gasInterfaceFlagMask        = flag_t(gasInterfaceFlagMask | flagField->getFlag(flagIDs::gasFlagID));
+
+   volume    = real_c(0);
+   nrOfCells = uint_c(0);
+
+   std::queue< Cell > cellQueue;
+   cellQueue.push(startCell);
+
+   std::vector< bool > addedLast;
+
+   using namespace stencil;
+   const int dirs[4]    = { N, S, T, B };
+   const uint_t numDirs = uint_c(4);
+
+   CellInterval fieldSizeInterval = flagField->xyzSize();
+
+   while (!cellQueue.empty())
+   {
+      // process first cell in queue
+      Cell& cell = cellQueue.front();
+
+      // go to the beginning of the x-line, i.e., find the minimum x-coordinate that still belongs to this bubble
+      while (isPartOfMaskSet(flagField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()), gasInterfaceFlagMask) &&
+             cell.x() > cell_idx_c(0) && bubbleField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()) != newBubbleID)
+      {
+         --cell.x();
+      }
+
+      // vector stores whether a cell in a direction from dirs was added already to make sure that for the whole x-line,
+      // only one neighboring cell per direction is added to the cellQueue
+      addedLast.assign(numDirs, false);
+
+      // loop to the end of the x-line and mark any cell that belongs to this bubble
+      while (isPartOfMaskSet(flagField->get(cell), gasInterfaceFlagMask) && bubbleField->get(cell) != newBubbleID &&
+             cell.x() < cell_idx_c(flagField->xSize()))
+      {
+         // set bubble ID to this cell
+         bubbleField->get(cell) = newBubbleID;
+         volume += real_c(1.0) - fillField->get(cell);
+         nrOfCells++;
+
+         // iterate over all directions in dirs to check which cells in y- and z-direction should be processed next,
+         // i.e., which cells should be added to cellQueue
+         for (uint_t i = uint_c(0); i < numDirs; ++i)
+         {
+            Cell neighborCell(cell.x(), cell.y() + cy[dirs[i]], cell.z() + cz[dirs[i]]);
+
+            // neighboring cell is not inside the field
+            if (!fieldSizeInterval.contains(neighborCell)) { continue; }
+
+            // neighboring cell is part of the bubble
+            if (!isPartOfMaskSet(flagField->get(neighborCell), gasInterfaceFlagMask) &&
+                bubbleField->get(neighborCell) != newBubbleID)
+            {
+               // make sure that for the whole x-line, only one neighboring cell per direction is added to the cellQueue
+               if (!addedLast[i])
+               {
+                  addedLast[i] = true;
+
+                  // add neighboring cell to queue such that it serves as starting cell in one of the next iterations
+                  cellQueue.push(neighborCell);
+               }
+            }
+            else { addedLast[i] = false; }
+         }
+         ++cell.x();
+      }
+
+      // remove the processed cell from cellQueue
+      cellQueue.pop();
+   }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/Geometry.h b/src/lbm/free_surface/bubble_model/Geometry.h
new file mode 100644
index 000000000..7201a632e
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/Geometry.h
@@ -0,0 +1,87 @@
+//======================================================================================================================
+//
+//  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 Geometry.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Geometrical helper functions for the bubble model.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/AABB.h"
+#include "core/math/Vector3.h"
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/FlagField.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Set fill levels in a scalar ghost layer field (GhostLayerField<real_t,1>) using a geometric body object. Specifying
+ * isGas determines whether the body is initialized as gas bubble or liquid drop.
+ *
+ * The function overlapVolume(body, cellmidpoint, dx) has to be defined for the body object.
+ *
+ * The volume fraction of the body is _SUBTRACTED_ from the each cells' current fill level, including ghost layer cells.
+ * Therefore, the field should be initialized with 1 in each cell ( "everywhere fluid").
+ * Then:
+ *  - if a cell is inside a body, its fill level is 0
+ *  - if a cell is completely outside the body, its fill level is not changed and remains 1
+ *  - if a cell is partially inside the sphere, the amount of overlap is subtracted from the current fill level; it can
+ *    not become negative and is limited to 0
+ **********************************************************************************************************************/
+template< typename Body_T >
+void addBodyToFillLevelField(StructuredBlockStorage& blockStorage, BlockDataID fillFieldID, const Body_T& body,
+                             bool isGas);
+
+/***********************************************************************************************************************
+ * Set flag field according to given fill level field.
+ *
+ * FillLevel <=0   : gas flag (and both other flags are removed if set)
+ * FillLevel >=1   : fluid flag (and both other flags are removed if set)
+ * otherwise       : interface flag (and both other flags are removed if set)
+ *
+ * The three FlagUIDs for liquid, gas and interface must already be registered at the flag field.
+ **********************************************************************************************************************/
+template< typename flag_t >
+void setFlagFieldFromFillLevels(FlagField< flag_t >* flagField, const GhostLayerField< real_t, 1 >* fillField,
+                                const FlagUID& liquid, const FlagUID& gas, const FlagUID& interFace);
+
+/***********************************************************************************************************************
+ * Check if interface layer is valid and correctly separates gas and fluid cells:
+ * - gas cell must not have a fluid cell in its (Stencil_T) neighborhood
+ * - fluid cell must not have a gas cell in its (Stencil_T) neighborhood
+ *
+ * The three FlagUIDs for liquid, gas and interface must already be registered at the flag field.
+ **********************************************************************************************************************/
+template< typename flag_t, typename Stencil_T >
+bool checkForValidInterfaceLayer(const FlagField< flag_t >* flagField, const FlagUID& liquid, const FlagUID& gas,
+                                 const FlagUID& interFace, bool printWarnings);
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "Geometry.impl.h"
\ No newline at end of file
diff --git a/src/lbm/free_surface/bubble_model/Geometry.impl.h b/src/lbm/free_surface/bubble_model/Geometry.impl.h
new file mode 100644
index 000000000..9b0fd0dba
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/Geometry.impl.h
@@ -0,0 +1,219 @@
+//======================================================================================================================
+//
+//  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 Geometry.impl.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Geometrical helper functions for the bubble model.
+//
+//======================================================================================================================
+
+#include "core/logging/Logging.h"
+
+#include "field/FlagField.h"
+#include "field/GhostLayerField.h"
+
+#include <limits>
+
+#include "Geometry.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Body_T >
+void addBodyToFillLevelField(StructuredBlockStorage& blockStorage, BlockDataID fillFieldID, const Body_T& body,
+                             bool isGas)
+{
+   const real_t dx = blockStorage.dx();
+
+   for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt)
+   {
+      // get block
+      IBlock* block = &(*blockIt);
+
+      // get fill level field
+      GhostLayerField< real_t, 1 >* const fillField = block->getData< GhostLayerField< real_t, 1 > >(fillFieldID);
+
+      // get the block's bounding box
+      AABB blockBB = block->getAABB();
+
+      // extend the bounding box with the ghost layer
+      blockBB.extend(dx * real_c(fillField->nrOfGhostLayers()));
+
+      // skip blocks that do not intersect the body
+      if (geometry::fastOverlapCheck(body, blockBB) == geometry::COMPLETELY_OUTSIDE) { continue; }
+
+      // get the global coordinates (via the bounding box) of the block's first cell
+      AABB firstCellBB;
+      blockStorage.getBlockLocalCellAABB(*block, fillField->beginWithGhostLayer().cell(), firstCellBB);
+
+      // get the global coordinates of the midpoint of the block's first cell
+      Vector3< real_t > firstCellMidpoint;
+      for (uint_t i = uint_c(0); i < uint_c(3); ++i)
+      {
+         firstCellMidpoint[i] = firstCellBB.min(i) + real_c(0.5) * firstCellBB.size(i);
+      }
+
+      // get the number of ghost layers
+      const uint_t numGl     = fillField->nrOfGhostLayers();
+      cell_idx_t glCellIndex = cell_idx_c(numGl);
+
+      // starting from a block's first cell, iterate over all cells and determine the overlap of the body with each cell
+      // to set the cell's fill level accordingly
+      Vector3< real_t > currentMidpoint;
+      currentMidpoint[2] = firstCellMidpoint[2];
+      for (cell_idx_t z = -glCellIndex; z < cell_idx_c(fillField->zSize() + numGl); ++z, currentMidpoint[2] += dx)
+      {
+         currentMidpoint[1] = firstCellMidpoint[1];
+         for (cell_idx_t y = -glCellIndex; y < cell_idx_c(fillField->ySize() + numGl); ++y, currentMidpoint[1] += dx)
+         {
+            currentMidpoint[0] = firstCellMidpoint[0];
+            for (cell_idx_t x = -glCellIndex; x < cell_idx_c(fillField->xSize() + numGl); ++x, currentMidpoint[0] += dx)
+            {
+               // get the current cell's overlap with the body (full overlap=1, no overlap=0, else in between)
+               real_t overlapFraction = geometry::overlapFraction(body, currentMidpoint, dx);
+
+               // get the current fill level
+               real_t& fillLevel = fillField->get(x, y, z);
+               WALBERLA_ASSERT(fillLevel >= 0 && fillLevel <= 1);
+
+               // initialize a gas bubble (fill level=0)
+               if (isGas)
+               {
+                  // subtract the fraction of the body's overlap from the fill level
+                  fillLevel -= overlapFraction;
+
+                  // limit the fill level such that it does not become negative during initialization
+                  fillLevel = std::max(fillLevel, real_c(0.0));
+               }
+               else // initialize a liquid drop (fill level=1)
+               {
+                  // add the fraction of the body's overlap from the fill level
+                  fillLevel += overlapFraction;
+
+                  // limit the fill level such that it does not become too large during initialization
+                  fillLevel = std::min(fillLevel, real_c(1.0));
+               }
+            }
+         }
+      }
+   }
+}
+
+template< typename flag_t >
+void setFlagFieldFromFillLevels(FlagField< flag_t >* flagField, const GhostLayerField< real_t, 1 >* fillField,
+                                const FlagUID& liquid, const FlagUID& gas, const FlagUID& interFace)
+{
+   WALBERLA_ASSERT(flagField->xyzSize() == fillField->xyzSize());
+
+   // get flags from flag field
+   flag_t liquidFlag    = flagField->getFlag(liquid);
+   flag_t gasFlag       = flagField->getFlag(gas);
+   flag_t interfaceFlag = flagField->getFlag(interFace);
+
+   flag_t allMask = liquidFlag | gasFlag | interfaceFlag;
+
+   using FillField_T = GhostLayerField< real_t, 1 >;
+
+   // iterate over all cells (including ghost layer) in fill level field and flag field
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillField, {
+      const typename FillField_T::ConstPtr fillFieldPtr(*fillField, x, y, z);
+      const typename FlagField< flag_t >::Ptr flagFieldPtr(*flagField, x, y, z);
+
+      // clear liquid, gas, and interface flags in flag field
+      removeMask(flagFieldPtr, allMask);
+
+      if (*fillFieldPtr <= real_c(0)) { addFlag(flagFieldPtr, gasFlag); }
+      else
+      {
+         if (*fillFieldPtr >= real_c(1)) { addFlag(flagFieldPtr, liquidFlag); }
+         else
+         {
+            // add interface flag for fill levels between 0 and 1
+            addFlag(flagFieldPtr, interfaceFlag);
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+template< typename flag_t, typename Stencil_T >
+bool checkForValidInterfaceLayer(const FlagField< flag_t >* flagField, const FlagUID& liquid, const FlagUID& gas,
+                                 const FlagUID& interFace, bool printWarnings)
+{
+   // variable only used when printWarnings is true; avoids premature termination of the search such that any non-valid
+   // cell can be print
+   bool valid = true;
+
+   // get flags
+   flag_t liquidFlag    = flagField->getFlag(liquid);
+   flag_t gasFlag       = flagField->getFlag(gas);
+   flag_t interfaceFlag = flagField->getFlag(interFace);
+
+   // iterate flag field
+   for (auto flagFieldIt = flagField->begin(); flagFieldIt != flagField->end(); ++flagFieldIt)
+   {
+      // check that not more than one flag is set for a cell
+      if (isMaskSet(flagFieldIt, flag_t(liquidFlag | gasFlag)) ||
+          isMaskSet(flagFieldIt, flag_t(liquidFlag | interfaceFlag)) ||
+          isMaskSet(flagFieldIt, flag_t(gasFlag | interfaceFlag)))
+      {
+         if (!printWarnings) { return false; }
+         valid = false;
+         WALBERLA_LOG_WARNING("More than one free surface flag is set in cell " << flagFieldIt.cell() << ".");
+      }
+
+      // if current cell is a gas cell it must not have a fluid cell in its neighborhood
+      if (isFlagSet(flagFieldIt, liquidFlag))
+      {
+         for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+         {
+            if (isFlagSet(flagFieldIt.neighbor(*dir), gasFlag))
+            {
+               if (!printWarnings) { return false; }
+               valid = false;
+               WALBERLA_LOG_WARNING("Fluid cell " << flagFieldIt.cell()
+                                                  << " has a gas cell in its direct neighborhood.");
+            }
+         }
+      }
+      // if current cell is a fluid cell it must not have a gas cell in its neighborhood
+      else
+      {
+         if (isFlagSet(flagFieldIt, gasFlag))
+         {
+            for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir)
+            {
+               if (isFlagSet(flagFieldIt.neighbor(*dir), liquidFlag))
+               {
+                  if (!printWarnings) { return false; }
+                  valid = false;
+                  WALBERLA_LOG_WARNING("Gas cell " << flagFieldIt.cell()
+                                                   << " has a fluid cell in its direct neighborhood.");
+               }
+            }
+         }
+      }
+   }
+
+   return valid;
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/bubble_model/MergeInformation.cpp b/src/lbm/free_surface/bubble_model/MergeInformation.cpp
new file mode 100644
index 000000000..15d6f6bd8
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/MergeInformation.cpp
@@ -0,0 +1,337 @@
+//======================================================================================================================
+//
+//  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 MergeInformation.cpp
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Manage merging of bubbles.
+//
+//======================================================================================================================
+
+#include "MergeInformation.h"
+
+#include "core/mpi/MPIManager.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+MergeInformation::MergeInformation(uint_t numberOfBubbles)
+{
+   hasMerges_ = false;
+   resize(numberOfBubbles);
+}
+
+/***********************************************************************************************************************
+ * Implementation Note (see also MergeInformationTest.cpp):
+ * ---------------------
+ *    renameVec_ = { 0, 1, 2, 3, 4, 5 }
+ *
+ *    position 5 is renamed to the ID at position 3:
+ *    (1)   registerMerge( 5, 3 );
+ *    renameVec_ = { 0, 1, 2, 3, 4, 3 }
+ *
+ *    position 3 is renamed to the ID at position 1:
+ *    (2)   registerMerge(3, 1);
+ *    renameVec_ = { 0, 1, 2, 1, 4, 3 }
+ *
+ *    since position 3 was already renamed to the ID at position 1, registerMerge(1, 0) is called internally and
+ *    position 1 is renamed to the ID at position 0; position 3 is renamed to the ID at position 0
+ *    (3)   registerMerge( 3, 0 );
+ *          -> leads to recursive registerMerge(1, 0);
+ *    renameVec_ = { 0, 0, 2, 0, 4, 3 }
+ *    since position 5 was already renamed to the ID of position 3, registerMerge(3, 2) is called internally; since
+ *    position 3 was already renamed to the ID of position 0, registerMerge(2, 0) is called internally; position 2 is
+ *    renamed to the ID at position 0, position 5 is renamed to the ID at position 0
+ *    (4)   registerMerge( 5, 2)
+ *          -> leads to recursive registerMerge( 3, 2)
+ *                                registerMerge( 0, 2)
+ *    renameVec_ = { 0, 0, 0, 0, 4, 0 }
+ *
+ *  Recursive call in Step(3) is necessary because otherwise the mapping from 3->1 would be "forgotten".
+ *  However, not all transitive renames are resolved by this function as seen in step (3):  5->3->1
+ *  Thus, the additional function resolveTransitiveRenames() is required.
+ **********************************************************************************************************************/
+void MergeInformation::registerMerge(BubbleID b0, BubbleID b1)
+{
+   WALBERLA_ASSERT_LESS(b0, renameVec_.size());
+   WALBERLA_ASSERT_LESS(b1, renameVec_.size());
+
+   // identical bubbles can not be merged
+   if (b0 == b1) { return; }
+
+   // register the merge using hasMerges_
+   hasMerges_ = true;
+
+   // ensure that b0 < b1 (increasing ID ordering is required for some functions later on)
+   if (b1 < b0) { std::swap(b0, b1); }
+
+   WALBERLA_ASSERT_LESS(b0, b1);
+
+   // if bubble b1 is also marked for merging with another bubble, e.g., bubble b2
+   if (isRenamed(b1))
+   {
+      // mark bubble b2 for merging with b0 (b2 = renameVec_[b1])
+      registerMerge(b0, renameVec_[b1]);
+
+      // mark bubble b1 for merging with bubble b0 or bubble b2 (depending on the ID order found in
+      // registerMerge(b0,b2) above)
+      renameVec_[b1] = renameVec_[renameVec_[b1]];
+   }
+   else
+   {
+      // mark bubble b1 for merging with bubble b0
+      renameVec_[b1] = b0;
+   }
+}
+
+void MergeInformation::mergeAndReorderBubbleVector(std::vector< Bubble >& bubbles)
+{
+   // no bubble merge possible with less than 2 bubbles
+   if (bubbles.size() < uint_c(2)) { return; }
+
+   // rename merged bubbles (always keep smallest bubble ID)
+   resolveTransitiveRenames();
+
+#ifndef NDEBUG
+   uint_t numberOfMerges = countNumberOfRenames();
+#endif
+
+   WALBERLA_ASSERT_EQUAL(bubbles.size(), renameVec_.size());
+   WALBERLA_ASSERT_EQUAL(renameVec_[0], 0);
+
+   for (size_t i = uint_c(0); i < bubbles.size(); ++i)
+   {
+      // any entry that does not point to itself needs to be merged
+      if (renameVec_[i] != i)
+      {
+         WALBERLA_ASSERT_LESS(renameVec_[i], i);
+
+         // merge bubbles
+         bubbles[renameVec_[i]].merge(bubbles[i]);
+      }
+   }
+
+   // create temporary vector with "index = bubble ID" to store the exchanged bubble IDs
+   std::vector< BubbleID > exchangeVector(bubbles.size());
+   for (size_t i = uint_c(0); i < exchangeVector.size(); ++i)
+   {
+      exchangeVector[i] = BubbleID(i);
+   }
+
+   // gapPointer searches for the renamed bubbles in increasing order; vector entries found by gapPointer are "gaps" and
+   // can be overwritten
+   uint_t gapPointer = uint_c(1);
+   while (gapPointer < bubbles.size() && !isRenamed(gapPointer)) // find first renamed bubble
+   {
+      ++gapPointer;
+   }
+
+   // lastPointer searches for not-renamed bubbles in decreasing order; vector entries found by lastPointer remain in
+   // the vector and will be copied to the gaps
+   uint_t lastPointer = uint_c(renameVec_.size() - 1);
+   while (isRenamed(lastPointer) && lastPointer > uint_c(0)) // find last non-renamed bubble
+   {
+      --lastPointer;
+   }
+
+   while (lastPointer > gapPointer)
+   {
+      // exchange the last valid (non-renamed) bubble ID with the first non-valid (renamed) bubble ID; anything
+      // gapPointer points to will be deleted later; this reorders the bubble vector
+      std::swap(bubbles[gapPointer], bubbles[lastPointer]);
+
+      // store the above exchange
+      exchangeVector[lastPointer] = BubbleID(gapPointer);
+      exchangeVector[gapPointer]  = BubbleID(lastPointer);
+
+      // update lastPointer, i.e., find next non-renamed bubble
+      do
+      {
+         --lastPointer;
+         // important condition: "lastPointer > gapPointer" since gapPointer is valid now
+      } while (isRenamed(lastPointer) && lastPointer > gapPointer);
+
+      // update gapPointer, i.e., find next renamed bubble
+      do
+      {
+         ++gapPointer;
+      } while (gapPointer < bubbles.size() && !isRenamed(gapPointer));
+   }
+
+   // shrink bubble vector (any element after lastPointer is not valid and can be removed)
+   uint_t newSize = lastPointer + uint_c(1);
+   WALBERLA_ASSERT_EQUAL(newSize, bubbles.size() - numberOfMerges);
+   WALBERLA_ASSERT_LESS_EQUAL(newSize, bubbles.size());
+   bubbles.resize(newSize);
+
+   // update renameVec_ with exchanged bubble IDs with the highest unnecessary bubble IDs being dropped; this ensures
+   // that bubble IDs are always numbered continuously from 0 upwards
+   for (size_t i = 0; i < renameVec_.size(); ++i)
+   {
+      renameVec_[i] = exchangeVector[renameVec_[i]];
+      WALBERLA_ASSERT_LESS(renameVec_[i], newSize);
+   }
+}
+
+void MergeInformation::communicateMerges()
+{
+   // merges can only be communicated if they occurred and are registered in renameVec_
+   if (renameVec_.empty()) { return; }
+
+   // rename process local bubble IDs
+   resolveTransitiveRenames();
+
+   // globally combine all rename vectors
+   int numProcesses = MPIManager::instance()->numProcesses();
+   std::vector< BubbleID > allRenameVectors(renameVec_.size() * uint_c(numProcesses));
+   WALBERLA_MPI_SECTION()
+   {
+      MPI_Allgather(&renameVec_[0], int_c(renameVec_.size()), MPITrait< BubbleID >::type(), &allRenameVectors[0],
+                    int_c(renameVec_.size()), MPITrait< BubbleID >::type(), MPI_COMM_WORLD);
+   }
+
+   // check for inter-process bubble merges
+   for (size_t i = renameVec_.size() - 1; i > 0; --i)
+      for (int process = 0; process < numProcesses; ++process)
+      {
+         if (process == MPIManager::instance()->rank()) { continue; } // local merges have already been treated
+
+         size_t idx = uint_c(process) * renameVec_.size() + i;
+
+         // register inter-process bubble merge (this updated renameVec_)
+         if (allRenameVectors[idx] != i) { registerMerge(allRenameVectors[idx], BubbleID(i)); }
+      }
+
+   // rename global bubble IDs
+   resolveTransitiveRenames();
+}
+
+void MergeInformation::renameOnBubbleField(BubbleField_T* bubbleField) const
+{
+   WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(bubbleField, {
+      const typename BubbleField_T::Ptr bubblePtr(*bubbleField, x, y, z);
+      if (*bubblePtr != INVALID_BUBBLE_ID) { *bubblePtr = renameVec_[*bubblePtr]; }
+   }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+}
+
+void MergeInformation::resizeAndClear(uint_t numberOfBubbles)
+{
+   renameVec_.resize(numberOfBubbles);
+   for (uint_t i = uint_c(0); i < numberOfBubbles; ++i)
+   {
+      renameVec_[i] = BubbleID(i);
+   }
+
+   hasMerges_ = false;
+}
+
+void MergeInformation::resize(uint_t numberOfBubbles)
+{
+   if (numberOfBubbles > renameVec_.size())
+   {
+      renameVec_.reserve(numberOfBubbles);
+      for (size_t i = renameVec_.size(); i < numberOfBubbles; ++i)
+      {
+         renameVec_.push_back(BubbleID(i));
+      }
+   }
+}
+
+void MergeInformation::resolveTransitiveRenames()
+{
+   WALBERLA_ASSERT_GREATER(renameVec_.size(), 0);
+   WALBERLA_ASSERT_EQUAL(renameVec_[0], 0);
+
+   for (size_t i = renameVec_.size() - 1; i > 0; --i)
+   {
+      // create new bubble ID for each entry in renameVec_
+      BubbleID& newBubbleID = renameVec_[i];
+
+      // example 1: "renameVec_[4] = 2" means that bubble 4 has merged with bubble 2
+      // => bubble 4 is renamed to bubble 2 (always the smaller ID is kept)
+      // example 2: "renameVec_[4] = 2" and "renameVec_[2] = 1" means above and that bubble 2 has merged with bubble 1
+      // => bubble 4 should finally be renamed to bubble 1
+
+      // this loop ensures that renaming is correct even if multiple bubbles have merged (as in example 2 from above)
+      while (renameVec_[newBubbleID] != newBubbleID)
+      {
+         newBubbleID = renameVec_[newBubbleID];
+      }
+   }
+
+   WALBERLA_ASSERT(transitiveRenamesResolved()); // ensure that renaming was resolved correctly
+}
+
+bool MergeInformation::transitiveRenamesResolved() const
+{
+   for (size_t i = 0; i < renameVec_.size(); ++i)
+   {
+      // A = renameVec_[i]
+      // B = renameVec_[A]
+      // A must be equal to B after bubble renaming
+      if (renameVec_[i] != renameVec_[renameVec_[i]]) { return false; }
+   }
+
+   // ensure that no entry points to an element that has been renamed
+   for (size_t i = 0; i < renameVec_.size(); ++i)
+   {
+      if (renameVec_[i] != i) // bubble at entry i has been renamed
+      {
+         for (size_t j = 0; j < renameVec_.size(); ++j)
+         {
+            // renameVec_[j] points to entry i although i has been renamed
+            if (i != j && renameVec_[j] == i) { return false; }
+         }
+      }
+   }
+
+   return true;
+}
+
+uint_t MergeInformation::countNumberOfRenames() const
+{
+   uint_t renames = uint_c(0);
+   for (size_t i = 0; i < renameVec_.size(); ++i)
+   {
+      // only bubbles with "bubble ID != index" have been renamed
+      if (renameVec_[i] != i) { ++renames; }
+   }
+
+   return renames;
+}
+
+void MergeInformation::print(std::ostream& os) const
+{
+   os << "Merge Information: ";
+
+   for (size_t i = 0; i < renameVec_.size(); ++i)
+      os << "( " << i << " -> " << renameVec_[i] << " ), ";
+
+   os << std::endl;
+}
+
+std::ostream& operator<<(std::ostream& os, const MergeInformation& mi)
+{
+   mi.print(os);
+   return os;
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/MergeInformation.h b/src/lbm/free_surface/bubble_model/MergeInformation.h
new file mode 100644
index 000000000..d8ce84faa
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/MergeInformation.h
@@ -0,0 +1,102 @@
+//======================================================================================================================
+//
+//  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 MergeInformation.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Manage merging of bubbles.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include <iostream>
+
+#include "Bubble.h"
+#include "BubbleDefinitions.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ * Class for managing bubble merging.
+ *
+ * - assumption: each process holds a synchronized, global vector of bubbles
+ * - each process creates a new MergeInformation class, which internally holds a mapping from BubbleID -> BubbleID
+ * - then during the time step, several merges can be registered using registerMerge() member function
+ * - this information is then communicated between all processes using communicateMerges()
+ * - Then the merges are executed on each process locally. This means that bubble volumes have to be added,
+ *   and bubbles have to be deleted. To still have a continuous array, the last elements are copied to the place where
+ *   the deleted bubbles have been -> renaming of bubbles.
+ * - the renaming has to be done also in the bubble field using renameOnBubbleField()
+ **********************************************************************************************************************/
+class MergeInformation
+{
+ public:
+   MergeInformation(uint_t numberOfBubbles = uint_c(0));
+
+   void registerMerge(BubbleID b0, BubbleID b1);
+
+   // globally communicate bubble merges and rename bubble IDs accordingly in renameVec_
+   void communicateMerges();
+
+   // merge bubbles according to renameVec_, and reorder and shrink the bubble vector such that bubble IDs are always
+   // numbered continuously from 0 upwards
+   void mergeAndReorderBubbleVector(std::vector< Bubble >& bubbles);
+
+   // rename all bubbles in the bubble field according to renameVec_
+   void renameOnBubbleField(BubbleField_T* bf) const;
+
+   void resizeAndClear(uint_t numberOfBubbles);
+
+   bool hasMerges() const { return hasMerges_; }
+
+   void print(std::ostream& os) const;
+
+   // vector for tracking bubble renaming:
+   // - "renameVec_[4] = 2": bubble 4 has merged with bubble 2, i.e., any 4 can be replaced by 2 in the bubble field
+   // - "renameVec_[i] <= i" for all i: when two bubbles merge, the smaller bubble ID is chosen for the resulting bubble
+   std::vector< BubbleID > renameVec_;
+
+ private:
+   // adapt size of permutation vector, and init new elements with "identity"
+   void resize(uint_t numberOfBubbles);
+
+   inline bool isRenamed(size_t i) const { return renameVec_[i] != i; }
+
+   // ensure that renaming of bubble IDs is correct even if multiple bubbles merge during the same time step
+   void resolveTransitiveRenames();
+
+   // check whether renaming has been done correctly, i.e., check the correctness of renameVec_
+   bool transitiveRenamesResolved() const;
+
+   uint_t countNumberOfRenames() const;
+
+   // signals that merges have to be treated in this time step, true whenever renameVec_ is not the identity mapping
+   bool hasMerges_;
+
+   // friend function used in unit tests (MergeInformationTest)
+   friend void checkRenameVec(const MergeInformation& mi, const std::vector< BubbleID >& vecCompare);
+}; // MergeInformation
+
+std::ostream& operator<<(std::ostream& os, const MergeInformation& mi);
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp
new file mode 100644
index 000000000..f36c504a4
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp
@@ -0,0 +1,232 @@
+//======================================================================================================================
+//
+//  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 NewBubbleCommunication.cpp
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Communication for the creation of new bubbles.
+//
+//======================================================================================================================
+
+#include "NewBubbleCommunication.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+class CommunicatedNewBubbles
+{
+ public:
+   CommunicatedNewBubbles(size_t nrOfBubblesBefore, size_t locallyCreatedBubbles, size_t localOffset)
+      : nextBubbleCtr_(0)
+   {
+      temporalIDToNewIDMap_.resize(locallyCreatedBubbles);
+      nrOfBubblesBefore_         = numeric_cast< BubbleID >(nrOfBubblesBefore);
+      nrOfLocallyCreatedBubbles_ = numeric_cast< BubbleID >(locallyCreatedBubbles);
+      localOffset_               = numeric_cast< BubbleID >(localOffset);
+   }
+
+   void storeNextBubble(BubbleID newBubbleID, Bubble& bubbleToStoreTo)
+   {
+      // store newBubbleID in map
+      if (wasNextBubbleCreatedOnThisProcess()) { temporalIDToNewIDMap_[nextBubbleCtr_ - localOffset_] = newBubbleID; }
+
+      // store bubble
+      recvBuffer_ >> bubbleToStoreTo;
+
+      ++nextBubbleCtr_;
+   }
+
+   // return whether there are new bubbles that have to be processed
+   bool hasMoreBubbles() const { return !recvBuffer_.isEmpty(); }
+
+   // map the preliminary bubble IDs to global new bubble IDs
+   void mapTemporalToNewBubbleID(BubbleID& id) const
+   {
+      if (id >= nrOfBubblesBefore_) { id = temporalIDToNewIDMap_[id - nrOfBubblesBefore_]; }
+      // else: bubble ID is already mapped correctly
+   }
+
+   mpi::RecvBuffer& recvBuffer() { return recvBuffer_; }
+
+ private:
+   // return whether the next bubble was created on this process
+   bool wasNextBubbleCreatedOnThisProcess() const
+   {
+      // return whether counter of current bubble is between offset and offset+numberOfLocallyCreatedBubbles
+      return nextBubbleCtr_ >= localOffset_ && nextBubbleCtr_ < localOffset_ + nrOfLocallyCreatedBubbles_;
+   }
+
+   mpi::RecvBuffer recvBuffer_;
+
+   BubbleID nrOfBubblesBefore_;         // size of global bubble array without new/deleted bubbles
+   BubbleID nrOfLocallyCreatedBubbles_; // number of bubbles that were added on this process
+   BubbleID localOffset_;               // the offset of the locally created bubbles in recvBuffer
+   BubbleID nextBubbleCtr_;             // number of bubbles that have already been stored with storeNextBubble()
+
+   std::vector< BubbleID > temporalIDToNewIDMap_; // maps the temporal bubble IDs to new global bubble IDs
+};                                                // class CommunicatedNewBubbles
+
+std::shared_ptr< CommunicatedNewBubbles > NewBubbleCommunication::communicate(size_t nrOfBubblesBefore)
+{
+   std::shared_ptr< MPIManager > mpiManager = MPIManager::instance();
+
+   // cast number of every process' new bubbles to int (int is used in MPI_Allgather)
+   int localNewBubbles = int_c(bubblesToCreate_.size());
+
+   std::vector< int > numNewBubblesPerProcess(uint_t(mpiManager->numProcesses()), 0);
+
+   // communicate, i.e., gather each process' number of new bubbles
+   WALBERLA_MPI_SECTION()
+   {
+      MPI_Allgather(&localNewBubbles, 1, MPITrait< int >::type(), &numNewBubblesPerProcess[0], 1,
+                    MPITrait< int >::type(), MPI_COMM_WORLD);
+   }
+   WALBERLA_NON_MPI_SECTION() { numNewBubblesPerProcess[0] = localNewBubbles; }
+
+   // offsetVector[i] is the number of new bubbles created on processes with rank smaller than i
+   std::vector< int > offsetVector;
+   offsetVector.push_back(0);
+   for (size_t i = 0; i < numNewBubblesPerProcess.size() - 1; ++i)
+   {
+      offsetVector.push_back(offsetVector.back() + numNewBubblesPerProcess[i]);
+   }
+
+   WALBERLA_ASSERT_EQUAL(offsetVector.size(), numNewBubblesPerProcess.size());
+
+   // get each process' individual offset
+   size_t offset = uint_c(offsetVector[uint_c(mpiManager->worldRank())]);
+
+   // get the total number of new bubbles
+   size_t numNewBubbles = uint_c(offsetVector.back() + numNewBubblesPerProcess.back());
+
+   mpi::SendBuffer sendBuffer;
+
+   size_t bytesPerBubble = mpi::BufferSizeTrait< Bubble >::size;
+
+   // reserve space for a bubble's size in bytes (clean way to create send buffer)
+   sendBuffer.reserve(bytesPerBubble);
+
+   // pack local new bubble data into sendBuffer
+   for (auto i = bubblesToCreate_.begin(); i != bubblesToCreate_.end(); ++i)
+   {
+      sendBuffer << *i;
+   }
+
+   // compute the byte size of numNewBubblesPerProcess[i] and offsetVector[i]
+   for (uint_t i = uint_c(0); i < uint_c(mpiManager->numProcesses()); ++i)
+   {
+      numNewBubblesPerProcess[i] *= int_c(bytesPerBubble);
+      offsetVector[i] *= int_c(bytesPerBubble);
+   }
+
+   // create new CommunicatedNewBubbles object to store the gathered new bubbles (see below)
+   auto result = std::make_shared< CommunicatedNewBubbles >(nrOfBubblesBefore, bubblesToCreate_.size(), offset);
+
+   // resize recvBuffer for storing the total number of new bubbles
+   result->recvBuffer().resize(bytesPerBubble * numNewBubbles);
+
+   WALBERLA_MPI_SECTION()
+   {
+      // communicate, i.e., gather all locally added bubbles (and store them in the new CommunicatedNewBubbles object)
+      MPI_Allgatherv(sendBuffer.ptr(), int_c(sendBuffer.size()), MPI_UNSIGNED_CHAR, result->recvBuffer().ptr(),
+                     &numNewBubblesPerProcess[0], &offsetVector[0], MPI_UNSIGNED_CHAR, MPI_COMM_WORLD);
+   }
+   WALBERLA_NON_MPI_SECTION() { result->recvBuffer() = sendBuffer; }
+
+   // all bubbles have been "created" and vector can be cleared
+   bubblesToCreate_.clear();
+
+   return result;
+}
+
+void NewBubbleCommunication::communicateAndApply(std::vector< Bubble >& vectorToAddBubbles,
+                                                 StructuredBlockStorage& blockStorage, BlockDataID bubbleFieldID)
+{
+   // communicate
+   std::shared_ptr< CommunicatedNewBubbles > newBubbleContainer = communicate(vectorToAddBubbles.size());
+
+   // append new bubbles to vectorToAddBubbles
+   BubbleID nextBubbleID = numeric_cast< BubbleID >(vectorToAddBubbles.size());
+   while (newBubbleContainer->hasMoreBubbles())
+   {
+      // create and append a new empty bubble
+      vectorToAddBubbles.emplace_back();
+
+      // update the empty bubble with the received bubble
+      newBubbleContainer->storeNextBubble(nextBubbleID, vectorToAddBubbles.back());
+
+      ++nextBubbleID;
+   }
+
+   // rename bubble IDs
+   for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt)
+   {
+      BubbleField_T* const bubbleField = blockIt->getData< BubbleField_T >(bubbleFieldID);
+      for (auto fieldIt = bubbleField->begin(); fieldIt != bubbleField->end(); ++fieldIt)
+      {
+         // rename an existing bubble ID in bubbleField
+         if (*fieldIt != INVALID_BUBBLE_ID) { newBubbleContainer->mapTemporalToNewBubbleID(*fieldIt); }
+      }
+   }
+}
+
+void NewBubbleCommunication::communicateAndApply(std::vector< Bubble >& vectorToAddBubbles,
+                                                 const std::vector< bool >& bubblesToOverwrite,
+                                                 StructuredBlockStorage& blockStorage, BlockDataID bubbleFieldID)
+{
+   WALBERLA_ASSERT_EQUAL(bubblesToOverwrite.size(), vectorToAddBubbles.size());
+
+   // communicate
+   std::shared_ptr< CommunicatedNewBubbles > newBubbleContainer = communicate(vectorToAddBubbles.size());
+
+   // overwrite existing bubbles with new bubbles; there must be at least the same number of new bubbles than in
+   // bubblesToOverwrite
+   for (size_t i = 0; i < bubblesToOverwrite.size(); ++i)
+   {
+      if (bubblesToOverwrite[i]) { newBubbleContainer->storeNextBubble(BubbleID(i), vectorToAddBubbles[i]); }
+   }
+
+   // append remaining new bubbles to vectorToAddBubbles
+   BubbleID nextBubbleID = numeric_cast< BubbleID >(vectorToAddBubbles.size());
+   while (newBubbleContainer->hasMoreBubbles())
+   {
+      // create and append a new empty bubble
+      vectorToAddBubbles.emplace_back();
+
+      // update the empty bubble with the received bubble
+      newBubbleContainer->storeNextBubble(nextBubbleID, vectorToAddBubbles.back());
+
+      ++nextBubbleID;
+   }
+
+   // rename bubble IDs
+   for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt)
+   {
+      BubbleField_T* const bubbleField = blockIt->getData< BubbleField_T >(bubbleFieldID);
+      for (auto fieldIt = bubbleField->begin(); fieldIt != bubbleField->end(); ++fieldIt)
+      {
+         // rename an existing bubble ID in bubbleField
+         if (*fieldIt != INVALID_BUBBLE_ID) { newBubbleContainer->mapTemporalToNewBubbleID(*fieldIt); }
+      }
+   }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/NewBubbleCommunication.h b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.h
new file mode 100644
index 000000000..0f5e83123
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.h
@@ -0,0 +1,126 @@
+//======================================================================================================================
+//
+//  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 NewBubbleCommunication.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Communication for the creation of new bubbles.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/logging/Logging.h"
+
+#include <vector>
+
+#include "Bubble.h"
+#include "BubbleDefinitions.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+// forward declaration of internal data structure
+class CommunicatedNewBubbles;
+
+/***********************************************************************************************************************
+ * Communication for the creation of new bubbles.
+ *
+ *  - Manages the distribution of newly created bubbles.
+ *  - Bubbles must have the same BubbleID everywhere, since BubbleID is an index in the global bubble array. Thus, a
+ *    problem arises when two (or more) processes add a new bubble in the same time step.
+ *  - Every bubble is first created via this class and assigned a temporary BubbleID that is not used, yet.
+ *  - These data are then synchronized in a communication step.
+ *  - New bubbles are sorted by process rank such that there is then a sorted array of new bubbles that is identical on
+ *    each process.
+ *  - This vector of new bubbles can then be appended/inserted into the global bubble array.
+ *  - Finally, temporary bubble IDs in the bubble field are renamed to global IDs.
+ *
+ *  Usage example:
+ *  \code
+     NewBubbleCommunication newBubbleComm;
+
+     //the following lines are usually different on different processes:
+     BubbleID newID     = newBubbleComm.createBubble( Bubble ( ... ) );
+     BubbleID anotherID = newBubbleComm.createBubble( Bubble ( ... ) );
+     // use the returned temporary bubble IDs in the BubbleField
+
+    // The following call, updates the global bubble vector and the bubble field.
+    // Here the new bubbles are appended to the vector and the temporary bubble IDs in the bubble field are
+    // mapped to global IDs.
+    newBubbleComm.communicateAndApply( globalBubbleVector, blockStorage, bubbleFieldID );
+
+ *  \endcode
+ *
+ *
+ **********************************************************************************************************************/
+class NewBubbleCommunication
+{
+ public:
+   explicit NewBubbleCommunication(uint_t nrOfExistingBubbles = uint_c(0))
+   {
+      // initialize the temporary bubble ID with the currently registered number of bubbles
+      nextFreeBubbleID_ = numeric_cast< BubbleID >(nrOfExistingBubbles);
+   }
+
+   // add a new bubble which gets assigned a temporary bubble ID that should be stored in bubbleField; temporary IDs are
+   // converted to valid global IDs in communicateAndApply() later
+   BubbleID createBubble(const Bubble& newBubble)
+   {
+      bubblesToCreate_.push_back(newBubble);
+
+      // get a temporary bubble ID
+      return nextFreeBubbleID_++;
+   }
+
+   // get the temporary bubble ID that is returned at the next call to createBubble()
+   BubbleID nextFreeBubbleID() const { return nextFreeBubbleID_; }
+
+   /********************************************************************************************************************
+    * Communicate all locally added bubbles and append them to a global bubble array. Convert the preliminary bubble IDs
+    * in the bubble field to global IDs.
+    *******************************************************************************************************************/
+   void communicateAndApply(std::vector< Bubble >& vectorToAddBubbles, StructuredBlockStorage& blockStorage,
+                            BlockDataID bubbleFieldID);
+
+   /********************************************************************************************************************
+    * Communicate all locally added bubbles and delete bubbles marked inside a boolean array. Convert the preliminary
+    * bubble IDs in the bubble field to global IDs. Instead of appending all new bubbles, first all "holes" in the
+    * globalVector are filled
+    *
+    * \Important: - the bubblesToOverwrite vector must be the same on all processes, i.e., it has to be communicated/
+    *               reduced before calling this function
+    *             - bubblesToCreate_.size() > number of "true"s in bubblesToOverwrite, i.e., more new bubbles have to be
+    *               created than deleted
+    *******************************************************************************************************************/
+   void communicateAndApply(std::vector< Bubble >& vectorToAddBubbles, const std::vector< bool >& bubblesToOverwrite,
+                            StructuredBlockStorage& blockStorage, BlockDataID bubbleFieldID);
+
+ private:
+   // communicate all new local bubbles and store the (MPI) gathered new bubbles in the returned object;
+   // bubblesToCreate_ is cleared
+   std::shared_ptr< CommunicatedNewBubbles > communicate(size_t nrOfBubblesBefore);
+
+   BubbleID nextFreeBubbleID_; // temporary bubble ID
+   std::vector< Bubble > bubblesToCreate_;
+}; // class NewBubbleCommunication
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/bubble_model/RegionalFloodFill.h b/src/lbm/free_surface/bubble_model/RegionalFloodFill.h
new file mode 100644
index 000000000..c0f75536a
--- /dev/null
+++ b/src/lbm/free_surface/bubble_model/RegionalFloodFill.h
@@ -0,0 +1,254 @@
+//======================================================================================================================
+//
+//  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 RegionalFloodFill.h
+//! \ingroup bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Flood fill algorithm in a constrained neighborhood around a cell.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "field/GhostLayerField.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+/***********************************************************************************************************************
+ *
+ * Flood fill algorithm in a constrained neighborhood around a cell.
+ *
+ * This algorithm is used to check if a bubble has split up. A split detection is triggered
+ * when an interface cell becomes a fluid cell. The normal case is to check in a 1-neighborhood if
+ * the bubble has split up: ("-" means no bubble, numbers indicate bubble IDs)
+ * Example:  0 0 -
+ *           - x -
+ *           0 0 -
+ * In this case, the bubble has split up.
+ *
+ * This check in a 1-neighborhood is performed by the function BubbleModel::mapNeighborhood().
+ *
+ * To look only in 1-neighborhoods has the drawback that we trigger splits when no split has occurred. This is OK, since
+ * later it is detected that the bubble has not really split. However, this process requires communication and is
+ * therefore expensive.
+ *
+ * The RegionalFloodFill has the task to rule out splits before they are triggered by looking at larger neighborhoods.
+ *
+ * Example: (same as above but with larger neighborhood)
+ *          0 0 0 - -
+ *          0 0 0 - -
+ *          0 - x - -
+ *          0 0 0 - -
+ *          0 0 0 - -
+ * By looking at the 2-neighborhood we see that the bubble has not really split up.
+ * The RegionalFloodFill needs as input the current cell (marked with x), a direction to start, e.g., N, and the size of
+ * the neighborhood to look at, e.g., 2. In this case, it creates a small 5x5x5 field where a flood fill is executed.
+ * The field might be smaller if the cell is near the boundary, as the algorithm is capable of detecting boundaries.
+ *
+ **********************************************************************************************************************/
+template< typename T, typename Stencil_T >
+class RegionalFloodFill
+{
+ public:
+   RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell,
+                     stencil::Direction startDirection, const T& searchValue, cell_idx_t neighborhood = 2);
+
+   RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell, const T& emptyValue,
+                     cell_idx_t neighborhood = 2);
+
+   ~RegionalFloodFill() { delete workingField_; }
+
+   inline bool connected(stencil::Direction d);
+   inline bool connected(cell_idx_t xOff, cell_idx_t yOff, cell_idx_t zOff);
+
+   const Field< bool, 1 >& workingField() const { return *workingField_; }
+
+ protected:
+   inline bool cellInsideAndNotMarked(const Cell& cell);
+
+   void runFloodFill(const Cell& startCell, stencil::Direction startDirection, const T& searchValue,
+                     cell_idx_t neighborhood);
+
+   const GhostLayerField< T, 1 >* externField_;
+   Field< bool, 1 >* workingField_;
+
+   // the size of the working field is 2*neighborhood+1 for storing startCell (+1) and neighborhood in each direction;
+   // boundaries in externField_ reduce the size of workingField_ accordingly
+   CellInterval workingFieldSize_;
+
+   T searchValue_;
+   Cell offset_; // offset of workingField_, allows coordinate transformations:
+                 // externFieldCoordinates = workingFieldCoordinates + offset
+   Cell startCellInWorkingField_;
+}; // class RegionalFloodFill
+
+// iterate over all neighboring cells and run flood fill with starting direction of first neighboring cell whose value
+// is not equal to emptyValue
+template< typename T, typename Stencil_T >
+RegionalFloodFill< T, Stencil_T >::RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell,
+                                                     const T& emptyValue, cell_idx_t neighborhood)
+   : externField_(externField), workingField_(nullptr)
+{
+   for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+   {
+      const T& neighborValue = externField_->getNeighbor(startCell, *d);
+      if (neighborValue != emptyValue)
+      {
+         // run flood fill with starting direction of first non-empty neighboring cell
+         runFloodFill(startCell, *d, neighborValue, neighborhood);
+         return;
+      }
+   }
+
+   WALBERLA_ASSERT(false); // should not happen, only empty values in neighborhood
+}
+
+/***********************************************************************************************************************
+ * Create a small overlay field in given neighborhood and execute a flood fill in this small field.
+ *
+ * The flood fill starts in startCell and startDirection. The neighboring cell of startCell in startDirection has to be
+ * set to searchValue. The size of the neighborhood is specified by neighborhood. In case of nearby boundary in a
+ * certain direction, the size of the neighborhood is automatically reduced in this direction.
+ **********************************************************************************************************************/
+template< typename T, typename Stencil_T >
+RegionalFloodFill< T, Stencil_T >::RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell,
+                                                     stencil::Direction startDirection, const T& searchValue,
+                                                     cell_idx_t neighborhood)
+   : externField_(externField), workingField_(nullptr)
+{
+   runFloodFill(startCell, startDirection, searchValue, neighborhood);
+}
+
+template< typename T, typename Stencil_T >
+void RegionalFloodFill< T, Stencil_T >::runFloodFill(const Cell& startCell, stencil::Direction startDirection,
+                                                     const T& searchValue, cell_idx_t neighborhood)
+{
+   searchValue_ = searchValue;
+
+   const cell_idx_t externFieldGhostLayers = cell_idx_c(externField_->nrOfGhostLayers());
+
+   // size of workingField_ is defined by the number of cells around startCell
+   cell_idx_t extendLower[3]; // number of cells in negative x-, y-, and z-direction of startCell
+   cell_idx_t extendUpper[3]; // number of cells in negative x-, y-, and z-direction of startCell
+
+   // determine the size of the working field with respect to boundaries of externField_
+   for (uint_t i = uint_c(0); i < uint_c(3); ++i)
+   {
+      const cell_idx_t minCoord = -externFieldGhostLayers;
+      const cell_idx_t maxCoord = cell_idx_c(externField_->size(i)) - cell_idx_c(1) + externFieldGhostLayers;
+
+      // in case of nearby boundaries in externField_, the size of workingField_ is adjusted such that these boundaries
+      // are respected
+      extendLower[i] = std::min(neighborhood, startCell[i] - minCoord);
+      extendUpper[i] = std::min(neighborhood, maxCoord - startCell[i]);
+   }
+
+   // offset_ can be used to transform coordinates from externField_ to coordinates of workingField_
+   offset_ = Cell(startCell[0] - extendLower[0], startCell[1] - extendLower[1], startCell[2] - extendLower[2]);
+
+   startCellInWorkingField_ = startCell - offset_;
+
+   // create workingField_
+   workingField_ = new Field< bool, 1 >(uint_c(1) + uint_c(extendLower[0] + extendUpper[0]),
+                                        uint_c(1) + uint_c(extendLower[1] + extendUpper[1]),
+                                        uint_c(1) + uint_c(extendLower[2] + extendUpper[2]), false);
+
+   workingFieldSize_ = workingField_->xyzSize();
+
+   // mark the startCell such that flood fill can not take a path across startCell
+   workingField_->get(startCellInWorkingField_) = true;
+
+   // use stack to store cells that still need to be searched
+   std::vector< Cell > stack;
+   stack.reserve(Stencil_T::Size);
+   stack.emplace_back(startCellInWorkingField_[0] + stencil::cx[startDirection],
+                      startCellInWorkingField_[1] + stencil::cy[startDirection],
+                      startCellInWorkingField_[2] + stencil::cz[startDirection]);
+
+   while (!stack.empty())
+   {
+      // next search cell is the last entry in stack
+      Cell currentCell = stack.back();
+
+      // remove the searched cell from stack
+      stack.pop_back();
+
+      WALBERLA_ASSERT_EQUAL(externField_->get(currentCell + offset_), searchValue_);
+
+      // mark the searched cell to be connected in workingField_
+      workingField_->get(currentCell) = true;
+
+      // iterate over all existing neighbors that are not yet marked and push them onto the stack
+      for (auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d)
+      {
+         Cell neighbor(currentCell[0] + d.cx(), currentCell[1] + d.cy(), currentCell[2] + d.cz());
+
+         // check if neighboring cell is:
+         // - inside workingField_
+         // - not yet marked as connected
+         // - connected to this cell
+         if (cellInsideAndNotMarked(neighbor))
+         {
+            // add neighbor to stack such that it gets marked as connected and used as start cell in the next iteration;
+            // cells that are not connected to startCell will never get marked as connected
+            stack.push_back(neighbor);
+         }
+      }
+   }
+}
+
+// check if startCell is connected to the neighboring cell in direction d
+template< typename T, typename Stencil_T >
+bool RegionalFloodFill< T, Stencil_T >::connected(stencil::Direction d)
+{
+   using namespace stencil;
+   return workingField_->get(startCellInWorkingField_[0] + cx[d], startCellInWorkingField_[1] + cy[d],
+                             startCellInWorkingField_[2] + cz[d]);
+}
+
+// check if startCell is connected to the cell with some offset from startCell
+template< typename T, typename Stencil_T >
+bool RegionalFloodFill< T, Stencil_T >::connected(cell_idx_t xOff, cell_idx_t yOff, cell_idx_t zOff)
+{
+   return workingField_->get(startCellInWorkingField_[0] + xOff, startCellInWorkingField_[1] + yOff,
+                             startCellInWorkingField_[2] + zOff);
+}
+
+// check if cell is:
+// - inside workingField_
+// - not yet marked as connected
+// - connected to this cell
+template< typename T, typename Stencil_T >
+bool RegionalFloodFill< T, Stencil_T >::cellInsideAndNotMarked(const Cell& cell)
+{
+   if (!workingFieldSize_.contains(cell)) { return false; }
+
+   // check if cell is already marked as connected
+   if (!workingField_->get(cell))
+   {
+      // test if cell is connected
+      return (externField_->get(cell + offset_) == searchValue_);
+   }
+   else { return false; }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/dynamics/CMakeLists.txt b/src/lbm/free_surface/dynamics/CMakeLists.txt
new file mode 100644
index 000000000..458fabdfe
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/CMakeLists.txt
@@ -0,0 +1,17 @@
+target_sources( lbm
+        PRIVATE
+        CellConversionSweep.h
+        ConversionFlagsResetSweep.h
+        ExcessMassDistributionModel.h
+        ExcessMassDistributionSweep.h
+        ExcessMassDistributionSweep.impl.h
+        ForceWeightingSweep.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/free_surface/dynamics/CellConversionSweep.h b/src/lbm/free_surface/dynamics/CellConversionSweep.h
new file mode 100644
index 000000000..4e913b657
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/CellConversionSweep.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 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/field/MacroscopicValueCalculation.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * 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 LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T >
+class CellConversionSweep
+{
+ public:
+   using FlagField_T = typename BoundaryHandling_T::FlagField;
+   using flag_t      = typename FlagField_T::flag_t;
+   using PdfField_T  = lbm::PdfField< LatticeModel_T >;
+   using Stencil_T   = typename LatticeModel_T::Stencil;
+
+   CellConversionSweep(BlockDataID handlingID, BlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                       BubbleModelBase* bubbleModel)
+      : handlingID_(handlingID), pdfFieldID_(pdfFieldID), bubbleModel_(bubbleModel), flagInfo_(flagInfo)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      BoundaryHandling_T* const handling = block->getData< BoundaryHandling_T >(handlingID_);
+      PdfField_T* const pdfField         = block->getData< PdfField_T >(pdfFieldID_);
+
+      FlagField_T* const flagField = handling->getFlagField();
+
+      // 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))
+         {
+            handling->removeFlag(flagInfo_.interfaceFlag, x, y, z);
+            handling->setFlag(flagInfo_.liquidFlag, x, y, z);
+            handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+
+            if (flagField->isInInnerPart(flagFieldPtr.cell()))
+            {
+               // register and detect splitting of bubbles
+               bubbleModel_->reportInterfaceToLiquidConversion(block, flagFieldPtr.cell());
+            }
+         }
+
+         // 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))
+         {
+            handling->removeFlag(flagInfo_.interfaceFlag, x, y, z);
+            handling->setFlag(flagInfo_.gasFlag, x, y, z);
+            handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+         }
+      }) // 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 = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d)
+               if (isMaskSet(flagFieldPtr.neighbor(*d), newGasFlagMask)) // newly converted gas cell is in neighborhood
+               {
+                  // convert the current cell to interface
+                  handling->removeFlag(flagInfo_.liquidFlag, x, y, z);
+                  handling->setFlag(flagInfo_.interfaceFlag, x, y, z);
+                  handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+
+                  if (flagField->isInInnerPart(flagFieldPtr.cell()))
+                  {
+                     // register and detect merging of bubbles
+                     bubbleModel_->reportLiquidToInterfaceConversion(block, flagFieldPtr.cell());
+                  }
+
+                  // 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 = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d)
+
+                  // newly converted liquid cell is in neighborhood
+                  if (isMaskSet(flagFieldPtr.neighbor(*d), newLiquidFlagMask))
+                  {
+                     // convert the current cell to interface
+                     handling->removeFlag(flagInfo_.gasFlag, x, y, z);
+                     handling->setFlag(flagInfo_.interfaceFlag, x, y, z);
+                     handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+                     handling->setFlag(flagInfo_.convertFromGasToInterfaceFlag, x, y, z);
+
+                     // 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
+            handling->removeFlag(flagInfo_.convertToInterfaceForInflowFlag, x, y, z);
+
+            // convert the current cell to interface
+            handling->removeFlag(flagInfo_.gasFlag, x, y, z);
+            handling->setFlag(flagInfo_.interfaceFlag, x, y, z);
+            handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+            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))
+            {
+               handling->removeFlag(flagInfo_.liquidFlag, x, y, z);
+               handling->setFlag(flagInfo_.interfaceFlag, x, y, z);
+               handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+               handling->removeFlag(flagInfo_.keepInterfaceForWettingFlag, x, y, z);
+               if (flagField->isInInnerPart(flagFieldPtr.cell()))
+               {
+                  // register and detect merging of bubbles
+                  bubbleModel_->reportLiquidToInterfaceConversion(block, flagFieldPtr.cell());
+               }
+            }
+            else
+            {
+               // convert gas cell to interface
+               if (isFlagSet(flagFieldPtr, flagInfo_.gasFlag))
+               {
+                  handling->removeFlag(flagInfo_.gasFlag, x, y, z);
+                  handling->setFlag(flagInfo_.interfaceFlag, x, y, z);
+                  handling->setFlag(flagInfo_.convertedFlag, x, y, z);
+                  handling->removeFlag(flagInfo_.keepInterfaceForWettingFlag, x, y, z);
+                  handling->setFlag(flagInfo_.convertFromGasToInterfaceFlag, x, y, z);
+               }
+            }
+         }
+      }) // 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
+      initializeFromInflow(convertedFromGasToInterfaceDueToInflow, flagField, pdfField, handling);
+   }
+
+ 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,
+                             BoundaryHandling_T* handling)
+   {
+      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
+         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 i = LatticeModel_T::Stencil::beginNoCenter(); i != LatticeModel_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());
+
+               // get velocity from UBB boundary
+               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 handlingID_;
+   BlockDataID pdfFieldID_;
+   BubbleModelBase* bubbleModel_;
+
+   FlagInfo< FlagField_T > flagInfo_;
+
+   std::set< Cell > convertedFromGasToInterfaceDueToInflow;
+}; // class CellConversionSweep
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h b/src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h
new file mode 100644
index 000000000..4eb74cede
--- /dev/null
+++ b/src/lbm/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/free_surface/FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * 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/free_surface/dynamics/ExcessMassDistributionModel.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h
new file mode 100644
index 000000000..2e4d0b798
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h
@@ -0,0 +1,214 @@
+//======================================================================================================================
+//
+//  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
+{
+/***********************************************************************************************************************
+ * 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.
+ *
+ *  - EvenlyLiquidAndAllInterface:
+ *      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.
+ *
+ *  - EvenlyLiquidAndAllInterfacePreferInterface:
+ *      Similar to EvenlyLiquidAndAllInterface, however, excess mass is preferably distributed to interface cells. It is
+ *      distributed to liquid cells only if there are no neighboring interface cells available.
+ * ********************************************************************************************************************/
+class ExcessMassDistributionModel
+{
+ public:
+   enum class ExcessMassModel {
+      EvenlyAllInterface,
+      EvenlyNewInterface,
+      EvenlyOldInterface,
+      WeightedAllInterface,
+      WeightedNewInterface,
+      WeightedOldInterface,
+      EvenlyLiquidAndAllInterface,
+      EvenlyLiquidAndAllInterfacePreferInterface
+   };
+
+   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::EvenlyLiquidAndAllInterface:
+         break;
+      case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface:
+         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 isEvenlyLiquidAndAllInterfacePreferInterfaceType() const
+   {
+      return modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterface ||
+             modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface;
+   }
+
+   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, "EvenlyLiquidAndAllInterface"))
+      {
+         return ExcessMassModel::EvenlyLiquidAndAllInterface;
+      }
+
+      if (!string_icompare(modelName, "EvenlyLiquidAndAllInterfacePreferInterface"))
+      {
+         return ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface;
+      }
+
+      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::EvenlyLiquidAndAllInterface:
+         modelName = "EvenlyLiquidAndAllInterface";
+         break;
+      case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface:
+         modelName = "EvenlyLiquidAndAllInterfacePreferInterface";
+         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::EvenlyLiquidAndAllInterface, ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface
+   };
+
+}; // class ExcessMassDistributionModel
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h
new file mode 100644
index 000000000..ab9efe397
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h
@@ -0,0 +1,212 @@
+//======================================================================================================================
+//
+//  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/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+
+#include "ExcessMassDistributionModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * 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 LatticeModel_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 getNumberOfEvenlyLiquidAndAllInterfacePreferInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell,
+                                                                       uint_t& liquidNeighbors,
+                                                                       uint_t& interfaceNeighbors);
+
+   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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepInterfaceEvenly
+   : public ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExcessMassDistributionSweepBase_T =
+      ExcessMassDistributionSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepInterfaceWeighted
+   : public ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExcessMassDistributionSweepBase_T =
+      ExcessMassDistributionSweepBase< LatticeModel_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 LatticeModel_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
+ *
+ * Neither the fill level, nor the density of liquid cells is modified. Instead, the excess mass is stored in an
+ * additional field.
+ **********************************************************************************************************************/
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExcessMassDistributionSweepInterfaceAndLiquid
+   : public ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExcessMassDistributionSweepBase_T =
+      ExcessMassDistributionSweepBase< LatticeModel_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/free_surface/dynamics/ExcessMassDistributionSweep.impl.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
new file mode 100644
index 000000000..ba30c8445
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
@@ -0,0 +1,594 @@
+//======================================================================================================================
+//
+//  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/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+
+#include "ExcessMassDistributionSweep.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_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::PdfField< LatticeModel_T >* const pdfField =
+      block->getData< const lbm::PdfField< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::
+   getNumberOfEvenlyLiquidAndAllInterfacePreferInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell,
+                                                                  uint_t& liquidNeighbors, uint_t& interfaceNeighbors)
+{
+   interfaceNeighbors = uint_c(0);
+   liquidNeighbors    = uint_c(0);
+
+   for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_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; }
+      else
+      {
+         if (isFlagSet(neighborFlags, flagInfo_.liquidFlag)) { ++liquidNeighbors; }
+      }
+   }
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepBase< LatticeModel_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 = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+template< typename PdfField_T >
+void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_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.");
+      return;
+   }
+
+   // get density of the current cell
+   const real_t density = 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 = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_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
+         const real_t neighborDensity = 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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_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::PdfField< LatticeModel_T >* const pdfField =
+      block->getData< const lbm::PdfField< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+template< typename PdfField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_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.");
+      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(LatticeModel_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.");
+
+   const real_t excessMass = excessFill * pdfField->getDensity(cell);
+
+   // distribute the excess mass
+   for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_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
+         const real_t neighborDensity = 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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_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 = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_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 =
+            normalField->get(cell) * 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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::
+   computeWeightWithNormal(real_t n_dot_ci, bool isNewLiquid, typename LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_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::PdfField< LatticeModel_T >* const pdfField =
+      block->getData< const lbm::PdfField< LatticeModel_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
+            srcExcessMassField->get(cell) = excessFill * 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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+template< typename PdfField_T >
+void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_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);
+   Base_T::getNumberOfEvenlyLiquidAndAllInterfacePreferInterfaceNeighbors(flagField, cell, liquidNeighbors,
+                                                                          interfaceNeighbors);
+   const uint_t EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors = liquidNeighbors + interfaceNeighbors;
+
+   if (EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors == uint_c(0))
+   {
+      WALBERLA_LOG_WARNING(
+         "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost.");
+      return;
+   }
+
+   const bool preferInterface =
+      Base_T::excessMassDistributionModel_.getModelType() ==
+         ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface &&
+      interfaceNeighbors > uint_c(0);
+
+   // compute mass to be distributed to neighboring cells
+   real_t deltaMass;
+   if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); }
+   else { deltaMass = excessMass / real_c(EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors); }
+
+   // distribute the excess mass
+   for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_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;
+      }
+
+      if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag))
+      {
+         // get density of neighboring interface cell
+         const real_t neighborDensity = pdfField->getDensity(neighborCell);
+
+         // add excess mass directly to fill level for neighboring interface cells
+         fillField->get(neighborCell) += deltaMass / neighborDensity;
+      }
+      else
+      {
+         if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface)
+         {
+            // add excess mass to excessMassField for neighboring liquid cells
+            dstExcessMassField->get(neighborCell) += deltaMass;
+         }
+      }
+   }
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/dynamics/ForceWeightingSweep.h b/src/lbm/free_surface/dynamics/ForceWeightingSweep.h
new file mode 100644
index 000000000..4a13fb3b4
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/ForceWeightingSweep.h
@@ -0,0 +1,96 @@
+//======================================================================================================================
+//
+//  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 ForceWeightingSweep.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/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * Weight the specified force in interface cells according to their density and fill level, as in equation 15 in Koerner
+ * et al., 2005.
+ * In liquid cells, the force is set to the specified constant global force.
+ **********************************************************************************************************************/
+template< typename LatticeModel_T, typename FlagField_T, typename VectorField_T, typename ScalarField_T >
+class ForceWeightingSweep
+{
+ public:
+   ForceWeightingSweep(BlockDataID forceFieldID, ConstBlockDataID pdfFieldID, ConstBlockDataID flagFieldID,
+                       ConstBlockDataID fillFieldID, const FlagInfo< FlagField_T >& flagInfo,
+                       const Vector3< real_t >& globalForce)
+      : forceFieldID_(forceFieldID), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID),
+        flagInfo_(flagInfo), globalForce_(globalForce)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+      VectorField_T* const forceField      = block->getData< VectorField_T >(forceFieldID_);
+      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(forceFieldIt, forceField, pdfFieldIt, pdfField, flagFieldIt, flagField, fillFieldIt,
+                             fillField, {
+                                flag_t flag = *flagFieldIt;
+
+                                // set force in interface cells to globalForce_ * density * fillLevel (see equation 15
+                                // in Koerner et al., 2005)
+                                if (flagInfo_.isInterface(flag))
+                                {
+                                   const real_t density   = pdfField->getDensity(pdfFieldIt.cell());
+                                   const real_t fillLevel = *fillFieldIt;
+                                   *forceFieldIt          = globalForce_ * fillLevel * density;
+                                }
+                                else
+                                {
+                                   // set force to globalForce_ in all non-interface cells
+                                   *forceFieldIt = globalForce_;
+                                }
+                             }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+ private:
+   using flag_t = typename FlagField_T::flag_t;
+
+   BlockDataID forceFieldID_;
+   ConstBlockDataID pdfFieldID_;
+   ConstBlockDataID flagFieldID_;
+   ConstBlockDataID fillFieldID_;
+   FlagInfo< FlagField_T > flagInfo_;
+   Vector3< real_t > globalForce_;
+}; // class ForceWeightingSweep
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/dynamics/PdfReconstructionModel.h b/src/lbm/free_surface/dynamics/PdfReconstructionModel.h
new file mode 100644
index 000000000..9468f06b3
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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/free_surface/dynamics/PdfRefillingModel.h b/src/lbm/free_surface/dynamics/PdfRefillingModel.h
new file mode 100644
index 000000000..ffd86279c
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ *   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/free_surface/dynamics/PdfRefillingSweep.h b/src/lbm/free_surface/dynamics/PdfRefillingSweep.h
new file mode 100644
index 000000000..8c1c74513
--- /dev/null
+++ b/src/lbm/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/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/dynamics/PdfRefillingModel.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+
+#include "stencil/D3Q6.h"
+
+#include <functional>
+#include <vector>
+
+#include "PdfRefillingModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * 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 LatticeModel_T, typename FlagField_T >
+class RefillingSweepBase
+{
+ public:
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+   using flag_t     = typename FlagField_T::flag_t;
+   using Stencil_T  = typename LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExtrapolationRefillingSweepBase : public RefillingSweepBase< LatticeModel_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_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::PdfField< LatticeModel_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::PdfField< LatticeModel_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::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_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::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_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::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_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 LatticeModel_T, typename FlagField_T >
+class EquilibriumRefillingSweep : public RefillingSweepBase< LatticeModel_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T >
+class AverageRefillingSweep : public RefillingSweepBase< LatticeModel_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class EquilibriumAndNonEquilibriumRefillingSweep
+   : public ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExtrapolationRefillingSweepBase_T =
+      ExtrapolationRefillingSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class ExtrapolationRefillingSweep
+   : public ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+{
+ public:
+   using ExtrapolationRefillingSweepBase_T =
+      ExtrapolationRefillingSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T >
+class GradsMomentsRefillingSweep : public RefillingSweepBase< LatticeModel_T, FlagField_T >
+{
+ public:
+   using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_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/free_surface/dynamics/PdfRefillingSweep.impl.h b/src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h
new file mode 100644
index 000000000..cc5c382e5
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h
@@ -0,0 +1,718 @@
+//======================================================================================================================
+//
+//  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 "domain_decomposition/IBlock.h"
+
+#include "field/GhostLayerField.h"
+
+#include "lbm/field/Equilibrium.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/dynamics/PdfRefillingModel.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+
+#include "stencil/D3Q6.h"
+
+#include <set>
+#include <vector>
+
+#include "PdfRefillingSweep.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+template< typename LatticeModel_T, typename FlagField_T >
+real_t RefillingSweepBase< LatticeModel_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++;
+         const typename PdfField_T::ConstPtr neighbor(pdfField, neighborCell[0], neighborCell[1], neighborCell[2]);
+         Vector3< real_t > neighborU;
+         real_t neighborRho;
+         neighborRho = lbm::getDensityAndMomentumDensity(neighborU, pdfField.latticeModel(), neighbor);
+         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 LatticeModel_T, typename FlagField_T >
+std::vector< real_t > RefillingSweepBase< LatticeModel_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::Equilibrium< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+Vector3< cell_idx_t > ExtrapolationRefillingSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+Vector3< cell_idx_t >
+   ExtrapolationRefillingSweepBase< LatticeModel_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, 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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+uint_t ExtrapolationRefillingSweepBase< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+std::vector< real_t > ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::
+   getNonEquilibriumPdfsInCell(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField)
+{
+   std::vector< real_t > nonEquilibriumPartOfPdfs(Stencil_T::Size);
+
+   Vector3< real_t > velocity;
+   const real_t density = pdfField.getDensityAndVelocity(velocity, cell);
+
+   // 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::EquilibriumDistribution< LatticeModel_T >::get(*dir, velocity, density);
+   }
+   return nonEquilibriumPartOfPdfs;
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+std::vector< real_t >
+   ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::getPdfsInCell(
+      const Cell& cell, lbm::PdfField< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::
+   applyQuadraticExtrapolation(
+      const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >&
+         getPdfFunc)
+{
+   // store the PDFs of the cells participating in the extrapolation
+   std::vector< real_t > pdfsXf(LatticeModel_T::Stencil::Size);   // cell + 1 * extrapolationDirection
+   std::vector< real_t > pdfsXff(LatticeModel_T::Stencil::Size);  // cell + 2 * extrapolationDirection
+   std::vector< real_t > pdfsXfff(LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::
+   applyLinearExtrapolation(
+      const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::
+   applyConstantExtrapolation(
+      const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection,
+      bool includeThisCell,
+      const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_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 LatticeModel_T, typename FlagField_T >
+void EquilibriumRefillingSweep< LatticeModel_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);
+
+         pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity);
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename LatticeModel_T, typename FlagField_T >
+void AverageRefillingSweep< LatticeModel_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void EquilibriumAndNonEquilibriumRefillingSweep< LatticeModel_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);
+         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 LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+void ExtrapolationRefillingSweep< LatticeModel_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);
+                  pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity);
+               }
+            }
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+template< typename LatticeModel_T, typename FlagField_T >
+Vector3< real_t > GradsMomentsRefillingSweep< LatticeModel_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]])
+   {
+      const Vector3< real_t > neighborVelocity1 = pdfField->getVelocity(cell + dir);
+      const Vector3< real_t > neighborVelocity2 = 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 = 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 = 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 LatticeModel_T, typename FlagField_T >
+void GradsMomentsRefillingSweep< LatticeModel_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 = lbm::internal::multiplyVelocityDirection(*q, avgVelocity);
+
+            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 = LatticeModel_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/free_surface/dynamics/StreamReconstructAdvectSweep.h b/src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h
new file mode 100644
index 000000000..ec5ca220d
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h
@@ -0,0 +1,202 @@
+//======================================================================================================================
+//
+//  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/free_surface/FlagInfo.h"
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+#include "lbm/sweeps/StreamPull.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
+{
+template< typename LatticeModel_T, typename BoundaryHandling_T, typename FlagField_T, typename FlagInfo_T,
+          typename ScalarField_T, typename VectorField_T, bool useCodegen >
+class StreamReconstructAdvectSweep
+{
+ public:
+   using flag_t     = typename FlagInfo_T::flag_t;
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+   StreamReconstructAdvectSweep(real_t sigma, BlockDataID handlingID, BlockDataID fillFieldID, BlockDataID flagFieldID,
+                                BlockDataID pdfField, ConstBlockDataID normalFieldID, ConstBlockDataID curvatureFieldID,
+                                const FlagInfo_T& flagInfo, BubbleModelBase* bubbleModel,
+                                const PdfReconstructionModel& pdfReconstructionModel, bool useSimpleMassExchange,
+                                real_t cellConversionThreshold, real_t cellConversionForceThreshold)
+      : sigma_(sigma), handlingID_(handlingID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID),
+        pdfFieldID_(pdfField), normalFieldID_(normalFieldID), curvatureFieldID_(curvatureFieldID), flagInfo_(flagInfo),
+        bubbleModel_(bubbleModel), neighborhoodFlagFieldClone_(flagFieldID), fillFieldClone_(fillFieldID),
+        pdfFieldClone_(pdfField), pdfReconstructionModel_(pdfReconstructionModel),
+        useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold),
+        cellConversionForceThreshold_(cellConversionForceThreshold)
+   {}
+
+   void operator()(IBlock* const block);
+
+ protected:
+   real_t sigma_; // surface tension
+
+   BlockDataID handlingID_;
+   BlockDataID fillFieldID_;
+   BlockDataID flagFieldID_;
+   BlockDataID pdfFieldID_;
+
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID curvatureFieldID_;
+
+   FlagInfo_T flagInfo_;
+   bubble_model::BubbleModelBase* const bubbleModel_;
+
+   // 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 LatticeModel_T, typename BoundaryHandling_T, typename FlagField_T, typename FlagInfo_T,
+          typename ScalarField_T, typename VectorField_T, bool useCodegen >
+void StreamReconstructAdvectSweep< LatticeModel_T, BoundaryHandling_T, FlagField_T, FlagInfo_T, ScalarField_T,
+                                   VectorField_T, useCodegen >::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 LatticeModel_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 bubbleDensity = bubbleModel_->getDensity(block, pdfSrcFieldIt.cell());
+            const real_t rhoGas        = computeDeltaRhoLaplacePressure(sigma_, *curvatureFieldIt) + bubbleDensity;
+
+            // 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< LatticeModel_T, FlagField_T >) (flagField, pdfSrcFieldIt, flagFieldIt,
+                                                                             normalFieldIt, flagInfo_, rhoGas,
+                                                                             pdfDstFieldIt, pdfReconstructionModel_);
+
+            // density before LBM stream (post-collision)
+            const real_t oldRho = lbm::getDensity(pdfSrcField->latticeModel(), pdfSrcFieldIt);
+
+            // density after LBM stream in reconstruction
+            const real_t newRho = lbm::getDensity(pdfDstField->latticeModel(), pdfDstFieldIt);
+
+            // compute mass advection using post-collision PDFs (explicitly not PDFs updated by stream above)
+            const real_t deltaMass =
+               (advectMass< LatticeModel_T, FlagField_T, typename ScalarField_T::iterator,
+                            typename PdfField_T::iterator, typename FlagField_T::iterator,
+                            typename FlagField_T::iterator, FlagInfo_T >) (flagField, fillSrcFieldIt, pdfSrcFieldIt,
+                                                                           flagFieldIt, neighborhoodFlagFieldIt,
+                                                                           flagInfo_, useSimpleMassExchange_);
+
+            // update fill level after LBM stream and mass exchange
+            *fillDstFieldIt        = (*fillSrcFieldIt * oldRho + deltaMass) / newRho;
+            const real_t deltaFill = *fillDstFieldIt - *fillSrcFieldIt;
+
+            // update the volume of bubbles
+            bubbleModel_->reportFillLevelChange(block, fillSrcFieldIt.cell(), deltaFill);
+         }
+         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();
+                  if constexpr (useCodegen)
+                  {
+                     auto lbmSweepGenerated = typename LatticeModel_T::Sweep(pdfFieldID_);
+                     const CellInterval ci(cell, cell);
+                     lbmSweepGenerated.streamInCellInterval(pdfSrcField, pdfDstField, ci);
+                  }
+                  else
+                  {
+                     lbm::StreamPull< LatticeModel_T >::execute(pdfSrcField, pdfDstField, cell[0], cell[1], cell[2]);
+                  }
+
+                  *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);
+
+   BoundaryHandling_T* const handling = block->getData< BoundaryHandling_T >(handlingID_);
+
+   // mark interface cell for conversion
+   findInterfaceCellConversions< LatticeModel_T >(handling, fillSrcField, flagField, neighborhoodFlagField, flagInfo_,
+                                                  cellConversionThreshold_, cellConversionForceThreshold_);
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h
new file mode 100644
index 000000000..c0489961a
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h
@@ -0,0 +1,441 @@
+//======================================================================================================================
+//
+//  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, 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/blockforest/communication/SimpleCommunication.h"
+#include "lbm/blockforest/communication/UpdateSecondGhostLayer.h"
+#include "lbm/free_surface/BlockStateDetectorSweep.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+#include "lbm/lattice_model/SmagorinskyLES.h"
+#include "lbm/sweeps/CellwiseSweep.h"
+#include "lbm/sweeps/SweepWrappers.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "CellConversionSweep.h"
+#include "ConversionFlagsResetSweep.h"
+#include "ExcessMassDistributionModel.h"
+#include "ExcessMassDistributionSweep.h"
+#include "ForceWeightingSweep.h"
+#include "PdfReconstructionModel.h"
+#include "PdfRefillingModel.h"
+#include "PdfRefillingSweep.h"
+#include "StreamReconstructAdvectSweep.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T,
+          bool useCodegen = false >
+class SurfaceDynamicsHandler
+{
+ protected:
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::Stencil >;
+
+   // communication in corner directions (D2Q9/D3Q27) is required for all fields but the PDF field
+   using CommunicationStencil_T =
+      typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+   using CommunicationCorner_T = blockforest::SimpleCommunication< CommunicationStencil_T >;
+
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+ public:
+   SurfaceDynamicsHandler(const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID,
+                          BlockDataID flagFieldID, BlockDataID fillFieldID, BlockDataID forceFieldID,
+                          ConstBlockDataID normalFieldID, ConstBlockDataID curvatureFieldID,
+                          const std::shared_ptr< FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                          const std::shared_ptr< BubbleModelBase >& bubbleModel,
+                          const std::string& pdfReconstructionModel, const std::string& pdfRefillingModel,
+                          const std::string& excessMassDistributionModel, real_t relaxationRate,
+                          const Vector3< real_t >& globalForce, real_t surfaceTension, bool enableForceWeighting,
+                          bool useSimpleMassExchange, real_t cellConversionThreshold,
+                          real_t cellConversionForceThreshold, BlockDataID relaxationRateFieldID = BlockDataID(),
+                          real_t smagorinskyConstant = real_c(0))
+      : blockForest_(blockForest), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID),
+        forceFieldID_(forceFieldID), normalFieldID_(normalFieldID), curvatureFieldID_(curvatureFieldID),
+        bubbleModel_(bubbleModel), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling),
+        pdfReconstructionModel_(pdfReconstructionModel), pdfRefillingModel_({ pdfRefillingModel }),
+        excessMassDistributionModel_({ excessMassDistributionModel }), relaxationRate_(relaxationRate),
+        globalForce_(globalForce), surfaceTension_(surfaceTension), enableForceWeighting_(enableForceWeighting),
+        useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold),
+        cellConversionForceThreshold_(cellConversionForceThreshold), relaxationRateFieldID_(relaxationRateFieldID),
+        smagorinskyConstant_(smagorinskyConstant)
+   {
+      WALBERLA_CHECK(LatticeModel_T::compressible,
+                     "The free surface lattice Boltzmann extension works only with compressible LBM models.");
+
+      if (useCodegen && !realIsEqual(smagorinskyConstant_, real_c(0)))
+      {
+         WALBERLA_ABORT("When using a generated LBM kernel from lbmpy, please use lbmpy's inbuilt-functionality to "
+                        "generate the Smagorinsky model directly into the kernel.");
+      }
+
+      if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType())
+      {
+         // 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 (LatticeModel_T::Stencil::D == uint_t(2))
+      {
+         WALBERLA_LOG_INFO_ON_ROOT(
+            "IMPORTANT REMARK: You are using a D2Q9 stencil in SurfaceDynamicsHandler. In the FSLBM, 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& flagInfo = freeSurfaceBoundaryHandling_->getFlagInfo();
+
+      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(freeSurfaceBoundaryHandling_->getBoundarySweep(), "Sweep: boundary handling",
+                              Set< SUID >::emptySet(), StateSweep::onlyGasAndBoundary)
+                     << Sweep(emptySweep, "Empty sweep: boundary handling", StateSweep::onlyGasAndBoundary);
+
+      if (enableForceWeighting_)
+      {
+         // add sweep for weighting force in interface cells with fill level and density
+         const ForceWeightingSweep< LatticeModel_T, FlagField_T, VectorField_T, ScalarField_T > forceWeightingSweep(
+            forceFieldID_, pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo, globalForce_);
+         timeloop.add() << Sweep(forceWeightingSweep, "Sweep: force weighting", Set< SUID >::emptySet(),
+                                 StateSweep::onlyGasAndBoundary)
+                        << Sweep(emptySweep, "Empty sweep: force weighting", StateSweep::onlyGasAndBoundary)
+                        << AfterFunction(CommunicationCorner_T(blockForest_, forceFieldID_),
+                                         "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< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T,
+                                          FlagField_T, typename FreeSurfaceBoundaryHandling_T::FlagInfo_T,
+                                          ScalarField_T, VectorField_T, useCodegen >
+         streamReconstructAdvectSweep(surfaceTension_, freeSurfaceBoundaryHandling_->getHandlingID(), fillFieldID_,
+                                      flagFieldID_, pdfFieldID_, normalFieldID_, curvatureFieldID_, flagInfo,
+                                      bubbleModel_.get(), 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(blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                          "Second ghost layer update: after StreamReconstructAdvect sweep (fill level field)")
+         << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_),
+                          "Second ghost layer update: after StreamReconstructAdvect sweep (flag field)");
+
+      if constexpr (useCodegen)
+      {
+         auto lbmSweepGenerated = typename LatticeModel_T::Sweep(pdfFieldID_);
+
+         // temporary class for being able to call the LBM collision with operator()
+         class CollideSweep
+         {
+          public:
+            CollideSweep(const typename LatticeModel_T::Sweep& sweep) : sweep_(sweep){};
+
+            void operator()(IBlock* const block, const uint_t numberOfGhostLayersToInclude = uint_t(1))
+            {
+               sweep_.collide(block, numberOfGhostLayersToInclude);
+            }
+
+          private:
+            typename LatticeModel_T::Sweep sweep_;
+         };
+
+         timeloop.add() << Sweep(CollideSweep(lbmSweepGenerated), "Sweep: collision (generated)",
+                                 StateSweep::fullFreeSurface)
+                        << Sweep(lbmSweepGenerated, "Sweep: streamCollide (generated)", StateSweep::onlyLBM)
+                        << Sweep(emptySweep, "Empty sweep: streamCollide (generated)")
+                        << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                         "Communication: after streamCollide (generated)");
+      }
+      else
+      {
+         // sweep for standard LBM stream and collision
+         const auto lbmSweep = lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >(pdfFieldID_, flagFieldID_,
+                                                                                     flagIDs::liquidInterfaceFlagIDs);
+
+         // LBM collision in interface cells; standard LBM stream and collision in liquid cells
+         if (!realIsEqual(smagorinskyConstant_, real_c(0), real_c(1e-14))) // using Smagorinsky turbulence model
+         {
+            const real_t kinematicViscosity = (real_c(1) / relaxationRate_ - real_c(0.5)) / real_c(3);
+
+            // standard LBM stream in liquid cells that have not been streamed, yet
+            timeloop.add() << Sweep(lbm::makeStreamSweep(lbmSweep), "Stream sweep", StateSweep::onlyLBM)
+                           << Sweep(emptySweep, "Deactivated Stream sweep")
+                           << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                            "Communication after Stream sweep");
+
+            // sweep for turbulence modelling
+            const lbm::SmagorinskyLES< LatticeModel_T > smagorinskySweep(
+               blockForest_, pdfFieldID_, relaxationRateFieldID_, kinematicViscosity, real_c(0.12));
+
+            timeloop.add()
+               // Smagorinsky turbulence model
+               << BeforeFunction(smagorinskySweep, "Sweep: Smagorinsky turbulence model")
+               << BeforeFunction(CommunicationCorner_T(blockForest_, relaxationRateFieldID_),
+                                 "Communication: after Smagorinsky sweep")
+               // standard LBM collision
+               << Sweep(lbm::makeCollideSweep(lbmSweep), "Sweep: collision after Smagorinsky sweep")
+               << AfterFunction(Communication_T(blockForest_, pdfFieldID_),
+                                "Communication: after collision sweep with preceding Smagorinsky sweep");
+         }
+         else
+         {
+            // no turbulence model
+            timeloop.add()
+               // collision in interface cells and liquid cells that have already been streamed (in
+               // streamReconstructAdvectSweep due to StateSweep::fullFreeSurface)
+               << Sweep(lbm::makeCollideSweep(lbmSweep), "Sweep: collision", StateSweep::fullFreeSurface)
+               // standard LBM stream-collide in liquid cells that have not been streamed, yet
+               << Sweep(*lbmSweep, "Sweep: streamCollide", StateSweep::onlyLBM)
+               << Sweep(emptySweep, "Empty sweep: streamCollide")
+               << AfterFunction(Communication_T(blockForest_, pdfFieldID_), "Communication: after streamCollide sweep");
+         }
+      }
+
+      // 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< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T,
+                                 ScalarField_T >
+         cellConvSweep(freeSurfaceBoundaryHandling_->getHandlingID(), pdfFieldID_, flagInfo, bubbleModel_.get());
+      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(blockforest::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< LatticeModel_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< LatticeModel_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< LatticeModel_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< LatticeModel_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< LatticeModel_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< LatticeModel_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(
+                              blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                              "Second ghost layer update: after excess mass distribution sweep (fill level field)")
+                        // update bubble model, i.e., perform registered bubble merges/splits; bubble merges/splits are
+                        // already detected and registered by CellConversionSweep
+                        << AfterFunction(std::bind(&bubble_model::BubbleModelBase::update, bubbleModel_),
+                                         "Sweep: bubble model update");
+      }
+      else
+      {
+         if (excessMassDistributionModel_.isWeightedType())
+         {
+            const ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_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(
+                                 blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                                 "Second ghost layer update: after excess mass distribution sweep (fill level field)")
+                           // update bubble model, i.e., perform registered bubble merges/splits; bubble merges/splits
+                           // are already detected and registered by CellConversionSweep
+                           << AfterFunction(std::bind(&bubble_model::BubbleModelBase::update, bubbleModel_),
+                                            "Sweep: bubble model update");
+         }
+         else
+         {
+            if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType())
+            {
+               const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T,
+                                                                    VectorField_T >
+                  distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo,
+                                      excessMassFieldID_);
+               timeloop.add()
+                  << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface)
+                  << Sweep(emptySweep, "Empty sweep: distribute excess mass")
+                  << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, excessMassFieldID_),
+                                   "Communication: after excess mass distribution sweep")
+                  << AfterFunction(blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                                   "Second ghost layer update: after excess mass distribution sweep (fill level field)")
+                  // update bubble model, i.e., perform registered bubble merges/splits; bubble
+                  // merges/splits are already detected and registered by CellConversionSweep
+                  << AfterFunction(std::bind(&bubble_model::BubbleModelBase::update, bubbleModel_),
+                                   "Sweep: bubble model update");
+            }
+         }
+      }
+
+      // 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(blockforest::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_;
+
+   BlockDataID pdfFieldID_;
+   BlockDataID flagFieldID_;
+   BlockDataID fillFieldID_;
+   BlockDataID forceFieldID_;
+
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID curvatureFieldID_;
+
+   std::shared_ptr< BubbleModelBase > bubbleModel_;
+   std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+
+   PdfReconstructionModel pdfReconstructionModel_;
+   PdfRefillingModel pdfRefillingModel_;
+   ExcessMassDistributionModel excessMassDistributionModel_;
+   real_t relaxationRate_;
+   Vector3< real_t > globalForce_;
+   real_t surfaceTension_;
+   bool enableForceWeighting_;
+   bool useSimpleMassExchange_;
+   real_t cellConversionThreshold_;
+   real_t cellConversionForceThreshold_;
+
+   BlockDataID relaxationRateFieldID_;
+   real_t smagorinskyConstant_;
+
+   BlockDataID excessMassFieldID_ = BlockDataID();
+}; // class SurfaceDynamicsHandler
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/dynamics/functionality/AdvectMass.h b/src/lbm/free_surface/dynamics/functionality/AdvectMass.h
new file mode 100644
index 000000000..5e198e2db
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/functionality/AdvectMass.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 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/field/MacroscopicValueCalculation.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+#include <type_traits>
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * 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 LatticeModel_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 = LatticeModel_T::Stencil::beginNoCenter(); dir != LatticeModel_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
+      const real_t neighborPdf = pdfFieldIt.neighbor(*dir, dir.toInvIdx());
+
+      // 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/free_surface/dynamics/functionality/CMakeLists.txt b/src/lbm/free_surface/dynamics/functionality/CMakeLists.txt
new file mode 100644
index 000000000..357412052
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/functionality/CMakeLists.txt
@@ -0,0 +1,8 @@
+target_sources( lbm
+        PRIVATE
+        AdvectMass.h
+        FindInterfaceCellConversion.h
+        GetLaplacePressure.h
+        GetOredNeighborhood.h
+        ReconstructInterfaceCellABB.h
+        )
diff --git a/src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h b/src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h
new file mode 100644
index 000000000..6445a61f6
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h
@@ -0,0 +1,255 @@
+//======================================================================================================================
+//
+//  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/free_surface/FlagInfo.h"
+
+#include <type_traits>
+
+namespace walberla
+{
+namespace free_surface
+{
+/***********************************************************************************************************************
+ * 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 LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T, typename FlagField_T,
+          typename ScalarIt_T, typename FlagIt_T >
+void findInterfaceCellConversion(const BoundaryHandling_T& handling, 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
+   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 = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_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 LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T, typename FlagField_T >
+void findInterfaceCellConversions(const BoundaryHandling_T& handling, 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 LatticeModel_T::Stencil >(flagFieldPtr);
+
+         (findInterfaceCellConversion< LatticeModel_T, BoundaryHandling_T, ScalarField_T,
+                                       FlagField_T >) (handling, 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 LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T, typename FlagField_T,
+          typename NeighField_T >
+void findInterfaceCellConversions(const BoundaryHandling_T& handling, 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< LatticeModel_T, BoundaryHandling_T, ScalarField_T,
+                                    FlagField_T >) (handling, 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/free_surface/dynamics/functionality/GetLaplacePressure.h b/src/lbm/free_surface/dynamics/functionality/GetLaplacePressure.h
new file mode 100644
index 000000000..36c313ee2
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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/free_surface/dynamics/functionality/GetOredNeighborhood.h b/src/lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h
new file mode 100644
index 000000000..187528a6c
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h b/src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h
new file mode 100644
index 000000000..b0f45113c
--- /dev/null
+++ b/src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h
@@ -0,0 +1,442 @@
+//======================================================================================================================
+//
+//  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/field/MacroscopicValueCalculation.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/dynamics/PdfReconstructionModel.h"
+#include "lbm/lattice_model/EquilibriumDistribution.h"
+
+#include "stencil/Directions.h"
+
+#include "GetLaplacePressure.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+// 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 LatticeModel_T, typename ConstPdfIt_T >
+inline real_t reconstructPressureAntiBounceBack(const stencil::Iterator< typename LatticeModel_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) * LatticeModel_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 LatticeModel_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 LatticeModel_T::Stencil;
+
+   // get velocity and density in interface cell
+   Vector3< real_t > u;
+   auto pdfField = dynamic_cast< const lbm::PdfField< LatticeModel_T >* >(pdfFieldIt.getField());
+   WALBERLA_ASSERT_NOT_NULLPTR(pdfField);
+   const real_t rho = lbm::getDensityAndMomentumDensity(u, pdfField->latticeModel(), pdfFieldIt);
+   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< LatticeModel_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< LatticeModel_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)[0] * real_c(dir.cx()) + (*normalFieldIt)[1] * real_c(dir.cy()) +
+                          (*normalFieldIt)[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< LatticeModel_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< LatticeModel_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< LatticeModel_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< LatticeModel_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/free_surface/surface_geometry/CMakeLists.txt b/src/lbm/free_surface/surface_geometry/CMakeLists.txt
new file mode 100644
index 000000000..42fb177b7
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/CMakeLists.txt
@@ -0,0 +1,22 @@
+target_sources( lbm
+        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/free_surface/surface_geometry/ContactAngle.h b/src/lbm/free_surface/surface_geometry/ContactAngle.h
new file mode 100644
index 000000000..55f26d5ce
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/surface_geometry/CurvatureModel.h b/src/lbm/free_surface/surface_geometry/CurvatureModel.h
new file mode 100644
index 000000000..dd245dacf
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/CurvatureModel.h
@@ -0,0 +1,90 @@
+//======================================================================================================================
+//
+//  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
+{
+// forward declaration
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+class FiniteDifferenceMethod
+{
+ private:
+   using SurfaceGeometryHandler_T =
+      walberla::free_surface::SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+class LocalTriangulation
+{
+ private:
+   using SurfaceGeometryHandler_T =
+      walberla::free_surface::SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_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 LatticeModel_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+class SimpleFiniteDifferenceMethod
+{
+ private:
+   using SurfaceGeometryHandler_T =
+      walberla::free_surface::SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_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/free_surface/surface_geometry/CurvatureModel.impl.h b/src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h
new file mode 100644
index 000000000..0aa62fc3c
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h
@@ -0,0 +1,269 @@
+//======================================================================================================================
+//
+//  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/blockforest/communication/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
+{
+namespace curvature_model
+{
+// empty sweep required for using selectors (e.g. StateSweep::fullFreeSurface)
+struct emptySweep
+{
+   void operator()(IBlock*) {}
+};
+
+template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+void FiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_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.freeSurfaceBoundaryHandling_->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 LatticeModel_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+void LocalTriangulation< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_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_)
+   {
+      const auto& flagInfo = geometryHandler.freeSurfaceBoundaryHandling_->getFlagInfo();
+
+      DetectWettingSweep<
+         Stencil_T,
+         typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::BoundaryHandling_T,
+         FlagField_T, ScalarField_T, VectorField_T >
+         detWetSweep(geometryHandler.freeSurfaceBoundaryHandling_->getHandlingID(), flagInfo,
+                     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(blockforest::UpdateSecondGhostLayer< FlagField_T >(geometryHandler.blockForest_,
+                                                                                         geometryHandler.flagFieldID_),
+                                      "Second ghost layer update: after wetting detection sweep (flag field)");
+   }
+}
+
+template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+void SimpleFiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_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/free_surface/surface_geometry/CurvatureSweep.h b/src/lbm/free_surface/surface_geometry/CurvatureSweep.h
new file mode 100644
index 000000000..dfe53545c
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/CurvatureSweep.h
@@ -0,0 +1,211 @@
+//======================================================================================================================
+//
+//  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
+{
+/***********************************************************************************************************************
+ * 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+   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)
+   {
+      // get reversed interface normal
+      const Vector3< real_t > n = -*normalFieldIt;
+
+      // get n_w, i.e., obstacle normal
+      const Vector3< real_t > nw = obstacleNormalFieldIt.neighbor(dir);
+
+      // 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+
+ 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+
+ 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/free_surface/surface_geometry/CurvatureSweep.impl.h b/src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h
new file mode 100644
index 000000000..f6d46d103
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h
@@ -0,0 +1,518 @@
+//======================================================================================================================
+//
+//  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
+{
+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);
+
+         if (isFlagSet(flagFieldIt, interfaceFlag)) // only treat interface cells
+         {
+            real_t weightSum = real_c(0);
+
+            if (normalFieldIt->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 = normalFieldIt.neighbor(*dir);
+                     }
+                     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;
+
+         // compute curvature only in interface points
+         if (!isFlagSet(flagFieldIt, interfaceFlag)) { continue; }
+
+         // normal of this cell also contributes to mean normal
+         meanInterfaceNormal = *normalFieldIt;
+
+         // 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 = normalFieldIt.neighbor(*dir); }
+
+               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 = normalFieldIt.neighbor(*dir2);
+
+                           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(normalFieldIt.neighbor(*dir2), 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 = obstacleNormalFieldIt.neighbor(*dir2);
+                           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(*normalFieldIt, *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() - (*normalFieldIt) * 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/free_surface/surface_geometry/DetectWettingSweep.h b/src/lbm/free_surface/surface_geometry/DetectWettingSweep.h
new file mode 100644
index 000000000..f4d69e0af
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/DetectWettingSweep.h
@@ -0,0 +1,334 @@
+//======================================================================================================================
+//
+//  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
+{
+/***********************************************************************************************************************
+ * 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 BoundaryHandling_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(BlockDataID boundaryHandling, const FlagInfo< FlagField_T >& flagInfo,
+                      const ConstBlockDataID& normalFieldID, const ConstBlockDataID& fillFieldID)
+      : boundaryHandlingID_(boundaryHandling), flagInfo_(flagInfo), normalFieldID_(normalFieldID),
+        fillFieldID_(fillFieldID)
+   {}
+
+   void operator()(IBlock* const block);
+
+ private:
+   BlockDataID boundaryHandlingID_;
+   FlagInfo< FlagField_T > flagInfo_;
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID fillFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+}; // class DetectWettingSweep
+
+template< typename Stencil_T, typename BoundaryHandling_T, typename FlagField_T, typename ScalarField_T,
+          typename VectorField_T >
+void DetectWettingSweep< Stencil_T, BoundaryHandling_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(
+   IBlock* const block)
+{
+   // get free surface boundary handling
+   BoundaryHandling_T* const boundaryHandling = block->getData< BoundaryHandling_T >(boundaryHandlingID_);
+
+   // get fields
+   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     = boundaryHandling->getFlagField();
+
+   // 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(*normalFieldIt, *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)),
+                                                                *normalFieldIt, interfacePointLocation);
+            if (intersection > real_c(0))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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)),
+                                                                *normalFieldIt, interfacePointLocation);
+            if (intersection > real_c(0))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               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))
+            {
+               boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(),
+                                         flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz());
+               continue;
+            }
+         }
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h
new file mode 100644
index 000000000..8d49e0c96
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+   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/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h
new file mode 100644
index 000000000..309bc79bb
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.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 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
+{
+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))
+      {
+         uint_t count     = uint_c(0);
+         vector_t& normal = *normalFieldIt;
+         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()]) * normalFieldIt.neighbor(*i);
+               ++count;
+            }
+         }
+
+         // normalize the normal
+         normal = normal.getNormalizedOrZero();
+      }
+   }) // WALBERLA_FOR_ALL_CELLS
+}
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/surface_geometry/NormalSweep.h b/src/lbm/free_surface/surface_geometry/NormalSweep.h
new file mode 100644
index 000000000..75cc8b6d9
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+   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/free_surface/surface_geometry/NormalSweep.impl.h b/src/lbm/free_surface/surface_geometry/NormalSweep.impl.h
new file mode 100644
index 000000000..a1ade27ec
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/NormalSweep.impl.h
@@ -0,0 +1,456 @@
+//======================================================================================================================
+//
+//  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
+{
+
+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 = normalField->get(x, y, z);
+
+      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/free_surface/surface_geometry/ObstacleFillLevelSweep.h b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h
new file mode 100644
index 000000000..c5bd8c858
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+   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/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h
new file mode 100644
index 000000000..7d4182022
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h
@@ -0,0 +1,88 @@
+//======================================================================================================================
+//
+//  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
+{
+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))
+      {
+         WALBERLA_CHECK_GREATER(obstacleNormalField->get(x, y, z).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(obstacleNormalField->get(x, y, z) * 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/free_surface/surface_geometry/ObstacleNormalSweep.h b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.h
new file mode 100644
index 000000000..cb1c6b01d
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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 = typename std::remove_const< typename VectorField_T::value_type >::type;
+   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/free_surface/surface_geometry/ObstacleNormalSweep.impl.h b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.h
new file mode 100644
index 000000000..6d3b72258
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.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 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
+{
+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 = obstacleNormalField->get(x, y, z);
+
+      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/free_surface/surface_geometry/SmoothingSweep.h b/src/lbm/free_surface/surface_geometry/SmoothingSweep.h
new file mode 100644
index 000000000..9b76ffd20
--- /dev/null
+++ b/src/lbm/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
+{
+// 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/free_surface/surface_geometry/SmoothingSweep.impl.h b/src/lbm/free_surface/surface_geometry/SmoothingSweep.impl.h
new file mode 100644
index 000000000..3b61d23d2
--- /dev/null
+++ b/src/lbm/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
+{
+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/free_surface/surface_geometry/SurfaceGeometryHandler.h b/src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h
new file mode 100644
index 000000000..f0f21c2e6
--- /dev/null
+++ b/src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h
@@ -0,0 +1,168 @@
+//======================================================================================================================
+//
+//  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/blockforest/communication/SimpleCommunication.h"
+#include "lbm/free_surface/BlockStateDetectorSweep.h"
+#include "lbm/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
+{
+/***********************************************************************************************************************
+ * Handles the surface geometry (normal and curvature computation) by creating fields and adding sweeps.
+ **********************************************************************************************************************/
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class SurfaceGeometryHandler
+{
+ protected:
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+   using vector_t                      = typename std::remove_const< typename VectorField_T::value_type >::type;
+
+   // 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< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+   using Communication_T = blockforest::SimpleCommunication< Stencil_T >;
+   using StateSweep      = BlockStateDetectorSweep< FlagField_T >; // used in friend classes
+
+ public:
+   SurfaceGeometryHandler(const std::shared_ptr< StructuredBlockForest >& blockForest,
+                          const std::shared_ptr< FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                          const BlockDataID& fillFieldID, const std::string& curvatureModel, bool computeCurvature,
+                          bool enableWetting, real_t contactAngleInDegrees)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), 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", vector_t(real_c(0)),
+                                                            field::fzyx, uint_c(1));
+      obstacleNormalFieldID_ = field::addToStorage< VectorField_T >(blockForest_, "Obstacle normal field",
+                                                                    vector_t(real_c(0)), field::fzyx, uint_c(1));
+
+      flagFieldID_ = freeSurfaceBoundaryHandling_->getFlagFieldID();
+
+      obstacleFlagIDSet_ = freeSurfaceBoundaryHandling_->getFlagInfo().getObstacleIDSet();
+
+      if (LatticeModel_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_; }
+
+   void addSweeps(SweepTimeloop& timeloop) const
+   {
+      auto blockStateUpdate = StateSweep(blockForest_, freeSurfaceBoundaryHandling_->getFlagInfo(), flagFieldID_);
+
+      if (!string_icompare(curvatureModel_, "FiniteDifferenceMethod"))
+      {
+         curvature_model::FiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+            model;
+         model.addSweeps(timeloop, *this);
+      }
+      else
+      {
+         if (!string_icompare(curvatureModel_, "LocalTriangulation"))
+         {
+            curvature_model::LocalTriangulation< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+               model;
+            model.addSweeps(timeloop, *this);
+         }
+         else
+         {
+            if (!string_icompare(curvatureModel_, "SimpleFiniteDifferenceMethod"))
+            {
+               curvature_model::SimpleFiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T,
+                                                              VectorField_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
+
+   std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+
+   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, LatticeModel_T, FlagField_T, ScalarField_T,
+                                                         VectorField_T >;
+   friend class curvature_model::LocalTriangulation< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T,
+                                                     VectorField_T >;
+   friend class curvature_model::SimpleFiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T,
+                                                               VectorField_T >;
+}; // class SurfaceGeometryHandler
+
+} // namespace free_surface
+} // namespace walberla
diff --git a/src/lbm/free_surface/surface_geometry/Utility.cpp b/src/lbm/free_surface/surface_geometry/Utility.cpp
new file mode 100644
index 000000000..9b1582c66
--- /dev/null
+++ b/src/lbm/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
+{
+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/free_surface/surface_geometry/Utility.h b/src/lbm/free_surface/surface_geometry/Utility.h
new file mode 100644
index 000000000..7be227e8c
--- /dev/null
+++ b/src/lbm/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
+{
+/***********************************************************************************************************************
+ * 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/tests/lbm/CMakeLists.txt b/tests/lbm/CMakeLists.txt
index d34cb0684..bb8d6dd57 100644
--- a/tests/lbm/CMakeLists.txt
+++ b/tests/lbm/CMakeLists.txt
@@ -130,10 +130,10 @@ waLBerla_compile_test( FILES codegen/FieldLayoutAndVectorizationTest.cpp DEPENDS
 waLBerla_execute_test( NAME FieldLayoutAndVectorizationTest )
 
 waLBerla_generate_target_from_python(NAME LbmPackInfoGenerationTestCodegen FILE codegen/LbmPackInfoGenerationTest.py
-                                     OUT_FILES AccessorBasedPackInfoEven.cpp AccessorBasedPackInfoEven.h
-                                               AccessorBasedPackInfoOdd.cpp AccessorBasedPackInfoOdd.h
-                                               FromKernelPackInfoPull.cpp FromKernelPackInfoPull.h
-                                               FromKernelPackInfoPush.cpp FromKernelPackInfoPush.h)
+        OUT_FILES AccessorBasedPackInfoEven.cpp AccessorBasedPackInfoEven.h
+        AccessorBasedPackInfoOdd.cpp AccessorBasedPackInfoOdd.h
+        FromKernelPackInfoPull.cpp FromKernelPackInfoPull.h
+        FromKernelPackInfoPush.cpp FromKernelPackInfoPush.h)
 
 waLBerla_link_files_to_builddir( "diff_packinfos.sh" )
 waLBerla_execute_test( NAME LbmPackInfoGenerationDiffTest COMMAND bash diff_packinfos.sh )
@@ -170,3 +170,139 @@ waLBerla_compile_test( FILES    codegen/StreamInCellIntervalCodegenTest.cpp
 waLBerla_execute_test( NAME     StreamInCellIntervalCodegenTest )
 
 endif()
+
+# Free Surface
+waLBerla_compile_test( FILES     free_surface/bubble_model/BubbleInitializationTest.cpp
+                       DEPENDS   blockforest field geometry lbm timeloop )
+waLBerla_execute_test( NAME      BubbleInitializationTest
+                       PROCESSES 2 )
+
+file( COPY        free_surface/bubble_model/MergeAndSplitTestUnconnected.png
+                  free_surface/bubble_model/MergeAndSplitTestConnected.png
+      DESTINATION ${CMAKE_CURRENT_BINARY_DIR} )
+waLBerla_compile_test( FILES        free_surface/bubble_model/MergeAndSplitTest.cpp
+                                    free_surface/bubble_model/BubbleBodyMover.impl.h
+                                    free_surface/bubble_model/BubbleModelTester.impl.h
+                       DEPENDS      blockforest field lbm timeloop )
+waLBerla_execute_test( NAME         MergeAndSplitTest
+                       PROCESSES    10 )
+
+waLBerla_compile_test( FILES     free_surface/bubble_model/MergeInformationTest.cpp
+                       DEPENDS   blockforest field lbm timeloop )
+waLBerla_execute_test( NAME      MergeInformationTest
+                       PROCESSES 3 )
+
+waLBerla_compile_test( FILES     free_surface/bubble_model/MovingSpheresTest.cpp
+                                 free_surface/bubble_model/BubbleBodyMover.impl.h
+                                 free_surface/bubble_model/BubbleModelTester.impl.h
+                       DEPENDS   blockforest field geometry lbm timeloop )
+waLBerla_execute_test( NAME      MovingSpheresTest
+                       PROCESSES 2 )
+
+waLBerla_compile_test( FILES    free_surface/bubble_model/RegionalFloodFillTest.cpp
+                       DEPENDS  field lbm )
+waLBerla_execute_test( NAME     RegionalFloodFillTest )
+
+waLBerla_compile_test( FILES    free_surface/bubble_model/SplitDetectionTest.cpp
+                       DEPENDS  field lbm)
+waLBerla_execute_test( NAME     SplitDetectionTest )
+
+waLBerla_compile_test( FILES    free_surface/dynamics/AdvectionTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     AdvectionTest )
+
+waLBerla_compile_test( FILES    free_surface/dynamics/CellConversionTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     CellConversionTest )
+
+if( WALBERLA_BUILD_WITH_CODEGEN )
+   walberla_generate_target_from_python( NAME      LatticeModelGenerationFreeSurfacePython
+                                         FILE      free_surface/dynamics/LatticeModelGenerationFreeSurface.py
+                                         OUT_FILES GeneratedLatticeModel_FreeSurface.cpp
+                                                   GeneratedLatticeModel_FreeSurface.h )
+   waLBerla_compile_test( FILES    free_surface/dynamics/CodegenTest.cpp
+                          DEPENDS  blockforest field lbm timeloop LatticeModelGenerationFreeSurfacePython )
+   waLBerla_execute_test( NAME     CodegenTest )
+endif()
+
+waLBerla_compile_test( FILES    free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     ExcessMassDistributionFallbackTest )
+
+waLBerla_compile_test( FILES        free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
+                       DEPENDS      blockforest field lbm timeloop )
+waLBerla_execute_test( NAME         ExcessMassDistributionParallelTest
+                       PROCESSES    2)
+
+waLBerla_compile_test( FILES    free_surface/dynamics/InflowTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     InflowTest )
+
+waLBerla_compile_test( FILES        free_surface/LoadBalancingTest.cpp
+                       DEPENDS      blockforest field lbm timeloop )
+waLBerla_execute_test( NAME         LoadBalancingTest
+                       PROCESSES    4)
+
+waLBerla_compile_test( FILES    free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     PdfReconstructionFreeSlipTest )
+
+waLBerla_compile_test( FILES    free_surface/dynamics/PdfReconstructionTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     PdfReconstructionTest )
+
+waLBerla_compile_test( FILES    free_surface/dynamics/PdfRefillingTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     PdfRefillingTest )
+
+waLBerla_compile_test( FILES    free_surface/dynamics/WettingConversionTest.cpp
+                       DEPENDS  blockforest field geometry lbm timeloop )
+waLBerla_execute_test( NAME     WettingConversionTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/CellFluidVolumeTest.cpp
+                       DEPENDS  lbm )
+waLBerla_execute_test( NAME     CellFluidVolumeTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/CurvatureOfSineTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     CurvatureOfSineTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/CurvatureOfSphereTest.cpp
+                       DEPENDS  blockforest field geometry lbm timeloop )
+waLBerla_execute_test( NAME     CurvatureOfSphereTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/DetectWettingTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     DetectWettingTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/GetInterfacePointTest.cpp
+                       DEPENDS  lbm )
+waLBerla_execute_test( NAME     GetInterfacePointTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/NormalsOfSineTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     NormalsOfSineTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/NormalsOfSphereTest.cpp
+                       DEPENDS  blockforest field geometry lbm timeloop )
+waLBerla_execute_test( NAME     NormalsOfSphereTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/NormalsEquivalenceTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     NormalsEquivalenceTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/NormalsNearSolidTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     NormalsNearSolidTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/ObstacleFillLevelTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     ObstacleFillLevelTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/ObstacleNormalsTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     ObstacleNormalsTest )
+
+waLBerla_compile_test( FILES    free_surface/surface_geometry/WettingCurvatureTest.cpp
+                       DEPENDS  blockforest field lbm timeloop )
+waLBerla_execute_test( NAME     WettingCurvatureTest )
\ No newline at end of file
diff --git a/tests/lbm/free_surface/LoadBalancingTest.cpp b/tests/lbm/free_surface/LoadBalancingTest.cpp
new file mode 100644
index 000000000..455dc2e17
--- /dev/null
+++ b/tests/lbm/free_surface/LoadBalancingTest.cpp
@@ -0,0 +1,192 @@
+//======================================================================================================================
+//
+//  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 LoadBalancingTest.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test FSLBM load balancing in simplistic setup with 3x3x1 blocks on a 12x12x1 domain.
+//
+//! An initially less optimal weight distribution should be increased after performing load balancing.
+//======================================================================================================================
+
+#include "lbm/free_surface/LoadBalancing.h"
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/BlockStateDetectorSweep.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace LoadBalancingTest
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT >;
+using Stencil_T      = LatticeModel_T::Stencil;
+
+using Communication_T = blockforest::SimpleCommunication< Stencil_T >;
+
+using FlagField_T                   = FlagField< uint32_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment walberlaEnv(argc, argv);
+
+   // define the domain size
+   const Vector3< uint_t > domainSize(uint_c(12), uint_c(12), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(uint_c(3), uint_c(3), uint_c(1));
+   const Vector3< uint_t > periodicity(uint_c(0), uint_c(0), uint_c(0));
+
+   Vector3< uint_t > numBlocks;
+   numBlocks[0] = uint_c(std::ceil(domainSize[0] / cellsPerBlock[0]));
+   numBlocks[1] = uint_c(std::ceil(domainSize[1] / cellsPerBlock[1]));
+   numBlocks[2] = uint_c(std::ceil(domainSize[2] / cellsPerBlock[2]));
+
+   uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses());
+
+   WALBERLA_CHECK_EQUAL(numProcesses, uint_c(4), "This test must be executed with four MPI processes.")
+
+   WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2],
+                             "The number of MPI processes is greater than the number of blocks as defined by "
+                             "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease "
+                             "the number of MPI processes or increase \"cellsPerBlock\".")
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // create (dummy) lattice model with dummy PDF field
+   LatticeModel_T latticeModel  = LatticeModel_T(lbm::collision_model::SRT(real_c(1.0)));
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1));
+
+   // initialize fill level field
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // cell in block-local coordinates
+         const Cell localCell = fillFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = fillFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // set liquid cells
+         if (globalCell[1] < cell_idx_c(5)) { *fillFieldIt = real_c(1); }
+
+         // set interface cells
+         if (globalCell[1] == cell_idx_c(5)) { *fillFieldIt = real_c(0.5); }
+
+         // set gas cells
+         if (globalCell[1] > cell_idx_c(5)) { *fillFieldIt = real_c(0); }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // create boundary handling for initializing flag field
+   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();
+   freeSurfaceBoundaryHandling->setNoSlipAtAllBorders(cell_idx_c(-1));
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // communication after initialization
+   Communication_T communication(blockForest, flagFieldID, fillFieldID);
+   communication();
+
+   Communication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   // create bubble model
+   std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel =
+      std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1));
+
+   // detect block states (detection performed during construction)
+   BlockStateDetectorSweep< FlagField_T > blockStateDetector(blockForest, flagInfo, flagFieldID);
+
+   // the initialization as chosen above results in the following block states
+   // |G|G|G|   with G: onlyGasAndBoundary
+   // |F|F|F|        F: fullFreeSurface
+   // |F|F|F|        L: onlyLBM
+   // |L|L|L|
+   //
+   // Note that the blocks in row 3 have also state F, although they only consist of gas cells. This is because there is
+   // an interface cell in the ghost layer that is synchronized from the blocks of row 2. The BlockStateDetectorSweep
+   // also checks the ghost layer, as e.g. during cell conversion, a block having an interface cell in its ghost layer
+   // must also perform a corresponding conversion on its inner cells.
+
+   uint_t blockWeightFullFreeSurface    = uint_c(50);
+   uint_t blockWeightOnlyLBM            = uint_c(10);
+   uint_t blockWeightOnlyGasAndBoundary = uint_c(5);
+
+   // evaluate process loads
+   ProcessLoadEvaluator< FlagField_T > loadEvaluator(blockForest, blockWeightFullFreeSurface, blockWeightOnlyLBM,
+                                                     blockWeightOnlyGasAndBoundary, uint_c(1));
+
+   // evaluate weight distribution on processes
+   std::vector< real_t > weightSum = loadEvaluator.computeWeightSumPerProcess();
+
+   // initial weight distribution before load balancing
+   WALBERLA_ROOT_SECTION()
+   {
+      WALBERLA_LOG_DEVEL("Checking initial weight distribution")
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[0], real_c(40), real_c(1e-14));
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[1], real_c(200), real_c(1e-14));
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[2], real_c(200), real_c(1e-14));
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[3], real_c(20), real_c(1e-14));
+   }
+
+   // perform load balancing
+   LoadBalancer< FlagField_T, Stencil_T, Stencil_T > loadBalancer(
+      blockForest, communication, pdfCommunication, bubbleModel, blockWeightFullFreeSurface, blockWeightOnlyLBM,
+      blockWeightOnlyGasAndBoundary, uint_c(1), false);
+   loadBalancer();
+
+   // evaluate weight distribution on processes
+   weightSum = loadEvaluator.computeWeightSumPerProcess();
+
+   // check weight distribution after load balancing
+   WALBERLA_ROOT_SECTION()
+   {
+      WALBERLA_LOG_DEVEL("Checking weight distribution after load balancing")
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[0], real_c(140), real_c(1e-14));
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[1], real_c(100), real_c(1e-14));
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[2], real_c(100), real_c(1e-14));
+      WALBERLA_CHECK_FLOAT_EQUAL(weightSum[3], real_c(120), real_c(1e-14));
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace LoadBalancingTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::LoadBalancingTest::main(argc, argv); }
diff --git a/tests/lbm/free_surface/bubble_model/BubbleBodyMover.h b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.h
new file mode 100644
index 000000000..f36124fcd
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.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 BubbleBodyMover.h
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Helper class for bubble model test cases.
+//!
+//! Add bubbles that are represented by geometric bodies. Move the bubbles by updating the fill level and flag field.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "geometry/initializer/OverlapFieldFromBody.h"
+
+#include "BubbleModelTester.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Body_T, typename Stencil_T >
+class BubbleBodyMover : public BubbleModelTester< Stencil_T >
+{
+ public:
+   BubbleBodyMover(const std::shared_ptr< StructuredBlockStorage >& blockStorage,
+                   const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel);
+
+   inline const std::vector< Body_T >& getBodies() const { return bodies_; }
+
+   void addBody(const Body_T& b, std::function< void(Body_T&) > moveFunction)
+   {
+      bodies_.push_back(b);
+      moveFunctions_.push_back(moveFunction);
+   }
+
+   // initialize the added bodies in the fill level and flag fields, and in the bubble model
+   void initAddedBodies();
+
+ protected:
+   void updateDestinationFields() override;
+
+   std::vector< Body_T > bodies_;
+
+   std::vector< std::function< void(Body_T&) > > moveFunctions_;
+
+   geometry::initializer::OverlapFieldFromBody srcFillInitializer_;
+   geometry::initializer::OverlapFieldFromBody dstFillInitializer_;
+};
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "BubbleBodyMover.impl.h"
\ No newline at end of file
diff --git a/tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h
new file mode 100644
index 000000000..f06d6ed68
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h
@@ -0,0 +1,99 @@
+//======================================================================================================================
+//
+//  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 BubbleBodyMover.impl.h
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+//======================================================================================================================
+
+#include "geometry/bodies/AABBBody.h"
+#include "geometry/bodies/Ellipsoid.h"
+#include "geometry/bodies/Sphere.h"
+
+#include "lbm/free_surface/bubble_model/Geometry.h"
+
+#include "BubbleBodyMover.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Body_T, typename Stencil_T >
+BubbleBodyMover< Body_T, Stencil_T >::BubbleBodyMover(const std::shared_ptr< StructuredBlockStorage >& blockStorage,
+                                                      const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel)
+   : BubbleModelTester< Stencil_T >(blockStorage, bubbleModel),
+     srcFillInitializer_(*blockStorage, BubbleModelTester< Stencil_T >::srcFillLevelFieldID_),
+     dstFillInitializer_(*blockStorage, BubbleModelTester< Stencil_T >::dstFillLevelFieldID_)
+{}
+
+template< typename Body_T, typename Stencil_T >
+void BubbleBodyMover< Body_T, Stencil_T >::initAddedBodies()
+{
+   // initialize fill levels
+   for (auto body = bodies_.begin(); body != bodies_.end(); ++body)
+   {
+      srcFillInitializer_.init(*body, false);
+      dstFillInitializer_.init(*body, false);
+   }
+
+   // set flags
+   BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills();
+   BubbleModelTester< Stencil_T >::dstFlagsFromDstFills();
+
+   // initialize bubble model
+   BubbleModelTester< Stencil_T >::bubbleModel_->initFromFillLevelField(
+      BubbleModelTester< Stencil_T >::dstFillLevelFieldID_);
+}
+
+template< typename Body_T, typename Stencil_T >
+void BubbleBodyMover< Body_T, Stencil_T >::updateDestinationFields()
+{
+   using BubbleModelTester_T = BubbleModelTester< Stencil_T >;
+
+   // move bodies according to moveFunctions_
+   for (uint_t i = uint_c(0); i < bodies_.size(); ++i)
+   {
+      moveFunctions_[i](bodies_[i]);
+   }
+
+   // clear destination field
+   for (auto blockIt = this->blockStorage_->begin(); blockIt != this->blockStorage_->end(); ++blockIt)
+   {
+      typename BubbleModelTester_T::FlagField_T* const flagField =
+         blockIt->template getData< typename BubbleModelTester_T::FlagField_T >(this->dstFlagFieldID_);
+      typename BubbleModelTester_T::ScalarField_T* const fillField =
+         blockIt->template getData< typename BubbleModelTester_T::ScalarField_T >(this->dstFillLevelFieldID_);
+
+      flagField->setWithGhostLayer(typename BubbleModelTester_T::flag_t(0));
+      fillField->setWithGhostLayer(real_c(1.0));
+   }
+
+   // update fill level field with new body position
+   for (auto body = bodies_.begin(); body != bodies_.end(); ++body)
+   {
+      dstFillInitializer_.init(*body, false);
+   }
+
+   // update flags
+   this->dstFlagsFromDstFills();
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp b/tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp
new file mode 100644
index 000000000..08641c5b8
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp
@@ -0,0 +1,154 @@
+//======================================================================================================================
+//
+//  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 BubbleInitializationTest.cpp
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test bubble initialization from fill level by evaluating initial bubble volumes (within and across blocks).
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/math/Constants.h"
+#include "core/mpi/MPIManager.h"
+
+#include "field/AddToStorage.h"
+#include "field/Printers.h"
+
+#include "geometry/bodies/Sphere.h"
+
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+#include "lbm/free_surface/bubble_model/Geometry.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace BubbleInitializationTest
+{
+// define field
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+
+// class derived from BubbleModel to access its protected members for testing purposes
+template< typename Stencil_T >
+class BubbleModelTest : public bubble_model::BubbleModel< Stencil_T >
+{
+ public:
+   BubbleModelTest(const std::shared_ptr< StructuredBlockForest >& blockForest)
+      : bubble_model::BubbleModel< Stencil_T >(blockForest, true)
+   {}
+
+   real_t getBubbleInitVolume(bubble_model::BubbleID id)
+   {
+      WALBERLA_ASSERT_GREATER(bubble_model::BubbleModel< Stencil_T >::getBubbles().size(), id);
+      return bubble_model::BubbleModel< Stencil_T >::getBubbles()[id].getInitVolume();
+   }
+}; // class BubbleModelTest
+
+template< typename Stencil_T >
+void testBubbleInitialization()
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(2), uint_c(1), uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(56), uint_c(16), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, false,                                   // periodicity
+                                          true);                                                // global info
+
+   // add fill level field
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+
+   const real_t xQuarter = real_c(domainSize[0]) / real_c(4);
+   const real_t yHalf    = real_c(domainSize[1]) / real_c(2);
+
+   // add sphere in the left half (in x-direction) of the domain
+   bubble_model::addBodyToFillLevelField(
+      *blockForest, fillFieldID,
+      geometry::Sphere(Vector3< real_t >(real_c(0.75) * xQuarter, yHalf, real_c(0)), xQuarter * real_c(0.25)), true);
+
+   // add sphere in the center of the domain (across blockForest)
+   bubble_model::addBodyToFillLevelField(
+      *blockForest, fillFieldID,
+      geometry::Sphere(Vector3< real_t >(real_c(domainSize[0]) * real_c(0.5), yHalf, real_c(0)), yHalf), true);
+
+   // add sphere in the right half (in x-direction) of the domain
+   bubble_model::addBodyToFillLevelField(
+      *blockForest, fillFieldID,
+      geometry::Sphere(Vector3< real_t >(real_c(3.25) * xQuarter, yHalf, real_c(0)), xQuarter * real_c(0.25)), true);
+
+   // create bubble model
+   BubbleModelTest< Stencil_T > bubbleModel(blockForest);
+   bubbleModel.initFromFillLevelField(fillFieldID);
+
+   // test correctness of bubble volumes (bubble IDs were determined empirically for this test)
+   // left bubble
+   WALBERLA_CHECK_LESS(
+      std::abs(bubbleModel.getBubbleInitVolume(2) - (xQuarter * real_c(0.25) * xQuarter * real_c(0.25) * math::pi)),
+      real_c(1.07));
+
+   // center bubble
+   WALBERLA_CHECK_LESS(std::abs(bubbleModel.getBubbleInitVolume(0) - (yHalf * yHalf * math::pi)), real_c(1.12));
+
+   // right bubble
+   WALBERLA_CHECK_LESS(
+      std::abs(bubbleModel.getBubbleInitVolume(1) - (xQuarter * real_c(0.25) * xQuarter * real_c(0.25) * math::pi)),
+      real_c(1.07));
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   auto mpiManager = MPIManager::instance();
+
+   WALBERLA_CHECK_EQUAL(mpiManager->numProcesses(), 2);
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D2Q9 stencil.");
+   testBubbleInitialization< stencil::D2Q9 >();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q19 stencil.");
+   testBubbleInitialization< stencil::D3Q19 >();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q27 stencil.");
+   testBubbleInitialization< stencil::D3Q27 >();
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace BubbleInitializationTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::BubbleInitializationTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/bubble_model/BubbleModelTester.h b/tests/lbm/free_surface/bubble_model/BubbleModelTester.h
new file mode 100644
index 000000000..f70d71476
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/BubbleModelTester.h
@@ -0,0 +1,96 @@
+//======================================================================================================================
+//
+//  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 BubbleModelTester.h
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Helper class for bubble model test cases.
+//!
+//! Provides a source and destination field for fill levels and flags. A derived class is supposed to work on the
+//! destination fields. This (base) class then updates the bubble model and converts cells with respect to any changes
+//! in the destination fields.
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "field/FlagField.h"
+#include "field/GhostLayerField.h"
+
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Stencil_T >
+class BubbleModelTester
+{
+ public:
+   // define fields
+   using flag_t        = uint32_t;
+   using FlagField_T   = FlagField< flag_t >;
+   using ScalarField_T = GhostLayerField< real_t, 1 >;
+
+   BubbleModelTester(const std::shared_ptr< StructuredBlockStorage >& blockStorage,
+                     const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel);
+   virtual ~BubbleModelTester() = default;
+
+   // swap source and destination fill level fields;
+   // swap source and destination flag fields;
+   // update bubble model;
+   // update destination fields;
+   virtual void operator()();
+
+   inline ConstBlockDataID getFlagFieldID() const { return srcFlagFieldID_; }
+   inline ConstBlockDataID getFillLevelFieldID() const { return srcFillLevelFieldID_; }
+
+ private:
+   // report cell conversions from liquid to interface;
+   // report changes in the fill level (destination fill level - source fill level);
+   // report cell conversions from interface to liquid;
+   // check interface layer for correctness
+   void updateBubbleModel();
+
+ protected:
+   virtual void updateDestinationFields() = 0;
+
+   // initialize flag field from fill level
+   void srcFlagsFromSrcFills();
+   void dstFlagsFromDstFills();
+
+   std::shared_ptr< StructuredBlockStorage > blockStorage_;
+   std::shared_ptr< BubbleModel< Stencil_T > > bubbleModel_;
+
+   uint32_t liquidFlag_;
+   uint32_t gasFlag_;
+   uint32_t interfaceFlag_;
+
+   BlockDataID srcFlagFieldID_;
+   BlockDataID dstFlagFieldID_;
+   BlockDataID srcFillLevelFieldID_;
+   BlockDataID dstFillLevelFieldID_;
+};
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
+
+#include "BubbleModelTester.impl.h"
\ No newline at end of file
diff --git a/tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h b/tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h
new file mode 100644
index 000000000..230655c16
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h
@@ -0,0 +1,160 @@
+//======================================================================================================================
+//
+//  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 BubbleModelTester.impl.h
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+//======================================================================================================================
+
+#include "field/AddToStorage.h"
+
+#include "lbm/free_surface/bubble_model/Geometry.h"
+
+#include "BubbleModelTester.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+template< typename Stencil_T >
+BubbleModelTester< Stencil_T >::BubbleModelTester(const std::shared_ptr< StructuredBlockStorage >& blockStorage,
+                                                  const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel)
+   : blockStorage_(blockStorage), bubbleModel_(bubbleModel)
+{
+   // add flag fields
+   srcFlagFieldID_ = field::addFlagFieldToStorage< FlagField_T >(blockStorage_, "FlagFieldSrc");
+   dstFlagFieldID_ = field::addFlagFieldToStorage< FlagField_T >(blockStorage_, "FlagFieldDst");
+
+   // add fill level fields
+   srcFillLevelFieldID_ =
+      field::addToStorage< ScalarField_T >(blockStorage_, "FillLevelSrc", real_c(1.0), field::fzyx, uint_c(1));
+   dstFillLevelFieldID_ =
+      field::addToStorage< ScalarField_T >(blockStorage_, "FillLevelDst", real_c(1.0), field::fzyx, uint_c(1));
+
+   // register flags
+   for (auto blockIt = blockStorage->begin(); blockIt != blockStorage->end(); ++blockIt)
+   {
+      FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_);
+      FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_);
+
+      liquidFlag_    = srcFlagField->registerFlag("liquid", uint_c(1));
+      gasFlag_       = srcFlagField->registerFlag("gas", uint_c(2));
+      interfaceFlag_ = srcFlagField->registerFlag("interface", uint_c(3));
+
+      dstFlagField->registerFlag("liquid", uint_c(1));
+      dstFlagField->registerFlag("gas", uint_c(2));
+      dstFlagField->registerFlag("interface", uint_c(3));
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills()
+{
+   // initialize flag field from fill level
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      FlagField_T* const srcFlagField         = blockIt->getData< FlagField_T >(srcFlagFieldID_);
+      const ScalarField_T* const srcFillLevel = blockIt->getData< const ScalarField_T >(srcFillLevelFieldID_);
+
+      bubble_model::setFlagFieldFromFillLevels(srcFlagField, srcFillLevel, "liquid", "gas", "interface");
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModelTester< Stencil_T >::dstFlagsFromDstFills()
+{
+   // initialize flag field from fill level
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      FlagField_T* const dstFlagField         = blockIt->getData< FlagField_T >(dstFlagFieldID_);
+      const ScalarField_T* const dstFillLevel = blockIt->getData< const ScalarField_T >(dstFillLevelFieldID_);
+
+      bubble_model::setFlagFieldFromFillLevels(dstFlagField, dstFillLevel, "liquid", "gas", "interface");
+   }
+}
+
+template< typename Stencil_T >
+void BubbleModelTester< Stencil_T >::operator()()
+{
+   // swap source and destination fill level fields, and flag fields
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      ScalarField_T* const srcFillLevel = blockIt->getData< ScalarField_T >(srcFillLevelFieldID_);
+      ScalarField_T* const dstFillLevel = blockIt->getData< ScalarField_T >(dstFillLevelFieldID_);
+
+      FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_);
+      FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_);
+
+      srcFlagField->swapDataPointers(*dstFlagField);
+      srcFillLevel->swapDataPointers(*dstFillLevel);
+   }
+
+   updateDestinationFields();
+   updateBubbleModel();
+}
+
+template< typename Stencil_T >
+void BubbleModelTester< Stencil_T >::updateBubbleModel()
+{
+   for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+   {
+      IBlock* thisBlock = &(*blockIt);
+
+      ScalarField_T* const srcFillLevel = blockIt->getData< ScalarField_T >(srcFillLevelFieldID_);
+      ScalarField_T* const dstFillLevel = blockIt->getData< ScalarField_T >(dstFillLevelFieldID_);
+      WALBERLA_ASSERT(srcFillLevel->hasSameSize(*dstFillLevel));
+      WALBERLA_ASSERT_EQUAL_2(srcFillLevel->xyzSize(), dstFillLevel->xyzSize());
+
+      FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_);
+      FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_);
+      WALBERLA_ASSERT_EQUAL_2(srcFlagField->xyzSize(), dstFlagField->xyzSize());
+
+      WALBERLA_ASSERT_EQUAL_2(srcFlagField->xyzSize(), srcFillLevel->xyzSize());
+
+      // report cell conversion from liquid to interface; explicitly avoid OpenMP when setting bubble IDs
+      WALBERLA_FOR_ALL_CELLS_OMP(srcFlagFieldIt, srcFlagField, dstFlagFieldIt, dstFlagField, omp critical, {
+         if (isFlagSet(srcFlagFieldIt, liquidFlag_) && isFlagSet(dstFlagFieldIt, interfaceFlag_))
+         {
+            bubbleModel_->reportLiquidToInterfaceConversion(thisBlock, srcFlagFieldIt.cell());
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+
+      // report changes in the fill level
+      WALBERLA_FOR_ALL_CELLS_OMP(
+         srcFlagFieldIt, srcFlagField, dstFlagFieldIt, dstFlagField, srcFillIt, srcFillLevel, dstFillIt, dstFillLevel,
+         omp critical, {
+            if (isFlagSet(srcFlagFieldIt, interfaceFlag_) || isFlagSet(dstFlagFieldIt, interfaceFlag_))
+            {
+               bubbleModel_->reportFillLevelChange(thisBlock, srcFillIt.cell(), *dstFillIt - *srcFillIt);
+            }
+         }) // WALBERLA_FOR_ALL_CELLS_OMP
+
+      // report cell conversion from interface to liquid
+      WALBERLA_FOR_ALL_CELLS_OMP(srcFlagFieldIt, srcFlagField, dstFlagFieldIt, dstFlagField, omp critical, {
+         if (isFlagSet(srcFlagFieldIt, interfaceFlag_) && isFlagSet(dstFlagFieldIt, liquidFlag_))
+         {
+            bubbleModel_->reportInterfaceToLiquidConversion(thisBlock, srcFlagFieldIt.cell());
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+   }
+}
+
+} // namespace bubble_model
+} // namespace free_surface
+} // namespace walberla
diff --git a/tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp b/tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp
new file mode 100644
index 000000000..b9dab0763
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp
@@ -0,0 +1,230 @@
+//======================================================================================================================
+//
+//  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 MergeAndSplitTest.cpp
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test bubble merging and splitting in a complex multi-process scenario.
+//!
+//! Initialize the fill levels, flags and bubble model from image MergeAndSplitTestUnconnected.png. This sets up a
+//! complex scenario of 12 bubbles that are located on 10 processes. Bubble merging is tested by loading image
+//! MergeAndSplitTestConnected.png. By loading the initial image in a second time step, bubble splitting is tested.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/mpi/MPIManager.h"
+
+#include "geometry/initializer/ScalarFieldFromGrayScaleImage.h"
+#include "geometry/structured/GrayScaleImage.h"
+
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <vector>
+
+#include "BubbleModelTester.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace MergeAndSplitTest
+{
+// class derived from BubbleModel to access its protected members for testing purposes
+template< typename Stencil_T >
+class BubbleModelTest : public bubble_model::BubbleModel< Stencil_T >
+{
+ public:
+   BubbleModelTest(const std::shared_ptr< StructuredBlockForest >& blockStorage)
+      : bubble_model::BubbleModel< Stencil_T >(blockStorage, true)
+   {}
+
+   static void testComplexMerge();
+}; // class BubbleModelTest
+
+// initialize and update a source and destination field for fill levels and flags from images; in an alternating manner,
+// update the fields such that the bubble model must either handle bubble merging or splitting on every second call
+template< typename Stencil_T >
+class ImageMover : public bubble_model::BubbleModelTester< Stencil_T >
+{
+ public:
+   ImageMover(const std::shared_ptr< StructuredBlockStorage >& blockStorage,
+              const std::shared_ptr< bubble_model::BubbleModel< Stencil_T > >& bubbleModel)
+      : bubble_model::BubbleModelTester< Stencil_T >(blockStorage, bubbleModel),
+        imgInitializerSrc_(*blockStorage, bubble_model::BubbleModelTester< Stencil_T >::srcFillLevelFieldID_),
+        imgInitializerDst_(*blockStorage, bubble_model::BubbleModelTester< Stencil_T >::dstFillLevelFieldID_),
+        calls_(uint_c(0))
+   {
+      // load image of test scenario with bubbles that are not connected
+      geometry::GrayScaleImage img("MergeAndSplitTestUnconnected.png");
+
+      // initialize fill level field from image
+      imgInitializerSrc_.init(img, 2, false);
+      imgInitializerDst_.init(img, 2, false);
+
+      // initialize flag field
+      bubble_model::BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills();
+      bubble_model::BubbleModelTester< Stencil_T >::dstFlagsFromDstFills();
+
+      // initialize bubble model
+      bubble_model::BubbleModelTester< Stencil_T >::bubbleModel_->initFromFillLevelField(
+         bubble_model::BubbleModelTester< Stencil_T >::dstFillLevelFieldID_);
+   }
+
+   // in an alternating manner, update fill levels such that the bubbles must either merge or split on every second call
+   void updateDestinationFields() override
+   {
+      // load image of test scenario with bubbles that are not connected
+      geometry::GrayScaleImage unconnected("MergeAndSplitTestUnconnected.png");
+
+      // load image of test scenario with bubbles that are connected
+      geometry::GrayScaleImage connected("MergeAndSplitTestConnected.png");
+
+      if (uint_c(calls_) % uint_c(2) == uint_c(0))
+      {
+         // bubbles must merge (destination field is updated to be connected)
+         imgInitializerSrc_.init(unconnected, 2, false);
+         imgInitializerDst_.init(connected, 2, false);
+      }
+      else
+      {
+         // bubbles must split (destination field is updated to be unconnected)
+         imgInitializerSrc_.init(connected, 2, false);
+         imgInitializerDst_.init(unconnected, 2, false);
+      }
+
+      // update flag field
+      bubble_model::BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills();
+      bubble_model::BubbleModelTester< Stencil_T >::dstFlagsFromDstFills();
+
+      ++calls_;
+   }
+
+ private:
+   geometry::initializer::ScalarFieldFromGrayScaleImage imgInitializerSrc_;
+   geometry::initializer::ScalarFieldFromGrayScaleImage imgInitializerDst_;
+   uint_t calls_;
+}; // class ImageMover
+
+template< typename Stencil_T >
+void BubbleModelTest< Stencil_T >::testComplexMerge()
+{
+   auto mpiManager = MPIManager::instance();
+
+   WALBERLA_CHECK_EQUAL(mpiManager->numProcesses(), 10);
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(2), uint_c(5), uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(100), uint_c(200), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, false);                                  // periodicity
+
+   // create bubble model
+   std::shared_ptr< BubbleModelTest > bubbleModel = std::make_shared< BubbleModelTest >(blockForest);
+
+   // create imageMover that initializes the fill level field, and bubble model; updates the fill level field in an
+   // alternating manner, such that bubbles must either merge or split on every second call
+   ImageMover< Stencil_T > imageMover(blockForest, bubbleModel);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(10));
+
+   // add imageMover to timeloop
+   timeloop.addFuncBeforeTimeStep(imageMover, "UpdateDomainFromImage");
+
+   // update bubble model in timeloop
+   timeloop.addFuncAfterTimeStep(std::bind(&bubble_model::BubbleModel< Stencil_T >::update, bubbleModel));
+
+   // ensure correctness of initialization (number of bubbles)
+   WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 12);
+
+   // compute total volume of all bubbles
+   real_t volumeBefore = real_c(0);
+   for (auto b = bubbleModel->getBubbles().begin(); b != bubbleModel->getBubbles().end(); ++b)
+   {
+      volumeBefore += b->getCurrentVolume();
+   }
+
+   // ensure correctness of initialization (total volume of all bubbles)
+   WALBERLA_CHECK_LESS(std::abs(volumeBefore - real_c(8905.51)), 0.1);
+
+   // merge bubbles
+   timeloop.singleStep();
+
+   // there must be a single bubble in the system
+   WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 1);
+
+   // the total volume of this single bubble must be slightly larger than the initial volume of all bubbles
+   real_t volumeAfter = bubbleModel->getBubbles()[0].getCurrentVolume();
+   WALBERLA_CHECK_LESS(std::abs(volumeAfter - real_c(8918.41)), 0.1);
+
+   // split bubbles
+   timeloop.singleStep();
+
+   // the number of bubbles must be as before merging
+   WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 12);
+
+   // compute total volume of all bubbles
+   real_t volumeAfterSplit = real_c(0);
+   for (auto b = bubbleModel->getBubbles().begin(); b != bubbleModel->getBubbles().end(); ++b)
+   {
+      volumeAfterSplit += b->getCurrentVolume();
+   }
+
+   // the total volume of all bubbles must be as before merging
+   WALBERLA_CHECK_LESS(std::abs(volumeAfterSplit - real_c(8905.51)), 0.1);
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D2Q9 stencil.");
+   BubbleModelTest< stencil::D2Q9 >::testComplexMerge();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q19 stencil.");
+   BubbleModelTest< stencil::D3Q19 >::testComplexMerge();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q27 stencil.");
+   BubbleModelTest< stencil::D3Q27 >::testComplexMerge();
+
+   return EXIT_SUCCESS;
+}
+} // namespace MergeAndSplitTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::MergeAndSplitTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png
new file mode 100644
index 0000000000000000000000000000000000000000..08b183c1a3e0cef8d84530859474d78a135ef764
GIT binary patch
literal 1681
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
bU(o#l%_AWiKn1LV00000NkvXXu0mjf{pt}>

literal 0
HcmV?d00001

diff --git a/tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e6039cdcffe6832eb04643fd830b6c8fd3bb7a6
GIT binary patch
literal 1649
zcmV-%29EiOP)<h;3K|Lk000e1NJLTq003kF0077c00000J69VW00001b5ch_0Itp)
z=>Px#0%A)?L;(MXkIcUS000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*k_2L&cj
zxEg~10013nR9JLFZ*6U5Zgc<u0000(a%Ew3Wn>_CX>@2HM@dakWG-a~000HdNkl<Z
zc-rk;NwVWG2qh{X7)}B=PA?+IngI>JyMA?cNi07xKd_S58_vJ3sa2G{W9{<SJ6*?C
z1WDeby6giI*P#iAr1$`}Rj)Sy(6cjrTLe;lrmPA8ne<ZV+k~O53IIg;^?nR()m1N}
zp^AIABOcj=S}N|j8g)TNW@&<aky!OC6H4goML=@`t644pl<qPlV}OodVR#b}DV%Q-
zMArfx2Pt%oXF#%W=0lRGNnj7ivc9}UiwSkfs8Fp<63VQCDu#%pmNbE6E7(|Yk~P7a
zGstSJZw`Id1eic+>x;`gEpG1;D?5=5E<EGfVaoM!psh9tyYI_`eMv9kO``A+1)(p<
z&AZenMqo<Oiemq%z2$w7a|S6Y#$DedU(9ZJV-$DEK~xmv<dR4+DA#2}9R-M@F$AW9
zWZ&XMjOO|hWK5|ZWpx-+d+`l&<?E#9Sk!P;qz6W9^@IU{uwB;M$~04^*IVnOJ{lJo
z=`vn`MNHN<@l*NKe+#Zb(zD4N1_%-`4;A;;UwYO-Y%aM!4HiKr3McyN30ZhoNtxNt
zAZ>+{_K~|$J2Ok}XvYRF&+Op@2zd~24upJC@C>V`Et=B2%Y}hcHX$bjPf6l3I`e2(
zVS^wD3`8>#Zx#gcQ`NF0Nfw)s&vywV)|KWSuRXa$@|sT;@0yK}4~6-b_}T8aXBcVi
zkGH0J2E;*phCS)M(Re3Awp~SwOJPtM7VtU@X2ZfwhQFC6-b|7;nri91+Ip$SDkGXo
z_K-oxQ-_g@sa;t225ctWiuSNSrjEujv3iBJG(`FC3+*6@`ots(N%~!Ign^xIQ}Nx~
z^+LoJWxzqRfu`1>l&1wlvy{jIBd`}F!t5e<uEf3{q~U6`AH+!twsl&+2ZVkAM1De&
zx&Fk95!opp1cAFhY2r+SU@?;XwwRbd1;z!NaM_^@s>_1m@qx%^d$LbmOqdBJN4$#%
zHY()3)<c}5VA2Q~1Q%PWZ!S3@x~+m=3w`I)8VGeV2!fP^h>|&N_Jl$KArlvVP@Wr8
zYO?`|dD>+=9J7Lusf^l9RIfjAcG;M=?<SC$NmugtqoKVP6`e~n&%%<mhAna^<?&i+
z7yFLMnR%{lVx$sb6f4e+!U2qAZuBWG;SffK6}F6^l+<*&LSe8S9N6bFkU*@;6hZkI
zFDvsna^c?gxURj`-FQ$nuet2Xie4$(=hfHNaA-m!G)|Smxg_Gjh_#k1)3M7;N1C}u
zGQx;nwt~h?n2emc#gKR?37caYYeOXr3AT<036i06M8VA<Ns%A`jx?6Hg5-=FDQ)$G
z)z;qROYPQySMS?>H`kw7xCX5baM+|UT9d@2KKX^Sm1{DzU7ml<0EjcoUDI)W%qEk>
zEc^h*@Ok_)hI*n?)-k?W@*fMvo*<};5{?=@qJ$ZS;zbE_sl*)c;!xLGdt^V9ya|N=
zz6+lLp>>~#4!eFh#_;2j0L!ApW*z$u9;_@-3%T}@Yg8q%|B9xM!m4+PS(pwAnp`-d
zkQD`+DJLi#SAc9v6R4j*e&485J2Z~+KIc=@Yq4`Sbs@8SG|TDJ2;QGoN|cBDSL6=o
z^Q|Cp`pn~bsk2mcj90OJR3OyRv(h?}F?Vsp4bf~CYl6&B5ZKhcxmi8a_Za`}hFA<Z
zeC0qpOT<Hc*Q36+(BlIn-ec<R1;ZvtwS_ULmLlHj5dV)a^e8&*2Vq;t|F_BS7~p6X
zg<ZL8?ngFt#yg;{ns%PBNriI6@UmGFv=E9jE-YImu@R_MNlYlq>SBYXj@ShKKL&F<
zSoK_Q9fYf_Qhl@|L8ip0`IPmw;g5Env(z0&cy0MN9~u*}*T4IhL6-RoZ<1tm94PaE
z-F2~5A;S?>6}4+A7{&N?I3`GPNm#%TtEAzWvXaz!mO=6|h<$PqR(CmylO1Vg-1{bF
vWhJf9FsUS>u4Q2oJ#VdNVKmw6N9xHR{KB}?8KFDH00000NkvXXu0mjfwGQ)R

literal 0
HcmV?d00001

diff --git a/tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp b/tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp
new file mode 100644
index 000000000..84e559b61
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp
@@ -0,0 +1,231 @@
+//======================================================================================================================
+//
+//  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 MergeInformationTest.cpp
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test bubble merge and reordering, merge registering, and merge communication.
+//
+//======================================================================================================================
+
+#include "lbm/free_surface/bubble_model/MergeInformation.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/mpi/MPIManager.h"
+
+#include <iostream>
+#include <vector>
+
+namespace std
+{
+// overload std::cout for printing vector content
+template< typename T >
+std::ostream& operator<<(std::ostream& os, const std::vector< T >& vec)
+{
+   os << "Vector (" << vec.size() << "):  ";
+   for (auto i = vec.begin(); i != vec.end(); ++i)
+   {
+      os << *i << " ";
+   }
+   os << std::endl;
+
+   return os;
+}
+} // namespace std
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace bubble_model
+{
+// test if renameVec_ and vecCompare are equal
+void checkRenameVec(const bubble_model::MergeInformation& mi, const std::vector< bubble_model::BubbleID >& vecCompare)
+{
+   // renameVec_ and vecCompare must have the same size
+   WALBERLA_ASSERT_EQUAL(mi.renameVec_.size(), vecCompare.size());
+
+   bool equal = true;
+   for (size_t i = 0; i < mi.renameVec_.size(); ++i)
+      if (mi.renameVec_[i] != vecCompare[i])
+      {
+         equal = false;
+         break;
+      }
+
+   WALBERLA_CRITICAL_SECTION_START
+   if (!equal)
+   {
+      WALBERLA_LOG_WARNING("Process " << MPIManager::instance()->rank()
+                                      << ": rename vector different than expected result.\n"
+                                      << "\tExpected:\n"
+                                      << "\t\t" << vecCompare << "\tResult:\n"
+                                      << "\t\t" << mi.renameVec_);
+   }
+   WALBERLA_CRITICAL_SECTION_END
+
+   WALBERLA_CHECK(equal);
+}
+} // namespace bubble_model
+
+namespace MergeInformationTest
+{
+void mergeAndReorderTest1()
+{
+   std::vector< bubble_model::Bubble > bubbles;
+   bubbles.emplace_back(real_c(10));                       // BubbleID 0
+   bubbles.emplace_back(bubble_model::Bubble(real_c(11))); // BubbleID 1
+   bubbles.emplace_back(bubble_model::Bubble(real_c(12))); // BubbleID 2
+   bubbles.emplace_back(bubble_model::Bubble(real_c(13))); // BubbleID 3
+
+   bubble_model::MergeInformation mi(bubbles.size());
+   mi.registerMerge(0, 1);
+
+   // merge bubbles with IDs 0 and 1 to ID 0:
+   // - ID 4 gets assigned ID 1 (highest ID, i.e., last position is copied to free space position 1)
+   // - highest ID is now 3 and bubbles vector must have reduced to size 2
+   mi.mergeAndReorderBubbleVector(bubbles);
+
+   WALBERLA_CHECK_EQUAL(bubbles.size(), 3);
+   WALBERLA_CHECK_FLOAT_EQUAL(bubbles[0].getInitVolume(), real_c(10 + 11));
+   WALBERLA_CHECK_FLOAT_EQUAL(bubbles[1].getInitVolume(), real_c(13));
+   WALBERLA_CHECK_FLOAT_EQUAL(bubbles[2].getInitVolume(), real_c(12));
+}
+
+void mergeAndReorderTest2()
+{
+   std::vector< bubble_model::Bubble > bubbles;
+   bubbles.emplace_back(bubble_model::Bubble(real_c(10))); // BubbleID 0
+   bubbles.emplace_back(bubble_model::Bubble(real_c(11))); // BubbleID 1
+   bubbles.emplace_back(bubble_model::Bubble(real_c(12))); // BubbleID 2
+   bubbles.emplace_back(bubble_model::Bubble(real_c(13))); // BubbleID 3
+   bubbles.emplace_back(bubble_model::Bubble(real_c(14))); // BubbleID 4
+   bubbles.emplace_back(real_c(15));                       // BubbleID 5
+
+   bubble_model::MergeInformation mi(bubbles.size());
+   mi.registerMerge(4, 3);
+   mi.registerMerge(3, 2);
+   mi.registerMerge(0, 1);
+
+   // merge bubbles with IDs 4, 3, 2 to ID 2, merge bubbles with IDs 0 and 1 to ID 0:
+   // - ID 5 gets assigned ID 1 (highest ID, i.e., last position is copied to free space position 1)
+   // - highest ID is now 3 and bubbles vector must have reduced to size 2
+   mi.mergeAndReorderBubbleVector(bubbles);
+
+   WALBERLA_CHECK_EQUAL(bubbles.size(), 3);
+   WALBERLA_CHECK_FLOAT_EQUAL(bubbles[0].getInitVolume(), real_c(10 + 11));
+   WALBERLA_CHECK_FLOAT_EQUAL(bubbles[1].getInitVolume(), real_c(15));
+   WALBERLA_CHECK_FLOAT_EQUAL(bubbles[2].getInitVolume(), real_c(12 + 13 + 14));
+}
+
+void mergeRegisterTest()
+{
+   // create new MergeInformation with 6 bubbles
+   bubble_model::MergeInformation mi(6);
+
+   // initial ID distribution
+   std::vector< bubble_model::BubbleID > correctRenameVec = { 0, 1, 2, 3, 4, 5 };
+   checkRenameVec(mi, correctRenameVec);
+
+   // Function registerMerge() only registers the merge by (temporarily) renaming bubble IDs appropriately. Therefore,
+   // in below comments, it is more meaningful to write "position x (in renameVec_) is renamed to the ID at position
+   // y" than "ID x is renamed to ID y".
+
+   // position 5 is renamed to the ID at position 3
+   mi.registerMerge(5, 3);
+   correctRenameVec = { 0, 1, 2, 3, 4, 3 };
+   checkRenameVec(mi, correctRenameVec);
+
+   // position 3 is renamed to the ID at position 1
+   mi.registerMerge(3, 1);
+   correctRenameVec = { 0, 1, 2, 1, 4, 3 };
+   checkRenameVec(mi, correctRenameVec);
+
+   // since position 3 was already renamed to the ID at position 1, registerMerge(1, 0) is called internally and
+   // position 1 is renamed to the ID at position 0; position 3 is renamed to the ID at position 0
+   mi.registerMerge(3, 0);
+   correctRenameVec = { 0, 0, 2, 0, 4, 3 };
+   checkRenameVec(mi, correctRenameVec);
+
+   // since position 5 was already renamed to the ID of position 3, registerMerge(3, 2) is called internally; since
+   // position 3 was already renamed to the ID of position 0, registerMerge(2, 0) is called internally; position 2 is
+   // renamed to the ID at position 0, position 5 is renamed to the ID at position 0
+   mi.registerMerge(5, 2);
+   correctRenameVec = { 0, 0, 0, 0, 4, 0 };
+   checkRenameVec(mi, correctRenameVec);
+}
+
+void mergeCommunicationTest()
+{
+   auto mpiManager = MPIManager::instance();
+
+   // this test is only meaningful with multiple processes
+   if (!mpiManager->isMPIInitialized() || mpiManager->numProcesses() < 3) { return; }
+
+   // create new MergeInformation with 5 bubbles and renameVec_={ 0, 1, 2, 3, 4 }
+   bubble_model::MergeInformation mi(5);
+
+   if (mpiManager->rank() == 0) { mi.registerMerge(1, 0); }
+   else
+   {
+      if (mpiManager->rank() == 1)
+      {
+         mi.registerMerge(3, 2);
+         mi.registerMerge(2, 1);
+      }
+      else
+      {
+         if (mpiManager->rank() == 2) { mi.registerMerge(4, 1); }
+      }
+   }
+
+   // before communication:
+   // process 0: renameVec_={ 0, 0, 2, 3, 4 }
+   // process 1: renameVec_={ 0, 1, 1, 1, 4 }
+   // process 2: renameVec_={ 0, 1, 2, 3, 1 }
+
+   mi.communicateMerges();
+
+   // after communication:
+   // process 0 to 2: renameVec_={ 0, 0, 0, 0, 0 }
+   std::vector< bubble_model::BubbleID > res = { 0, 0, 0, 0, 0 };
+   checkRenameVec(mi, res);
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   // the MPI communicator is normally created with the block forest; we will not use a block forest in this test and
+   // thus choose the MPI communicator manually
+   WALBERLA_MPI_SECTION() { MPIManager::instance()->useWorldComm(); }
+
+   mergeAndReorderTest1();
+   mergeAndReorderTest2();
+
+   mergeRegisterTest();
+   mergeCommunicationTest();
+
+   return EXIT_SUCCESS;
+}
+} // namespace MergeInformationTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::MergeInformationTest::main(argc, argv); }
diff --git a/tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp b/tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp
new file mode 100644
index 000000000..36f09079f
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp
@@ -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 MovingSpheresTest.cpp
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//!
+//! \brief Test volume conservation of moving bubbles, bubble merging and bubble splitting.
+//!
+//! A spherical bubble is moved towards a static spherical bubble in the center of the domain. The bubbles merge and
+//! split again, as the movement of the spherical volume is continued after merging.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/mpi/MPIManager.h"
+
+#include "geometry/bodies/Sphere.h"
+
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "BubbleBodyMover.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace MovingSpheresTest
+{
+// class derived from BubbleModel to access its protected members for testing purposes
+template< typename Stencil_T >
+class BubbleModelTest : public bubble_model::BubbleModel< Stencil_T >
+{
+ public:
+   BubbleModelTest(const std::shared_ptr< StructuredBlockForest >& blockForest)
+      : bubble_model::BubbleModel< Stencil_T >(blockForest, true)
+   {}
+
+   static void testMovingSpheres();
+}; // class BubbleModelTest
+
+template< typename Stencil_T >
+void BubbleModelTest< Stencil_T >::testMovingSpheres()
+{
+   auto mpiManager = MPIManager::instance();
+
+   WALBERLA_CHECK_EQUAL(mpiManager->numProcesses(), 2);
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(2), uint_c(1), uint_c(1));
+   Vector3< uint_t > domainSize(uint_c(60), uint_c(16), uint_c(16));
+
+   if (Stencil_T::D == uint_c(2)) { domainSize[2] = uint_c(1); }
+
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, false);                                  // periodicity
+
+   // create bubble model
+   std::shared_ptr< BubbleModelTest > bubbleModel = std::make_shared< BubbleModelTest >(blockForest);
+
+   // create bubble body mover for moving bubbles
+   bubble_model::BubbleBodyMover< geometry::Sphere, Stencil_T > bubbleSphereMover(blockForest, bubbleModel);
+
+   Vector3< real_t > domainCenter(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5),
+                                  real_c(domainSize[2]) * real_c(0.5));
+
+   // create a static spherical bubble in the center of the domain
+   geometry::Sphere sphereCenter(domainCenter, real_c(5));
+   auto doNotMoveSphere = [](geometry::Sphere&) {};
+   bubbleSphereMover.addBody(sphereCenter, doNotMoveSphere);
+
+   // create a moving spherical bubble in the left (in x-direction) half of the domain
+   geometry::Sphere sphereLeft(Vector3< real_t >(real_c(8.0), domainCenter[1], domainCenter[2]), real_c(5));
+   auto moveSphere = [](geometry::Sphere& sphere) {
+      // midpoint of the sphere is shifted by 1 cell in positive x-direction at each call
+      sphere.setMidpoint(sphere.midpoint() + Vector3< real_t >(real_c(1), real_c(0), real_c(0)));
+   };
+   bubbleSphereMover.addBody(sphereLeft, moveSphere);
+
+   // initialize the just added bubbles
+   bubbleSphereMover.initAddedBodies();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(40));
+   timeloop.addFuncBeforeTimeStep(bubbleSphereMover, "Move bubbles");
+   timeloop.addFuncAfterTimeStep(std::bind(&bubble_model::BubbleModel< Stencil_T >::update, bubbleModel));
+
+   real_t singleBubbleVolume = real_c(523.346);
+
+   if (Stencil_T::D == uint_c(2)) { singleBubbleVolume = real_c(78.1987); }
+
+   uint_t timestep = uint_c(0);
+
+   // at time step 8, there should still be two bubbles
+   for (; timestep < uint_c(8); ++timestep)
+   {
+      timeloop.singleStep();
+   }
+   WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 2);
+   WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[0].getInitVolume() - singleBubbleVolume), real_c(0.5));
+   WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[1].getInitVolume() - singleBubbleVolume), real_c(0.5));
+
+   // at time step 12 the bubbles should have merged
+   for (; timestep < uint_c(12); ++timestep)
+   {
+      timeloop.singleStep();
+   }
+   WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 1);
+   WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[0].getInitVolume() - 2 * singleBubbleVolume), real_c(0.5));
+
+   // at time step 35 the bubbles should have split again
+   for (; timestep < uint_c(35); ++timestep)
+   {
+      timeloop.singleStep();
+   }
+   WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 2);
+   WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[0].getInitVolume() - singleBubbleVolume), real_c(0.5));
+   WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[1].getInitVolume() - singleBubbleVolume), real_c(0.5));
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D2Q9 stencil.")
+   BubbleModelTest< stencil::D2Q9 >::testMovingSpheres();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q19 stencil.")
+   BubbleModelTest< stencil::D3Q19 >::testMovingSpheres();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q27 stencil.")
+   BubbleModelTest< stencil::D3Q27 >::testMovingSpheres();
+
+   return EXIT_SUCCESS;
+}
+} // namespace MovingSpheresTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::MovingSpheresTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp b/tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp
new file mode 100644
index 000000000..6a41cd8e9
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp
@@ -0,0 +1,88 @@
+//======================================================================================================================
+//
+//  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 RegionalFloodFillTest.cpp
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test flood fill algorithm with D3Q7 and D3Q19 stencil.
+//
+//======================================================================================================================
+
+#include "lbm/free_surface/bubble_model/RegionalFloodFill.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/Printers.h"
+
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q7.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace RegionalFloodFillTest
+{
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment walberlaEnv(argc, argv);
+
+   // create 3x3 field with 1 ghost layer (initialized with 0)
+   GhostLayerField< int, 1 > field(3, 3, 1, 1, 0);
+
+   // initial values of the field; the flood fill starting cell is marked with /**/;
+   // attention: the y coordinate is upside down in this array
+   const int initValues[5][5] = {
+      { 1, 1, 1, 1, 1 }, { 0, 0 /**/, 0, 1, 0 }, { 1, 0, 0, 1, 0 }, { 1, 0, 0, 1, 0 }, { 1, 1, 1, 0, 0 },
+   };
+
+   // test scenario: detect the connection from (1,0) and (0,2) with starting cell (0,0)
+   for (cell_idx_t y = cell_idx_c(-1); y < cell_idx_c(4); ++y)
+   {
+      for (cell_idx_t x = cell_idx_c(-1); x < cell_idx_c(4); ++x)
+      {
+         field(x, y, cell_idx_c(0)) = initValues[y + 1][x + 1];
+      }
+   }
+
+   // print the initialized field (for debugging purposes)
+   //   std::cout << "Initialized field:" << std::endl;
+   //   field::printSlice(std::cout, field, 2, 0);
+
+   // connection should not be found since search neighborhood (2) is too small
+   bubble_model::RegionalFloodFill< int, stencil::D3Q19 > neigh2_D3Q19(
+      &field, Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), stencil::S, 1, cell_idx_c(2));
+   WALBERLA_CHECK_EQUAL(neigh2_D3Q19.connected(stencil::NW), false);
+
+   // connection should be found since search neighborhood (3) is large enough
+   bubble_model::RegionalFloodFill< int, stencil::D3Q19 > neigh3_D3Q19(
+      &field, Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), stencil::S, 1, cell_idx_c(3));
+   WALBERLA_CHECK_EQUAL(neigh3_D3Q19.connected(stencil::NW), true);
+
+   // connection should be found since search neighborhood (3) is large enough
+   bubble_model::RegionalFloodFill< int, stencil::D3Q7 > neigh3_D3Q7(
+      &field, Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), stencil::S, 1, cell_idx_c(3));
+   WALBERLA_CHECK_EQUAL(neigh3_D3Q7.connected(stencil::NW), false);
+
+   return EXIT_SUCCESS;
+}
+} // namespace RegionalFloodFillTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::RegionalFloodFillTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp b/tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp
new file mode 100644
index 000000000..adfbce45f
--- /dev/null
+++ b/tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp
@@ -0,0 +1,152 @@
+//======================================================================================================================
+//
+//  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 SplitDetectionTest.cpp
+//! \ingroup lbm/free_surface/bubble_model
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test bubble split detection with 2D and 3D bubbles.
+//
+//======================================================================================================================
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/free_surface/bubble_model/BubbleModel.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace SplitDetectionTest
+{
+using namespace bubble_model;
+
+// class derived from BubbleModel to access its protected members for testing purposes
+template< typename Stencil_T >
+class BubbleModelTest : public BubbleModel< Stencil_T >
+{
+ public:
+   static bool checkForSplit(BubbleField_T* bf, const Cell& cell, BubbleID prevID)
+   {
+      return BubbleModel< Stencil_T >::checkForSplit(bf, cell, prevID);
+   }
+}; // class BubbleModelTest
+
+void initSlice(BubbleField_T* bf, cell_idx_t z, const BubbleID array3x3[])
+{
+   for (cell_idx_t y = cell_idx_c(0); y < cell_idx_c(3); ++y)
+   {
+      for (cell_idx_t x = cell_idx_c(0); x < cell_idx_c(3); ++x)
+      {
+         bf->get(x, y, z) = array3x3[3 * (2 - y) + x];
+      }
+   }
+}
+
+void test2D_notConnected()
+{
+   // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs
+   BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID);
+
+   // initialize a 2D slice of the bubble field (disconnected bubble)
+   const BubbleID N      = INVALID_BUBBLE_ID;
+   const BubbleID init[] = { 2, 2, 2, N, N, N, 2, 2, 2 };
+
+   initSlice(&bubbleField, cell_idx_c(1), init);
+
+   WALBERLA_CHECK(BubbleModelTest< stencil::D2Q9 >::checkForSplit(
+                     &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == true);
+}
+
+void test2D_connected()
+{
+   // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs
+   BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID);
+
+   // initialize a 2D slice of the bubble field (connected bubble)
+   const BubbleID N      = INVALID_BUBBLE_ID;
+   const BubbleID init[] = { 2, 2, 2, 2, N, N, 2, 2, 2 };
+
+   initSlice(&bubbleField, cell_idx_c(1), init);
+
+   WALBERLA_CHECK(BubbleModelTest< stencil::D2Q9 >::checkForSplit(
+                     &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == false);
+}
+
+void test3D_connected()
+{
+   // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs
+   BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID);
+
+   // initialize whole bubble field (connected bubble)
+   const BubbleID N    = INVALID_BUBBLE_ID;
+   const BubbleID z0[] = { 2, 2, 2, N, N, N, 2, 2, 2 };
+   const BubbleID z1[] = { N, 2, N, N, N, N, N, N, N };
+   const BubbleID z2[] = { N, 2, N, N, 2, N, N, 2, N };
+
+   initSlice(&bubbleField, cell_idx_c(0), z0);
+   initSlice(&bubbleField, cell_idx_c(0), z1);
+   initSlice(&bubbleField, cell_idx_c(0), z2);
+
+   WALBERLA_CHECK(BubbleModelTest< stencil::D3Q19 >::checkForSplit(
+                     &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == false);
+   WALBERLA_CHECK(BubbleModelTest< stencil::D3Q27 >::checkForSplit(
+                     &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == false);
+}
+
+void test3D_notConnected()
+{
+   // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs
+   BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID);
+
+   // initialize whole bubble field (disconnected bubble)
+   const BubbleID N    = INVALID_BUBBLE_ID;
+   const BubbleID z0[] = { 2, 2, 2, N, N, N, 2, 2, 2 };
+   const BubbleID z1[] = { N, N, N, N, N, N, N, N, N };
+   const BubbleID z2[] = { N, 2, N, N, 2, N, N, 2, N };
+
+   initSlice(&bubbleField, 0, z0);
+   initSlice(&bubbleField, 1, z1);
+   initSlice(&bubbleField, 2, z2);
+
+   WALBERLA_CHECK(BubbleModelTest< stencil::D3Q19 >::checkForSplit(
+                     &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == true);
+   WALBERLA_CHECK(BubbleModelTest< stencil::D3Q27 >::checkForSplit(
+                     &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == true);
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   test2D_notConnected();
+   test2D_connected();
+
+   test3D_connected();
+   test3D_notConnected();
+
+   return EXIT_SUCCESS;
+}
+} // namespace SplitDetectionTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::SplitDetectionTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/AdvectionTest.cpp b/tests/lbm/free_surface/dynamics/AdvectionTest.cpp
new file mode 100644
index 000000000..1c3acc672
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/AdvectionTest.cpp
@@ -0,0 +1,220 @@
+//======================================================================================================================
+//
+//  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 AdvectionTest.cpp
+//! \ingroup lbm/free_surface/dynamics
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test mass advection on a flat surface.
+//!
+//! Initialize a pool of liquid in half of the domain and a box-shaped atmosphere (density 1.01) bubble in the remaining
+//! cells. Only the StreamReconstructAdvectSweep is performed. Due to the higher density in the gas, the interface cells
+//! that separate liquid and gas are emptied and their fill level must become negative. These cells must be marked
+//! for conversion and the fluid density must balance the atmosphere density.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/AddToStorage.h"
+#include "field/adaptors/AdaptorCreators.h"
+#include "field/communication/PackInfo.h"
+
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface//FlagInfo.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/dynamics/PdfReconstructionModel.h"
+#include "lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/ForceModel.h"
+#include "lbm/sweeps/CellwiseSweep.h"
+#include "lbm/sweeps/SweepWrappers.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace AdvectionTest
+{
+// define types
+using Flag_T        = uint32_t;
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< Flag_T >;
+
+template< typename LatticeModel_T >
+void testAdvection()
+{
+   // define types
+   using Stencil_T                     = typename LatticeModel_T::Stencil;
+   using PdfField_T                    = lbm::PdfField< LatticeModel_T >;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(10), uint_c(2));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, true);                                   // periodicity
+
+   // create lattice model with omega=0.51
+   LatticeModel_T latticeModel(real_c(0.51));
+
+   // add fields
+   BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+   BlockDataID curvatureFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0), real_c(1), real_c(0)), field::fzyx, uint_c(1));
+
+   BlockDataID densityAdaptor = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::Density >(
+      blockForest, pdfFieldID, "DensityAdaptor");
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+   // add box-shaped gas bubble (occupies about half of the domain in y-direction)
+   AABB box    = blockForest->getDomain();
+   auto newMin = box.min() + Vector3< real_t >(real_c(0), real_c(0.5) * box.ySize() + real_c(0.01), real_c(0));
+   box.initMinMaxCorner(newMin, box.max());
+   freeSurfaceBoundaryHandling->addFreeSurfaceObject(box);
+
+   // set no slip boundary conditions at the southern and northern domain borders
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::S);
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::N);
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // add bubble model
+   bubble_model::BubbleModel< Stencil_T > bubbleModel(blockForest, false);
+   bubbleModel.initFromFillLevelField(fillFieldID);
+
+   // get some cell located inside the bubble (used as representative cell to set bubble to atmosphere bubble type)
+   Cell cellInBubble;
+   cellInBubble.x() = cell_idx_c(box.xMin() + real_c(0.5) * box.xSize());
+   cellInBubble.y() = cell_idx_c(box.yMin() + real_c(0.5) * box.ySize());
+   cellInBubble.z() = cell_idx_c(box.zMin() + real_c(0.5) * box.zSize());
+
+   // set bubble to atmosphere with constant density higher than the initial fluid density
+   const real_t atmDensity = real_c(1.01);
+   bubbleModel.setAtmosphere(cellInBubble, atmDensity);
+
+   // create timeloop; 300 time steps are required (for very low omega of 0.51) to ensure that fluid density has
+   // stabilized
+   SweepTimeloop timeloop(blockForest, uint_c(300));
+
+   // add communication
+   blockforest::communication::UniformBufferedScheme< typename LatticeModel_T::Stencil > comm(blockForest);
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID));
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID));
+   comm.addPackInfo(
+      std::make_shared< field::communication::PackInfo< FlagField_T > >(freeSurfaceBoundaryHandling->getFlagFieldID()));
+
+   // communicate
+   comm();
+
+   const PdfReconstructionModel pdfRecModel = PdfReconstructionModel("NormalBasedKeepCenter");
+
+   // add free surface boundary sweep for
+   // - reconstruction of PDFs in interface cells
+   // - advection of mass
+   // - marking interface cells for conversion
+   // - update bubble volumes
+   StreamReconstructAdvectSweep< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T,
+                                 FlagField_T, typename FreeSurfaceBoundaryHandling_T::FlagInfo_T, ScalarField_T,
+                                 VectorField_T, false >
+      streamReconstructAdvectSweep(real_c(0), freeSurfaceBoundaryHandling->getHandlingID(), fillFieldID,
+                                   freeSurfaceBoundaryHandling->getFlagFieldID(), pdfFieldID, normalFieldID,
+                                   curvatureFieldID, flagInfo, &bubbleModel, pdfRecModel, false, real_c(1e-3),
+                                   real_c(1e-1));
+   timeloop.add() << Sweep(streamReconstructAdvectSweep);
+
+   // add boundary handling sweep
+   timeloop.add() << BeforeFunction(comm) << Sweep(freeSurfaceBoundaryHandling->getBoundarySweep());
+
+   // add LBM collision sweep
+   auto lbmSweep = lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >(
+      pdfFieldID, freeSurfaceBoundaryHandling->getFlagFieldID(), flagIDs::liquidInterfaceFlagIDs);
+   timeloop.add() << Sweep(lbm::makeCollideSweep(lbmSweep));
+
+   timeloop.run();
+
+   // evaluate
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+      const typename lbm::Adaptor< LatticeModel_T >::Density* const densityField =
+         blockIt->getData< const typename lbm::Adaptor< LatticeModel_T >::Density >(densityAdaptor);
+      const FlagField_T* const flagField =
+         blockIt->getData< const FlagField_T >(freeSurfaceBoundaryHandling->getFlagFieldID());
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, densityFieldIt, densityField, flagFieldIt, flagField, {
+         if (flagInfo.isInterface(flagFieldIt))
+         {
+            // fill level in interface cells must be negative
+            WALBERLA_CHECK_LESS(*fillFieldIt, real_c(0));
+
+            // due to negative fill level, these cells must be marked for conversion to gas
+            WALBERLA_CHECK(isFlagSet(flagFieldIt, flagInfo.convertToGasFlag));
+         }
+
+         // in the absence of forces, fluid density must balance atmosphere density
+         if (flagInfo.isLiquid(flagFieldIt)) { WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, atmDensity); }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_INFO("Testing with D2Q9 stencil.");
+   testAdvection< lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >();
+
+   WALBERLA_LOG_INFO("Testing with D3Q19 stencil.");
+   testAdvection< lbm::D3Q19< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >();
+
+   WALBERLA_LOG_INFO("Testing with D3Q27 stencil.");
+   testAdvection< lbm::D3Q27< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >();
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace AdvectionTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::AdvectionTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/CellConversionTest.cpp b/tests/lbm/free_surface/dynamics/CellConversionTest.cpp
new file mode 100644
index 000000000..6f7645c84
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/CellConversionTest.cpp
@@ -0,0 +1,280 @@
+//======================================================================================================================
+//
+//  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 CellConversionTest.cpp
+//! \ingroup lbm/free_surface/dynamics
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test cell conversions on a flat surface by checking mass conservation and density balance.
+//!
+//! Initialize a pool of liquid in half of the domain and a box-shaped atmosphere (density 1.1) bubble in the remaining
+//! cells. All sweeps from SurfaceDynamicsHandler are performed. Due to the higher density in the gas, the interface
+//! cells that separate liquid and gas are emptied and their fill level must become negative. These cells are converted
+//! to liquid, while former fluid cells must become interface cells. After this process, mass must be conserved and the
+//! fluid density must balance the atmosphere density.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/AddToStorage.h"
+#include "field/communication/PackInfo.h"
+
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/ForceModel.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <limits>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace CellConversionTest
+{
+// define types
+using flag_t = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+// compute the total mass of the fluid (in all interface and liquid cells)
+template< typename LatticeModel_T, typename FlagField_T >
+real_t computeTotalMass(
+   const std::weak_ptr< StructuredBlockForest >& blockForest, ConstBlockDataID pdfField, ConstBlockDataID fillField,
+   ConstBlockDataID flagField,
+   const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flags);
+
+// compute the minimum and maximum density in all interface and liquid cells
+template< typename LatticeModel_T, typename FlagField_T >
+void computeMinMaxDensity(
+   const std::weak_ptr< StructuredBlockForest >& blockForest, ConstBlockDataID pdfField, ConstBlockDataID flagField,
+   const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flags,
+   real_t& minDensity, real_t& maxDensity);
+
+template< typename LatticeModel_T >
+void testCellConversion()
+{
+   // define types
+   using Stencil_T  = typename LatticeModel_T::Stencil;
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(10), uint_c(2));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, true);                                   // periodicity
+
+   real_t relaxRate = real_c(0.51);
+
+   // create lattice model with omega=0.51
+   LatticeModel_T latticeModel(relaxRate);
+
+   // add fields
+   BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+   BlockDataID forceFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Force field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID curvatureFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0), real_c(1), real_c(0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   BlockDataID flagFieldID                                            = freeSurfaceBoundaryHandling->getFlagFieldID();
+   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+   // add box-shaped gas bubble (occupies about half of the domain in y-direction)
+   AABB box    = blockForest->getDomain();
+   auto newMin = box.min() + Vector3< real_t >(real_c(0), real_c(0.5) * box.ySize() + real_c(1 - 0.02), real_c(0));
+   box.initMinMaxCorner(newMin, box.max());
+   freeSurfaceBoundaryHandling->addFreeSurfaceObject(box);
+
+   // set no slip boundary conditions at the southern and northern domain borders
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::S);
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::N);
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // add bubble model
+   auto bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, false);
+   bubbleModel->initFromFillLevelField(fillFieldID);
+
+   // get some cell located inside the bubble (used as representative cell to set bubble to atmosphere bubble type)
+   Cell cellInBubble;
+   cellInBubble.x() = cell_idx_c(box.xMin() + real_c(0.5) * box.xSize());
+   cellInBubble.y() = cell_idx_c(box.yMin() + real_c(0.5) * box.ySize());
+   cellInBubble.z() = cell_idx_c(box.zMin() + real_c(0.5) * box.zSize());
+
+   // set bubble to atmosphere with constant density higher than the initial fluid density
+   const real_t atmDensity = real_c(1.1);
+   bubbleModel->setAtmosphere(cellInBubble, atmDensity);
+
+   // create timeloop; 400 time steps are required (for very low omega of 0.51) to ensure that fluid density has
+   // stabilized
+   SweepTimeloop timeloop(blockForest, uint_c(400));
+
+   // add communication
+   blockforest::communication::UniformBufferedScheme< Stencil_T > comm(blockForest);
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID));
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID));
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< FlagField_T > >(flagFieldID));
+
+   // communicate
+   comm();
+
+   // add various sweeps for surface dynamics
+   SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, "NormalBasedKeepCenter", "EquilibriumRefilling", "EvenlyNewInterface",
+      relaxRate, Vector3< real_t >(real_c(0)), real_c(0), false, false, real_c(1e-3), real_c(1e-1));
+   dynamicsHandler.addSweeps(timeloop);
+
+   real_t initialMass =
+      computeTotalMass< LatticeModel_T, FlagField_T >(blockForest, pdfFieldID, fillFieldID, flagFieldID, flagInfo);
+
+   timeloop.run();
+
+   // in the absence of forces, fluid density must balance atmosphere density
+   real_t rhoMin = real_c(0);
+   real_t rhoMax = real_c(0);
+   computeMinMaxDensity< LatticeModel_T, FlagField_T >(blockForest, pdfFieldID, flagFieldID, flagInfo, rhoMin, rhoMax);
+   WALBERLA_CHECK_FLOAT_EQUAL(atmDensity, rhoMin);
+   WALBERLA_CHECK_FLOAT_EQUAL(atmDensity, rhoMax);
+
+   // mass must be conserved
+   real_t finalMass =
+      computeTotalMass< LatticeModel_T, FlagField_T >(blockForest, pdfFieldID, fillFieldID, flagFieldID, flagInfo);
+   WALBERLA_CHECK_FLOAT_EQUAL(initialMass, finalMass);
+
+   MPIManager::instance()->resetMPI();
+}
+
+// compute the total mass of the fluid (in all interface and liquid cells)
+template< typename LatticeModel_T, typename FlagField_T >
+real_t computeTotalMass(
+   const std::weak_ptr< StructuredBlockForest >& blockForestPtr, ConstBlockDataID pdfFieldID,
+   ConstBlockDataID fillFieldID, ConstBlockDataID flagFieldID,
+   const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flagInfo)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   real_t mass = real_c(0);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const lbm::PdfField< LatticeModel_T >* const pdfField =
+         blockIt->getData< const lbm::PdfField< LatticeModel_T > >(pdfFieldID);
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+      const FlagField_T* const flagField   = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      // iterate over all interface and liquid cells and compute the total mass of fluid in the domain
+      WALBERLA_FOR_ALL_CELLS_OMP(fillFieldIt, fillField, pdfFieldIt, pdfField, flagFieldIt, flagField,
+                                 omp parallel for schedule(static) reduction(+:mass),
+                                 {
+         const real_t rho = lbm::getDensity< LatticeModel_T >(pdfField->latticeModel(), pdfFieldIt);
+         if (flagInfo.isInterface(flagFieldIt)) { mass += rho * (*fillFieldIt); }
+         else
+         {
+            if (flagInfo.isLiquid(flagFieldIt)) { mass += rho; }
+         }
+                                 }); // WALBERLA_FOR_ALL_CELLS_OMP
+   }
+
+   WALBERLA_LOG_RESULT("Total current mass " << mass << ".");
+
+   return mass;
+}
+
+// compute the minimum and maximum density in all interface and liquid cells
+template< typename LatticeModel_T, typename FlagField_T >
+void computeMinMaxDensity(
+   const std::weak_ptr< StructuredBlockForest >& blockForestPtr, ConstBlockDataID pdfFieldID,
+   ConstBlockDataID flagFieldID,
+   const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flags,
+   real_t& minDensity, real_t& maxDensity)
+{
+   const auto blockForest = blockForestPtr.lock();
+   WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+   minDensity = std::numeric_limits< real_t >::max();
+   maxDensity = std::numeric_limits< real_t >::min();
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const lbm::PdfField< LatticeModel_T >* const pdfField =
+         blockIt->getData< const lbm::PdfField< LatticeModel_T > >(pdfFieldID);
+      const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      // iterate over all interface and liquid cells and find the minimum and maximum density; explicitly avoid OpenMP
+      // (problematic to reduce max and min)
+      WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, omp critical, {
+         if (flags.isInterface(flagFieldIt) || flags.isLiquid(flagFieldIt))
+         {
+            const real_t rho = lbm::getDensity< LatticeModel_T >(pdfField->latticeModel(), pdfFieldIt);
+            minDensity       = std::min(rho, minDensity);
+            maxDensity       = std::max(rho, maxDensity);
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_INFO("Testing with D2Q9 stencil.")
+   testCellConversion< walberla::lbm::D2Q9< walberla::lbm::collision_model::SRT, true > >();
+
+   WALBERLA_LOG_INFO("Testing with D3Q19 stencil.")
+   testCellConversion< walberla::lbm::D3Q19< walberla::lbm::collision_model::SRT, true > >();
+
+   WALBERLA_LOG_INFO("Testing with D3Q27 stencil.")
+   testCellConversion< walberla::lbm::D3Q27< walberla::lbm::collision_model::SRT, true > >();
+
+   return EXIT_SUCCESS;
+}
+} // namespace CellConversionTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::CellConversionTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/CodegenTest.cpp b/tests/lbm/free_surface/dynamics/CodegenTest.cpp
new file mode 100644
index 000000000..a141a0a3c
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/CodegenTest.cpp
@@ -0,0 +1,227 @@
+//======================================================================================================================
+//
+//  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 CodegenTest.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test equivalence of generated LBM kernels in the free surface implementation.
+//!
+//! Simulates 100 time steps of a moving drop with diameter 2 in a periodic 4x4x4 domain. The drop moves due to a
+//! constant body force.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/field/AddToStorage.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/lattice_model/D3Q19.h"
+
+#include <type_traits>
+
+// include files generated by lbmpy
+#include "GeneratedLatticeModel_FreeSurface.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace CodegenTest
+{
+
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+template< bool useCodegen >
+void runSimulation()
+{
+   using CollisionModel_T = lbm::collision_model::SRT;
+   using ForceModel_T     = lbm::force_model::GuoField< VectorField_T >;
+   using LatticeModel_T   = typename std::conditional< useCodegen, lbm::GeneratedLatticeModel_FreeSurface,
+                                                     lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 > >::type;
+
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >;
+
+   using flag_t                        = uint32_t;
+   using FlagField_T                   = FlagField< flag_t >;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(4), uint_c(4), uint_c(4));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, true, true);                                    // periodicity
+
+   // physics parameters
+   const real_t dropDiameter     = real_c(2);
+   const real_t relaxationRate   = real_c(1.8);
+   const real_t surfaceTension   = real_c(1e-5);
+   const bool enableWetting      = false;
+   const real_t contactAngle     = real_c(0);
+   const Vector3< real_t > force = Vector3< real_t >(real_c(1e-5), real_c(0), real_c(0));
+
+   // model parameters
+   const std::string pdfReconstructionModel      = "NormalBasedKeepCenter";
+   const std::string pdfRefillingModel           = "EquilibriumRefilling";
+   const std::string excessMassDistributionModel = "EvenlyAllInterface";
+   const std::string curvatureModel              = "FiniteDifferenceMethod";
+   const bool enableForceWeighting               = false;
+   const bool useSimpleMassExchange              = false;
+   const real_t cellConversionThreshold          = real_c(1e-2);
+   const real_t cellConversionForceThreshold     = real_c(1e-1);
+
+   // add force field
+   const BlockDataID forceFieldID =
+      field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1));
+
+   std::shared_ptr< LatticeModel_T > latticeModel;
+
+   // create lattice model
+   if constexpr (useCodegen)
+   {
+      latticeModel = std::make_shared< LatticeModel_T >(force[0], force[1], force[2], relaxationRate);
+   }
+   else
+   {
+      latticeModel = std::make_shared< LatticeModel_T >(CollisionModel_T(relaxationRate), ForceModel_T(forceFieldID));
+   }
+
+   // add various fields
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", *latticeModel, field::fzyx);
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+
+   // add drop to fill level field
+   const geometry::Sphere sphereDrop(Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]),
+                                                       real_c(0.5) * real_c(domainSize[1]),
+                                                       real_c(0.5) * real_c(domainSize[2])),
+                                     real_c(dropDiameter) * real_c(0.5));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false);
+
+   // initialize flag field from fill level field
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // initial Communication_Tunication
+   Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, forceFieldID)();
+
+   // add bubble model
+   const std::shared_ptr< bubble_model::BubbleModelConstantPressure > bubbleModel =
+      std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(100));
+
+   // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension
+   bool computeCurvature = false;
+   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,
+      contactAngle);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   const ConstBlockDataID normalFieldID    = geometryHandler.getConstNormalFieldID();
+
+   geometryHandler.addSweeps(timeloop);
+
+   // add boundary handling for standard boundaries and free surface boundaries
+   SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T, useCodegen > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
+      relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold);
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   timeloop.run();
+
+   // check fill level (must be identical in lattice model from waLBerla and from lbmpy)
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4));
+         }
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(2)))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4));
+         }
+      }); // WALBERLA_FOR_ALL_CELLS
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment walberlaEnv(argc, argv);
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with lattice model from waLBerla.");
+   runSimulation< false >();
+
+   WALBERLA_LOG_INFO_ON_ROOT("Testing with lattice model generated by lbmpy.");
+   runSimulation< true >();
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace CodegenTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::CodegenTest::main(argc, argv); }
diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp b/tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp
new file mode 100644
index 000000000..69c075f9e
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp
@@ -0,0 +1,360 @@
+//======================================================================================================================
+//
+//  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 ExcessMassDistributionFallbackTest.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test excess mass distribution in cases where the chosen model is not applicable.
+//!
+//! Test if fall back to other models works correctly with a simple two-dimensional 3x3 grid, where the center cell at
+//! (1,1) is assumed to have converted from interface to liquid with excess mass 0.1.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/FieldClone.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/bubble_model/Geometry.h"
+#include "lbm/free_surface/dynamics/ExcessMassDistributionModel.h"
+#include "lbm/free_surface/dynamics/ExcessMassDistributionSweep.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace ExcessMassDistributionFallbackTest
+{
+using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >;
+using Stencil        = typename LatticeModel_T::Stencil;
+
+using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >;
+
+using PdfField_T    = lbm::PdfField< LatticeModel_T >;
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+void runSimulation(const ExcessMassDistributionModel& excessMassDistributionModel,
+                   const std::vector< Cell >& newInterfaceCells, const std::vector< Cell >& oldInterfaceCells,
+                   const Vector3< real_t >& interfaceNormal)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create (dummy) lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8)));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel,
+                                                            Vector3< real_t >(real_c(0)), real_c(1), field::fzyx);
+
+   // add normal field
+   const BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add fill level field
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1));
+
+   // initialize fill levels and interface normal
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField   = blockIt->getData< ScalarField_T >(fillFieldID);
+      VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, normalFieldIt, normalField, {
+         // initialize interface cells
+         for (const Cell& cell : newInterfaceCells)
+         {
+            if (fillFieldIt.cell() == cell) { *fillFieldIt = real_c(0.5); }
+         }
+
+         for (const Cell& cell : oldInterfaceCells)
+         {
+            if (fillFieldIt.cell() == cell) { *fillFieldIt = real_c(0.5); }
+         }
+
+         // this cell is assigned a fill level of 1.1 leading to an excess mass equivalent to a fill level of 0.1
+         if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) { *fillFieldIt = real_c(1.1); }
+
+         if (normalFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+         {
+            *normalFieldIt = interfaceNormal;
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // 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();
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // initialize flags
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, {
+         // flag the cell with excess mass as newly converted to liquid
+         if (flagFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+         {
+            field::addFlag(flagFieldIt, flagInfo.convertedFlag | flagInfo.convertToLiquidFlag);
+         }
+
+         // consider these cells to be newly-converted to interface
+         for (const Cell& cell : newInterfaceCells)
+         {
+            if (flagFieldIt.cell() == cell) { field::addFlag(flagFieldIt, flagInfo.convertedFlag); }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // initial communication
+   Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, normalFieldID)();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   if (excessMassDistributionModel.isEvenlyType())
+   {
+      const ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+         distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo);
+      timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass");
+   }
+   else
+   {
+      if (excessMassDistributionModel.isWeightedType())
+      {
+         const ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+            distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo,
+                                normalFieldID);
+         timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass");
+      }
+   }
+
+   timeloop.singleStep();
+
+   // check if excess mass was distributed correctly; expected solutions were obtained manually
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyOldInterface)
+         {
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterface)
+         {
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+                ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface &&
+             !oldInterfaceCells.empty())
+         {
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+                ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface &&
+             !newInterfaceCells.empty())
+         {
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+                ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface &&
+             oldInterfaceCells.empty())
+         {
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+                ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface &&
+             newInterfaceCells.empty())
+         {
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4));
+            }
+
+            if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4));
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   Vector3< real_t > interfaceNormal(real_c(0));
+
+   // (0,0) is the only interface cell (newly-converted) => EvenlyOldInterface must fall back to EvenlyNewInterface
+   ExcessMassDistributionModel model = ExcessMassDistributionModel("EvenlyOldInterface");
+   std::vector< Cell > newInterfaceCells{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) };
+   std::vector< Cell > oldInterfaceCells{};
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal);
+
+   // (0,0) is the only interface cell (not newly-converted) => EvenlyNewInterface must fall back to EvenlyOldInterface
+   model             = ExcessMassDistributionModel("EvenlyNewInterface");
+   newInterfaceCells = std::vector< Cell >{};
+   oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) };
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal);
+
+   interfaceNormal = Vector3< real_t >(real_c(0.71), real_c(0.71), real_c(0));
+
+   // (0,0) is old interface cell; (2,2) is newly-converted interface cell; interface normal points in direction (1,1)
+   // => WeightedOldInterface must fall back to WeightedNewInterface
+   model             = ExcessMassDistributionModel("WeightedOldInterface");
+   newInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) };
+   oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) };
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal);
+
+   // (0,0) is newly-converted interface cell; (2,2) is old interface cell; interface normal points in direction (1,1)
+   // => WeightedNewInterface must fall back to WeightedOldInterface
+   model             = ExcessMassDistributionModel("WeightedNewInterface");
+   newInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) };
+   oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) };
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal);
+
+   interfaceNormal = Vector3< real_t >(real_c(0), real_c(-1), real_c(0));
+
+   // (1,2) and (2,2) are newly-converted interface cells; interface normal points in direction (0,-1)
+   // => WeightedOldInterface must fall back to EvenlyAllInterface, as no interface cell is available in normal
+   // direction
+   model             = ExcessMassDistributionModel("WeightedOldInterface");
+   newInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)),
+                                            Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) };
+   oldInterfaceCells = std::vector< Cell >{};
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal);
+
+   // (1,2) and (2,2) are old interface cells; interface normal points in direction (0,-1)
+   // => WeightedNewInterface must fall back to EvenlyAllInterface, as no interface cell is available in normal
+   // direction
+   model             = ExcessMassDistributionModel("WeightedNewInterface");
+   newInterfaceCells = std::vector< Cell >{};
+   oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)),
+                                            Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) };
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal);
+
+   return EXIT_SUCCESS;
+}
+} // namespace ExcessMassDistributionFallbackTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::ExcessMassDistributionFallbackTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
new file mode 100644
index 000000000..8c99cf872
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
@@ -0,0 +1,1154 @@
+//======================================================================================================================
+//
+//  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 ExcessMassDistributionParallelTest.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test distribution of excess mass using two blocks (to verify also on a parallel environment).
+//!
+//! The tests and their expected solutions are also shown in the file
+//! "/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.odp".
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/FieldClone.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/bubble_model/Geometry.h"
+#include "lbm/free_surface/dynamics/ExcessMassDistributionModel.h"
+#include "lbm/free_surface/dynamics/ExcessMassDistributionSweep.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace ExcessMassDistributionTest
+{
+using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >;
+using Stencil        = typename LatticeModel_T::Stencil;
+
+using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >;
+
+using PdfField_T    = lbm::PdfField< LatticeModel_T >;
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+void runSimulation(const ExcessMassDistributionModel& excessMassDistributionModel)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(2), uint_c(1), uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(6), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create (dummy) lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8)));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel,
+                                                            Vector3< real_t >(real_c(0)), real_c(1), field::fzyx);
+
+   // add normal field
+   const BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add fill level field
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1));
+
+   // add field for excess mass
+   const BlockDataID excessMassFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Excess mass field", real_c(0), field::fzyx, uint_c(1));
+
+   // 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();
+
+   // initialize cells as in file "/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.odp"
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField       = blockIt->getData< ScalarField_T >(fillFieldID);
+      ScalarField_T* const excessMassField = blockIt->getData< ScalarField_T >(excessMassFieldID);
+      FlagField_T* const flagField         = blockIt->getData< FlagField_T >(flagFieldID);
+      PdfField_T* const pdfField           = blockIt->getData< PdfField_T >(pdfFieldID);
+      VectorField_T* const normalField     = blockIt->getData< VectorField_T >(normalFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(
+         fillFieldIt, fillField, excessMassFieldIt, excessMassField, flagFieldIt, flagField, pdfFieldIt, pdfField,
+         normalFieldIt, normalField, {
+            const Cell localCell = fillFieldIt.cell();
+
+            // get global coordinate of this cell
+            Cell globalCell;
+            blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               *fillFieldIt       = real_c(1);
+               *excessMassFieldIt = real_c(0.2);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.5));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.4));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               *fillFieldIt       = real_c(1);
+               *excessMassFieldIt = real_c(0.1);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(1.1);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag | flagInfo.convertedFlag);
+               *normalFieldIt = Vector3< real_t >(real_c(0.71), real_c(0.71), real_c(0));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.3));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.gasFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.1));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.2));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.5));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.6));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               *fillFieldIt       = real_c(1);
+               *excessMassFieldIt = real_c(0.03);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(1.2);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.95));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag | flagInfo.convertedFlag);
+               *normalFieldIt = Vector3< real_t >(real_c(0.71), real_c(0.71), real_c(0));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.7));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               *fillFieldIt       = real_c(1);
+               *excessMassFieldIt = real_c(0.02);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.9));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               *fillFieldIt = real_c(0.5);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.8));
+               field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag);
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               *fillFieldIt       = real_c(1);
+               *excessMassFieldIt = real_c(0.01);
+               pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1));
+               field::addFlag(flagFieldIt, flagInfo.liquidFlag);
+            }
+         }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // initial communication
+   Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, normalFieldID, excessMassFieldID)();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   if (excessMassDistributionModel.isEvenlyType())
+   {
+      const ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+         distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo);
+      timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass");
+   }
+   else
+   {
+      if (excessMassDistributionModel.isWeightedType())
+      {
+         const ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+            distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo,
+                                normalFieldID);
+         timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass");
+      }
+      else
+      {
+         if (excessMassDistributionModel.isEvenlyLiquidAndAllInterfacePreferInterfaceType())
+         {
+            const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T,
+                                                                 VectorField_T >
+               distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo,
+                                   excessMassFieldID);
+            timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass");
+         }
+      }
+   }
+
+   timeloop.singleStep();
+
+   // check if excess mass was distributed correctly; expected solutions were obtained manually, see file
+   // "/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.odp"
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField       = blockIt->getData< const ScalarField_T >(fillFieldID);
+      const ScalarField_T* const excessMassField = blockIt->getData< const ScalarField_T >(excessMassFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, excessMassFieldIt, excessMassField, {
+         Cell globalCell;
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, fillFieldIt.cell());
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.513333333333333), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.53125), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533653846153846), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.518181818181818), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.536458333333333), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5475), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.539583333333333), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533928571428571), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.526388888888889), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5296875), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyOldInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.557738095238095), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.562179487179487), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.567361111111111), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.552777777777778), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533333333333333), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.545454545454546), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.595), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.579166666666667), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.567857142857143), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.559375), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::WeightedAllInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.519230769230769), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.522727272727273), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.541666666666667), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.567857142857143), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.552777777777778), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.61875), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.525641025641026), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.555555555555556), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.711111111111111), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.590909090909091), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.59047619047619), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.658333333333333), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.039285714285714), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.570634920634921), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.527168367346939), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.080952380952381), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.091666666666667), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.529258241758242), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.535714285714286), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.531696428571429), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5475), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.562916666666667), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.004), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.558690476190476), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.013333333333333), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.526388888888889), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538854166666667), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.004), real_c(1e-4));
+            }
+         }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface)
+         {
+            // left block
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.68), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.53125), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533653846153846), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.563636363636364), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.536458333333333), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            // right block
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5475), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.575694444444444), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.57202380952381), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.526388888888889), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.544270833333333), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+
+            if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4));
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   ExcessMassDistributionModel model = ExcessMassDistributionModel("EvenlyAllInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("EvenlyOldInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("EvenlyNewInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("WeightedAllInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("WeightedOldInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("WeightedNewInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterfacePreferInterface");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   return EXIT_SUCCESS;
+}
+} // namespace ExcessMassDistributionTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::ExcessMassDistributionTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods
new file mode 100644
index 0000000000000000000000000000000000000000..fa159c7b1b01c41933c16c886af01b6344a26bf9
GIT binary patch
literal 16650
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

literal 0
HcmV?d00001

diff --git a/tests/lbm/free_surface/dynamics/InflowTest.cpp b/tests/lbm/free_surface/dynamics/InflowTest.cpp
new file mode 100644
index 000000000..da8b5df18
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/InflowTest.cpp
@@ -0,0 +1,281 @@
+//======================================================================================================================
+//
+//  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 InflowTest.cpp
+//! \ingroup lbm/free_surface/dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test inflow boundary condition.
+//!
+//! Set inflow boundaries and initialize gas everywhere. After performing one time step, it is evaluated whether gas
+//! cells have been converted to interface and initialized correctly according to the neighboring inflow cells.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/AddToStorage.h"
+#include "field/adaptors/AdaptorCreators.h"
+#include "field/communication/PackInfo.h"
+
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface//FlagInfo.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h"
+#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h"
+#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/ForceModel.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <algorithm>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace InflowTest
+{
+// define types
+using Flag_T         = uint32_t;
+using ScalarField_T  = GhostLayerField< real_t, 1 >;
+using VectorField_T  = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T    = FlagField< Flag_T >;
+using LatticeModel_T = lbm::D3Q19< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >;
+
+void testInflow()
+{
+   // define types
+   using Stencil_T                     = typename LatticeModel_T::Stencil;
+   using PdfField_T                    = lbm::PdfField< LatticeModel_T >;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(10), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, true);                                  // periodicity
+
+   real_t relaxRate = real_c(0.51);
+
+   // create lattice model with omega=0.51
+   LatticeModel_T latticeModel(relaxRate);
+
+   // add fields
+   BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(0.0), field::fzyx, uint_c(2));
+   BlockDataID forceFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Force field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID densityAdaptor = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::Density >(
+      blockForest, pdfFieldID, "DensityAdaptor");
+   BlockDataID velocityAdaptor = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::VelocityVector >(
+      blockForest, pdfFieldID, "VelocityAdaptor");
+
+   // 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();
+
+   // set inflow boundary conditions in some cells of the western domain border
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(0), cell_idx_c(-1), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(0), real_c(0.01), real_c(0)));
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(0), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(0.01), real_c(0), real_c(0)));
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(1), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(0.02), real_c(0), real_c(0)));
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(2), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(0.03), real_c(0.01), real_c(0)));
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(3), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(0.04), real_c(0), real_c(0)));
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(4), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(0.05), real_c(0.02), real_c(0)));
+
+   // these inflow cells should not have any influence as their velocity direction points away from the neighboring gas
+   // cells
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(5), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(-0.06), real_c(0), real_c(0)));
+   freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(6), cell_idx_c(0)),
+                                                Vector3< real_t >(real_c(-0.07), real_c(0), real_c(0)));
+
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // add bubble model
+   auto bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, true);
+   bubbleModel->initFromFillLevelField(fillFieldID);
+   bubbleModel->setAtmosphere(Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), real_c(1));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   // add communication
+   blockforest::communication::UniformBufferedScheme< typename LatticeModel_T::Stencil > comm(blockForest);
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID));
+   comm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID));
+   comm.addPackInfo(
+      std::make_shared< field::communication::PackInfo< FlagField_T > >(freeSurfaceBoundaryHandling->getFlagFieldID()));
+
+   // communicate
+   comm();
+
+   // add surface geometry handler
+   SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler(
+      blockForest, freeSurfaceBoundaryHandling, fillFieldID, "FiniteDifferenceMethod", false, false, real_c(0));
+
+   ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   ConstBlockDataID normalFieldID    = geometryHandler.getConstNormalFieldID();
+
+   geometryHandler.addSweeps(timeloop);
+
+   // add boundary handling for standard boundaries and free surface boundaries
+   SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, "NormalBasedKeepCenter", "EquilibriumRefilling", "EvenlyNewInterface",
+      relaxRate, Vector3< real_t >(real_c(0)), real_c(0), false, false, real_c(1e-3), real_c(1e-1));
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   timeloop.singleStep();
+
+   // evaluate if inflow boundary has generated the correct interface cells
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+      const typename lbm::Adaptor< LatticeModel_T >::Density* const densityField =
+         blockIt->getData< const typename lbm::Adaptor< LatticeModel_T >::Density >(densityAdaptor);
+      const typename lbm::Adaptor< LatticeModel_T >::VelocityVector* const velocityField =
+         blockIt->getData< const typename lbm::Adaptor< LatticeModel_T >::VelocityVector >(velocityAdaptor);
+      const FlagField_T* const flagField =
+         blockIt->getData< const FlagField_T >(freeSurfaceBoundaryHandling->getFlagFieldID());
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, velocityFieldIt, velocityField, densityFieldIt, densityField,
+                             flagFieldIt, flagField, {
+                                if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
+                                {
+                                   WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt));
+
+                                   // velocity must be the average from inflow cells (-1,0,0) and (1,-1,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.005), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.005), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15));
+                                   continue;
+                                }
+
+                                if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+                                {
+                                   WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt));
+
+                                   // velocity must be identical to the inflow cell (-1,1,0); no other inflow cell
+                                   // should influence this cell
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.02), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15));
+                                   continue;
+                                }
+
+                                if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)))
+                                {
+                                   WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt));
+
+                                   // velocity must be identical to the inflow cell (-1,2,0); no other inflow cell
+                                   // should influence this cell
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.03), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.01), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15));
+                                   continue;
+                                }
+
+                                if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(3), cell_idx_c(0)))
+                                {
+                                   WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt));
+
+                                   // velocity must be the average from inflow cells (-1,2,0) and (-1,3,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.035), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.005), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15));
+                                   continue;
+                                }
+
+                                if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(4), cell_idx_c(0)))
+                                {
+                                   WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt));
+
+                                   // velocity must be identical to inflow cell (-1,4,0); no other inflow cell
+                                   // should influence this cell
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.05), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.02), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15));
+                                   WALBERLA_LOG_DEVEL_VAR(*velocityFieldIt);
+                                   WALBERLA_LOG_DEVEL_VAR(flagInfo.isInterface(flagFieldIt));
+                                   continue;
+                                }
+
+                                if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(5), cell_idx_c(0)))
+                                {
+                                   // cell must be converted due to velocity vector pointing from inflow
+                                   // cell(-1,4,0) to this cell
+                                   WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt));
+
+                                   // velocity must be identical to inflow cell (-1,4,0); no other inflow cell
+                                   // should influence this cell
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.05), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.02), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15));
+                                   WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15));
+                                   continue;
+                                }
+
+                                // cell(0,6,0)
+                                WALBERLA_CHECK(flagInfo.isGas(flagFieldIt));
+                             }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   testInflow();
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace InflowTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::InflowTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py b/tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py
new file mode 100644
index 000000000..0ef55dce4
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py
@@ -0,0 +1,34 @@
+import sympy as sp
+
+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
+
+# general parameters
+stencil = LBStencil(Stencil.D3Q19)
+omega = sp.Symbol('omega')
+force = sp.symbols('force_:3')
+layout = 'fzyx'
+
+# method definition
+lbm_config = LBMConfig(stencil=stencil,
+                       method=Method.SRT,
+                       relaxation_rate=omega,
+                       compressible=True,
+                       force=force,
+                       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)
+
+with CodeGeneration() as ctx:
+    generate_lattice_model(ctx, "GeneratedLatticeModel_FreeSurface", collision_rule, field_layout=layout)
diff --git a/tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp b/tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp
new file mode 100644
index 000000000..15ff0d96d
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp
@@ -0,0 +1,201 @@
+//======================================================================================================================
+//
+//  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 PdfReconstructionFreeSlipTest.cpp
+//! \ingroup lbm/free_surface/dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test PDF reconstruction due to a free-slip cell near the free surface boundary.
+//!
+//! Initialize a 3x3 grid and test reconstruction of PDFs due to a free slip boundary condition.
+//!     [L][L][L]       with L: liquid cell; I: interface cell; G: gas cell; f: free-slip cell
+//!     [L][I][G]            F: free-slip cell of interest (for the following explanation)
+//!     [f][f][F]
+//! The PDF that streams from the free-slip cell in the lower right corner (F) into the interface cell (I) must be
+//! reconstructed. This is because PDFs in the free-slip cells are specularly reflected. Therefore, this free-slip
+//! cell's PDF in direction (-1,1) is the same as the the gas cell's PDF in direction (-1,-1). However, since PDFs in
+//! gas cells are not available, this PDF must be reconstructed.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/FieldClone.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/bubble_model/Geometry.h"
+#include "lbm/free_surface/dynamics/PdfReconstructionModel.h"
+#include "lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace PdfReconstructionFreeSlipTest
+{
+using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >;
+using Stencil        = typename LatticeModel_T::Stencil;
+
+using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >;
+
+using PdfField_T    = lbm::PdfField< LatticeModel_T >;
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+void runSimulation()
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create (dummy) lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8)));
+
+   // add pdf source and destination fields
+   const BlockDataID pdfSrcFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF source field", latticeModel, uint_c(0), field::fzyx);
+   const BlockDataID pdfDstFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF destination field", latticeModel, uint_c(0), field::fzyx);
+
+   // add (dummy) normal field
+   const BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add fill level field (MUST be initialized with 1, i.e., fluid everywhere for this test; otherwise the fluid
+   // flag is not detected below by initFlagsFromFillLevel())
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1));
+
+   // central interface cell, in which the reconstruction and evaluation will be performed
+   const Cell centralCell = Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0));
+
+   // construct 3x3 grid with flags according to the description at the top of this file
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField   = blockIt->getData< ScalarField_T >(fillFieldID);
+      VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(
+         fillFieldIt, fillField, normalFieldIt, normalField,
+
+         // initialize gas cell
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) { *fillFieldIt = real_c(0); }
+
+         // initialize interface cells
+         if (fillFieldIt.cell() == centralCell) { *fillFieldIt = real_c(0.5); }
+
+         // initialize fluid cells
+         if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) {
+            *fillFieldIt = real_c(1);
+         }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfSrcFieldID, fillFieldID);
+   const BlockDataID flagFieldID                                      = freeSurfaceBoundaryHandling->getFlagFieldID();
+   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+   freeSurfaceBoundaryHandling->setFreeSlipAtBorder(stencil::S, cell_idx_c(0));
+
+   // initial communication
+   Communication_T(blockForest, pdfSrcFieldID, fillFieldID, flagFieldID)();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   // perform reconstruction
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const PdfField_T* const pdfSrcField    = blockIt->getData< const PdfField_T >(pdfSrcFieldID);
+      PdfField_T* const pdfDstField          = blockIt->getData< PdfField_T >(pdfDstFieldID);
+      const VectorField_T* const normalField = blockIt->getData< const VectorField_T >(normalFieldID);
+      const FlagField_T* const flagField     = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(
+         pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, flagFieldIt, flagField, normalFieldIt, normalField,
+         if (pdfSrcFieldIt.cell() == centralCell) {
+            // reconstruct with rhoGas!=1 such that reconstructed values differ from those in pdfSrcField
+            reconstructInterfaceCellLegacy< LatticeModel_T >(flagField, pdfSrcFieldIt, flagFieldIt, normalFieldIt,
+                                                             flagInfo, real_c(2), pdfDstFieldIt,
+                                                             PdfReconstructionModel("OnlyMissing"));
+         }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // evaluate if the correct cells were reconstructed
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const PdfField_T* const pdfSrcField = blockIt->getData< const PdfField_T >(pdfSrcFieldID);
+      const PdfField_T* const pdfDstField = blockIt->getData< const PdfField_T >(pdfDstFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(
+         pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, if (pdfSrcFieldIt.cell() == centralCell) {
+            // the boundary handling is not executed so the only change must be the PDF coming from the free-slip
+            // boundary cell that reflects the PDF from the gas cell
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]);   // N, (0,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+            WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]);   // E, (1,0,0)
+            WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]);   // NE, (1,1,0)
+
+            // this is the PDF that must be reconstructed due to having
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+         })
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   runSimulation();
+
+   return EXIT_SUCCESS;
+}
+} // namespace PdfReconstructionFreeSlipTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::PdfReconstructionFreeSlipTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp b/tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp
new file mode 100644
index 000000000..2638379cd
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp
@@ -0,0 +1,606 @@
+//======================================================================================================================
+//
+//  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 PdfReconstructionTest.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test PDF reconstruction at free surface boundary.
+//!
+//! Initialize 3x3 grid similar to figure 3 in publication of Koerner et al., 2005 and test reconstruction of PDFs at
+//! the free surface boundary with respect to the models specified in PdfReconstructionModel.h.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/FieldClone.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/bubble_model/Geometry.h"
+#include "lbm/free_surface/dynamics/PdfReconstructionModel.h"
+#include "lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h"
+#include "lbm/lattice_model/D2Q9.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace PdfReconstructionTest
+{
+using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >;
+using Stencil        = typename LatticeModel_T::Stencil;
+
+using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >;
+
+using PdfField_T    = lbm::PdfField< LatticeModel_T >;
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+void runSimulation(const PdfReconstructionModel& pdfReconstructionModel)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create (dummy) lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8)));
+
+   // add pdf source and destination fields
+   const BlockDataID pdfSrcFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF source field", latticeModel, uint_c(0), field::fzyx);
+   const BlockDataID pdfDstFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF destination field", latticeModel, uint_c(0), field::fzyx);
+
+   // add normal field
+   const BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add fill level field (MUST be initialized with 1, i.e., fluid everywhere for this test; otherwise the fluid
+   // flag is not detected below by initFlagsFromFillLevel())
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1));
+
+   // central interface cell, in which the reconstruction and evaluation will be performed
+   const Cell centralCell = Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0));
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField   = blockIt->getData< ScalarField_T >(fillFieldID);
+      VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, normalFieldIt, normalField, {
+         // initialize gas cells as in figure 3 from Koerner et al., 2005
+         if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)))
+         {
+            *fillFieldIt = real_c(0);
+         }
+
+         // initialize interface cells as in figure 3 from Koerner et al., 2005
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)) ||
+             fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)))
+         {
+            *fillFieldIt = real_c(0.5);
+         }
+
+         // initialize fluid cell as in figure 3 from Koerner et al., 2005
+         if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) { *fillFieldIt = real_c(1); }
+
+         // initialize interface normal as in figure 3 from Koerner et al., 2005 (values estimated)
+         // IMPORTANT REMARK: In this waLBerla's free surface implementation, the normal is defined to point from fluid
+         // to gas, whereas in Koerner et al., the normal is defined to point from gas to fluid. Therefore, we
+         // initialize the normal in opposite direction than in figure 3.
+         if (normalFieldIt.cell() == centralCell)
+         {
+            *normalFieldIt = Vector3< real_t >(real_c(-0.83867), real_c(-0.54464), real_c(0));
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfSrcFieldID, fillFieldID);
+   const BlockDataID flagFieldID                                      = freeSurfaceBoundaryHandling->getFlagFieldID();
+   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // initial communication
+   Communication_T(blockForest, pdfSrcFieldID, fillFieldID, flagFieldID)();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   // perform reconstruction
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const PdfField_T* const pdfSrcField    = blockIt->getData< const PdfField_T >(pdfSrcFieldID);
+      PdfField_T* const pdfDstField          = blockIt->getData< PdfField_T >(pdfDstFieldID);
+      const VectorField_T* const normalField = blockIt->getData< const VectorField_T >(normalFieldID);
+      const FlagField_T* const flagField     = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(
+         pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, flagFieldIt, flagField, normalFieldIt, normalField, {
+            if (pdfSrcFieldIt.cell() == centralCell)
+            {
+               // reconstruct with rhoGas!=1 such that reconstructed values differ from those in pdfSrcField
+               reconstructInterfaceCellLegacy< LatticeModel_T >(flagField, pdfSrcFieldIt, flagFieldIt, normalFieldIt,
+                                                                flagInfo, real_c(2), pdfDstFieldIt,
+                                                                pdfReconstructionModel);
+            }
+         }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   const PdfReconstructionModel::ReconstructionModel reconstructionModel = pdfReconstructionModel.getModelType();
+   const uint_t minReconstruct                               = pdfReconstructionModel.getNumMinReconstruct();
+   const PdfReconstructionModel::FallbackModel fallbackModel = pdfReconstructionModel.getFallbackModel();
+
+   // evaluate if the correct cells were reconstructed:
+   // 1. equality/ inequality between pdfSrc and pdfDst was verified manually, i.e., by hand
+   // 2. comparison with expected (reconstructed) values:
+   //    - results only valid for rhoGas=2 (as specified above)
+   //    - results obtained with a version that is believed to be correct
+   //    - was included for detection of changes in PDF reconstruction boundary condition
+   //    - makes 1. actually obsolete (1. was kept for as this is what the test was actually intended for, i.e., find
+   //       out whether the correct PDFs are reconstructed)
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const PdfField_T* const pdfSrcField = blockIt->getData< const PdfField_T >(pdfSrcFieldID);
+      const PdfField_T* const pdfDstField = blockIt->getData< const PdfField_T >(pdfDstFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, {
+         if (pdfSrcFieldIt.cell() == centralCell)
+         {
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedKeepCenter ||
+                (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                 minReconstruct > uint_c(3) &&
+                 fallbackModel == PdfReconstructionModel::FallbackModel::NormalBasedKeepCenter))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedReconstructCenter)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::All)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            // in the setup here, 3 PDFs will always be reconstructed as they are coming from the gas-side and are
+            // therefore missing
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissing ||
+                (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                 (minReconstruct == uint_c(0) || minReconstruct == uint_c(1) || minReconstruct == uint_c(2) ||
+                  minReconstruct == uint_c(3)) &&
+                 (fallbackModel == PdfReconstructionModel::FallbackModel::Smallest ||
+                  fallbackModel == PdfReconstructionModel::FallbackModel::Largest ||
+                  fallbackModel == PdfReconstructionModel::FallbackModel::NormalBasedKeepCenter)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]);   // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(4) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]);   // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(4) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]);   // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(5) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(5) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]);   // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(6) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]);   // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(6) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]);   // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(7) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]);   // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(7) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]);   // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(8) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]);   // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(8) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest)
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]);   // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+
+            if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin &&
+                minReconstruct == uint_c(9) &&
+                (fallbackModel == PdfReconstructionModel::FallbackModel::Smallest ||
+                 fallbackModel == PdfReconstructionModel::FallbackModel::Largest))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6));  // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6));  // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6));  // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6));  // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6));  // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0)
+
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0)
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   PdfReconstructionModel model = PdfReconstructionModel("NormalBasedKeepCenter");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = PdfReconstructionModel("NormalBasedReconstructCenter");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = PdfReconstructionModel("All");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = PdfReconstructionModel("OnlyMissing");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   for (uint_t i = uint_c(0); i != uint_c(10); ++i)
+   {
+      model = PdfReconstructionModel("OnlyMissingMin-" + std::to_string(i) + "-smallest");
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+      runSimulation(model);
+
+      model = PdfReconstructionModel("OnlyMissingMin-" + std::to_string(i) + "-largest");
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+      runSimulation(model);
+
+      model = PdfReconstructionModel("OnlyMissingMin-" + std::to_string(i) + "-normalBasedKeepCenter");
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+      runSimulation(model);
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace PdfReconstructionTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::PdfReconstructionTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp b/tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp
new file mode 100644
index 000000000..ae4ec83d1
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp
@@ -0,0 +1,1123 @@
+//======================================================================================================================
+//
+//  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 PdfRefillingTest.cpp
+//! \ingroup lbm/free_surface/dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \author Michael Zikeli
+//! \brief Test PDF refilling of gas cells that are converted to interface cells at the free surface boundary.
+//!
+//! For each test two helper functions are necessary, that are both passed in the main as callback functions
+//!     - The initialization function which fills four sets (gasCells, interfaceCells, liquidCells, conversionCells).
+//!     - The verification function, that checks whether the values are calculated correctly.
+//! The domain is consists of 5x3x1 cells with periodic boundaries. Different scenarios are tested. The expected
+//! solutions are obtained manually from the file "/tests/lbm/free_surface/dynamics/PdfRefillingTest.odp".
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/dynamics/PdfRefillingModel.h"
+#include "lbm/free_surface/dynamics/PdfRefillingSweep.h"
+#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm/lattice_model/D2Q9.h"
+#include "lbm/sweeps/CellwiseSweep.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <functional>
+#include <iostream>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace PdfRefillingTest
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >;
+using Stencil_T      = typename LatticeModel_T::Stencil;
+
+using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = field::FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+using FlagInfo_T                    = FlagInfo< FlagField_T >;
+
+using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+using RefillingModel_T = PdfRefillingModel::RefillingModel;
+
+using initCallback_T =
+   std::function< void(std::set< Cell >&, std::set< Cell >&, std::set< Cell >&, std::set< Cell >&) >;
+using verifyCallback_T = std::function< void(const PdfRefillingModel&, const Cell&, const PdfField_T* const) >;
+
+void runSimulation(const PdfRefillingModel& pdfRefillingModel, const initCallback_T& fillInitializationSets,
+                   const verifyCallback_T& verifyResults);
+
+/***********************************************************************************************************************
+ * Test: noRefillingCells
+ *      | G | G | G | G | G |       | G | G | G | G | G |
+ *      | G | G | G | G | G |  ==>  | G | G | I | G | G |
+ *      | G | G | G | G | G |       | G | G | G | G | G |
+ **********************************************************************************************************************/
+void init_noRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, std::set< Cell >& liquidCells,
+                           std::set< Cell >& conversionCells);
+void verify_noRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                             const PdfField_T* const pdfField);
+
+/***********************************************************************************************************************
+ * Test: fullRefillingCells
+ *      | I | I | I | I | I |       | I | I | I | I | I |
+ *      | I | G | I | I | I |  ==>  | I | I | I | I | I |
+ *      | I | I | I | I | I |       | I | I | I | I | I |
+ **********************************************************************************************************************/
+void init_fullRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells,
+                             std::set< Cell >& liquidCells, std::set< Cell >& conversionCells);
+void verify_fullRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                               const PdfField_T* const pdfField);
+
+/***********************************************************************************************************************
+ * Test: someRefillingCells
+ *      | G | I | I | I | I |       | I | I | I | I | I |
+ *      | G | G | I | I | I |  ==>  | G | I | I | I | I |
+ *      | G | G | G | I | I |       | G | G | I | I | I |
+ **********************************************************************************************************************/
+void init_someRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells,
+                             std::set< Cell >& liquidCells, std::set< Cell >& conversionCells);
+void verify_someRefillingCells(PdfRefillingModel const& pdfRefillingModel, Cell const& cell,
+                               PdfField_T const* const pdfField);
+
+/***********************************************************************************************************************
+ * Test: straightRefillingCells
+ *      | G | G | I | I | I |       | G | G | I | I | I |
+ *      | G | G | I | I | I |  ==>  | G | I | I | I | I |
+ *      | G | G | I | I | I |       | G | G | I | I | I |
+ **********************************************************************************************************************/
+void init_straightRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells,
+                                 std::set< Cell >& liquidCells, std::set< Cell >& conversionCells);
+void verify_straightRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                                   const PdfField_T* const pdfField);
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   for (auto modelType : PdfRefillingModel::getTypeIterator())
+   {
+      PdfRefillingModel model = PdfRefillingModel(modelType);
+
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification()
+                                                 << " with setup \"noRefillingCells\":"
+                                                 << "\n\t | G | G | G | G | G |       | G | G | G | G | G | "
+                                                 << "\n\t | G | G | G | G | G |  ==>  | G | G | I | G | G | "
+                                                 << "\n\t | G | G | G | G | G |       | G | G | G | G | G | \n");
+      runSimulation(model, &init_noRefillingCells, &verify_noRefillingCells);
+
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification()
+                                                 << " with setup \"fullRefillingCells\":"
+                                                 << "\n\t | I | I | I | I | I |       | I | I | I | I | I | "
+                                                 << "\n\t | I | G | I | I | I |  ==>  | I | I | I | I | I | "
+                                                 << "\n\t | I | I | I | I | I |       | I | I | I | I | I | \n");
+      runSimulation(model, init_fullRefillingCells, verify_fullRefillingCells);
+
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification()
+                                                 << " with setup \"someRefillingCells\":"
+                                                 << "\n\t | G | I | I | I | I |       | I | I | I | I | I | "
+                                                 << "\n\t | G | G | I | I | I |  ==>  | G | I | I | I | I | "
+                                                 << "\n\t | G | G | G | I | I |       | G | G | I | I | I | \n");
+      runSimulation(model, &init_someRefillingCells, &verify_someRefillingCells);
+
+      WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification()
+                                                 << " with setup \"straightRefillingCells\":"
+                                                 << "\n\t | G | G | I | I | I |       | G | G | I | I | I | "
+                                                 << "\n\t | G | G | I | I | I |  ==>  | G | I | I | I | I | "
+                                                 << "\n\t | G | G | I | I | I |       | G | G | I | I | I | \n");
+      runSimulation(model, &init_straightRefillingCells, &verify_straightRefillingCells);
+   }
+
+   return EXIT_SUCCESS;
+}
+
+void init_noRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, std::set< Cell >& liquidCells,
+                           std::set< Cell >& conversionCells)
+{
+   gasCells = std::set< Cell >(
+      { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)),
+        Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) });
+
+   interfaceCells = std::set< Cell >({});
+
+   liquidCells = std::set< Cell >({});
+
+   conversionCells = std::set< Cell >({ Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)) });
+}
+
+void init_fullRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells,
+                             std::set< Cell >& liquidCells, std::set< Cell >& conversionCells)
+{
+   gasCells = std::set< Cell >({ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) });
+
+   interfaceCells = std::set< Cell >(
+      { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)),
+        Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) });
+
+   liquidCells = std::set< Cell >({});
+
+   conversionCells = std::set< Cell >({ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) });
+}
+
+void init_someRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells,
+                             std::set< Cell >& liquidCells, std::set< Cell >& conversionCells)
+{
+   gasCells = std::set< Cell >(
+      { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)) });
+
+   interfaceCells = std::set< Cell >(
+      { Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) });
+
+   liquidCells = std::set< Cell >({});
+
+   conversionCells = std::set< Cell >({ Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)),
+                                        Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+                                        Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)) });
+}
+
+void init_straightRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells,
+                                 std::set< Cell >& liquidCells, std::set< Cell >& conversionCells)
+{
+   gasCells = std::set< Cell >(
+      { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)) });
+
+   interfaceCells = std::set< Cell >(
+      { Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)),
+        Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)),
+        Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) });
+
+   liquidCells = std::set< Cell >({});
+
+   conversionCells = std::set< Cell >({ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) });
+}
+
+void verify_noRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                             const PdfField_T* const pdfField)
+{
+   switch (pdfRefillingModel.getModelType())
+   {
+   case RefillingModel_T::EquilibriumRefilling:
+   case RefillingModel_T::AverageRefilling:
+   case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling:
+   case RefillingModel_T::ExtrapolationRefilling:
+   case RefillingModel_T::GradsMomentsRefilling:
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.444444444444444),
+                                         real_c(1e-6)); // C, (0,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.111111111111111),
+                                         real_c(1e-6)); // N, (0,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.111111111111111),
+                                         real_c(1e-6)); // S, (0,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.111111111111111),
+                                         real_c(1e-6)); // W, (-1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.111111111111111),
+                                         real_c(1e-6)); // E, (1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.027777777777778),
+                                         real_c(1e-6)); // NW, (-1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.027777777777778),
+                                         real_c(1e-6)); // NE, (1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.027777777777778),
+                                         real_c(1e-6)); // SW, (-1,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.027777777777778),
+                                         real_c(1e-6)); // SE, (1,-1,0)
+      break;
+   default:
+      WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName()
+                                                          << " is not available.\nSomething went wrong!");
+   }
+}
+
+void verify_fullRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                               const PdfField_T* const pdfField)
+{
+   switch (pdfRefillingModel.getModelType())
+   {
+   case RefillingModel_T::EquilibriumRefilling:
+   case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling:
+   case RefillingModel_T::ExtrapolationRefilling:
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443017361111111),
+                                         real_c(1e-6)); // C, (0,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.101584288194444),
+                                         real_c(1e-6)); // N, (0,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.120750954861111),
+                                         real_c(1e-6)); // S, (0,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.099328038194445),
+                                         real_c(1e-6)); // W, (-1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.123494704861111),
+                                         real_c(1e-6)); // E, (1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.022800043402778),
+                                         real_c(1e-6)); // NW, (-1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028320616319445),
+                                         real_c(1e-6)); // NE, (1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027070616319445),
+                                         real_c(1e-6)); // SW, (-1,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.033633376736111),
+                                         real_c(1e-6)); // SE, (1,-1,0)
+      break;
+   case RefillingModel_T::AverageRefilling:
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.440902777777778),
+                                         real_c(1e-6)); // C, (0,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.102829861111111),
+                                         real_c(1e-6)); // N, (0,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.119496527777777),
+                                         real_c(1e-6)); // S, (0,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.097361111111111),
+                                         real_c(1e-6)); // W, (-1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.122777777777778),
+                                         real_c(1e-6)); // E, (1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.022803819444445),
+                                         real_c(1e-6)); // NW, (-1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.029470486111111),
+                                         real_c(1e-6)); // NE, (1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.030095486111111),
+                                         real_c(1e-6)); // SW, (-1,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.034262152777778),
+                                         real_c(1e-6)); // SE, (1,-1,0)
+      break;
+   case RefillingModel_T::GradsMomentsRefilling:
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.449190200617283),
+                                         real_c(1e-6)); // C, (0,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.098497868441358),
+                                         real_c(1e-6)); // N, (0,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.117664535108025),
+                                         real_c(1e-6)); // S, (0,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.100871248070988),
+                                         real_c(1e-6)); // W, (-1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.125037914737654),
+                                         real_c(1e-6)); // E, (1,0,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.024343253279321),
+                                         real_c(1e-6)); // NW, (-1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.025234196566358),
+                                         real_c(1e-6)); // NE, (1,1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.023984196566358),
+                                         real_c(1e-6)); // SW, (-1,-1,0)
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.035176586612654),
+                                         real_c(1e-6)); // SE, (1,-1,0)
+      break;
+   default:
+      WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName()
+                                                          << " is not available.\nSomething went wrong!");
+   }
+}
+
+void verify_someRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                               const PdfField_T* const pdfField)
+{
+   switch (pdfRefillingModel.getModelType())
+   {
+   case RefillingModel_T::EquilibriumRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443777777777778),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.107661111111111),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.114327777777778),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101394444444444),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121394444444444),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.024602777777778),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.029452777777778),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.026119444444445),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.031269444444445),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else
+      {
+         if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444),
+                                               real_c(1e-6)); // C, (0,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111),
+                                               real_c(1e-6)); // N, (0,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111),
+                                               real_c(1e-6)); // S, (0,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111),
+                                               real_c(1e-6)); // W, (-1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111),
+                                               real_c(1e-6)); // E, (1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023688715277778),
+                                               real_c(1e-6)); // NW, (-1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028351215277778),
+                                               real_c(1e-6)); // NE, (1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027101215277778),
+                                               real_c(1e-6)); // SW, (-1,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.032438715277778),
+                                               real_c(1e-6)); // SE, (1,-1,0)
+         }
+         else
+         {
+            if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.44262037037037),
+                                                  real_c(1e-6)); // C,  (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.104188425925926),
+                                                  real_c(1e-6)); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.117521759259259),
+                                                  real_c(1e-6)); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.095712037037037),
+                                                  real_c(1e-6)); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.127934259259259),
+                                                  real_c(1e-6)); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.022553009259259),
+                                                  real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.030125231481482),
+                                                  real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.025403009259259),
+                                                  real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.033941898148148),
+                                                  real_c(1e-6)); // SE, (1,-1,0)
+            }
+            else { WALBERLA_ABORT("The cells that need refilling are not set correctly."); }
+         }
+      }
+      break;
+   case RefillingModel_T::AverageRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.441666666666667),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.110416666666666),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.110416666666666),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.095833333333333),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.119166666666667),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.023958333333333),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.032291666666667),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.033958333333333),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.032291666666667),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.441111111111111),
+                                            real_c(1e-6)); // C,  (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.099340277777778),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.116840277777778),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.098715277777778),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.108715277777778),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.022256944444444),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.042881944444445),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.035381944444445),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.034756944444445),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.44),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.102708333333333),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.109375),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.092847222222222),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.126736111111111),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.021805555555556),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.035694444444445),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.035138888888889),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.035694444444445),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+      break;
+   case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.452777777777777),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.144811111111111),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.101477777777778),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.051994444444444),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.111994444444444),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.021427777777778),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.020377777777778),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.062044444444445),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.033094444444445),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else
+      {
+         if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444),
+                                               real_c(1e-6)); // C, (0,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111),
+                                               real_c(1e-6)); // N, (0,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111),
+                                               real_c(1e-6)); // S, (0,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111),
+                                               real_c(1e-6)); // W, (-1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111),
+                                               real_c(1e-6)); // E, (1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023688715277778),
+                                               real_c(1e-6)); // NW, (-1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028351215277778),
+                                               real_c(1e-6)); // NE, (1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027101215277778),
+                                               real_c(1e-6)); // SW, (-1,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.032438715277778),
+                                               real_c(1e-6)); // SE, (1,-1,0)
+         }
+         else
+         {
+            if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.43522037037037),
+                                                  real_c(1e-6)); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.112788425925926),
+                                                  real_c(1e-6)); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.046121759259259),
+                                                  real_c(1e-6)); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.078962037037037),
+                                                  real_c(1e-6)); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.181184259259259),
+                                                  real_c(1e-6)); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.021353009259259),
+                                                  real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.008175231481481),
+                                                  real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.078453009259259),
+                                                  real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.037741898148148),
+                                                  real_c(1e-6)); // SE, (1,-1,0)
+            }
+            else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+         }
+      }
+      break;
+   case RefillingModel_T::ExtrapolationRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.452777777777777),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.141527777777778),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.104861111111111),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.128611111111111),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.035277777777778),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.037986111111111),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.002152777777778),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.083819444444445),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.012986111111111),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else
+      {
+         if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444),
+                                               real_c(1e-6)); // C, (0,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111),
+                                               real_c(1e-6)); // N, (0,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111),
+                                               real_c(1e-6)); // S, (0,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111),
+                                               real_c(1e-6)); // W, (-1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111),
+                                               real_c(1e-6)); // E, (1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023688715277778),
+                                               real_c(1e-6)); // NW, (-1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028351215277778),
+                                               real_c(1e-6)); // NE, (1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027101215277778),
+                                               real_c(1e-6)); // SW, (-1,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.032438715277778),
+                                               real_c(1e-6)); // SE, (1,-1,0)
+         }
+         else
+         {
+            if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.429444444444444),
+                                                  real_c(1e-6)); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.076527777777778),
+                                                  real_c(1e-6)); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.083194444444444),
+                                                  real_c(1e-6)); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.066111111111111),
+                                                  real_c(1e-6)); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.196111111111111),
+                                                  real_c(1e-6)); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.011319444444444),
+                                                  real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.001319444444444),
+                                                  real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.082986111111111),
+                                                  real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.052986111111111),
+                                                  real_c(1e-6)); // SE, (1,-1,0)
+            }
+            else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+         }
+      }
+      break;
+   case RefillingModel_T::GradsMomentsRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.46353086419753),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.110747530864197),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.117414197530864),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.09336975308642),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.11336975308642),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.023522530864198),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.02559475308642),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.022261419753087),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.030189197530864),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else
+      {
+         if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0))
+         {
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444),
+                                               real_c(1e-6)); // C, (0,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111),
+                                               real_c(1e-6)); // N, (0,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111),
+                                               real_c(1e-6)); // S, (0,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111),
+                                               real_c(1e-6)); // W, (-1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111),
+                                               real_c(1e-6)); // E, (1,0,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.022994270833333),
+                                               real_c(1e-6)); // NW, (-1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.029045659722222),
+                                               real_c(1e-6)); // NE, (1,1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027795659722222),
+                                               real_c(1e-6)); // SW, (-1,-1,0)
+            WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.031744270833333),
+                                               real_c(1e-6)); // SE, (1,-1,0)
+         }
+         else
+         {
+            if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.454143004115226),
+                                                  real_c(1e-6)); // C, (0,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.104291306584362),
+                                                  real_c(1e-6)); // N, (0,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.117624639917695),
+                                                  real_c(1e-6)); // S, (0,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.092728497942387),
+                                                  real_c(1e-6)); // W, (-1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.124950720164609),
+                                                  real_c(1e-6)); // E, (1,0,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.02389045781893),
+                                                  real_c(1e-6)); // NW, (-1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.025907124485597),
+                                                  real_c(1e-6)); // NE, (1,1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.021184902263375),
+                                                  real_c(1e-6)); // SW, (-1,-1,0)
+               WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.035279346707819),
+                                                  real_c(1e-6)); // SE, (1,-1,0)
+            }
+            else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+         }
+      }
+      break;
+   default:
+      WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName()
+                                                          << " is not available.\nSomething went wrong!");
+   }
+}
+
+void verify_straightRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell,
+                                   const PdfField_T* const pdfField)
+{
+   switch (pdfRefillingModel.getModelType())
+   {
+   case RefillingModel_T::EquilibriumRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.444259259259259),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.107781481481481),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.114448148148148),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.106709259259259),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.115598148148148),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.025889814814815),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.02804537037037),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027489814814815),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.029778703703704),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+      break;
+   case RefillingModel_T::AverageRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.442222222222222),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.110555555555555),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.110555555555555),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.101111111111111),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.113333333333333),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.025277777777778),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.030833333333333),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.035277777777778),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.030833333333333),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+      break;
+   case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.449659259259259),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.10168148148148),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.188348148148148),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.121459259259259),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.060348148148148),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.027489814814815),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.050095370370371),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(-0.025460185185185),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.026378703703704),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+      break;
+   case RefillingModel_T::ExtrapolationRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.441111111111112),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.128194444444443),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.154861111111111),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.094861111111111),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.098194444444445),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.025694444444445),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.069027777777778),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(-0.037638888888889),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.025694444444444),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+      break;
+   case RefillingModel_T::GradsMomentsRefilling:
+      if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0))
+      {
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.465658436213991),
+                                            real_c(1e-6)); // C, (0,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.113131275720165),
+                                            real_c(1e-6)); // N, (0,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.119797942386831),
+                                            real_c(1e-6)); // S, (0,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.096009670781893),
+                                            real_c(1e-6)); // W, (-1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.104898559670782),
+                                            real_c(1e-6)); // E, (1,0,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023677880658436),
+                                            real_c(1e-6)); // NW, (-1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.024907510288066),
+                                            real_c(1e-6)); // NE, (1,1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.02435195473251),
+                                            real_c(1e-6)); // SW, (-1,-1,0)
+         WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.027566769547325),
+                                            real_c(1e-6)); // SE, (1,-1,0)
+      }
+      else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); }
+      break;
+   default:
+      WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName()
+                                                          << " is not available.\nSomething went wrong!");
+   }
+}
+
+void runSimulation(const PdfRefillingModel& pdfRefillingModel, const initCallback_T& fillInitializationSets,
+                   const verifyCallback_T& verifyResults)
+{
+   // define the domain size (5x3x1)
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(5), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, true, false);                                   // periodicity
+
+   // relaxation rate (used by GradsMomentsRefilling)
+   real_t omega = real_c(1.8);
+
+   // create lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(omega));
+
+   // add PDF field (and a copy that stores the original PDF field)
+   const BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF source field", latticeModel, uint_c(1), field::fzyx);
+
+   // field to store PDFs before refilling, i.e., to test if non-refilling cells are modified
+   const BlockDataID pdfOrgFieldID =
+      lbm::addPdfFieldToStorage(blockForest, "PDF destination field", latticeModel, uint_c(1), field::fzyx);
+
+   // add fill level field (MUST be initialized with 1, i.e., fluid everywhere for this test; otherwise the fluid
+   // flag is not detected below by initFlagsFromFillLevel())
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1));
+
+   // define the initial properties of the cells ( gas, liquid, interface )
+   std::set< Cell > gasCells;
+   std::set< Cell > interfaceCells;
+   std::set< Cell > liquidCells;
+   std::set< Cell > conversionCells;
+   fillInitializationSets(gasCells, interfaceCells, liquidCells, conversionCells);
+
+   real_t initDensity = real_c(1.0);
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+      PdfField_T* const pdfField     = blockIt->getData< PdfField_T >(pdfFieldID);
+      PdfField_T* pdfOrgField        = blockIt->getData< PdfField_T >(pdfOrgFieldID);
+
+      std::vector< std::pair< Cell, Vector3< real_t > > > velocities = {
+         { Cell{ cell_idx_c(0), cell_idx_c(0), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } },
+         { Cell{ cell_idx_c(1), cell_idx_c(0), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.00), real_c(-0.05), real_c(0.00) } },
+         { Cell{ cell_idx_c(2), cell_idx_c(0), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.00), real_c(0.00), real_c(0.00) } },
+         { Cell{ cell_idx_c(3), cell_idx_c(0), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } },
+         { Cell{ cell_idx_c(4), cell_idx_c(0), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.00), real_c(-0.05), real_c(0.00) } },
+         { Cell{ cell_idx_c(0), cell_idx_c(1), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.10), real_c(-0.05), real_c(0.00) } },
+         { Cell{ cell_idx_c(1), cell_idx_c(1), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } },
+         { Cell{ cell_idx_c(2), cell_idx_c(1), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.10), real_c(0.00), real_c(0.00) } },
+         { Cell{ cell_idx_c(3), cell_idx_c(1), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.10), real_c(-0.05), real_c(0.00) } },
+         { Cell{ cell_idx_c(4), cell_idx_c(1), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } },
+         { Cell{ cell_idx_c(0), cell_idx_c(2), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } },
+         { Cell{ cell_idx_c(1), cell_idx_c(2), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } },
+         { Cell{ cell_idx_c(2), cell_idx_c(2), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.00), real_c(0.00), real_c(0.00) } },
+         { Cell{ cell_idx_c(3), cell_idx_c(2), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } },
+         { Cell{ cell_idx_c(4), cell_idx_c(2), cell_idx_c(0) },
+           Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } }
+      };
+
+      for (auto velocity = velocities.begin(); velocity != velocities.end(); velocity++)
+      {
+         pdfField->setDensityAndVelocity(velocity->first, velocity->second, initDensity);
+         pdfOrgField->setDensityAndVelocity(velocity->first, velocity->second, initDensity);
+      }
+
+      // modify the PDFs to achieve inequality for the equalAndNonEqualRefilling
+      for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+      {
+         using D = stencil::Direction;
+         D dir   = d.direction();
+         Cell cell1(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0));
+         Cell cell2(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0));
+         Cell cell3(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0));
+         Cell cell4(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0));
+         Cell cell5(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0));
+         real_t PDFOffset(real_c(0.03));
+
+         if (dir == D::SW)
+         {
+            pdfField->get(cell1, dir) += PDFOffset;
+            pdfOrgField->get(cell1, dir) += PDFOffset;
+            pdfField->get(cell3, dir) += PDFOffset;
+            pdfOrgField->get(cell3, dir) += PDFOffset;
+            pdfField->get(cell4, dir) += PDFOffset;
+            pdfOrgField->get(cell4, dir) += PDFOffset;
+         }
+         if (dir == D::E)
+         {
+            pdfField->get(cell1, dir) -= PDFOffset;
+            pdfOrgField->get(cell1, dir) -= PDFOffset;
+            pdfField->get(cell3, dir) -= PDFOffset;
+            pdfOrgField->get(cell3, dir) -= PDFOffset;
+            pdfField->get(cell5, dir) -= PDFOffset;
+            pdfOrgField->get(cell5, dir) -= PDFOffset;
+         }
+         if (dir == D::S)
+         {
+            pdfField->get(cell2, dir) -= PDFOffset;
+            pdfOrgField->get(cell2, dir) -= PDFOffset;
+            pdfField->get(cell3, dir) -= PDFOffset;
+            pdfOrgField->get(cell3, dir) -= PDFOffset;
+            pdfField->get(cell4, dir) -= PDFOffset;
+            pdfOrgField->get(cell4, dir) -= PDFOffset;
+         }
+         if (dir == D::NE)
+         {
+            pdfField->get(cell2, dir) += PDFOffset;
+            pdfOrgField->get(cell2, dir) += PDFOffset;
+            pdfField->get(cell3, dir) += PDFOffset;
+            pdfOrgField->get(cell3, dir) += PDFOffset;
+            pdfField->get(cell5, dir) += PDFOffset;
+            pdfOrgField->get(cell5, dir) += PDFOffset;
+         }
+      }
+
+      pdfOrgField = pdfField->clone();
+
+      // initialize fill level
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         if (gasCells.find(fillFieldIt.cell()) != gasCells.end()) { *fillFieldIt = real_c(0.0); }
+         else
+         {
+            if (interfaceCells.find(fillFieldIt.cell()) != interfaceCells.end()) { *fillFieldIt = real_c(0.5); }
+            else
+            {
+               if (liquidCells.find(fillFieldIt.cell()) != liquidCells.end()) { *fillFieldIt = real_c(1.0); }
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   const BlockDataID flagFieldID          = freeSurfaceBoundaryHandling->getFlagFieldID();
+   const FlagInfo< FlagField_T > flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // create timeloop
+   uint_t timesteps = uint_c(1);
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   Communication_T(blockForest, pdfFieldID, pdfOrgFieldID, flagFieldID)();
+
+   using geometryHandler   = SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >;
+   auto geometryHandlerPtr = std::make_shared< geometryHandler >(blockForest, freeSurfaceBoundaryHandling, fillFieldID,
+                                                                 "FiniteDifferenceMethod", false, false, real_c(0.0));
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+      FlagField_T* const flagField   = blockIt->getData< FlagField_T >(flagFieldID);
+
+      // convert a cell from gas to interface
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, flagFieldIt, flagField, {
+         if (conversionCells.find(flagFieldIt.cell()) != conversionCells.end())
+         {
+            // fill one cell entirely and create an interface cell from it; the fill level must be 1.1 to obtain the
+            // normal as used in the manual computations of the expected solutions
+            (*fillFieldIt) = real_c(1.1);
+
+            // mark cell to be reinitialized
+            field::removeFlag(flagFieldIt, flagInfo.gasFlag);
+            field::addFlag(flagFieldIt, flagInfo.interfaceFlag);
+            field::addFlag(flagFieldIt, flagInfo.convertedFlag);
+            field::addFlag(flagFieldIt, flagInfo.convertFromGasToInterfaceFlag);
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+
+      // perform refilling
+      switch (pdfRefillingModel.getModelType())
+      { // the scope for each "case" is required since variables are defined within "case"
+      case PdfRefillingModel::RefillingModel::EquilibriumRefilling: {
+         EquilibriumRefillingSweep< LatticeModel_T, FlagField_T > equilibriumRefillingSweep(pdfFieldID, flagFieldID,
+                                                                                            flagInfo, true);
+         equilibriumRefillingSweep(blockIt.get());
+         break;
+      }
+
+      case PdfRefillingModel::RefillingModel::AverageRefilling: {
+         AverageRefillingSweep< LatticeModel_T, FlagField_T > averageRefillingSweep(pdfFieldID, flagFieldID, flagInfo,
+                                                                                    true);
+         averageRefillingSweep(blockIt.get());
+         break;
+      }
+
+      case PdfRefillingModel::RefillingModel::EquilibriumAndNonEquilibriumRefilling: {
+         EquilibriumAndNonEquilibriumRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+            equilibriumAndNonEquilibriumRefillingSweep(pdfFieldID, flagFieldID, fillFieldID, flagInfo, uint_c(3), true);
+         equilibriumAndNonEquilibriumRefillingSweep(blockIt.get());
+         break;
+      }
+
+      case PdfRefillingModel::RefillingModel::ExtrapolationRefilling: {
+         ExtrapolationRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >
+            extrapolationRefillingSweep(pdfFieldID, flagFieldID, fillFieldID, flagInfo, uint_c(3), true);
+         extrapolationRefillingSweep(blockIt.get());
+         break;
+      }
+
+      case PdfRefillingModel::RefillingModel::GradsMomentsRefilling: {
+         GradsMomentsRefillingSweep< LatticeModel_T, FlagField_T > gradsMomentsRefillingSweep(pdfFieldID, flagFieldID,
+                                                                                              flagInfo, omega, true);
+         gradsMomentsRefillingSweep(blockIt.get());
+         break;
+      }
+      default:
+         WALBERLA_ABORT("The specified pdf refilling model is not available.");
+      }
+   }
+
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const PdfField_T* const pdfField     = blockIt->getData< const PdfField_T >(pdfFieldID);
+      const PdfField_T* const pdfOrgField  = blockIt->getData< const PdfField_T >(pdfOrgFieldID);
+      const FlagField_T* const flagField   = blockIt->getData< const FlagField_T >(flagFieldID);
+      const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, pdfFieldOrgIt, pdfOrgField, flagFieldIt, flagField, fillFieldIt,
+                             fillField, {
+                                if (conversionCells.find(flagFieldIt.cell()) == conversionCells.end())
+                                {
+                                   // check cells that were not converted if their PDFs changed
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[0], pdfFieldOrgIt[0]); // C, (0,0,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[1], pdfFieldOrgIt[1]); // N, (0,1,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[2], pdfFieldOrgIt[2]); // S, (0,-1,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[3], pdfFieldOrgIt[3]); // W, (-1,0,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[4], pdfFieldOrgIt[4]); // E, (1,0,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[5], pdfFieldOrgIt[5]); // NW, (-1,1,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[6], pdfFieldOrgIt[6]); // NE, (1,1,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[7], pdfFieldOrgIt[7]); // SW, (-1,-1,0)
+                                   WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[8], pdfFieldOrgIt[8]); // SE, (1,-1,0)
+                                }
+                                else
+                                {
+                                   // check cells converted from gas to interface
+                                   verifyResults(pdfRefillingModel, pdfFieldIt.cell(), pdfField);
+                                }
+                             }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   MPIManager::instance()->resetMPI();
+}
+
+} // namespace PdfRefillingTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::PdfRefillingTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/dynamics/PdfRefillingTest.ods b/tests/lbm/free_surface/dynamics/PdfRefillingTest.ods
new file mode 100644
index 0000000000000000000000000000000000000000..90899e2de1bc6574ddbf680509e901c84f202018
GIT binary patch
literal 37960
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

literal 0
HcmV?d00001

diff --git a/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp b/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp
new file mode 100644
index 000000000..8a2590c69
--- /dev/null
+++ b/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp
@@ -0,0 +1,247 @@
+//======================================================================================================================
+//
+//  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 WettingConversionTest.cpp
+//! \ingroup lbm/free_surface/dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test cell conversion initiated by wetting.
+//!
+//! Initialize drop as a cylinder section near a solid wall. Run a free surface LBM simulation with local triangulation
+//! and wetting, and evaluate the converted interface cells for correctness.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/field/Adaptors.h"
+#include "lbm/field/AddToStorage.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/lattice_model/D3Q19.h"
+
+#include <algorithm>
+#include <vector>
+namespace walberla
+{
+namespace free_surface
+{
+namespace WettingConversionTest
+{
+// define types
+using flag_t      = uint32_t;
+using FlagField_T = FlagField< flag_t >;
+
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+template< typename LatticeModel_T >
+std::vector< Cell > runSimulation(uint_t timesteps, real_t contactAngle)
+{
+   using Stencil_T                     = typename LatticeModel_T::Stencil;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(14), uint_c(7), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, true);                                  // periodicity
+
+   real_t relaxRate = real_c(1.8);
+
+   // create lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(relaxRate));
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+
+   // add fill level field (initialized with 0, i.e., gas everywhere)
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1));
+
+   // add dummy force field
+   BlockDataID forceFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Force field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // 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();
+
+   // add liquid drop (as cylinder section)
+   Vector3< real_t > midpoint1(real_c(14) * real_c(0.5), real_c(1), real_c(0));
+   Vector3< real_t > midpoint2(real_c(14) * real_c(0.5), real_c(1), real_c(5));
+   geometry::Cylinder cylinder(midpoint1, midpoint2, real_c(14) * real_c(0.2));
+   bubble_model::addBodyToFillLevelField< geometry::Cylinder >(*blockForest, fillFieldID, cylinder, false);
+
+   // initialize bottom (in y-direction) of domain as no-slip boundary
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::S, cell_idx_c(0));
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // initial communication
+   Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, forceFieldID)();
+
+   // add (dummy) bubble model
+   const bool disableSplits = true; // necessary if a gas bubble could split
+   auto bubbleModel         = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, disableSplits);
+   bubbleModel->initFromFillLevelField(fillFieldID);
+   bubbleModel->setAtmosphere(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), real_c(1));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // add surface geometry handler
+   SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler(
+      blockForest, freeSurfaceBoundaryHandling, fillFieldID, "LocalTriangulation", true, true, contactAngle);
+   geometryHandler.addSweeps(timeloop);
+
+   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
+   const ConstBlockDataID normalFieldID    = geometryHandler.getConstNormalFieldID();
+
+   // add surface dynamics handler
+   SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID,
+      freeSurfaceBoundaryHandling, bubbleModel, "NormalBasedKeepCenter", "EquilibriumRefilling", "EvenlyNewInterface",
+      relaxRate, Vector3< real_t >(real_c(0)), real_c(1e-2), false, false, real_c(1e-3), real_c(1e-1));
+   dynamicsHandler.addSweeps(timeloop);
+
+   timeloop.run();
+
+   // get interface cells after performing simulation
+   std::vector< Cell > interfaceCells;
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, omp critical, {
+         if (flagInfo.isInterface(flagFieldIt)) { interfaceCells.emplace_back(flagFieldIt.cell()); }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+   }
+
+   return interfaceCells;
+}
+
+template< typename LatticeModel_T >
+void testWettingConversion()
+{
+   uint_t timesteps;
+   real_t contactAngle;
+   std::vector< Cell > expectedInterfaceCells;
+   std::vector< Cell > computedInterfaceCells;
+   bool vectorsEqual;
+
+   // test different contact angles; expected results have been determined using a version of the code that was assumed
+   // to be correct
+   timesteps              = uint_c(200);
+   contactAngle           = real_c(120);
+   expectedInterfaceCells = std::vector< Cell >{
+      Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(4), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(5), cell_idx_c(3), cell_idx_c(0)),
+      Cell(cell_idx_c(6), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(7), cell_idx_c(3), cell_idx_c(0)),
+      Cell(cell_idx_c(8), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(3), cell_idx_c(0))
+   };
+   WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees");
+   computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle);
+   vectorsEqual           = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(),
+                                                expectedInterfaceCells.begin(), expectedInterfaceCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+   MPIManager::instance()->resetMPI();
+
+   timesteps              = uint_c(200);
+   contactAngle           = real_c(80);
+   expectedInterfaceCells = std::vector< Cell >{
+      Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(9), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(10), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(8), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(5), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(6), cell_idx_c(3), cell_idx_c(0)),
+      Cell(cell_idx_c(7), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(8), cell_idx_c(3), cell_idx_c(0))
+   };
+   WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees");
+   computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle);
+   vectorsEqual           = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(),
+                                                expectedInterfaceCells.begin(), expectedInterfaceCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+   MPIManager::instance()->resetMPI();
+
+   timesteps              = uint_c(200);
+   contactAngle           = real_c(45);
+   expectedInterfaceCells = std::vector< Cell >{
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),  Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(10), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(11), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(6), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(7), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(8), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(10), cell_idx_c(2), cell_idx_c(0))
+   };
+   WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees");
+   computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle);
+   vectorsEqual           = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(),
+                                                expectedInterfaceCells.begin(), expectedInterfaceCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+   MPIManager::instance()->resetMPI();
+
+   timesteps              = uint_c(500);
+   contactAngle           = real_c(1);
+   expectedInterfaceCells = std::vector< Cell >{
+      Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),  Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)),  Cell(cell_idx_c(10), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(11), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(12), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(6), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(7), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(8), cell_idx_c(2), cell_idx_c(0)),
+      Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)),  Cell(cell_idx_c(10), cell_idx_c(2), cell_idx_c(0))
+   };
+   WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees");
+   computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle);
+   vectorsEqual           = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(),
+                                                expectedInterfaceCells.begin(), expectedInterfaceCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   MPIManager::instance()->resetMPI();
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_INFO("Testing with D3Q19 stencil.");
+   testWettingConversion< lbm::D3Q19< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >();
+
+   WALBERLA_LOG_INFO("Testing with D3Q27 stencil.");
+   testWettingConversion< lbm::D3Q27< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >();
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace WettingConversionTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::WettingConversionTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp b/tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp
new file mode 100644
index 000000000..24fcc259b
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp
@@ -0,0 +1,74 @@
+//======================================================================================================================
+//
+//  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 CellFluidVolumeTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Calculate fluid volume within a cell and compare with results obtained with ParaView.
+//
+//======================================================================================================================
+
+#include "core/Environment.h"
+#include "core/debug/Debug.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/free_surface/surface_geometry/CurvatureSweep.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace CellFluidVolumeTest
+{
+inline void test(const Vector3< real_t >& normal, real_t offset, real_t expectedVolume, real_t tolerance)
+{
+   real_t volume = computeCellFluidVolume(normal, offset);
+   WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(expectedVolume, volume, tolerance);
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   // allowed deviation from the expected result
+   real_t tolerance = real_c(1e-3);
+
+   // test case where simple formula is applied (Figure 2.12, p. 24 in dissertation of Thomas Pohl)
+   const Vector3< real_t > normalSimpleCase(real_c(0), real_c(0.5), real_c(0.866));
+   const real_t offsetSimpleCase         = real_c(-0.1);
+   const real_t expectedVolumeSimpleCase = real_c(0.3845); // obtained with ParaView
+   test(normalSimpleCase, offsetSimpleCase, expectedVolumeSimpleCase, tolerance);
+
+   // test cases as in dissertation of Thomas Pohl, page 25
+   const Vector3< real_t > normal(real_c(0.37), real_c(0.61), real_c(0.7));
+   const std::vector< real_t > offsetList{ real_c(-0.52), real_c(-0.3), real_c(-0.17), real_c(0) };
+   const std::vector< real_t > volumeList{ real_c(0.0347), real_c(0.1612), real_c(0.2887),
+                                           real_c(0.5) }; // obtained with ParaView
+
+   WALBERLA_ASSERT_EQUAL(offsetList.size(), volumeList.size());
+
+   for (size_t i = 0; i != offsetList.size(); ++i)
+   {
+      test(normal, offsetList[i], volumeList[i], tolerance);
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace CellFluidVolumeTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::CellFluidVolumeTest::main(argc, argv); }
diff --git a/tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp b/tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp
new file mode 100644
index 000000000..9220f8022
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp
@@ -0,0 +1,545 @@
+//======================================================================================================================
+//
+//  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 CurvatureOfSineTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Initialize sine profile and compare computed curvature with analytical curvature.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/math/Constants.h"
+#include "core/math/DistributedSample.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/CurvatureSweep.h"
+#include "lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm/free_surface/surface_geometry/SmoothingSweep.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D2Q9.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/D3Q27.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <cmath>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace CurvatureOfSineTest
+{
+// define types
+using flag_t = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+// function describing the global sine profile
+inline real_t function(real_t x, real_t amplitude, real_t offset, uint_t domainWidth)
+{
+   return amplitude * std::sin(x / real_c(domainWidth) * real_c(2) * math::pi) + offset;
+}
+
+// derivative of the function that is describing the sine profile
+inline real_t derivative(real_t x, real_t amplitude, uint_t domainWidth)
+{
+   const real_t domainWidthInv = real_c(1) / real_c(domainWidth);
+   return amplitude * std::cos(x * domainWidthInv * real_c(2) * math::pi) * real_c(2) * math::pi * domainWidthInv;
+}
+
+// second derivative of the function that is describing the sine profile
+inline real_t secondDerivative(real_t x, real_t amplitude, uint_t domainWidth)
+{
+   const real_t domainWidthInv = real_c(1) / real_c(domainWidth);
+
+   // the minus has been neglected on purpose: due to the definition of the normal (pointing from liquid to gas), the
+   // curvature also has a different sign
+   return amplitude * std::sin(x * domainWidthInv * real_c(2) * math::pi) * real_c(4) * math::pi * math::pi *
+          domainWidthInv * domainWidthInv;
+}
+
+// compute the analytical normal vector and evaluate the error of the computed normal (absolute error in angle)
+template< typename Stencil_T >
+class ComputeAnalyticalNormal
+{
+ public:
+   ComputeAnalyticalNormal(const std::weak_ptr< const StructuredBlockForest >& blockForest, BlockDataID& normalFieldID,
+                           const ConstBlockDataID& flagFieldID, const field::FlagUID& interfaceFlagID,
+                           const Vector3< uint_t >& domainSize, real_t amplitude)
+      : blockForest_(blockForest), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID),
+        interfaceFlagID_(interfaceFlagID), domainSize_(domainSize), amplitude_(amplitude)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // compute analytical normals
+      const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_);
+      VectorField_T* const normalField   = block->getData< VectorField_T >(normalFieldID_);
+
+      const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+      // explicitly use either D2Q9 or D3Q27 here, as the geometry operations require (or are most accurate with) the
+      // full neighborhood;
+      using NeighborhoodStencil_T =
+         typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type;
+
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, {
+         // only treat interface cells
+         if (!isFlagSet(flagFieldIt, interfaceFlag) &&
+             !field::isFlagInNeighborhood< NeighborhoodStencil_T >(flagFieldIt, interfaceFlag))
+         {
+            continue;
+         }
+
+         Cell globalCell = flagFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *block, flagFieldIt.cell());
+
+         // global x-location of this cell's center
+         const real_t globalXCenter = real_c(globalCell[0]) + real_c(0.5);
+
+         // get normal vector (slope of the negative inverse derivative gives direction of the normal)
+         Vector3< real_t > normalVec =
+            Vector3< real_t >(real_c(1), -real_c(1) / derivative(globalXCenter, amplitude_, domainSize_[0]), real_c(0));
+         normalVec = normalVec.getNormalized();
+
+         // mirror vectors that are pointing downwards, as normal vector is defined to point from fluid to gas in FSLBM
+         if (normalVec[1] < real_c(0)) { normalVec *= -real_c(1); }
+
+         *normalFieldIt = normalVec;
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   BlockDataID normalFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   field::FlagUID interfaceFlagID_;
+
+   Vector3< uint_t > domainSize_;
+   real_t amplitude_;
+}; // class ComputeAnalyticalNormal
+
+// compute the analytical normal vector and evaluate the error of the computed normal (absolute error in angle)
+template< typename Stencil_T >
+class ComputeCurvatureError
+{
+ public:
+   ComputeCurvatureError(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                         const ConstBlockDataID& curvatureFieldID, const ConstBlockDataID& flagFieldID,
+                         const field::FlagUID& interfaceFlagID, const Vector3< uint_t >& domainSize, real_t amplitude,
+                         const std::shared_ptr< real_t >& l2Error)
+      : blockForest_(blockForest), curvatureFieldID_(curvatureFieldID), flagFieldID_(flagFieldID),
+        interfaceFlagID_(interfaceFlagID), domainSize_(domainSize), amplitude_(amplitude), l2Error_(l2Error)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // compute analytical normals and compare their directions with the computed normals
+      const FlagField_T* const flagField        = block->getData< const FlagField_T >(flagFieldID_);
+      const ScalarField_T* const curvatureField = block->getData< const ScalarField_T >(curvatureFieldID_);
+
+      const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+      real_t curvDiffSum2 = real_c(0);
+      real_t analCurvSum2 = real_c(0);
+
+      WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, curvatureFieldIt, curvatureField,
+                                 omp parallel for schedule(static) reduction(+:curvDiffSum2) reduction(+:analCurvSum2),
+      {
+         // only treat interface cells
+         if (isFlagSet(flagFieldIt, interfaceFlag))
+         {
+            Cell globalCell = flagFieldIt.cell();
+            blockForest->transformBlockLocalToGlobalCell(globalCell, *block, flagFieldIt.cell());
+
+            // normalized global x-location of this cell's center
+            const real_t globalXCenter = real_c(globalCell[0]) + real_c(0.5);
+
+            // get analytical curvature
+            const real_t analyticalCurvature = secondDerivative(globalXCenter, amplitude_, domainSize_[0]);
+
+            // calculate the relative error in curvature (dx=1/domainSize[0] for converting from LBM to physical units)
+            const real_t curvDiff = *curvatureFieldIt - analyticalCurvature;
+            curvDiffSum2 += curvDiff * curvDiff;
+            analCurvSum2 += analyticalCurvature * analyticalCurvature;
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+
+      mpi::allReduceInplace< real_t >(curvDiffSum2, mpi::SUM);
+      mpi::allReduceInplace< real_t >(analCurvSum2, mpi::SUM);
+
+      *l2Error_ = std::pow(curvDiffSum2 / analCurvSum2, real_c(0.5));
+
+      WALBERLA_LOG_RESULT("Relative error in curvature according to L2 norm = " << *l2Error_);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   ConstBlockDataID curvatureFieldID_;
+   ConstBlockDataID flagFieldID_;
+
+   field::FlagUID interfaceFlagID_;
+
+   Vector3< uint_t > domainSize_;
+   real_t amplitude_;
+
+   std::shared_ptr< real_t > l2Error_;
+}; // class ComputeCurvatureError
+
+template< typename LatticeModel_T >
+real_t test(uint_t domainWidth, real_t amplitude, real_t offset, uint_t fillLevelInitSamples, bool useTriangulation,
+            bool useAnalyticalNormal)
+{
+   // define types
+   using Stencil_T       = typename LatticeModel_T::Stencil;
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   Vector3< uint_t > domainSize(domainWidth);
+   domainSize[2] = uint_c(1);
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, true);                                   // periodicity
+
+   // create lattice model with omega=1
+   LatticeModel_T latticeModel(real_c(1.0));
+
+   // add fields
+   BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID curvatureFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0), field::fzyx, uint_c(1));
+   BlockDataID smoothFillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), field::fzyx, uint_c(1));
+
+   // obstacle normal field is only a dummy field here (wetting effects are not considered in this test)
+   BlockDataID dummyObstNormalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Dummy obstacle normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+
+   // initialize sine profile such that there is exactly one period; every length is normalized with domainSize[0]
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const uint_t numTotalPoints = fillLevelInitSamples * fillLevelInitSamples;
+      const real_t stepsize       = real_c(1) / real_c(fillLevelInitSamples);
+
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // cell in block-local coordinates
+         const Cell localCell = fillFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = fillFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // Monte-Carlo like estimation of the fill level:
+         // create uniformly-distributed sample points in each cell and count the number of points below the sine
+         // profile; this fraction of points is used as the fill level to initialize the profile
+         uint_t numPointsBelow = uint_c(0);
+
+         for (uint_t xSample = uint_c(0); xSample < fillLevelInitSamples; ++xSample)
+         {
+            // value of the sine-function
+            const real_t functionValue =
+               function(real_c(globalCell[0]) + real_c(xSample) * stepsize, amplitude, offset, domainSize[0]);
+
+            for (uint_t ySample = uint_c(0); ySample < fillLevelInitSamples; ++ySample)
+            {
+               const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize;
+               if (yPoint < functionValue) { ++numPointsBelow; }
+            }
+         }
+
+         // fill level is fraction of points below sine profile
+         fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints);
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // communicate fill level field to have meaningful values in ghost layer cells in periodic directions
+   Communication_T(blockForest, fillFieldID)();
+
+   // initialize fill level field
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // communicate initialized flag field
+   Communication_T(blockForest, flagFieldID)();
+
+   // contact angle is only a dummy object here (wetting effects are not considered in this test)
+   ContactAngle dummyContactAngle(real_c(0));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(50));
+
+   BlockDataID* relevantFillFieldID = &fillFieldID;
+
+   if (useAnalyticalNormal)
+   {
+      // add sweep for getting analytical interface normal
+      ComputeAnalyticalNormal< Stencil_T > analyticalNormalSweep(blockForest, normalFieldID, flagFieldID,
+                                                                 flagIDs::interfaceFlagID, domainSize, amplitude);
+      timeloop.add() << Sweep(analyticalNormalSweep, "Analytical normal sweep")
+                     << AfterFunction(Communication_T(blockForest, normalFieldID),
+                                      "Communication after analytical normal sweep");
+   }
+   else
+   {
+      if (!useTriangulation)
+      {
+         smoothFillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0),
+                                                                  field::fzyx, uint_c(1));
+
+         // add sweep for smoothing the fill level field when not using local triangulation
+         SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep(
+            smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs,
+            freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false);
+         timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep")
+                        << AfterFunction(Communication_T(blockForest, smoothFillFieldID),
+                                         "Communication after smoothing sweep");
+
+         relevantFillFieldID = &smoothFillFieldID;
+      }
+
+      NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+         normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+         freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), !useTriangulation, false, false, false);
+      timeloop.add() << Sweep(normalsSweep, "Normal sweep")
+                     << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep");
+   }
+
+   if (useTriangulation) // use local triangulation for curvature computation
+   {
+      CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep(
+         blockForest, curvatureFieldID, normalFieldID, fillFieldID, flagFieldID, dummyObstNormalFieldID,
+         flagIDs::interfaceFlagID, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false,
+         dummyContactAngle);
+      timeloop.add() << Sweep(curvatureSweep, "Curvature sweep");
+   }
+   else // use finite differences for curvature computation
+   {
+      CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep(
+         curvatureFieldID, normalFieldID, dummyObstNormalFieldID, flagFieldID, flagIDs::interfaceFlagID,
+         flagIDs::liquidInterfaceGasFlagIDs, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false,
+         dummyContactAngle);
+      timeloop.add() << Sweep(curvatureSweep, "Curvature sweep");
+   }
+
+   // add sweep for computing the error (with respect to analytical solution) of the interface curvature
+   std::shared_ptr< real_t > l2Error = std::make_shared< real_t >(real_c(0));
+   ComputeCurvatureError< Stencil_T > errorEvaluationSweep(blockForest, curvatureFieldID, flagFieldID,
+                                                           flagIDs::interfaceFlagID, domainSize, amplitude, l2Error);
+   timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep");
+
+   // perform a single time step
+   timeloop.singleStep();
+
+   MPIManager::instance()->resetMPI();
+
+   return *l2Error;
+}
+
+template< typename LatticeModel_T >
+void runAllTests()
+{
+   using Stencil_T = typename LatticeModel_T::Stencil;
+
+   real_t l2Error;
+
+   // used for initializing the fill level in a Monte-Carlo-like fashion; each cell is sampled by the specified value in
+   // x- and y-direction
+   const uint_t fillLevelInitSamples = uint_c(100);
+
+   // test with various domain sizes, i.e., resolutions
+   for (uint_t domainWidth = uint_c(50); domainWidth <= uint_c(200); domainWidth += uint_c(50))
+   {
+      const real_t amplitude = real_c(0.1) * real_c(domainWidth); // amplitude of the sine profile
+      const real_t offset    = real_c(0.5) * real_c(domainWidth); // offset the sine profile in y-direction
+
+      WALBERLA_LOG_RESULT("Domain width " << domainWidth << " cells; curvature with finite difference method;");
+      l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, false);
+      if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >)
+      {
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.63));
+      }
+      // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27)
+      if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.756)); }
+
+      WALBERLA_LOG_RESULT("Domain width "
+                          << domainWidth
+                          << " cells; curvature with finite difference method; normal from analytical solution;");
+      l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, true);
+      if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >)
+      {
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.52));
+      }
+      // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27)
+      if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.58)); }
+
+      if constexpr (!std::is_same_v< Stencil_T, stencil::D2Q9 >)
+      {
+         WALBERLA_LOG_RESULT("Domain width " << domainWidth << " cells; curvature with local triangulation;");
+         l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, false);
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.79)); }
+         // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27)
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.845)); }
+
+         WALBERLA_LOG_RESULT("Domain width "
+                             << domainWidth
+                             << " cells; curvature with local triangulation; normal from analytical solution;");
+         l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, true);
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.71));
+      }
+   }
+
+   // test with various amplitudes
+   for (real_t i = real_c(0.1); i <= real_c(0.3); i += real_c(0.05))
+   {
+      const uint_t domainWidth = uint_c(100);                       // size of the domain in x- and y-direction
+      const real_t amplitude   = i * real_c(domainWidth);           // amplitude of the sine profile
+      const real_t offset      = real_c(0.5) * real_c(domainWidth); // offset the sine profile in y-direction
+
+      WALBERLA_LOG_RESULT("Amplitude " << amplitude << " cells; curvature with finite difference method;");
+      l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, false);
+      if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >)
+      {
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.76));
+      }
+      // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27)
+      if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.79)); }
+
+      WALBERLA_LOG_RESULT(
+         "Amplitude " << amplitude
+                      << " cells; curvature with finite difference method; normal from analytical solution;");
+      l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, true);
+      if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >)
+      {
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.77));
+      }
+      // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27)
+      if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.79)); }
+
+      if constexpr (!std::is_same_v< Stencil_T, stencil::D2Q9 >)
+      {
+         WALBERLA_LOG_RESULT("Amplitude " << amplitude << " cells; curvature with local triangulation;");
+         l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, false);
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.8));
+
+         WALBERLA_LOG_RESULT("Amplitude "
+                             << amplitude
+                             << " cells; curvature with local triangulation; normal from analytical solution;");
+         l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, true);
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.8));
+      }
+   }
+
+   // test with various offsets
+   for (real_t i = real_c(0.4); i <= real_c(0.6); i += real_c(0.04))
+   {
+      const uint_t domainWidth = uint_c(100);                       // size of the domain in x- and y-direction
+      const real_t amplitude   = real_c(0.2) * real_c(domainWidth); // amplitude of the sine profile
+      const real_t offset      = i * real_c(domainWidth);           // offset the sine profile in y-direction
+
+      WALBERLA_LOG_RESULT("Offset " << offset << " cells; curvature with finite difference method;");
+      l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, false);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.72));
+
+      WALBERLA_LOG_RESULT(
+         "Offset " << offset << " cells; curvature with finite difference method; normal from analytical solution;");
+      l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, true);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.73));
+
+      if constexpr (!std::is_same_v< Stencil_T, stencil::D2Q9 >)
+      {
+         WALBERLA_LOG_RESULT("Offset " << offset << " cells; curvature with local triangulation;");
+         l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, false);
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.699));
+
+         WALBERLA_LOG_RESULT(
+            "Offset " << offset << " cells; curvature with local triangulation; normal from analytical solution;");
+         l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, true);
+         WALBERLA_CHECK_LESS(l2Error, real_c(0.706));
+      }
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_RESULT("##################################");
+   WALBERLA_LOG_RESULT("### Testing with D2Q9 stencil. ###");
+   WALBERLA_LOG_RESULT("##################################");
+   runAllTests< lbm::D2Q9< lbm::collision_model::SRT > >();
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >();
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >();
+
+   return EXIT_SUCCESS;
+}
+} // namespace CurvatureOfSineTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::CurvatureOfSineTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp b/tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp
new file mode 100644
index 000000000..1a42213e6
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp
@@ -0,0 +1,373 @@
+//======================================================================================================================
+//
+//  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 CurvatureOfSphereTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compare computed mean curvature of spherical gas bubbles with analytical curvature.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/math/DistributedSample.h"
+
+#include "field/AddToStorage.h"
+
+#include "geometry/bodies/Sphere.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/ContactAngle.h"
+#include "lbm/free_surface/surface_geometry/CurvatureSweep.h"
+#include "lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm/free_surface/surface_geometry/SmoothingSweep.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace CurvatureOfSphereTest
+{
+// define types
+using flag_t = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+template< typename Stencil_T >
+class ComputeAnalyticalNormal
+{
+ public:
+   ComputeAnalyticalNormal(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                           const BlockDataID& normalField, const ConstBlockDataID& flagField,
+                           const field::FlagUID& interfaceFlag, const Vector3< real_t > sphereMidpoint)
+      : blockForest_(blockForest), normalFieldID_(normalField), flagFieldID_(flagField),
+        interfaceFlagID_(interfaceFlag), sphereMidpoint_(sphereMidpoint)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // get fields
+      const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_);
+      VectorField_T* const normalField   = block->getData< VectorField_T >(normalFieldID_);
+
+      flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, {
+         // evaluate normal only in interface cell (and possibly in an interface cell's neighborhood)
+         if (isFlagSet(flagFieldIt, interfaceFlag) ||
+             field::isFlagInNeighborhood< Stencil_T >(flagFieldIt, interfaceFlag))
+         {
+            // get a vector pointing from sphere's center to current cell's center
+            Vector3< real_t > r;
+
+            // get the global coordinate of the current cell's center
+            blockForest->getBlockLocalCellCenter(*block, flagFieldIt.cell(), r);
+            r -= sphereMidpoint_;
+
+            // invert normal direction: in this FSLBM implementation, the normal vector is defined to point from liquid
+            // to gas
+            r *= real_c(-1);
+            normalize(r);
+
+            // store analytical normal
+            *normalFieldIt = r;
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+
+   BlockDataID normalFieldID_;
+   ConstBlockDataID flagFieldID_;
+   field::FlagUID interfaceFlagID_;
+
+   Vector3< real_t > sphereMidpoint_;
+}; // class ComputeAnalyticalNormal
+
+class ComputeCurvatureError
+{
+ public:
+   ComputeCurvatureError(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                         const ConstBlockDataID& curvatureFieldID, const ConstBlockDataID& flagFieldID,
+                         const field::FlagUID& interfaceFlagID, real_t sphereDiameter,
+                         const Vector3< uint_t >& domainSize, const std::shared_ptr< real_t >& l2Error)
+      : blockForest_(blockForest), curvatureFieldID_(curvatureFieldID), flagFieldID_(flagFieldID),
+        interfaceFlagID_(interfaceFlagID), sphereDiameter_(sphereDiameter), domainSize_(domainSize), l2Error_(l2Error)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // get fields
+      const ScalarField_T* const curvatureField = block->getData< const ScalarField_T >(curvatureFieldID_);
+      const FlagField_T* const flagField        = block->getData< const FlagField_T >(flagFieldID_);
+
+      const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+      const real_t analyticalCurvature = real_c(-1) / (real_c(0.5) * real_c(sphereDiameter_));
+
+      real_t curvDiffSum2 = real_c(0);
+      real_t analCurvSum2 = real_c(0);
+
+      WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, curvatureFieldIt, curvatureField,
+                                 omp parallel for schedule(static) reduction(+:curvDiffSum2) reduction(+:analCurvSum2),
+      {
+         // skip non-interface cells
+         if (isFlagSet(flagFieldIt, interfaceFlag))
+         {
+            const real_t curvDiff = *curvatureFieldIt - analyticalCurvature;
+            curvDiffSum2 += curvDiff * curvDiff;
+            analCurvSum2 += analyticalCurvature * analyticalCurvature;
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+
+      mpi::allReduceInplace< real_t >(curvDiffSum2, mpi::SUM);
+      mpi::allReduceInplace< real_t >(analCurvSum2, mpi::SUM);
+
+      *l2Error_ = std::pow(curvDiffSum2 / analCurvSum2, real_c(0.5));
+
+      WALBERLA_LOG_RESULT("Relative error in curvature according to L2 norm = " << *l2Error_);
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+
+   ConstBlockDataID curvatureFieldID_;
+   ConstBlockDataID flagFieldID_;
+   field::FlagUID interfaceFlagID_;
+
+   real_t sphereDiameter_;
+   Vector3< uint_t > domainSize_;
+
+   std::shared_ptr< real_t > l2Error_;
+}; // class ComputeCurvatureError
+
+template< typename LatticeModel_T >
+real_t test(uint_t sphereDiameter, Vector3< real_t > offset, bool useTriangulation, bool useAnalyticalNormal)
+{
+   // define types
+   using Stencil_T                     = typename LatticeModel_T::Stencil;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(sphereDiameter + uint_c(6));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create lattice model with omega=1
+   LatticeModel_T latticeModel(real_c(1.0));
+
+   // add fields
+   BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID curvatureFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0), field::fzyx, uint_c(1));
+   BlockDataID smoothFillFieldID;
+
+   // obstacle normal field is only a dummy field here (wetting effects are not considered in this test)
+   BlockDataID dummyObstNormalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Dummy obstacle normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+
+   // add gas sphere, i.e., bubble
+   Vector3< real_t > midpoint(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5),
+                              real_c(domainSize[2]) * real_c(0.5));
+   geometry::Sphere sphere(midpoint + offset, real_c(sphereDiameter) * real_c(0.5));
+   freeSurfaceBoundaryHandling->addFreeSurfaceObject(sphere);
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // contact angle is only a dummy object here (wetting effects are not considered in this test)
+   ContactAngle dummyContactAngle(real_c(0));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   BlockDataID* relevantFillFieldID = &fillFieldID;
+
+   if (useAnalyticalNormal)
+   {
+      // add sweep for getting analytical interface normal
+      ComputeAnalyticalNormal< Stencil_T > analyticalNormalSweep(blockForest, normalFieldID, flagFieldID,
+                                                                 flagIDs::interfaceFlagID, midpoint);
+      timeloop.add() << Sweep(analyticalNormalSweep, "Analytical normal sweep")
+                     << AfterFunction(Communication_T(blockForest, normalFieldID),
+                                      "Communication after analytical normal sweep");
+   }
+   else
+   {
+      if (!useTriangulation)
+      {
+         smoothFillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0),
+                                                                  field::fzyx, uint_c(1));
+
+         // add sweep for smoothing the fill level field when not using local triangulation
+         SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep(
+            smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs,
+            freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false);
+         timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep")
+                        << AfterFunction(Communication_T(blockForest, smoothFillFieldID),
+                                         "Communication after smoothing sweep");
+
+         relevantFillFieldID = &smoothFillFieldID;
+      }
+
+      NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+         normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+         freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), true, false, false, false);
+      timeloop.add() << Sweep(normalsSweep, "Normal sweep")
+                     << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep");
+   }
+
+   if (useTriangulation) // use local triangulation for curvature computation
+   {
+      CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep(
+         blockForest, curvatureFieldID, normalFieldID, fillFieldID, flagFieldID, dummyObstNormalFieldID,
+         flagIDs::interfaceFlagID, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false,
+         dummyContactAngle);
+      timeloop.add() << Sweep(curvatureSweep, "Curvature sweep");
+   }
+   else // use finite differences for curvature computation
+   {
+      CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep(
+         curvatureFieldID, normalFieldID, dummyObstNormalFieldID, flagFieldID, flagIDs::interfaceFlagID,
+         flagIDs::liquidInterfaceGasFlagIDs, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false,
+         dummyContactAngle);
+      timeloop.add() << Sweep(curvatureSweep, "Curvature sweep");
+   }
+
+   const std::shared_ptr< real_t > l2Error = std::make_shared< real_t >(real_c(0));
+   ComputeCurvatureError errorEvaluationSweep(blockForest, curvatureFieldID, flagFieldID, flagIDs::interfaceFlagID,
+                                              real_c(sphereDiameter), domainSize, l2Error);
+   timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep");
+
+   // perform a single time step
+   timeloop.singleStep();
+
+   MPIManager::instance()->resetMPI();
+
+   return *l2Error;
+}
+
+template< typename LatticeModel_T >
+void runAllTests()
+{
+   real_t l2Error;
+
+   // test with various bubble diameters
+   for (uint_t diameter = uint_c(10); diameter <= uint_c(50); diameter += uint_c(10))
+   {
+      WALBERLA_LOG_RESULT("Bubble diameter " << diameter << " cells; curvature with finite difference method;")
+      l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), false, false);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.2));
+
+      WALBERLA_LOG_RESULT("Bubble diameter "
+                          << diameter
+                          << " cells; curvature with finite difference method; normal from analytical solution;")
+      l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), false, true);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.19));
+
+      WALBERLA_LOG_RESULT("Bubble diameter " << diameter << " cells; curvature with local triangulation;")
+      l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), true, false);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.29));
+
+      WALBERLA_LOG_RESULT("Bubble diameter "
+                          << diameter << " cells; curvature with local triangulation; normal from analytical solution;")
+      l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), true, true);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.29));
+   }
+
+   // test with various offsets of sphere's center
+   for (real_t off = real_c(0.1); off < real_c(1.0); off += real_c(0.2))
+   {
+      WALBERLA_LOG_RESULT("Sphere center offset " << off << " cells; curvature with finite difference method;")
+      l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), false, false);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.82));
+
+      WALBERLA_LOG_RESULT("Sphere center offset "
+                          << off << " cells; curvature with finite difference method; normal from analytical solution;")
+      l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), false, true);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.83));
+
+      WALBERLA_LOG_RESULT("Sphere center offset " << off << " cells; curvature with local triangulation;")
+      l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), true, false);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.13));
+
+      WALBERLA_LOG_RESULT("Sphere center offset "
+                          << off << " cells; curvature with local triangulation; normal from analytical solution;")
+      l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), true, true);
+      WALBERLA_CHECK_LESS(l2Error, real_c(0.16));
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >();
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >();
+
+   return EXIT_SUCCESS;
+}
+} // namespace CurvatureOfSphereTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::CurvatureOfSphereTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp b/tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp
new file mode 100644
index 000000000..4c86d76ad
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp
@@ -0,0 +1,294 @@
+//======================================================================================================================
+//
+//  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 DetectWettingTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test the DetectWettingSweep using a single interface cell surrounded by gas cells and obstacle cells.
+//!
+//! 3x4x3 domain where the first and fourth layer in y-direction are solid cells. The cell at (1,1,1) is an interface
+//! cell with given normal and fill level. All remaining cells are gas cells that might be marked for conversion to
+//! wetting cells by DetectWettingSweep.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/DetectWettingSweep.h"
+#include "lbm/lattice_model/D3Q19.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <algorithm>
+#include <vector>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DetectWettingTest
+{
+using LatticeModel_T = lbm::D3Q19< lbm::collision_model::SRT >;
+using PdfField_T     = lbm::PdfField< LatticeModel_T >;
+using Stencil_T      = LatticeModel_T::Stencil;
+
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+std::vector< Cell > detectWettingCells(const Vector3< real_t >& normal, const real_t fillLevel)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(4), uint_c(3));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create lattice model (dummy, not relevant for this test)
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1)));
+
+   // add fields
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   const BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1));
+   const BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normal field", Vector3< real_t >(real_c(0.0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   const BlockDataID flagFieldID                                      = freeSurfaceBoundaryHandling->getFlagFieldID();
+   const BlockDataID boundaryHandlingID                               = freeSurfaceBoundaryHandling->getHandlingID();
+   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+   // initialize domain
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField   = blockIt->getData< ScalarField_T >(fillFieldID);
+      VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID);
+      FreeSurfaceBoundaryHandling_T::BoundaryHandling_T* const boundaryHandling =
+         blockIt->getData< FreeSurfaceBoundaryHandling_T::BoundaryHandling_T >(boundaryHandlingID);
+
+      WALBERLA_FOR_ALL_CELLS_XYZ(fillField, {
+         // set boundary cells at bottom and top of domain
+         if (y == 0 || y == 3) { boundaryHandling->setBoundary(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z); }
+
+         // set interface cell in the center of the domain
+         if (x == 1 && y == 1 && z == 1)
+         {
+            boundaryHandling->setFlag(flagInfo.interfaceFlag, x, y, z);
+            fillField->get(x, y, z)   = fillLevel;
+            normalField->get(x, y, z) = normal.getNormalized();
+         }
+
+         // set remaining domain to gas
+         if ((y == 1 || y == 2) && !(x == 1 && y == 1 && z == 1))
+         {
+            boundaryHandling->setFlag(flagInfo.gasFlag, x, y, z);
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ
+   }
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, 1);
+
+   // add DetectWettingSweep
+   DetectWettingSweep< Stencil_T, FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, FlagField_T, ScalarField_T,
+                       VectorField_T >
+      detWetSweep(boundaryHandlingID, flagInfo, normalFieldID, fillFieldID);
+   timeloop.add() << Sweep(detWetSweep, "Detect wetting sweep");
+
+   timeloop.singleStep();
+
+   std::vector< Cell > markedCells;
+
+   // get cells that were marked by DetectWettingSweep
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS_OMP(
+         flagFieldIt, flagField, omp critical, if (flagInfo.isKeepInterfaceForWetting(flagFieldIt)) {
+            markedCells.emplace_back(flagFieldIt.cell());
+         }) // WALBERLA_FOR_ALL_CELLS_OMP
+   }
+
+   return markedCells;
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   Vector3< real_t > normal;
+   real_t fillLevel;
+   std::vector< Cell > expectedWettingCells;
+   std::vector< Cell > computedWettingCells;
+   bool vectorsEqual;
+
+   // test various different normals and fill levels; expected results have been determined using ParaView
+   normal               = Vector3< real_t >(real_c(1), real_c(1), real_c(1));
+   fillLevel            = real_c(0.01);
+   expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)) };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(-1), real_c(-1), real_c(-1));
+   fillLevel            = real_c(0.01);
+   expectedWettingCells = std::vector< Cell >{
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)),
+      Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)),
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2))
+   };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(1), real_c(1), real_c(0));
+   fillLevel            = real_c(0.01);
+   expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)),
+                                               Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(2)),
+                                               Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)) };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(0), real_c(1), real_c(1));
+   fillLevel            = real_c(0.01);
+   expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)),
+                                               Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)) };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(1), real_c(0), real_c(1));
+   fillLevel            = real_c(0.01);
+   expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)),
+                                               Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)),
+                                               Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)),
+                                               Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)) };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(-1), real_c(0), real_c(-1));
+   fillLevel            = real_c(0.01);
+   expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)),
+                                               Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)),
+                                               Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)),
+                                               Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)),
+                                               Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2)) };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(-1), real_c(0), real_c(-1));
+   fillLevel            = real_c(0.5);
+   expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+                                               Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)),
+                                               Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)),
+                                               Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)),
+                                               Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)) };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(1), real_c(1), real_c(1));
+   fillLevel            = real_c(0.9);
+   expectedWettingCells = std::vector< Cell >{
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)),
+      Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)),
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2))
+   };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(1), real_c(1), real_c(0));
+   fillLevel            = real_c(0.9);
+   expectedWettingCells = std::vector< Cell >{
+      Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)),
+      Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)),
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2))
+   };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   normal               = Vector3< real_t >(real_c(0), real_c(1), real_c(0));
+   fillLevel            = real_c(0.9);
+   expectedWettingCells = std::vector< Cell >{
+      Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)),
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)),
+      Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(2)),
+      Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2))
+   };
+   WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel);
+   computedWettingCells = detectWettingCells(normal, fillLevel);
+   vectorsEqual         = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(),
+                                              expectedWettingCells.begin(), expectedWettingCells.end());
+   if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); }
+
+   return EXIT_SUCCESS;
+}
+} // namespace DetectWettingTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DetectWettingTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp b/tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp
new file mode 100644
index 000000000..7123e9ba7
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp
@@ -0,0 +1,76 @@
+//======================================================================================================================
+//
+//  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 GetInterfacePointTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute the interface point from normal and fill level, and compare with results obtained with ParaView.
+//
+//======================================================================================================================
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/free_surface/surface_geometry/CurvatureSweep.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace GetInterfacePointTest
+{
+inline void test(const Vector3< real_t >& normal, real_t fillLevel, Vector3< real_t > expectedInterfacePoint,
+                 real_t tolerance)
+{
+   const Vector3< real_t > interfacePoint = getInterfacePoint(normal, fillLevel);
+
+   WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(interfacePoint[0], expectedInterfacePoint[0], tolerance);
+   WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(interfacePoint[1], expectedInterfacePoint[1], tolerance);
+   WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(interfacePoint[2], expectedInterfacePoint[2], tolerance);
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   // allowed deviation from the expected result
+   real_t tolerance = real_c(1e-2);
+
+   // tests identical to those in CellFluidVolumeTest.cpp, results obtained with ParaView
+   Vector3< real_t > normal(real_c(0), real_c(0.5), real_c(0.866));
+   const real_t offset                      = real_c(-0.1);
+   const real_t fillLevel                   = real_c(0.3845);
+   Vector3< real_t > expectedInterfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal;
+   test(normal, fillLevel, expectedInterfacePoint, tolerance);
+
+   normal = Vector3< real_t >(real_c(0.37), real_c(0.61), real_c(0.7));
+   const std::vector< real_t > offsetList{ real_c(-0.52), real_c(-0.3), real_c(-0.17), real_c(0) };
+   const std::vector< real_t > fillLevelList{ real_c(0.0347), real_c(0.1612), real_c(0.2887), real_c(0.5) };
+
+   WALBERLA_ASSERT_EQUAL(offsetList.size(), fillLevelList.size());
+   for (size_t i = 0; i != offsetList.size(); ++i)
+   {
+      expectedInterfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal;
+      test(normal, fillLevel, expectedInterfacePoint, tolerance);
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace GetInterfacePointTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::GetInterfacePointTest::main(argc, argv); }
diff --git a/tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp
new file mode 100644
index 000000000..c7b5fbd3f
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp
@@ -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 NormalsEquivalenceTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Matthias Markl <matthias.markl@fau.de>
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test if the NormalSweep (unrolled version) is equal to a loop-based computation of the interface normal.
+//!
+//! Test (with respect to a maximum error of 1e-13) if the explicit normal computation (implemented in NormalSweep)
+//! gives the same result as the loop-based computation of the interface normal. The former method is faster and does
+//! not loop over all D3Q27 directions but calculates the interface normal explicitly. See function computeNormal() in
+//! src/lbm/free_surface/surface_geometry/NormalSweep.impl.h
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/free_surface/FlagInfo.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <cstdlib>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace NormalsEquivalenceTest
+{
+// define types
+using Stencil_T = stencil::D3Q27;
+using flag_t    = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(32), uint_c(32), uint_c(32));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, true, true);                                    // periodicity
+
+   // add fields
+   BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >(blockForest, "Flags", uint_c(2));
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldLoopID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals loop", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID normalFieldExplID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals explicit", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const auto flagInfo = FlagInfo< FlagField_T >(Set< FlagUID >(), Set< FlagUID >());
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      flagInfo.registerFlags(blockIt->getData< FlagField_T >(flagFieldID));
+   }
+   WALBERLA_ASSERT(flagInfo.isConsistentAcrossBlocksAndProcesses(blockForest, flagFieldID));
+
+   // add communication
+   blockforest::SimpleCommunication< stencil::D3Q27 > commFill(blockForest, fillFieldID);
+   blockforest::SimpleCommunication< stencil::D3Q27 > commFlag(blockForest, flagFieldID);
+
+   // initialize flag field (only interface cells) and fill levels (random values)
+   std::srand(static_cast< unsigned int >(std::time(nullptr)));
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+      FlagField_T* const flagField   = blockIt->getData< FlagField_T >(flagFieldID);
+
+      // set whole flag field to interface
+      flagField->set(flagInfo.interfaceFlag);
+
+      // set random fill levels in fill field
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         *fillFieldIt = real_c(std::rand()) / real_c(std::numeric_limits< int >::max());
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // communicate fill level field, and flag field
+   commFill();
+   commFlag();
+
+   // loop-based normal computation, i.e., old and slower version of NormalSweep operator()
+   auto normalsFunc = [&](IBlock* block) {
+      // get fields
+      VectorField_T* const normalField     = block->getData< VectorField_T >(normalFieldLoopID);
+      const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID);
+      const FlagField_T* const flagField   = block->getData< const FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(normalFieldIt, normalField, fillFieldIt, fillField, flagFieldIt, flagField, {
+         bool computeNormalInCell = flagInfo.isInterface(flagFieldIt);
+
+         Vector3< real_t >& normal = *normalFieldIt;
+
+         if (computeNormalInCell)
+         {
+            if (!isFlagInNeighborhood< stencil::D3Q27 >(flagFieldIt, flagInfo.obstacleFlagMask))
+            {
+               normal.set(real_c(0), real_c(0), real_c(0));
+
+               // loop over all directions to compute interface normal; compare this loop with the explicit (non-loop)
+               // computation in function computeNormal() in src/lbm/free_surface/surface_geometry/NormalSweep.impl.h
+               for (auto d = stencil::D3Q27::beginNoCenter(); d != stencil::D3Q27::end(); ++d)
+               {
+                  const real_t weightedFill =
+                     real_c(stencil::gaussianMultipliers[d.toIdx()]) * fillFieldIt.neighbor(*d);
+                  normal[0] += real_c(d.cx()) * weightedFill;
+                  normal[1] += real_c(d.cy()) * weightedFill;
+                  normal[2] += real_c(d.cz()) * weightedFill;
+               }
+            }
+
+            // normalize and negate normal (to make it point from gas to liquid)
+            normal = real_c(-1) * normal.getNormalizedOrZero();
+         }
+         else { normal.set(real_c(0), real_c(0), real_c(0)); }
+      }) // WALBERLA_FOR_ALL_CELLS
+   };
+
+   real_t err_max  = real_c(0);
+   real_t err_mean = real_c(0);
+
+#ifdef WALBERLA_DOUBLE_ACCURACY
+   real_t tolerance = real_c(1e-13);
+#else
+   real_t tolerance = real_c(1e-4);
+#endif
+
+   auto evaluateError = [&](IBlock* block) {
+      const VectorField_T* const loopField = block->getData< const VectorField_T >(normalFieldLoopID);
+      const VectorField_T* const explField = block->getData< const VectorField_T >(normalFieldExplID);
+
+      err_mean = real_c(0);
+      err_max  = real_c(0);
+
+      WALBERLA_FOR_ALL_CELLS(loopFieldIt, loopField, explFieldIt, explField, {
+         const Vector3< real_t > diff = *loopFieldIt - *explFieldIt;
+         const real_t length          = diff.length();
+
+         if (length > tolerance)
+         {
+            WALBERLA_ABORT("Unequal normals at " << loopFieldIt.cell() << " diff=" << diff << ", |diff|=" << length);
+         }
+         err_mean += length;
+         err_max = std::max(err_max, length);
+      }) // WALBERLA_FOR_ALL_CELLS
+   };
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   // add regular sweep for computing interface normals
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+      normalFieldExplID, fillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+      flagIDs::gasFlagID, false, false, false, false);
+   timeloop.add() << Sweep(normalsSweep, "Normal sweep (explicit)");
+
+   // add sweeps
+   timeloop.add() << Sweep(normalsFunc, "Normal loop-based");
+   timeloop.add() << Sweep(evaluateError, "Error computation");
+
+   WcTimingPool timeloopTiming;
+   timeloop.run(timeloopTiming);
+   // timeloopTiming.logResultOnRoot();
+
+   WALBERLA_LOG_RESULT("Max Error: " << err_max);
+   WALBERLA_LOG_RESULT("Mean Error: " << err_mean / real_c(domainSize[0] * domainSize[1] * domainSize[2]));
+
+   return EXIT_SUCCESS;
+}
+} // namespace NormalsEquivalenceTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::NormalsEquivalenceTest::main(argc, argv); }
diff --git a/tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp
new file mode 100644
index 000000000..8491557fe
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp
@@ -0,0 +1,178 @@
+//======================================================================================================================
+//
+//  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 NormalsNearSolidTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test interface normal computation as influenced by obstacle cells, i.e., test narrower Parker-Youngs scheme.
+//
+//! The setup is similar to Figure 6.11 in the dissertation of S. Donath, 2011.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace NormalsNearSolidTest
+{
+// define types
+using LatticeModel_T = lbm::D3Q27< lbm::collision_model::SRT, true >;
+using Stencil_T      = LatticeModel_T::Stencil;
+using flag_t         = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+using Communication_T               = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(11), uint_c(3), uint_c(1));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, true);                                  // periodicity
+
+   // create lattice model with omega=1
+   LatticeModel_T latticeModel(real_c(1.0));
+
+   // add fields
+   BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+
+   // initialize domain as in Figure 6.11 in dissertation of S. Donath, 2011
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         if (fillFieldIt.y() == cell_idx_c(0)) { *fillFieldIt = real_c(1); }
+         if (fillFieldIt.y() == cell_idx_c(1)) { *fillFieldIt = real_c(fillFieldIt.x()) / real_c(25); }
+         if (fillFieldIt.y() == cell_idx_c(2)) { *fillFieldIt = real_c(0); }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+   // set solid obstacle cells
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::E, cell_idx_c(0));
+   freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::W, cell_idx_c(0));
+   freeSurfaceBoundaryHandling->setNoSlipInCell(Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)));
+
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // initial communication (to initialize ghost layer in periodic z-direction)
+   Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID)();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   // add sweep for computing interface normals
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+      normalFieldID, fillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+      FreeSurfaceBoundaryHandling_T::noSlipFlagID, false, false, true, false);
+   timeloop.add() << Sweep(normalsSweep, "Normals sweep");
+
+   // perform a single time step
+   timeloop.singleStep();
+
+   // check correctness of computed interface normals; reference values have been obtained with a version of the code
+   // that is assumed to be correct; results are also qualitatively verified with Figure 6.11 in dissertation of S.
+   // Donath, 2011
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const FlagField_T* const flagField     = blockIt->getData< const FlagField_T >(flagFieldID);
+      const VectorField_T* const normalField = blockIt->getData< const VectorField_T >(normalFieldID);
+      const flag_t interfaceFlag             = flagField->getFlag(flagIDs::interfaceFlagID);
+
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, {
+         if (isFlagSet(flagFieldIt, interfaceFlag))
+         {
+            // regular Parker-Youngs normal computation
+            if (flagFieldIt.x() >= cell_idx_c(2) && flagFieldIt.x() <= cell_idx_c(7))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.0399680383488715887), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.999200958721789267), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(0), real_c(1e-6));
+            }
+
+            // modified, i.e., narrower Parker-Youngs normal computation near solid boundaries
+            if (flagFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.0199960011996001309), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.999800059980007094), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(0), real_c(1e-6));
+            }
+            if (flagFieldIt.cell() == Cell(cell_idx_c(8), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.129339184067768065), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.991600411186221775), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(-2.99157e-17), real_c(1e-6));
+            }
+            if (flagFieldIt.cell() == Cell(cell_idx_c(9), cell_idx_c(1), cell_idx_c(0)))
+            {
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.0461047666084008420), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.998936609848685486), real_c(1e-6));
+               WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(-8.5311e-17), real_c(1e-6));
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace NormalsNearSolidTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::NormalsNearSolidTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp
new file mode 100644
index 000000000..3ca66b6f5
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp
@@ -0,0 +1,470 @@
+//======================================================================================================================
+//
+//  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 NormalsOfSineTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Initialize sine profile and compare calculated normals to analytical normals.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/math/Constants.h"
+#include "core/math/DistributedSample.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm/free_surface/surface_geometry/SmoothingSweep.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D2Q9.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/D3Q27.h"
+
+#include "stencil/D2Q9.h"
+#include "stencil/D3Q19.h"
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <cmath>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace NormalsOfSineTest
+{
+// define types
+using flag_t = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+// function describing the global sine profile
+inline real_t function(real_t x, real_t amplitude, real_t offset, uint_t domainWidth)
+{
+   return amplitude * std::sin(x / real_c(domainWidth) * real_c(2) * math::pi) + offset;
+}
+
+// derivative of the function that is describing the sine profile
+inline real_t derivative(real_t x, real_t amplitude, uint_t domainWidth)
+{
+   const real_t domainWidthInv = real_c(1) / real_c(domainWidth);
+   return amplitude * std::cos(x * domainWidthInv * real_c(2) * math::pi) * real_c(2) * math::pi * domainWidthInv;
+}
+
+// compute and evaluate the absolute error of the angle of the interface normal in each interface cell;
+// reference: derivative of sine function, i.e., analytically computed normal
+template< typename Stencil_T >
+class EvaluateNormalError
+{
+ public:
+   EvaluateNormalError(const std::weak_ptr< const StructuredBlockForest >& blockForest,
+                       const ConstBlockDataID& normalFieldID, const ConstBlockDataID& flagFieldID,
+                       const field::FlagUID& interfaceFlagID, const Vector3< uint_t >& domainSize, real_t amplitude,
+                       bool computeNormalsInInterfaceNeighbors, bool smoothFillLevel)
+      : blockForest_(blockForest), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID),
+        interfaceFlagID_(interfaceFlagID), domainSize_(domainSize), amplitude_(amplitude),
+        computeNormalsInInterfaceNeighbors_(computeNormalsInInterfaceNeighbors), smoothFillLevel_(smoothFillLevel)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // compute analytical normals and compare their directions with the computed normals
+      const FlagField_T* const flagField     = block->getData< const FlagField_T >(flagFieldID_);
+      const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_);
+
+      const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+      math::DistributedSample errorSample;
+
+      // avoid OpenMP parallelization as DistributedSample can not be reduced by OpenMP
+      WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, normalFieldIt, normalField, omp critical, {
+         if (isFlagSet(flagFieldIt, interfaceFlag) ||
+             (computeNormalsInInterfaceNeighbors_ && isFlagInNeighborhood< Stencil_T >(flagFieldIt, interfaceFlag)))
+         {
+            Cell globalCell = flagFieldIt.cell();
+            blockForest->transformBlockLocalToGlobalCell(globalCell, *block, flagFieldIt.cell());
+
+            // global x-location of this cell's center
+            const real_t globalXCenter = real_c(globalCell[0]) + real_c(0.5);
+
+            // get normal vector (slope of the negative inverse derivative gives direction of the normal)
+            Vector3< real_t > analyticalNormal = Vector3< real_t >(
+               real_c(1), -real_c(1) / derivative(globalXCenter, amplitude_, domainSize_[0]), real_c(0));
+            analyticalNormal = analyticalNormal.getNormalized();
+
+            // mirror vectors that are pointing upwards, i.e., from gas to liquid; in this FSLBM implementation, the
+            // normal vector is defined to point from liquid to gas
+            if (analyticalNormal[1] < real_c(0)) { analyticalNormal *= -real_c(1); }
+
+            // calculate the error in the angle of the interface normal
+            // the domain of arccosine is [-1,1]; due to numerical inaccuracies, the dot product
+            // "*normalFieldIt*analyticalNormal" might be slightly out of this range; these cases are ignored here
+            const real_t dotProduct = *normalFieldIt * analyticalNormal;
+            if (dotProduct >= real_c(-1) && dotProduct <= real_c(1))
+            {
+               const real_t angleDiff = std::acos(dotProduct);
+               errorSample.insert(angleDiff);
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+
+      errorSample.mpiAllGather();
+
+      const real_t maxError  = errorSample.max();
+      const real_t meanError = errorSample.mean();
+
+      WALBERLA_LOG_RESULT("Mean absolute error in angle of normal = " << meanError);
+      WALBERLA_LOG_RESULT("Maximum absolute error in angle of normal = " << maxError);
+      WALBERLA_LOG_RESULT("Minimum absolute error in angle of normal = " << errorSample.min());
+
+      // the following reference errors have been obtained with a version of the code that is believed to be correct
+      if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >)
+      {
+         if (computeNormalsInInterfaceNeighbors_ && smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.036));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.135));
+         }
+
+         if (!computeNormalsInInterfaceNeighbors_ && smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.019));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.044));
+         }
+
+         if (!computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.027));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.07));
+         }
+
+         if (computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.158));
+            WALBERLA_CHECK_LESS(maxError, real_c(1.58));
+         }
+      }
+
+      // normal computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27)
+      if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >)
+      {
+         if (computeNormalsInInterfaceNeighbors_ && smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.036));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.135));
+         }
+
+         if (!computeNormalsInInterfaceNeighbors_ && smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.023));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.046));
+         }
+
+         if (!computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.048));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.101));
+         }
+
+         if (computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.158));
+            WALBERLA_CHECK_LESS(maxError, real_c(1.58));
+         }
+      }
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID flagFieldID_;
+   field::FlagUID interfaceFlagID_;
+
+   Vector3< uint_t > domainSize_;
+   real_t amplitude_;
+
+   bool computeNormalsInInterfaceNeighbors_;
+   bool smoothFillLevel_;
+}; // class EvaluateNormalError
+
+template< typename LatticeModel_T >
+void test(uint_t domainWidth, real_t amplitude, real_t offset, uint_t fillLevelInitSamples,
+          bool computeNormalsInInterfaceNeighbors, bool smoothFillLevel)
+{
+   // define types
+   using Stencil_T       = typename LatticeModel_T::Stencil;
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   Vector3< uint_t > domainSize(domainWidth);
+   domainSize[2] = uint_c(1);
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, false, true);                                   // periodicity
+
+   // create (dummy) lattice model with omega=1
+   LatticeModel_T latticeModel(real_c(1.0));
+
+   // add fields
+   BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID smoothFillFieldID;
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+
+   // initialize sine profile such that there is exactly one period; every length is normalized with domainSize[0]
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      uint_t numTotalPoints = fillLevelInitSamples * fillLevelInitSamples;
+      const real_t stepsize = real_c(1) / real_c(fillLevelInitSamples);
+
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, {
+         // cell in block-local coordinates
+         const Cell localCell = fillFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = fillFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // Monte-Carlo like estimation of the fill level:
+         // create uniformly-distributed sample points in each cell and count the number of points below the sine
+         // profile; this fraction of points is used as the fill level to initialize the profile
+         uint_t numPointsBelow = uint_c(0);
+
+         for (uint_t xSample = uint_c(0); xSample < fillLevelInitSamples; ++xSample)
+         {
+            // value of the sine-function
+            const real_t functionValue =
+               function(real_c(globalCell[0]) + real_c(xSample) * stepsize, amplitude, offset, domainSize[0]);
+
+            for (uint_t ySample = uint_c(0); ySample < fillLevelInitSamples; ++ySample)
+            {
+               const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize;
+               if (yPoint < functionValue) { ++numPointsBelow; }
+            }
+         }
+
+         // fill level is fraction of points below sine profile
+         fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints);
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // communicate fill level field to have meaningful values in ghost layer cells in periodic directions
+   Communication_T(blockForest, fillFieldID)();
+
+   // initialize fill level field
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // communicate initialized flag field
+   Communication_T(blockForest, flagFieldID)();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(50));
+
+   BlockDataID* relevantFillFieldID = &fillFieldID;
+
+   if (smoothFillLevel)
+   {
+      smoothFillFieldID =
+         field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(0.0), field::fzyx, uint_c(1));
+
+      // add sweep for smoothing the fill level field
+      SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep(
+         smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs,
+         freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false);
+      timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep")
+                     << AfterFunction(Communication_T(blockForest, smoothFillFieldID),
+                                      "Communication after smoothing sweep");
+      relevantFillFieldID = &smoothFillFieldID;
+   }
+
+   // add sweep for computing interface normals
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+      normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+      freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), computeNormalsInInterfaceNeighbors, false, false,
+      false);
+   timeloop.add() << Sweep(normalsSweep, "Normals sweep")
+                  << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep");
+
+   // add sweep for evaluating the error of the interface normals (by comparing with analytical normal)
+   EvaluateNormalError< Stencil_T > errorEvaluationSweep(blockForest, normalFieldID, flagFieldID,
+                                                         flagIDs::interfaceFlagID, domainSize, amplitude,
+                                                         computeNormalsInInterfaceNeighbors, smoothFillLevel);
+   timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep");
+
+   // perform a single time step
+   timeloop.singleStep();
+
+   MPIManager::instance()->resetMPI();
+}
+
+template< typename LatticeModel_T >
+void runAllTests()
+{
+   // used for initializing the fill level in a Monte-Carlo-like fashion; each cell is sampled by the specified value in
+   // x- and y-direction
+   const uint_t fillLevelInitSamples = uint_c(100);
+
+   // test with various domain sizes, i.e., resolutions
+   for (uint_t i = uint_c(50); i <= uint_c(200); i += uint_c(50))
+   {
+      const real_t amplitude = real_c(0.1) * real_c(i); // amplitude of the sine profile
+      const real_t offset    = real_c(0.5) * real_c(i); // offset the sine profile in y-direction
+
+      WALBERLA_LOG_RESULT("Domain size " << i << " cells; normals computed only in interface cells;");
+      test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, false, false);
+
+      WALBERLA_LOG_RESULT(
+         "Domain size " << i << " cells; normals computed only in interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, false, true);
+
+      WALBERLA_LOG_RESULT("Domain size " << i
+                                         << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;");
+      test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, true, false);
+
+      WALBERLA_LOG_RESULT(
+         "Domain size "
+         << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, true, true);
+   }
+
+   // default values
+   const uint_t domainWidth = uint_c(100);                       // size of the domain in x- and y-direction
+   const real_t amplitude   = real_c(0.1) * real_c(domainWidth); // amplitude of the sine profile
+   const real_t offset      = real_c(0.5) * real_c(domainWidth); // offset the sine profile in y-direction
+
+   // test with various amplitudes
+   for (real_t i = real_c(0.01); i < real_c(0.3); i += real_c(0.03))
+   {
+      WALBERLA_LOG_RESULT("Amplitude " << i << " cells; normals computed only in interface cells;");
+      test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, false, false);
+
+      WALBERLA_LOG_RESULT(
+         "Amplitude " << i << " cells; normals computed only in interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, false, true);
+
+      WALBERLA_LOG_RESULT("Amplitude " << i
+                                       << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;");
+      test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, true, false);
+
+      WALBERLA_LOG_RESULT(
+         "Amplitude "
+         << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, true, true);
+   }
+
+   // test with various offsets, i.e., position of the sine-profile's zero-line
+   for (real_t i = real_c(0.4); i < real_c(0.6); i += real_c(0.03))
+   {
+      WALBERLA_LOG_RESULT("Offset " << i << " cells; normals computed only in interface cells;");
+      test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, false, false);
+
+      WALBERLA_LOG_RESULT("Offset " << i
+                                    << " cells; normals computed only in interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, false, true);
+
+      WALBERLA_LOG_RESULT("Offset " << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;");
+      test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, true, false);
+
+      WALBERLA_LOG_RESULT(
+         "Offset "
+         << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, true, true);
+   }
+
+   // test with different fill level initialization samples (more samples => initialization of fill levels is
+   // closer to the real sine-profile)
+   for (uint_t i = uint_c(50); i <= uint_c(200); i += uint_c(50))
+   {
+      WALBERLA_LOG_RESULT("Fill level initialization sample " << i
+                                                              << " cells; normals computed only in interface cells;");
+      test< LatticeModel_T >(domainWidth, amplitude, offset, i, false, false);
+
+      WALBERLA_LOG_RESULT("Fill level initialization sample "
+                          << i << " cells; normals computed only in interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(domainWidth, amplitude, offset, i, false, true);
+
+      WALBERLA_LOG_RESULT("Fill level initialization sample "
+                          << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;");
+      test< LatticeModel_T >(domainWidth, amplitude, offset, i, true, false);
+
+      WALBERLA_LOG_RESULT(
+         "Fill level initialization sample "
+         << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(domainWidth, amplitude, offset, i, true, true);
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_RESULT("##################################");
+   WALBERLA_LOG_RESULT("### Testing with D2Q9 stencil. ###");
+   WALBERLA_LOG_RESULT("##################################");
+   runAllTests< lbm::D2Q9< lbm::collision_model::SRT > >();
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >();
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >();
+
+   return EXIT_SUCCESS;
+}
+} // namespace NormalsOfSineTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::NormalsOfSineTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp
new file mode 100644
index 000000000..ca3c6b4f1
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp
@@ -0,0 +1,363 @@
+//======================================================================================================================
+//
+//  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 NormalsOfSphereTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Initialize spherical bubble and compare calculated normals to analytical (radial) normals.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/math/DistributedSample.h"
+
+#include "field/AddToStorage.h"
+
+#include "geometry/bodies/Sphere.h"
+
+#include "lbm/blockforest/communication/SimpleCommunication.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm/free_surface/surface_geometry/SmoothingSweep.h"
+#include "lbm/lattice_model/CollisionModel.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <cmath>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace NormalsOfSphereTest
+{
+// define types
+using flag_t = uint32_t;
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+// compute and evaluate the absolute error of the angle of the interface normal in each interface cell;
+// reference: vector that points from the gas bubble's, i.e., sphere's center to the current interface cell's center
+template< typename Stencil_T >
+class EvaluateNormalError
+{
+ public:
+   EvaluateNormalError(const std::weak_ptr< const StructuredBlockForest >& blockForest, const geometry::Sphere& sphere,
+                       const ConstBlockDataID& normalFieldID, const ConstBlockDataID& flagFieldID,
+                       const field::FlagUID& interfaceFlagID, bool computeNormalsInInterfaceNeighbors,
+                       bool smoothFillLevel)
+      : blockForest_(blockForest), sphere_(sphere), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID),
+        interfaceFlagID_(interfaceFlagID), computeNormalsInInterfaceNeighbors_(computeNormalsInInterfaceNeighbors),
+        smoothFillLevel_(smoothFillLevel)
+   {}
+
+   void operator()(IBlock* const block)
+   {
+      const auto blockForest = blockForest_.lock();
+      WALBERLA_CHECK_NOT_NULLPTR(blockForest);
+
+      // get fields
+      const FlagField_T* const flagField     = block->getData< const FlagField_T >(flagFieldID_);
+      const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_);
+
+      const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_);
+
+      math::DistributedSample errorSample;
+
+      WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, normalFieldIt, normalField, omp critical, {
+         // evaluate normal only in interface cell (and possibly in an interface cell's neighborhood)
+         if (isFlagSet(flagFieldIt, interfaceFlag) ||
+             (computeNormalsInInterfaceNeighbors_ && isFlagInNeighborhood< Stencil_T >(flagFieldIt, interfaceFlag)))
+         {
+            // get a vector pointing from sphere's center to current cell's center
+            Vector3< real_t > r;
+
+            // get the global coordinate of the current cell's center
+            blockForest->getBlockLocalCellCenter(*block, flagFieldIt.cell(), r);
+            r -= sphere_.midpoint();
+
+            // invert normal direction: in this FSLBM implementation, the normal vector is defined to point from liquid
+            // to gas
+            r *= real_c(-1);
+            normalize(r);
+
+            // calculate the error in the angle of the interface normal
+            // the domain of arccosine is [-1,1]; due to numerical inaccuracies, the dot product "*normalFieldIt*r"
+            // might be slightly out of this range; these cases are ignored here
+            const real_t dotProduct = *normalFieldIt * r;
+            if (dotProduct >= real_c(-1) && dotProduct <= real_c(1))
+            {
+               const real_t angleDiff = std::acos(dotProduct);
+               errorSample.insert(angleDiff);
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_OMP
+
+      errorSample.mpiAllGather();
+
+      const real_t maxError  = errorSample.max();
+      const real_t meanError = errorSample.mean();
+
+      WALBERLA_LOG_RESULT("Mean absolute error in angle of normal = " << meanError);
+      WALBERLA_LOG_RESULT("Maximum absolute error in angle of normal = " << maxError);
+      WALBERLA_LOG_RESULT("Minimum absolute error in angle of normal = " << errorSample.min());
+
+      // the following reference errors have been obtained with a version of the code that is believed to be correct
+      if (computeNormalsInInterfaceNeighbors_ && smoothFillLevel_)
+      {
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.036));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.2));
+         }
+         else
+         {
+            if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >)
+            {
+               WALBERLA_CHECK_LESS(meanError, real_c(0.036));
+               WALBERLA_CHECK_LESS(maxError, real_c(0.19));
+            }
+         }
+      }
+
+      if (!computeNormalsInInterfaceNeighbors_ && smoothFillLevel_)
+      {
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.016));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.045));
+         }
+         else
+         {
+            if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >)
+            {
+               WALBERLA_CHECK_LESS(meanError, real_c(0.0147));
+               WALBERLA_CHECK_LESS(maxError, real_c(0.0457));
+            }
+         }
+      }
+
+      if (!computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_)
+      {
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.021));
+            WALBERLA_CHECK_LESS(maxError, real_c(0.08));
+         }
+         else
+         {
+            if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >)
+            {
+               WALBERLA_CHECK_LESS(meanError, real_c(0.037));
+               WALBERLA_CHECK_LESS(maxError, real_c(0.096));
+            }
+         }
+      }
+
+      if (computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_)
+      {
+         if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >)
+         {
+            WALBERLA_CHECK_LESS(meanError, real_c(0.139));
+            WALBERLA_CHECK_LESS(maxError, real_c(1.58));
+         }
+         else
+         {
+            if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >)
+            {
+               WALBERLA_CHECK_LESS(meanError, real_c(0.121));
+               WALBERLA_CHECK_LESS(maxError, real_c(1.58));
+            }
+         }
+      }
+   }
+
+ private:
+   std::weak_ptr< const StructuredBlockForest > blockForest_;
+   geometry::Sphere sphere_;
+
+   ConstBlockDataID normalFieldID_;
+   ConstBlockDataID flagFieldID_;
+   field::FlagUID interfaceFlagID_;
+
+   bool computeNormalsInInterfaceNeighbors_;
+   bool smoothFillLevel_;
+}; // class EvaluateNormalError
+
+template< typename LatticeModel_T >
+void test(uint_t numCells, Vector3< real_t > offset, bool computeNormalsInInterfaceNeighbors, bool smoothFillLevel)
+{
+   // define types
+   using Stencil_T                     = typename LatticeModel_T::Stencil;
+   using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+   using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >;
+
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(numCells + uint_c(6));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // create lattice model with omega=1
+   LatticeModel_T latticeModel(real_c(1.0));
+
+   // add fields
+   BlockDataID pdfFieldID =
+      lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID smoothFillFieldID;
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
+
+   // add gas sphere, i.e., bubble
+   Vector3< real_t > midpoint(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5),
+                              real_c(domainSize[2]) * real_c(0.5));
+   geometry::Sphere sphere(midpoint + offset, real_c(numCells) * real_c(0.5));
+   freeSurfaceBoundaryHandling->addFreeSurfaceObject(sphere);
+   freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   BlockDataID* relevantFillFieldID = &fillFieldID;
+
+   if (smoothFillLevel)
+   {
+      smoothFillFieldID =
+         field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), field::fzyx, uint_c(1));
+
+      // add sweep for smoothing the fill level field
+      SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep(
+         smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs,
+         freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false);
+      timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep")
+                     << AfterFunction(Communication_T(blockForest, smoothFillFieldID),
+                                      "Communication after smoothing sweep");
+
+      relevantFillFieldID = &smoothFillFieldID;
+   }
+
+   // add sweep for computing interface normals
+   NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+      normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs,
+      freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), computeNormalsInInterfaceNeighbors, false, false,
+      false);
+   timeloop.add() << Sweep(normalsSweep, "Normals sweep")
+                  << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep");
+
+   // add sweep for evaluating the error of the interface normals (by comparing with analytical normal)
+   EvaluateNormalError< Stencil_T > errorEvaluationSweep(blockForest, sphere, normalFieldID, flagFieldID,
+                                                         flagIDs::interfaceFlagID, computeNormalsInInterfaceNeighbors,
+                                                         smoothFillLevel);
+   timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep");
+
+   // perform a single time step
+   timeloop.singleStep();
+
+   MPIManager::instance()->resetMPI();
+}
+
+template< typename LatticeModel_T >
+void runAllTests()
+{
+   // test with various bubble diameters
+   for (uint_t i = uint_c(10); i <= uint_c(30); i += uint_c(10))
+   {
+      WALBERLA_LOG_RESULT("Bubble diameter " << i << " cells; normals computed only in interface cells;");
+      test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), false, false);
+
+      WALBERLA_LOG_RESULT("Bubble diameter "
+                          << i << " cells; normals computed only in interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), false, true);
+
+      WALBERLA_LOG_RESULT("Bubble diameter " << i
+                                             << " cells; normals computed in D3Q27 neighborhood of interface cells;");
+      test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), true, false);
+
+      WALBERLA_LOG_RESULT(
+         "Bubble diameter "
+         << i << " cells; normals computed in D3Q27 neighborhood of interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), true, true);
+   }
+
+   // test with various offsets of sphere's center
+   for (real_t off = real_c(0.0); off < real_c(1.0); off += real_c(0.2))
+   {
+      WALBERLA_LOG_RESULT("Bubble offset " << off << " cells; normals computed only in interface cells;");
+      test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), false, false);
+
+      WALBERLA_LOG_RESULT("Bubble offset "
+                          << off << " cells; normals computed only in interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), false, true);
+
+      WALBERLA_LOG_RESULT("Bubble offset " << off
+                                           << " cells; normals computed in D3Q27 neighborhood of interface cells;");
+      test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), true, false);
+
+      WALBERLA_LOG_RESULT(
+         "Bubble offset "
+         << off << " cells; normals computed in D3Q27 neighborhood of interface cells; fill level field smoothed;");
+      test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), true, true);
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >();
+
+   WALBERLA_LOG_RESULT("###################################");
+   WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###");
+   WALBERLA_LOG_RESULT("###################################");
+   runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >();
+
+   return EXIT_SUCCESS;
+}
+} // namespace NormalsOfSphereTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::NormalsOfSphereTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp b/tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp
new file mode 100644
index 000000000..6ccbff11b
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp
@@ -0,0 +1,283 @@
+//======================================================================================================================
+//
+//  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 ObstacleFillLevelTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test the ObstacleFillLevelSweep with an obstacle cell that is surrounded by different other cells.
+//!
+//! 3x3x1 domain, with obstacle cell at (1,1,0) and given obstacle normal.
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "lbm/field/AddToStorage.h"
+#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h"
+#include "lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h"
+#include "lbm/lattice_model/D3Q19.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <unordered_map>
+#include <unordered_set>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace ObstacleFillLevelTest
+{
+using LatticeModel_T = lbm::D3Q19< lbm::collision_model::SRT >;
+using PdfField_T     = lbm::PdfField< LatticeModel_T >;
+using Stencil_T      = LatticeModel_T::Stencil;
+
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+using flag_t                        = uint32_t;
+using FlagField_T                   = FlagField< flag_t >;
+using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
+
+real_t computeObstacleFillLevel(const std::unordered_map< Cell, real_t >& interfaceCells,
+                                const std::unordered_set< Cell >& liquidCells,
+                                const std::unordered_set< Cell >& gasCells,
+                                const std::unordered_set< Cell >& solidCells,
+                                const Vector3< real_t >& centerObstacleNormal)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(3));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          true, true, true);                                    // periodicity
+
+   // create lattice model (dummy, not relevant for this test)
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1)));
+
+   // add fields
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1));
+   BlockDataID obstaclefillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Obstacle fill level field",
+                                                                          real_c(0.0), field::fzyx, uint_c(1));
+   const BlockDataID obstacleNormalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Obstacle normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+
+   // add boundary handling
+   const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling =
+      std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID);
+   const BlockDataID flagFieldID                                      = freeSurfaceBoundaryHandling->getFlagFieldID();
+   const BlockDataID boundaryHandlingID                               = freeSurfaceBoundaryHandling->getHandlingID();
+   const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
+
+   // initialize domain
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      ScalarField_T* const fillField           = blockIt->getData< ScalarField_T >(fillFieldID);
+      VectorField_T* const obstacleNormalField = blockIt->getData< VectorField_T >(obstacleNormalFieldID);
+      FreeSurfaceBoundaryHandling_T::BoundaryHandling_T* const boundaryHandling =
+         blockIt->getData< FreeSurfaceBoundaryHandling_T::BoundaryHandling_T >(boundaryHandlingID);
+
+      WALBERLA_FOR_ALL_CELLS_XYZ(fillField, {
+         // set no slip at domain boundaries in z-direction to reduce problem to 2D
+         if (z == cell_idx_c(0) || z == cell_idx_c(2))
+         {
+            boundaryHandling->setBoundary(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z);
+
+            // obstacle normal must be normalized to not trigger the assertion in ObstacleFillLevelSweep
+            obstacleNormalField->get(x, y, z) = Vector3< real_t >(real_c(1), real_c(1), real_c(1)).getNormalized();
+            continue;
+         }
+
+         // obstacle cell (to be evaluated) in the domain center
+         if (x == cell_idx_c(1) && y == cell_idx_c(1) && z == cell_idx_c(1))
+         {
+            boundaryHandling->setBoundary(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z);
+            obstacleNormalField->get(x, y, z) = centerObstacleNormal.getNormalized();
+            fillField->get(x, y, z)           = real_c(-5); // dummy value to check that the value did not change
+            continue;
+         }
+
+         if (interfaceCells.find(Cell(x, y, z)) != interfaceCells.end())
+         {
+            boundaryHandling->setFlag(flagIDs::interfaceFlagID, x, y, z);
+            fillField->get(x, y, z) = interfaceCells.find(Cell(x, y, z))->second;
+            continue;
+         }
+
+         if (liquidCells.find(Cell(x, y, z)) != liquidCells.end())
+         {
+            boundaryHandling->setFlag(flagIDs::liquidFlagID, x, y, z);
+            fillField->get(x, y, z) = real_c(1);
+            continue;
+         }
+
+         if (gasCells.find(Cell(x, y, z)) != gasCells.end())
+         {
+            boundaryHandling->setFlag(flagIDs::gasFlagID, x, y, z);
+            fillField->get(x, y, z) = real_c(0);
+            continue;
+         }
+
+         if (solidCells.find(Cell(x, y, z)) != solidCells.end())
+         {
+            boundaryHandling->setFlag(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z);
+
+            // obstacle normal must be normalized to not trigger the assertion in ObstacleFillLevelSweep
+            obstacleNormalField->get(x, y, z) = Vector3< real_t >(real_c(1), real_c(1), real_c(1)).getNormalized();
+            continue;
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ
+   }
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, 1);
+
+   // add ObstacleFillLevelSweep
+   ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > obstFillSweep(
+      obstaclefillFieldID, fillFieldID, flagFieldID, obstacleNormalFieldID, flagIDs::liquidInterfaceGasFlagIDs,
+      flagInfo.getObstacleIDSet());
+   timeloop.add() << Sweep(obstFillSweep, "Obstacle fill level sweep");
+
+   timeloop.singleStep();
+
+   real_t obstacleFillLevel = real_c(0);
+
+   // get fill level of (central) solid cell
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const obstaclefillField = blockIt->getData< const ScalarField_T >(obstaclefillFieldID);
+
+      obstacleFillLevel = obstaclefillField->get(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1));
+   }
+
+   return obstacleFillLevel;
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   Vector3< real_t > centerObstacleNormal;
+   std::unordered_map< Cell, real_t > interfaceCells;
+   std::unordered_set< Cell > liquidCells;
+   std::unordered_set< Cell > gasCells;
+   std::unordered_set< Cell > solidCells;
+   real_t expectedFillLevel;
+   real_t computedFillLevel;
+
+   // IMPORTANT REMARK:
+   // the fill level of the obstacle center cell at (1, 1, 1) is going to be computed; therefore, Cell(1, 1, 1) must not
+   // be set here
+
+   // test case 1: interface neighbors at the top, no liquid or gas neighbors, remaining cells are solid, obstacle
+   // normal points upwards
+   WALBERLA_LOG_RESULT("Performing test case 1")
+   centerObstacleNormal = Vector3< real_t >(real_c(0), real_c(1), real_c(0));
+   solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)));
+   interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   expectedFillLevel = real_c(0.5); // verified by hand
+   computedFillLevel =
+      computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal);
+   WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel)
+
+   interfaceCells.clear();
+   liquidCells.clear();
+   gasCells.clear();
+   solidCells.clear();
+
+   // test case 2: interface neighbors at the top, left cell is liquid, right cell is gas, bottom cells are solid,
+   // obstacle normal points upwards
+   WALBERLA_LOG_RESULT("Performing test case 2")
+   centerObstacleNormal = Vector3< real_t >(real_c(0), real_c(1), real_c(0));
+   solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1)));
+   interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.75));
+   interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   liquidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)));
+   gasCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)));
+   expectedFillLevel = real_c(0.5732233); // verified by hand
+   computedFillLevel =
+      computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal);
+   WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel)
+
+   interfaceCells.clear();
+   liquidCells.clear();
+   gasCells.clear();
+   solidCells.clear();
+
+   // test case 3: same as testcase 2 but obstacle normal points to the upper left corner
+   WALBERLA_LOG_RESULT("Performing test case 3")
+   centerObstacleNormal = Vector3< real_t >(real_c(-1), real_c(1), real_c(0));
+   solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1)));
+   solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1)));
+   interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.75));
+   interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   liquidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)));
+   gasCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)));
+   expectedFillLevel = real_c(0.58009431); // verified by hand
+   computedFillLevel =
+      computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal);
+   WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel)
+
+   interfaceCells.clear();
+   liquidCells.clear();
+   gasCells.clear();
+   solidCells.clear();
+
+   // test case 4: only interface cells in neighborhood (all with same fill level)
+   WALBERLA_LOG_RESULT("Performing test case 4")
+   centerObstacleNormal = Vector3< real_t >(real_c(-1), real_c(1), real_c(0));
+   interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5));
+   expectedFillLevel = real_c(0.5); // dummy value as initialized in code above
+   computedFillLevel =
+      computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal);
+   WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel)
+
+   return EXIT_SUCCESS;
+}
+} // namespace ObstacleFillLevelTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::ObstacleFillLevelTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp b/tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp
new file mode 100644
index 000000000..0d45f6685
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp
@@ -0,0 +1,186 @@
+//======================================================================================================================
+//
+//  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 ObstacleNormalsTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Test if mean obstacle normal computation is correct in setup with inclined plane and different inclination
+//! angles.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/math/Constants.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm/free_surface/surface_geometry/ObstacleNormalSweep.h"
+
+#include "stencil/D3Q27.h"
+#include "stencil/D3Q7.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <cmath>
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace ObstacleNormalsTest
+{
+// define types
+using Stencil_T = stencil::D3Q27;
+using flag_t    = uint8_t;
+
+const FlagUID Fluid_Flag("fluid");
+const FlagUID FluidNearSolid_Flag("fluid near solid");
+const FlagUID Solid_Flag("no slip");
+const Set< FlagUID > All_Fluid_Flags = setUnion< FlagUID >(Fluid_Flag, FluidNearSolid_Flag);
+
+// define fields
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+void test(real_t degreeInclinationAngle)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(20), uint_c(3), uint_c(20));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // add fields
+   BlockDataID obstacleNormalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Obstacle normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >(
+      blockForest, "flags", uint_c(2)); // flag field must have two ghost layers as in regular FSLBM application code
+
+   // initialize flag field as inclination, the lower part of the domain will be solid
+   const real_t tanAngle = real_c(std::tan(degreeInclinationAngle * math::pi / real_c(180)));
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+
+      const auto fluidFlag          = flagField->registerFlag(Fluid_Flag);
+      const auto fluidNearSolidFlag = flagField->registerFlag(FluidNearSolid_Flag);
+      const auto solidFlag          = flagField->registerFlag(Solid_Flag);
+
+      // create inclination in flag field
+      WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(
+         flagField, uint_c(2),
+         // all cells below the specified inclination angle are marked as solid
+         if (real_c(x) * tanAngle <= real_c(z)) { flagField->get(x, y, z) = solidFlag; } else {
+            flagField->get(x, y, z) = fluidFlag;
+         }); // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ
+
+      // mark fluid cells that have neighboring solid cells ( only in these cells, the obstacle normal will be computed)
+      WALBERLA_FOR_ALL_CELLS(
+         flagFieldIt, flagField,
+         if (isFlagSet(flagFieldIt, solidFlag) && isFlagInNeighborhood< stencil::D3Q7 >(flagFieldIt, fluidFlag)) {
+            *flagFieldIt = fluidNearSolidFlag;
+         }); // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   // add obstacle normals sweep
+   ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstNormalsSweep(
+      obstacleNormalFieldID, flagFieldID, FluidNearSolid_Flag, All_Fluid_Flags, Solid_Flag, true, false, false);
+   timeloop.add() << Sweep(obstNormalsSweep, "Obstacle normals sweep");
+
+   // perform a single time step
+   timeloop.singleStep();
+
+   // compute analytical normal
+   const real_t sinAngle = real_c(std::sin(degreeInclinationAngle * math::pi / real_c(180)));
+   const real_t cosAngle = real_c(std::cos(degreeInclinationAngle * math::pi / real_c(180)));
+   const Vector3< real_t > analyticalNormal(-sinAngle, real_c(0), cosAngle);
+
+   // compare analytical normal with the average of all computed obstacle normals
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const VectorField_T* const obstacleNormalField = blockIt->getData< const VectorField_T >(obstacleNormalFieldID);
+      const FlagField_T* const flagField             = blockIt->getData< const FlagField_T >(flagFieldID);
+
+      const auto fluidNearSolidFlag = flagField->getFlag(FluidNearSolid_Flag);
+
+      real_t averageObstNormalX = real_c(0);
+      real_t averageObstNormalY = real_c(0);
+      real_t averageObstNormalZ = real_c(0);
+      uint_t cellCount          = uint_c(0);
+      WALBERLA_FOR_ALL_CELLS_XYZ_OMP(obstacleNormalField,
+            omp parallel for schedule(static) reduction(+:averageObstNormalX) reduction(+:averageObstNormalY)
+                                              reduction(+:averageObstNormalZ) reduction(+:cellCount), {
+         // skip cells at the domain boundary; obstacle normal in these cells deviates further from analytical solution
+         // since neighborhood for obstacle computation is too small
+         if (x == cell_idx_c(0) || y == cell_idx_c(0) || z == cell_idx_c(0) ||
+             x == cell_idx_c(domainSize[0] - uint_c(1)) || y == cell_idx_c(domainSize[1] - uint_c(1)) ||
+             z == cell_idx_c(domainSize[2] - uint_c(1)))
+         {
+            continue;
+         }
+
+         if (!isFlagSet(flagField->get(x, y, z), fluidNearSolidFlag))
+         {
+            // obstacle normal must be zero in all cells that are not of type fluidNearSolid
+            WALBERLA_CHECK_FLOAT_EQUAL(obstacleNormalField->get(x, y, z), Vector3< real_t >(real_c(0)));
+         }
+         else
+         {
+            ++cellCount;
+            averageObstNormalX += obstacleNormalField->get(x, y, z)[0];
+            averageObstNormalY += obstacleNormalField->get(x, y, z)[1];
+            averageObstNormalZ += obstacleNormalField->get(x, y, z)[2];
+         }}); // WALBERLA_FOR_ALL_CELLS_XYZ_OMP
+
+      // compute average obstacle normal
+      Vector3< real_t > averageObstNormal(averageObstNormalX, averageObstNormalY, averageObstNormalZ);
+      averageObstNormal /= real_c(cellCount);
+
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(averageObstNormal, analyticalNormal, real_c(0.1));
+   }
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+   // test with various inclination angles (only up to 87 degree, as otherwise in this setup no more fluid cells remain)
+   for (uint_t angle = uint_c(1); angle <= uint_c(86); angle += uint_c(1))
+   {
+      WALBERLA_LOG_INFO_ON_ROOT("Testing inclination angle " << real_c(angle) << " degree.")
+      test(real_c(angle));
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace ObstacleNormalsTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::ObstacleNormalsTest::main(argc, argv); }
\ No newline at end of file
diff --git a/tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp b/tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp
new file mode 100644
index 000000000..2bcff6248
--- /dev/null
+++ b/tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp
@@ -0,0 +1,341 @@
+//======================================================================================================================
+//
+//  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 WettingCurvatureTest.cpp
+//! \ingroup lbm/free_surface/surface_geometry
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Initialize a drop as a cylinder section near a solid wall and evaluate the resulting wetting curvature.
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+
+#include "field/AddToStorage.h"
+#include "field/vtk/FlagFieldCellFilter.h"
+#include "field/vtk/VTKWriter.h"
+
+#include "geometry/bodies/Cylinder.h"
+#include "geometry/initializer/OverlapFieldFromBody.h"
+
+#include "lbm/free_surface/surface_geometry/ContactAngle.h"
+#include "lbm/free_surface/surface_geometry/CurvatureSweep.h"
+#include "lbm/free_surface/surface_geometry/NormalSweep.h"
+#include "lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h"
+#include "lbm/free_surface/surface_geometry/ObstacleNormalSweep.h"
+#include "lbm/free_surface/surface_geometry/SmoothingSweep.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "vtk/Initialization.h"
+#include "vtk/VTKOutput.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace WettingCurvatureTest
+{
+// define types
+using Stencil_T = stencil::D3Q27;
+
+using flag_t = uint32_t;
+
+const FlagUID liquidFlagID("liquid");
+const FlagUID interfaceFlagID("interface");
+const FlagUID gasFlagID("gas");
+const FlagUID solidID("solid");
+const Set< FlagUID > liquidInterfaceGasFlagIDs =
+   setUnion< FlagUID >(setUnion< FlagUID >(liquidFlagID, interfaceFlagID), gasFlagID);
+
+// define fields
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+using FlagField_T   = FlagField< flag_t >;
+
+real_t computeCurvature(real_t contactAngle, bool useTriangulation)
+{
+   // define the domain size
+   const Vector3< uint_t > numBlocks(uint_c(1));
+   const Vector3< uint_t > domainSize(uint_c(6), uint_c(5), uint_c(5));
+   const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1],
+                                         domainSize[2] / numBlocks[2]);
+
+   // create block grid
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2],             // blocks
+                                          cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells
+                                          real_c(1.0),                                          // dx
+                                          true,                                                 // one block per process
+                                          false, false, false);                                 // periodicity
+
+   // add fields
+   BlockDataID fillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(0.0), field::fzyx, uint_c(1));
+   BlockDataID normalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID curvatureFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0), field::fzyx, uint_c(1));
+   BlockDataID obstacleNormalFieldID = field::addToStorage< VectorField_T >(
+      blockForest, "Obstacle normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1));
+   BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >(
+      blockForest, "Flags", uint_c(2)); // flag field must have two ghost layers as in regular FSLBM application code
+   BlockDataID smoothFillFieldID =
+      field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), field::fzyx, uint_c(1));
+
+   // add liquid drop
+   Vector3< real_t > midpoint1(real_c(5), real_c(1), real_c(0));
+   Vector3< real_t > midpoint2(real_c(5), real_c(1), real_c(5));
+   geometry::Cylinder cylinder(midpoint1, midpoint2, real_c(5) * real_c(0.5));
+   geometry::initializer::OverlapFieldFromBody(*blockForest, fillFieldID).init(cylinder, true);
+
+   // set flags
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      FlagField_T* const flagField   = blockIt->getData< FlagField_T >(flagFieldID);
+      ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID);
+
+      const auto gas       = flagField->registerFlag(gasFlagID);
+      const auto liquid    = flagField->registerFlag(liquidFlagID);
+      const auto interface = flagField->registerFlag(interfaceFlagID);
+      const auto solid     = flagField->registerFlag(solidID);
+
+      // initialize whole flag field as gas
+      WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, { *flagFieldIt = gas; }) // WALBERLA_FOR_ALL_CELLS
+
+      // initialize flag field according to fill level
+      WALBERLA_FOR_ALL_CELLS_XYZ(
+         flagField,
+         if (y == 0) {
+            flagField->get(x, y, z) = solid;
+            fillField->get(x, y, z) = real_c(0);
+         }
+
+         if (fillField->get(x, y, z) <= real_c(0) && flagField->get(x, y, z) != solid) {
+            flagField->get(x, y, z) = gas;
+         }
+
+         if (fillField->get(x, y, z) >= real_c(1)) { flagField->get(x, y, z) = liquid; }
+
+         // not using else here to avoid overwriting solid flags
+         if (fillField->get(x, y, z) < real_c(1) && fillField->get(x, y, z) > real_c(0)) {
+            flagField->get(x, y, z) = interface;
+         }) // WALBERLA_FOR_ALL_CELLS_XYZ
+   }
+
+   ContactAngle contactAngleObj = ContactAngle(contactAngle);
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, uint_c(1));
+
+   if (useTriangulation) // use local triangulation for curvature computation
+   {
+      // add sweep for computing obstacle normals in interface cells near obstacle cells
+      ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstNormalsSweep(
+         obstacleNormalFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, true, false, false);
+      timeloop.add() << Sweep(obstNormalsSweep, "Obstacle normals sweep");
+
+      // add sweep for computing interface normals
+      NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+         normalFieldID, fillFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, false, false,
+         true, false);
+      timeloop.add() << Sweep(normalsSweep, "Normal sweep");
+
+      // add sweep for computing curvature (including wetting)
+      CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep(
+         blockForest, curvatureFieldID, normalFieldID, fillFieldID, flagFieldID, obstacleNormalFieldID, interfaceFlagID,
+         solidID, true, contactAngleObj);
+      timeloop.add() << Sweep(curvatureSweep, "Curvature sweep");
+   }
+   else // use finite differences for curvature computation
+   {
+      // add sweep for computing obstacle normals in obstacle cells
+      ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstNormalsSweep(
+         obstacleNormalFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, false, true, true);
+      timeloop.add() << Sweep(obstNormalsSweep, "Obstacle normals sweep");
+
+      // add sweep for reflecting fill level into obstacle cells
+      ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > obstacleFillLevelSweep(
+         smoothFillFieldID, fillFieldID, flagFieldID, obstacleNormalFieldID, liquidInterfaceGasFlagIDs, solidID);
+      timeloop.add() << Sweep(obstacleFillLevelSweep, "Obstacle fill level sweep");
+
+      // add sweep for smoothing the fill level field (uses fill level values from obstacle cells set by
+      // ObstacelFillLevelSweep)
+      SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep(
+         smoothFillFieldID, fillFieldID, flagFieldID, liquidInterfaceGasFlagIDs, solidID, true);
+      timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep");
+
+      // add sweep for computing interface normals
+      NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep(
+         normalFieldID, smoothFillFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, true, true,
+         false, true);
+      timeloop.add() << Sweep(normalsSweep, "Normal sweep");
+
+      // add sweep for computing curvature (including wetting)
+      CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep(
+         curvatureFieldID, normalFieldID, obstacleNormalFieldID, flagFieldID, interfaceFlagID,
+         liquidInterfaceGasFlagIDs, solidID, true, contactAngleObj);
+      timeloop.add() << Sweep(curvatureSweep, "Curvature sweep");
+   }
+
+   // run one time step
+   timeloop.singleStep();
+
+   // get the curvature in cell (2, 1, 2)
+   real_t curvature = real_c(0);
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      const ScalarField_T* const curvatureField = blockIt->getData< const ScalarField_T >(curvatureFieldID);
+      curvature                                 = curvatureField->get(2, 1, 2);
+   }
+
+   //   auto vtkFlagField =
+   //      field::createVTKOutput< FlagField_T >(flagFieldID, *blockForest, "flag_field", uint_c(1), uint_c(0));
+   //   vtkFlagField();
+   //   auto vtkFillField =
+   //      field::createVTKOutput< ScalarField_T >(fillFieldID, *blockForest, "fill_field", uint_c(1), uint_c(0));
+   //   vtkFillField();
+   //   auto vtkCurvatureField =
+   //      field::createVTKOutput< ScalarField_T >(curvatureFieldID, *blockForest, "curvature_field", uint_c(1),
+   //      uint_c(0));
+   //   vtkCurvatureField();
+   //   auto vtkNormalField =
+   //      field::createVTKOutput< VectorField_T >(normalFieldID, *blockForest, "normal_field", uint_c(1), uint_c(0));
+   //   vtkNormalField();
+   //   auto vtkObstNormalField = field::createVTKOutput< VectorField_T >(obstacleNormalFieldID, *blockForest,
+   //                                                                     "obst_normal_field", uint_c(1), uint_c(0));
+   //   vtkObstNormalField();
+   //   auto vtkSmoothFillField = field::createVTKOutput< ScalarField_T >(smoothFillFieldID, *blockForest,
+   //                                                                     "smooth_fill_field", uint_c(1), uint_c(0));
+   //   vtkSmoothFillField();
+
+   return curvature;
+}
+
+// the following values have been obtained with a version of the local triangulation curvature computation algorithm
+// that is assumed to be correct (the angles computed in computeArtificalWallPoint() have been manually verified with
+// ParaView for some of the values)
+std::vector< std::pair< real_t, real_t > > expectedSolutionTriangulation()
+{
+   // pair contains: [0] contact angle, [1] expected curvature
+   std::vector< std::pair< real_t, real_t > > testcases;
+
+   testcases.emplace_back(real_c(0), real_c(-0.208893));
+   testcases.emplace_back(real_c(1), real_c(-0.208832));
+   testcases.emplace_back(real_c(10), real_c(-0.202813));
+   testcases.emplace_back(real_c(30), real_c(-0.15704));
+   testcases.emplace_back(real_c(45), real_c(-0.0994945));
+   testcases.emplace_back(real_c(65), real_c(-0.00311236));
+   testcases.emplace_back(real_c(65.5), real_c(-0.000512801));
+
+   // IMPORTANT REMARK REGARDING THE CHANGE IN THE SIGN OF THE CURVATURE:
+   // A change in the sign of the curvature would only be expected when the specified contact angle is equivalent to the
+   // angle that is already present. However, the algorithm ensures that the constructed virtual wall point is valid
+   // during curvature computation (such that the computed triangle by local triangulation is not degenerated).
+   // Therefore, the algorithm internally changes the target contact angle (see lines 10 to 17 in algorithm 6.2 in
+   // dissertation of S. Donath). The specified contact angle will then slowly be approached in subsequent time steps.
+
+   testcases.emplace_back(real_c(65.8), real_c(0.00105015));
+   testcases.emplace_back(real_c(66), real_c(0.00209344));
+   testcases.emplace_back(real_c(66.5), real_c(0.00470618));
+   testcases.emplace_back(real_c(67), real_c(0.00732526));
+   testcases.emplace_back(real_c(70), real_c(0.0231628));
+   testcases.emplace_back(real_c(75), real_c(0.0499505));
+   testcases.emplace_back(real_c(77), real_c(0.0607714));
+   testcases.emplace_back(real_c(78), real_c(0.0661992));
+
+   // this contact angle is already reached by the initial setup
+   // => the algorithm should take the route with if(realIsEqual(...)) in CurvatureSweepTR
+   // => this route has been found to cause problems when using single precision (see comment in CurvatureSweepTR())
+#ifdef WALBERLA_DOUBLE_ACCURACY
+   testcases.emplace_back(real_c(78.40817854), real_c(0.0684177));
+#endif
+
+   testcases.emplace_back(real_c(80), real_c(0.0770832));
+   testcases.emplace_back(real_c(90), real_c(0.131739));
+   testcases.emplace_back(real_c(120), real_c(0.287743));
+   testcases.emplace_back(real_c(160), real_c(0.431406));
+   testcases.emplace_back(real_c(180), real_c(0.312837));
+
+   return testcases;
+}
+
+// the following values have been obtained with a version of the finite difference curvature computation algorithm
+// that is assumed to be correct
+std::vector< std::pair< real_t, real_t > > expectedSolutionFiniteDifferences()
+{
+   std::vector< std::pair< real_t, real_t > > testcases;
+
+   testcases.emplace_back(real_c(0), real_c(-0.0454018));
+   testcases.emplace_back(real_c(1), real_c(-0.0474911));
+   testcases.emplace_back(real_c(10), real_c(-0.0641205));
+   testcases.emplace_back(real_c(30), real_c(-0.0810346));
+   testcases.emplace_back(real_c(45), real_c(-0.0622399));
+   testcases.emplace_back(real_c(65), real_c(0.0397875));
+   testcases.emplace_back(real_c(65.5), real_c(0.0436974));
+   testcases.emplace_back(real_c(65.8), real_c(0.046073));
+   testcases.emplace_back(real_c(66), real_c(0.0476689));
+   testcases.emplace_back(real_c(66.5), real_c(0.0517007));
+   testcases.emplace_back(real_c(67), real_c(0.0557916));
+   testcases.emplace_back(real_c(70), real_c(0.0814937));
+   testcases.emplace_back(real_c(75), real_c(0.127884));
+   testcases.emplace_back(real_c(77), real_c(0.147265));
+   testcases.emplace_back(real_c(78), real_c(0.157051));
+   testcases.emplace_back(real_c(78.40817854), real_c(0.161057));
+   testcases.emplace_back(real_c(80), real_c(0.176711));
+   testcases.emplace_back(real_c(90), real_c(0.271672));
+   testcases.emplace_back(real_c(120), real_c(0.448388));
+   testcases.emplace_back(real_c(160), real_c(0.487812));
+   testcases.emplace_back(real_c(180), real_c(0.467033));
+
+   return testcases;
+}
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+   Environment env(argc, argv);
+
+   // test with local triangulation curvature computation
+   std::vector< std::pair< real_t, real_t > > testcases = expectedSolutionTriangulation();
+   for (const auto& i : testcases)
+   {
+      WALBERLA_LOG_INFO_ON_ROOT("Testing contact angle=" << i.first
+                                                         << " with local triangulation curvature computation");
+      const real_t curvature = computeCurvature(i.first, true);
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(curvature, i.second, real_c(1e-5));
+   }
+
+   // test with finite difference curvature computation
+   testcases = expectedSolutionFiniteDifferences();
+   for (const auto& i : testcases)
+   {
+      WALBERLA_LOG_INFO_ON_ROOT("Testing contact angle=" << i.first << " with finite difference curvature computation");
+      const real_t curvature = computeCurvature(i.first, false);
+      WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(curvature, i.second, real_c(1e-6));
+   }
+
+   return EXIT_SUCCESS;
+}
+} // namespace WettingCurvatureTest
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::WettingCurvatureTest::main(argc, argv); }
\ No newline at end of file
-- 
GitLab