diff --git a/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp b/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp
index a9cca407c81d4ff61b905bb58abc754b880b51f5..0f436644770816f6d9fb1ae2b3897e81106c2555 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 6330a83bdde5027a2d0fd8e030862b7bf521dcd4..c6f0534cc6679f996777742823e5901aca8233f4 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 0000000000000000000000000000000000000000..00ae3e9f495e5ca7f465dcfff85100288eac5842
--- /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 0000000000000000000000000000000000000000..c6e7d413c5ef6b864f59f6c521542e6ff825a0e4
--- /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 0000000000000000000000000000000000000000..0e6edd5b2290c74048a0fcc2c34c8f4423c00178
--- /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 0000000000000000000000000000000000000000..d36003bc023db76f5b65c256c02857040452d278
--- /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 0000000000000000000000000000000000000000..8d172cadd60e0b0ed616fb4c724c814a2ca23dde
--- /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 0000000000000000000000000000000000000000..129cda8de97d38b1c4d0b48614d0a09ced8f70ea
--- /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 0000000000000000000000000000000000000000..de81d688e114b52e226f3c0b42b3f599b31bf33b
--- /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 0000000000000000000000000000000000000000..e3a4a5e4e0ac257caedac82631069b64e6142e0d
--- /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 0000000000000000000000000000000000000000..34fdca4b018ce9af4e4f80cf285ee545845246d2
--- /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 0000000000000000000000000000000000000000..143885a05538da4452ff2cff491e9a010fc39de2
--- /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 0000000000000000000000000000000000000000..07afacce7480bf9a0307180e4f5da17c1deebbbe
--- /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 0000000000000000000000000000000000000000..20445deadacfbdda832dac862bf997006dd82066
--- /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 0000000000000000000000000000000000000000..bdfbe4e1ee632724634ad98b8ae2dfd5a8b1cd09
--- /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 0000000000000000000000000000000000000000..6faca3c70eb392845872bfb2b4e701519db7326d
--- /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 0000000000000000000000000000000000000000..363a59c84abbc665dde520ebe4c1c1c89136874e
--- /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 0000000000000000000000000000000000000000..a6b41aea0bc98dfd558dabe42f12a50675263df3
--- /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 0000000000000000000000000000000000000000..22f474fcfbc9011c21311c48a77b097f69f51410
--- /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 0000000000000000000000000000000000000000..478ee193c7ba3c70bb6f9eb8e3b91ae9ce2b307e
--- /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 0000000000000000000000000000000000000000..184963db1f00e71bb9a3557987d5415a076de5b4
--- /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 0000000000000000000000000000000000000000..3dabdf5da79d90ede7a87400c858f8230b9ebe47
--- /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 0000000000000000000000000000000000000000..5d483426beb659bfe93db4ee94fe595e29c67255
--- /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 0000000000000000000000000000000000000000..f32cfb982e282ee8bb57d8a8ce10b48e4e426792
--- /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 0000000000000000000000000000000000000000..a2b289aeeb75b6a9e2c57819602b595c755da687
--- /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 780cfb9de146f2eb82c9dcee247b6b47d34cd3e8..9ac7590fae38f92b276939cb2e839b4318daaa1d 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 730bf592867b6d2b7abbf134300431b3de25bfbf..02b5f3937216ea5345368505ef8664ec53b1eb34 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 63a650dbd0dd11ef5a9ef6f03564131c8efe0e0a..8f41297b78a1ff66d4cc7a9f39f98692d11d1b49 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 d031b53d309dbd5c3a848bb1ba26e9022b8606d5..10c897956d7fe466bcf109e49a8448e8f4b4d56d 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 511aafa84a04ca44385c22acba0cd02891640cc3..829e0505b5ff2d832c87aac94e39f93e1d3961b9 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 0000000000000000000000000000000000000000..a39e2f18741edaabe67ef1f946974f625f102bc6
--- /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 0000000000000000000000000000000000000000..42bffe263761d7c541a43ebff4b21e17eac168c1
--- /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 0000000000000000000000000000000000000000..1e9c69ed1c1bd38efd71f22775199ef5693713b2
--- /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 0000000000000000000000000000000000000000..46c43aa70a9b9958955ca21a3c2e5892234b975f
--- /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 31709c60e0e538dddaaf537fe0d661fb2b4833c1..11f7698b3a79b9596219eede7d177af33cf0b211 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 0000000000000000000000000000000000000000..f2ef50bd14813126e512dc42a00ff225892af86c
--- /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 0000000000000000000000000000000000000000..54b500251a5be6f3e48f70addb53a4e5fd090a69
--- /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 0000000000000000000000000000000000000000..4dd93303a0f79fc445ad09e5709cd57e89215b98
--- /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 0000000000000000000000000000000000000000..e332cf8dce916946fc1f36a18c3ce4aa0b8864d9
--- /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 0000000000000000000000000000000000000000..574001ea45642a76efc5dbb48d6a5d2b7cce6580
--- /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 0000000000000000000000000000000000000000..b98f55d54dbb411fa877e2a95690265fb6b4c80f
--- /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 0000000000000000000000000000000000000000..ef113e327032695d73df64109f0581c3b03cc4bd
--- /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 0000000000000000000000000000000000000000..c9df54478e5c4af223f0be17fe128f86427f21ed
--- /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 0000000000000000000000000000000000000000..a3e2b0cb287e1a9ad427869047a0404eafd89ce1
--- /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 0000000000000000000000000000000000000000..334f9d714d2e3dc3f6eb4a465aa9f3606a775a22
--- /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 0000000000000000000000000000000000000000..192ec875594b98dc87bff0e1418c200544965590
--- /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 0000000000000000000000000000000000000000..36f056740a2a566fee8e70a19ff56084f74b4b43
--- /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 0000000000000000000000000000000000000000..9b3f1195a0381d0d6cbf3137539a305863dde5d1
--- /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 0000000000000000000000000000000000000000..f21e971cefcc5b0e152586be0bf70e0bc249abe1
--- /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 0000000000000000000000000000000000000000..e5fde552fe8ff32e4b5a70f72aa4b03700149667
--- /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 0000000000000000000000000000000000000000..534d965f9ea15cb34554e70297b0133c45beed69
--- /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 0000000000000000000000000000000000000000..bb3b9a074f28256fd3693cee690be43f7134dfb4
--- /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 0000000000000000000000000000000000000000..b67397be121de63912a58f2c8787ae4895dc8e46
--- /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 0000000000000000000000000000000000000000..8f6a24a687f3950c76eda732c405ba6c0c80c118
--- /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 0000000000000000000000000000000000000000..05b0f0b23dc1c235c1cc72c3902722ac55441e88
--- /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 0000000000000000000000000000000000000000..a22d540eaac0904f62d7652c0f2c5e91c66a900c
--- /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 0000000000000000000000000000000000000000..7ee915c2a5517379c1c13b605933740f9ff24127
--- /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 0000000000000000000000000000000000000000..dfd86821b9828fbd2a1791469f11a43643e0f506
--- /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 0000000000000000000000000000000000000000..4ffa219cb59d7a121cf568369f8e866571e35114
--- /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 0000000000000000000000000000000000000000..712930e3823899d801b4b1b60b16cbdf4cf9b478
--- /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 0000000000000000000000000000000000000000..278536feee3fb0df7213959528365980037b105b
--- /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 0000000000000000000000000000000000000000..6f8e4221816332bb82e1d6c61230a1dedb343b33
--- /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 0000000000000000000000000000000000000000..db90cae4cda8d7dd3dad37b75338fb43696ccbd9
--- /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 0000000000000000000000000000000000000000..72efc7ca9c387c02b4d1f7479f183ef318f64dd4
--- /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 0000000000000000000000000000000000000000..b23d4d774e9376bf104198c64ea6f6394c41f77e
--- /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 0000000000000000000000000000000000000000..96f1c0361466b96fcf71ac093acee0e14fa57415
--- /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 0000000000000000000000000000000000000000..7201a632eaea0df4d18a4e1884c02059ecf40796
--- /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 0000000000000000000000000000000000000000..9b0fd0dbaa1fef4ea6d969181d53e68f73f36d61
--- /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 0000000000000000000000000000000000000000..15d6f6bd8ba75e3fa9473551907b87cd20a79c8f
--- /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 0000000000000000000000000000000000000000..d8ce84faaa5fdf2d34b09ef353536cf164b4ce3d
--- /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 0000000000000000000000000000000000000000..f36c504a494c6abb8324aa999754339d82050e73
--- /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 0000000000000000000000000000000000000000..0f5e831239520354ed8f5adc45ea5c1a4b2f8207
--- /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 0000000000000000000000000000000000000000..c0f75536a9346c890729971d734383850bcc8fb5
--- /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 0000000000000000000000000000000000000000..458fabdfe2354a6879e1c826fa2577b0b7cbc866
--- /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 0000000000000000000000000000000000000000..4e913b657c1c2c9727ce2976a145db82e36dc68a
--- /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 0000000000000000000000000000000000000000..4eb74cedef75e52794aa801080244a9e4c549fd6
--- /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 0000000000000000000000000000000000000000..2e4d0b798ca3fb8bf6e325f38abfaa30119291fc
--- /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 0000000000000000000000000000000000000000..ab9efe397121b33391eceedefc407811aec16ac8
--- /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 0000000000000000000000000000000000000000..ba30c8445d19276136859641d97673fc8faff98c
--- /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 0000000000000000000000000000000000000000..4a13fb3b41dd7cce966776acd4db6f79a10788c7
--- /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 0000000000000000000000000000000000000000..9468f06b33e271ee6e014722e333bb9e5125c363
--- /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 0000000000000000000000000000000000000000..ffd86279ceee8e81b9e6f51e80b4a04dc388ed9a
--- /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 0000000000000000000000000000000000000000..8c1c745130aa0f37bb3fe353dc3251bc44e02801
--- /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 0000000000000000000000000000000000000000..cc5c382e5bd5f1fe91374d608f306bdbd54aed24
--- /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 0000000000000000000000000000000000000000..ec5ca220d3d9d3cfa5dcc20de3ba02b3dd0f4155
--- /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 0000000000000000000000000000000000000000..c0489961ab14243c568471d1c3d48621fb5dacc0
--- /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 0000000000000000000000000000000000000000..5e198e2db93fe707297fa8360e95a63a9005c665
--- /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 0000000000000000000000000000000000000000..357412052a8cdfc0d8edab2f9a71cd2e3144dba7
--- /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 0000000000000000000000000000000000000000..6445a61f6df00edc768aa8b4bc5e96cf33c2184c
--- /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 0000000000000000000000000000000000000000..36c313ee2a0027498a3d758b6378603ed160a797
--- /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 0000000000000000000000000000000000000000..187528a6c08c3faf479a7e557e799fc031967f75
--- /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 0000000000000000000000000000000000000000..b0f45113c95155be79c6118406bcd911890b9ffa
--- /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 0000000000000000000000000000000000000000..42fb177b77f77d2fa745866c7f5ef2cb677ea00c
--- /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 0000000000000000000000000000000000000000..55f26d5ce82404d1e420440ce482d78178bcfd84
--- /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 0000000000000000000000000000000000000000..dd245dacfaeeb8ac5aaab3a07495c3e107d5d335
--- /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 0000000000000000000000000000000000000000..0aa62fc3c53e3f538607c7f8cfa64b28fde44699
--- /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 0000000000000000000000000000000000000000..dfe53545c609478d5543e6a86ba328137c49b369
--- /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 0000000000000000000000000000000000000000..f6d46d10334e86a23e20593a06de5af1b37e5b2b
--- /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 0000000000000000000000000000000000000000..f4d69e0afc47fd98952f69344fd646f64e976e9a
--- /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 0000000000000000000000000000000000000000..8d49e0c963fd6aaa96fc85d6abaa782a8600cf1e
--- /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 0000000000000000000000000000000000000000..309bc79bb5c9868243c156e04f462f1e649cf790
--- /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 0000000000000000000000000000000000000000..75cc8b6d91ce5aa8a781fec2b83dae75a93231d9
--- /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 0000000000000000000000000000000000000000..a1ade27ec0021d29497258101212e29c06ad9395
--- /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 0000000000000000000000000000000000000000..c5bd8c858c0699580818d3388fea8291adf86f78
--- /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 0000000000000000000000000000000000000000..7d4182022d5fdb135f6c74619938a19a347be828
--- /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 0000000000000000000000000000000000000000..cb1c6b01dc1cec8f14a51e6a247074c1de5d450b
--- /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 0000000000000000000000000000000000000000..6d3b722580e0dd573834754581d8b176c827374c
--- /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 0000000000000000000000000000000000000000..9b76ffd20336cac435d7b2cb96d76fbcee5cb090
--- /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 0000000000000000000000000000000000000000..3b61d23d248562c5c27a7d5aab330142de8944b8
--- /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 0000000000000000000000000000000000000000..f0f21c2e6b162c19bba9756a33e6b5ed838eb681
--- /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 0000000000000000000000000000000000000000..9b1582c665e3173207918a27154afaedeb500dd9
--- /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 0000000000000000000000000000000000000000..7be227e8cc98ae75ad3b14d3044a37accd910c3b
--- /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 d34cb0684b80ba7292dcf616c5c9a25cf2efd98f..bb8d6dd57faf11a0e0bf97523190bd2da8b39e46 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 0000000000000000000000000000000000000000..455dc2e17455b691ddbaa24a6fb4f4a069c08a93
--- /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 0000000000000000000000000000000000000000..f36124fcd685f00c2b9f13df7ad5fb8827807a7b
--- /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 0000000000000000000000000000000000000000..f06d6ed6845e07173d04a497916975ea9d4ab659
--- /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 0000000000000000000000000000000000000000..08641c5b839a4e7f6e38ff3a71ce97c1966fd612
--- /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 0000000000000000000000000000000000000000..f70d7147696bed18eef2f98bb8361bdecbe4da0a
--- /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 0000000000000000000000000000000000000000..230655c16e475d6dd624420fed9781eff54123a3
--- /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 0000000000000000000000000000000000000000..b9dab07637351533d04a823070ddbe659721d0be
--- /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
Binary files /dev/null and b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png differ
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
Binary files /dev/null and b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png differ
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 0000000000000000000000000000000000000000..84e559b610e90ce9cc353453fc577cc77ba9a560
--- /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 0000000000000000000000000000000000000000..36f09079f29978b00fddbffad4b02ab883ea2340
--- /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 0000000000000000000000000000000000000000..6a41cd8e919c401c75c9b1379f4f84c97a58326c
--- /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 0000000000000000000000000000000000000000..adfbce45ff63ba2bdf32bcb484e41fd87eb264be
--- /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 0000000000000000000000000000000000000000..1c3acc672ca28f1d5ed59fbf33bee4644a276d06
--- /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 0000000000000000000000000000000000000000..6f7645c8437ae6de25eb5e4a51defa3385aac3e4
--- /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 0000000000000000000000000000000000000000..a141a0a3c8ea4f2032192383cb3ea27209f40a6f
--- /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 0000000000000000000000000000000000000000..69c075f9eb27626d2708301b296e29b114c3b6e4
--- /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 0000000000000000000000000000000000000000..8c99cf8726ecd46fff29da30d9706edebb45ffcc
--- /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
Binary files /dev/null and b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods differ
diff --git a/tests/lbm/free_surface/dynamics/InflowTest.cpp b/tests/lbm/free_surface/dynamics/InflowTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..da8b5df18d1207957ca33bc9f14327c17caf3279
--- /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 0000000000000000000000000000000000000000..0ef55dce4bf764a8f60d310e24427593c1de7834
--- /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 0000000000000000000000000000000000000000..15ff0d96d078da1d3e33c958dc67f04b628c1795
--- /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 0000000000000000000000000000000000000000..2638379cd25f67ed3343f462e21fda35d1127c69
--- /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 0000000000000000000000000000000000000000..ae4ec83d12985f40aa95482eac3c2c6380d2884a
--- /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
Binary files /dev/null and b/tests/lbm/free_surface/dynamics/PdfRefillingTest.ods differ
diff --git a/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp b/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8a2590c6983bd65f0e640a7288c0a09ba4f30ab2
--- /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 0000000000000000000000000000000000000000..24fcc259bd67fd9ac1a1b736f67ab261e06d46fd
--- /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 0000000000000000000000000000000000000000..9220f802244306482e9c796bb2946171a56f7a1a
--- /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 0000000000000000000000000000000000000000..1a42213e6188b2db7e74a6dabdd9066bd4c00326
--- /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 0000000000000000000000000000000000000000..4c86d76ad67cdbc9a3db4544b9eac2e82808d1fc
--- /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 0000000000000000000000000000000000000000..7123e9ba797fc84c708ac0f8f0ac2d82a54de755
--- /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 0000000000000000000000000000000000000000..c7b5fbd3f32333465b5fc5d91dfcd4ba4433982a
--- /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 0000000000000000000000000000000000000000..8491557fee4e6d3587284ba73436ea4a1893ed29
--- /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 0000000000000000000000000000000000000000..3ca66b6f5a33ab6b71738da0e721ddc542643c06
--- /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 0000000000000000000000000000000000000000..ca3c6b4f1f802ac224d1ae94b64bcbd7d52a2c56
--- /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 0000000000000000000000000000000000000000..6ccbff11bc22d74015706c8406113da9206c4e73
--- /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 0000000000000000000000000000000000000000..0d45f6685679d9ff08a018049a4ddacfc56ae048
--- /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 0000000000000000000000000000000000000000..2bcff6248899965c14434f594187204c633c0a0e
--- /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