diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt
index 2adab475060437d4b2934f215c45d470b3b34b0a..3f5e6a95a0f0c6a8199de013da440e37e7f78afb 100644
--- a/apps/benchmarks/CMakeLists.txt
+++ b/apps/benchmarks/CMakeLists.txt
@@ -4,6 +4,7 @@ add_subdirectory( ComplexGeometry )
 add_subdirectory( DEM )
 add_subdirectory( MeshDistance )
 add_subdirectory( CouetteFlow )
+add_subdirectory( FreeSurfaceAdvection )
 add_subdirectory( FluidParticleCoupling )
 add_subdirectory( FluidParticleCouplingWithLoadBalancing )
 add_subdirectory( ForcesOnSphereNearPlaneInShearFlow )
diff --git a/apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt b/apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e14b5fe37fda409f4e80790593e378c8f20b98f5
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt
@@ -0,0 +1,13 @@
+waLBerla_link_files_to_builddir( *.prm )
+
+waLBerla_add_executable(NAME    DeformationField
+                        FILES   DeformationField.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    SingleVortex
+                        FILES   SingleVortex.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
+waLBerla_add_executable(NAME    ZalesakDisk
+                        FILES   ZalesakDisk.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
\ No newline at end of file
diff --git a/apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fcbc4ca54fe71364c93c275000f9b210276a56c7
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp
@@ -0,0 +1,351 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file DeformationField.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This benchmark simulates the advection of a spherical bubble in a vortex field with very high deformation. The vortex
+// field changes periodically so that the bubble returns to its initial position, where it should take its initial form.
+// The relative geometrical error of the bubble's shape after one period is evaluated. There is no LBM flow simulation
+// performed here. It is a test case for the FSLBM's mass advection only. This benchmark is based on Viktor Haag's
+// master thesis (https://www10.cs.fau.de/publications/theses/2017/Haag_MT_2017.pdf).
+//======================================================================================================================
+
+#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/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm/free_surface/surface_geometry/Utility.h"
+#include "lbm/lattice_model/D3Q19.h"
+
+#include "functionality/AdvectionDynamicsHandler.h"
+#include "functionality/GeometricalErrorEvaluator.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace DeformationField
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+// Lattice model is only created for dummy purposes; no LBM simulation is performed
+using CollisionModel_T      = lbm::collision_model::SRT;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true >;
+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 >;
+
+// function describing the initialization velocity profile (in global cell coordinates)
+inline Vector3< real_t > velocityProfile(Cell globalCell, real_t timePeriod, uint_t timestep,
+                                         const Vector3< real_t >& domainSize)
+{
+   // add 0.5 to get the cell's center
+   const real_t x = (real_c(globalCell.x()) + real_c(0.5)) / domainSize[0];
+   const real_t y = (real_c(globalCell.y()) + real_c(0.5)) / domainSize[1];
+   const real_t z = (real_c(globalCell.z()) + real_c(0.5)) / domainSize[2];
+
+   const real_t timeTerm = real_c(std::cos(math::pi * real_t(timestep) / timePeriod));
+
+   const real_t sinpix = real_c(std::sin(math::pi * x));
+   const real_t sinpiy = real_c(std::sin(math::pi * y));
+   const real_t sinpiz = real_c(std::sin(math::pi * z));
+
+   const real_t sin2pix = real_c(std::sin(real_c(2) * math::pi * x));
+   const real_t sin2piy = real_c(std::sin(real_c(2) * math::pi * y));
+   const real_t sin2piz = real_c(std::sin(real_c(2) * math::pi * z));
+
+   const real_t velocityX = real_c(2) * sinpix * sinpix * sin2piy * sin2piz * timeTerm;
+   const real_t velocityY = -sin2pix * sinpiy * sinpiy * sin2piz * timeTerm;
+   const real_t velocityZ = -sin2pix * sin2piy * sinpiz * sinpiz * timeTerm;
+
+   return Vector3< real_t >(velocityX, velocityY, velocityZ);
+}
+
+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");
+
+   // get domain parameters from parameter file
+   auto domainParameters    = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth");
+
+   const real_t bubbleDiameter          = real_c(domainWidth) * real_c(0.3);
+   const Vector3< real_t > bubbleCenter = domainWidth * Vector3< real_t >(real_c(0.35), real_c(0.35), real_c(0.35));
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = domainWidth;
+   domainSize[1] = domainWidth;
+   domainSize[2] = domainWidth;
+
+   // 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
+   const 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(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleCenter);
+
+   // get physics parameters from parameter file
+   auto physicsParameters                  = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps                  = physicsParameters.getParameter< uint_t >("timesteps");
+   const uint_t timestepsToInitialPosition = physicsParameters.getParameter< uint_t >("timestepsToInitialPosition");
+
+   // compute CFL number
+   const real_t dx_SI = real_c(1) / real_c(domainWidth);
+   const real_t dt_SI = real_c(3) / real_c(timestepsToInitialPosition);
+   const real_t CFL   = dt_SI / dx_SI; // with velocity_SI = 1
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(CFL);
+
+   // dummy collision model (LBM not simulated in this benchmark)
+   const CollisionModel_T collisionModel = CollisionModel_T(real_c(1));
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timestepsToInitialPosition);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+
+   // 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 excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   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");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   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");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel);
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+
+   // add fill level field (initialized with 1, i.e., liquid 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 the velocity profile
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      PdfField_T* const pdfField   = blockIt->getData< PdfField_T >(pdfFieldID);
+      FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+         //  cell in block-local coordinates
+         const Cell localCell = pdfFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = pdfFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM)
+         const Vector3< real_t > initialVelocity =
+            CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), uint_c(0), domainSize);
+         pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1));
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // create the spherical bubble
+   const geometry::Sphere sphereBubble(real_c(domainWidth) * Vector3< real_t >(real_c(0.5), real_c(0.75), real_c(0.25)),
+                                       real_c(domainWidth) * real_c(0.15));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true);
+
+   // 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);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   const ConstBlockDataID initialFillFieldID =
+      field::addCloneToStorage< ScalarField_T >(blockForest, fillFieldID, "Initial fill level field");
+
+   // add bubble model
+   const std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel =
+      std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // add surface geometry handler
+   const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler(
+      blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, false, false, real_c(0));
+
+   geometryHandler.addSweeps(timeloop);
+
+   // get fields created by surface geometry handler
+   const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID();
+
+   // add boundary handling for standard boundaries and free surface boundaries
+   const AdvectionDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, normalFieldID, freeSurfaceBoundaryHandling, bubbleModel,
+      pdfReconstructionModel, excessMassDistributionModel, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold);
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   // add evaluator for geometrical
+   const std::shared_ptr< real_t > geometricalError = std::make_shared< real_t >(real_c(0));
+   const GeometricalErrorEvaluator< FreeSurfaceBoundaryHandling_T, FlagField_T, ScalarField_T >
+      geometricalErrorEvaluator(blockForest, freeSurfaceBoundaryHandling, initialFillFieldID, fillFieldID,
+                                evaluationFrequency, geometricalError);
+   timeloop.addFuncAfterTimeStep(geometricalErrorEvaluator, "Evaluator: geometrical error");
+
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, BlockDataID(),
+      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);
+
+      if (t % evaluationFrequency == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass << "\n\t\texcess mass = "
+                                                   << *excessMass << "\n\t\tgeometrical error = " << *geometricalError);
+      }
+
+      // set the constant velocity profile
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         PdfField_T* const pdfField   = blockIt->getData< PdfField_T >(pdfFieldID);
+         FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+
+         WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+            const Cell localCell = pdfFieldIt.cell();
+
+            // get cell in global coordinates
+            Cell globalCell = pdfFieldIt.cell();
+            blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+            // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM)
+            const Vector3< real_t > velocity =
+               CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), t, domainSize);
+            pdfField->setDensityAndVelocity(localCell, velocity, real_c(1));
+         }) // WALBERLA_FOR_ALL_CELLS
+      }
+
+      pdfCommunication();
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace DeformationField
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::DeformationField::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm
new file mode 100644
index 0000000000000000000000000000000000000000..359fd4a98093e95ace99e755819f022cca40e5c9
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm
@@ -0,0 +1,96 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 25, 25, 25 >;
+   periodicity                   < 1, 1, 1 >;
+}
+
+DomainParameters
+{
+   domainWidth       50;
+}
+
+PhysicsParameters
+{
+   timestepsToInitialPosition 3000;
+   timesteps                  3001;
+
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+}
+
+EvaluationParameters
+{
+   evaluationFrequency 300;
+   performanceLogFrequency 10000;
+}
+
+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 300;
+   baseFolder     mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency       300;
+      ghostLayers          0;
+      baseFolder           vtk-out;
+      samplingResolution   1;
+
+      writers
+      {
+         fill_level;
+         mapped_flag;
+         velocity;
+         density;
+         //curvature;
+         //normal;
+         //obstacle_normal;
+         //pdf;
+         //flag;
+      }
+
+      inclusion_filters
+      {
+         //liquidInterfaceFilter; // only include liquid and interface cells in VTK output
+      }
+
+      before_functions
+      {
+         //ghost_layer_synchronization;   // only needed if writing the ghost layer
+         gas_cell_zero_setter;            // sets velocity=0 and density=1 all gas cells before writing VTK output
+      }
+   }
+
+   domain_decomposition
+   {
+      writeFrequency             0;
+      baseFolder                 vtk-out;
+      outputDomainDecomposition  true;
+   }
+}
\ No newline at end of file
diff --git a/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d3c8e01ed776fac44d0b33f7cc7e94a6508c08ed
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp
@@ -0,0 +1,351 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SingleVortex.cpp
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//
+// This benchmark simulates the advection of a spherical bubble in a vortex field.
+// The vortex field changes periodically so that the bubble returns to its initial position, where it should take its
+// initial form. The relative geometrical error of the bubble's shape after one period is evaluated. There is no LBM
+// flow simulation performed here, it is a test case for the FSLBM's mass advection. This benchmark is based on the
+// two-dimensional test case from Rider and Kothe (doi: 10.1006/jcph.1998.5906). The extension to three-dimensional
+// space is based on the Viktor Haag's master thesis
+// (https://www10.cs.fau.de/publications/theses/2017/Haag_MT_2017.pdf).
+//======================================================================================================================
+
+#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/surface_geometry/SurfaceGeometryHandler.h"
+#include "lbm/free_surface/surface_geometry/Utility.h"
+#include "lbm/lattice_model/D3Q19.h"
+
+#include "functionality/AdvectionDynamicsHandler.h"
+#include "functionality/GeometricalErrorEvaluator.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+namespace SingleVortex
+{
+using ScalarField_T = GhostLayerField< real_t, 1 >;
+using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
+
+// Lattice model is only created for dummy purposes; no LBM simulation is performed
+using CollisionModel_T      = lbm::collision_model::SRT;
+using LatticeModel_T        = lbm::D3Q19< CollisionModel_T, true >;
+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 >;
+
+// function describing the initialization velocity profile (in global cell coordinates)
+inline Vector3< real_t > velocityProfile(Cell globalCell, real_t timePeriod, uint_t timestep,
+                                         const Vector3< real_t >& domainSize)
+{
+   // add 0.5 to get the cell's center
+   const real_t x = (real_c(globalCell.x()) + real_c(0.5)) / domainSize[0];
+   const real_t y = (real_c(globalCell.y()) + real_c(0.5)) / domainSize[1];
+
+   const real_t xToDomainCenter = x - real_c(0.5);
+   const real_t yToDomainCenter = y - real_c(0.5);
+   const real_t r     = real_c(std::sqrt(xToDomainCenter * xToDomainCenter + yToDomainCenter * yToDomainCenter));
+   const real_t rTerm = (real_c(1) - real_c(2) * r) * (real_c(1) - real_c(2) * r);
+
+   const real_t timeTerm = real_c(std::cos(math::pi * real_t(timestep) / timePeriod));
+
+   const real_t velocityX = real_c(std::sin(real_c(2) * math::pi * y)) * real_c(std::sin(math::pi * x)) *
+                            real_c(std::sin(math::pi * x)) * timeTerm;
+   const real_t velocityY = -real_c(std::sin(real_c(2) * math::pi * x)) * real_c(std::sin(math::pi * y)) *
+                            real_c(std::sin(math::pi * y)) * timeTerm;
+   const real_t velocityZ = rTerm * timeTerm;
+
+   return Vector3< real_t >(velocityX, velocityY, velocityZ);
+}
+
+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");
+
+   // get domain parameters from parameter file
+   auto domainParameters    = walberlaEnv.config()->getOneBlock("DomainParameters");
+   const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth");
+
+   const real_t bubbleDiameter          = real_c(domainWidth) * real_c(0.075);
+   const Vector3< real_t > bubbleCenter = domainWidth * Vector3< real_t >(real_c(0.5), real_c(0.75), real_c(0.25));
+
+   // define domain size
+   Vector3< uint_t > domainSize;
+   domainSize[0] = domainWidth;
+   domainSize[1] = domainWidth;
+   domainSize[2] = domainWidth;
+
+   // 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
+   const 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(periodicity);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleCenter);
+
+   // get physics parameters from parameter file
+   auto physicsParameters                  = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps                  = physicsParameters.getParameter< uint_t >("timesteps");
+   const uint_t timestepsToInitialPosition = physicsParameters.getParameter< uint_t >("timestepsToInitialPosition");
+
+   // compute CFL number
+   const real_t dx_SI = real_c(1) / real_c(domainWidth);
+   const real_t dt_SI = real_c(3) / real_c(timestepsToInitialPosition);
+   const real_t CFL   = dt_SI / dx_SI; // with velocity_SI = 1
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(CFL);
+
+   // dummy collision model (LBM not simulated in this benchmark)
+   const CollisionModel_T collisionModel = CollisionModel_T(real_c(1));
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timestepsToInitialPosition);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps);
+
+   // 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 excessMassDistributionModel =
+      modelParameters.getParameter< std::string >("excessMassDistributionModel");
+   const std::string curvatureModel          = modelParameters.getParameter< std::string >("curvatureModel");
+   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");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange);
+   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");
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
+
+   // create non-uniform block forest (non-uniformity required for load balancing)
+   const std::shared_ptr< StructuredBlockForest > blockForest =
+      createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity);
+
+   // create lattice model
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel);
+
+   // add pdf field
+   const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx);
+
+   // add fill level field (initialized with 1, i.e., liquid 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 the velocity profile
+   for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+   {
+      PdfField_T* const pdfField   = blockIt->getData< PdfField_T >(pdfFieldID);
+      FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+
+      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+         //  cell in block-local coordinates
+         const Cell localCell = pdfFieldIt.cell();
+
+         // get cell in global coordinates
+         Cell globalCell = pdfFieldIt.cell();
+         blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+         // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM)
+         const Vector3< real_t > initialVelocity =
+            CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), uint_c(0), domainSize);
+         pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1));
+      }) // WALBERLA_FOR_ALL_CELLS
+   }
+
+   // create the spherical bubble
+   const geometry::Sphere sphereBubble(real_c(domainWidth) * Vector3< real_t >(real_c(0.5), real_c(0.75), real_c(0.25)),
+                                       real_c(domainWidth) * real_c(0.15));
+   bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true);
+
+   // 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);
+   communication();
+
+   PdfCommunication_T pdfCommunication(blockForest, pdfFieldID);
+   pdfCommunication();
+
+   const ConstBlockDataID initialFillFieldID =
+      field::addCloneToStorage< ScalarField_T >(blockForest, fillFieldID, "Initial fill level field");
+
+   // add bubble model
+   const std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel =
+      std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1));
+
+   // create timeloop
+   SweepTimeloop timeloop(blockForest, timesteps);
+
+   // add surface geometry handler
+   const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler(
+      blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, false, false, real_c(0));
+
+   geometryHandler.addSweeps(timeloop);
+
+   // get fields created by surface geometry handler
+   const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID();
+
+   // add boundary handling for standard boundaries and free surface boundaries
+   const AdvectionDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, normalFieldID, freeSurfaceBoundaryHandling, bubbleModel,
+      pdfReconstructionModel, excessMassDistributionModel, useSimpleMassExchange, cellConversionThreshold,
+      cellConversionForceThreshold);
+
+   dynamicsHandler.addSweeps(timeloop);
+
+   // add evaluator for geometrical
+   const std::shared_ptr< real_t > geometricalError = std::make_shared< real_t >(real_c(0));
+   const GeometricalErrorEvaluator< FreeSurfaceBoundaryHandling_T, FlagField_T, ScalarField_T >
+      geometricalErrorEvaluator(blockForest, freeSurfaceBoundaryHandling, initialFillFieldID, fillFieldID,
+                                evaluationFrequency, geometricalError);
+   timeloop.addFuncAfterTimeStep(geometricalErrorEvaluator, "Evaluator: geometrical error");
+
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
+   // add VTK output
+   addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, BlockDataID(),
+      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);
+
+      if (t % evaluationFrequency == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass << "\n\t\texcess mass = "
+                                                   << *excessMass << "\n\t\tgeometrical error = " << *geometricalError);
+      }
+
+      // set the constant velocity profile
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         PdfField_T* const pdfField   = blockIt->getData< PdfField_T >(pdfFieldID);
+         FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+
+         WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+            const Cell localCell = pdfFieldIt.cell();
+
+            // get cell in global coordinates
+            Cell globalCell = pdfFieldIt.cell();
+            blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+            // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM)
+            const Vector3< real_t > velocity =
+               CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), t, domainSize);
+            pdfField->setDensityAndVelocity(localCell, velocity, real_c(1));
+         }) // WALBERLA_FOR_ALL_CELLS
+      }
+
+      pdfCommunication();
+
+      if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace SingleVortex
+} // namespace free_surface
+} // namespace walberla
+
+int main(int argc, char** argv) { return walberla::free_surface::SingleVortex::main(argc, argv); }
\ No newline at end of file
diff --git a/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm
new file mode 100644
index 0000000000000000000000000000000000000000..359fd4a98093e95ace99e755819f022cca40e5c9
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm
@@ -0,0 +1,96 @@
+BlockForestParameters
+{
+   cellsPerBlock                 < 25, 25, 25 >;
+   periodicity                   < 1, 1, 1 >;
+}
+
+DomainParameters
+{
+   domainWidth       50;
+}
+
+PhysicsParameters
+{
+   timestepsToInitialPosition 3000;
+   timesteps                  3001;
+
+}
+
+ModelParameters
+{
+   pdfReconstructionModel        OnlyMissing;
+   excessMassDistributionModel   EvenlyAllInterface;
+   curvatureModel                FiniteDifferenceMethod;
+   useSimpleMassExchange         false;
+   cellConversionThreshold       1e-2;
+   cellConversionForceThreshold  1e-1;
+}
+
+EvaluationParameters
+{
+   evaluationFrequency 300;
+   performanceLogFrequency 10000;
+}
+
+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 300;
+   baseFolder     mesh-out;
+}
+
+VTK
+{
+   fluid_field
+   {
+      writeFrequency       300;
+      ghostLayers          0;
+      baseFolder           vtk-out;
+      samplingResolution   1;
+
+      writers
+      {
+         fill_level;
+         mapped_flag;
+         velocity;
+         density;
+         //curvature;
+         //normal;
+         //obstacle_normal;
+         //pdf;
+         //flag;
+      }
+
+      inclusion_filters
+      {
+         //liquidInterfaceFilter; // only include liquid and interface cells in VTK output
+      }
+
+      before_functions
+      {
+         //ghost_layer_synchronization;   // only needed if writing the ghost layer
+         gas_cell_zero_setter;            // sets velocity=0 and density=1 all gas cells before writing VTK output
+      }
+   }
+
+   domain_decomposition
+   {
+      writeFrequency             0;
+      baseFolder                 vtk-out;
+      outputDomainDecomposition  true;
+   }
+}
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/ZalesakDisk.cpp b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.cpp
similarity index 65%
rename from apps/showcases/FreeSurface/ZalesakDisk.cpp
rename to apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.cpp
index edbaab294c474804a66bfc56a0066b7e73ce301e..f2177d55950a4a2a57e113b7dcc12344aa4b459b 100644
--- a/apps/showcases/FreeSurface/ZalesakDisk.cpp
+++ b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.cpp
@@ -16,12 +16,14 @@
 //! \file ZalesakDisk.cpp
 //! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
 //
-// This showcase simulates a slotted disk of gas in a constant rotating velocity field. This benchmark is commonly
-// referred to as Zalesak's rotating disk (see doi: 10.1016/0021-9991(79)90051-2).
+// This benchmark simulates the advection of a slotted disk of gas in a constant rotating velocity field. The disk
+// returns to its initial position, where it should take its initial form. The relative geometrical error of the
+// bubble's shape after one rotation is evaluated. There is no LBM flow simulation performed here, it is a test case for
+// the FSLBM's mass advection. This benchmark is commonly referred to as Zalesak's rotating disk (see
+// doi: 10.1016/0021-9991(79)90051-2). The setup chosen here is identical to the one used by Janssen (see
+// doi: 10.1016/j.camwa.2009.08.064).
 //======================================================================================================================
 
-#include "blockforest/Initialization.h"
-
 #include "core/Environment.h"
 
 #include "field/Gather.h"
@@ -33,11 +35,13 @@
 #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 "functionality/AdvectionDynamicsHandler.h"
+#include "functionality/GeometricalErrorEvaluator.h"
+
 namespace walberla
 {
 namespace free_surface
@@ -47,9 +51,9 @@ namespace ZalesakDisk
 using ScalarField_T = GhostLayerField< real_t, 1 >;
 using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >;
 
+// Lattice model is only created for dummy purposes; no LBM simulation is performed
 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 LatticeModel_T        = lbm::D2Q9< CollisionModel_T, true >;
 using LatticeModelStencil_T = LatticeModel_T::Stencil;
 using PdfField_T            = lbm::PdfField< LatticeModel_T >;
 using PdfCommunication_T    = blockforest::SimpleCommunication< LatticeModelStencil_T >;
@@ -65,7 +69,7 @@ using FlagField_T                   = FlagField< flag_t >;
 using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >;
 
 // function describing the initialization velocity profile (in global cell coordinates)
-inline Vector3< real_t > velocityProfile(real_t angularVelocity, Cell globalCell, Vector3< real_t > domainCenter)
+inline Vector3< real_t > velocityProfile(real_t angularVelocity, Cell globalCell, const Vector3< real_t >& domainCenter)
 {
    // add 0.5 to get Cell's center
    const real_t velocityX = -angularVelocity * ((real_c(globalCell.y()) + real_c(0.5)) - domainCenter[0]);
@@ -84,21 +88,19 @@ int main(int argc, char** argv)
    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");
+   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");
 
    // get domain parameters from parameter file
    auto domainParameters    = walberlaEnv.config()->getOneBlock("DomainParameters");
    const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth");
 
-   const real_t diskRadius = real_c(domainWidth) * real_c(0.15);
+   const real_t diskRadius = real_c(domainWidth) * real_c(0.125);
    const Vector3< real_t > diskCenter =
       Vector3< real_t >(real_c(domainWidth) * real_c(0.5), real_c(domainWidth) * real_c(0.75), real_c(0.5));
-   const real_t diskSlotLength = real_c(0.25) * real_c(domainWidth);
-   const real_t diskSlotWidth  = real_c(0.05) * real_c(domainWidth);
+   const real_t diskSlotLength = real_c(2) * diskRadius - real_c(0.1) * real_c(domainWidth);
+   const real_t diskSlotWidth  = real_c(0.06) * real_c(domainWidth);
 
    // define domain size
    Vector3< uint_t > domainSize;
@@ -115,7 +117,7 @@ int main(int argc, char** argv)
    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());
+   const 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 "
@@ -127,79 +129,68 @@ int main(int argc, char** argv)
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks);
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth);
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity);
-   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency);
-   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics);
+
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskRadius);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskCenter);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskSlotLength);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskSlotWidth);
 
    // get physics parameters from parameter file
-   auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters");
-   const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps");
+   auto physicsParameters             = walberlaEnv.config()->getOneBlock("PhysicsParameters");
+   const uint_t timesteps             = physicsParameters.getParameter< uint_t >("timesteps");
+   const uint_t timestepsFullRotation = physicsParameters.getParameter< uint_t >("timestepsFullRotation");
 
-   const real_t relaxationRate           = physicsParameters.getParameter< real_t >("relaxationRate");
-   const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate);
-   const real_t viscosity                = collisionModel.viscosity();
+   // compute CFL number
+   const real_t dx_SI = real_c(4) / real_c(domainWidth);
+   const real_t dt_SI = real_c(12.59652) / real_c(timestepsFullRotation);
+   const real_t CFL   = dt_SI / dx_SI; // with velocity_SI = 1
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(CFL);
 
-   const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber");
-   const real_t angularVelocity =
-      reynoldsNumber * viscosity / (real_c(0.5) * real_c(domainWidth) * real_c(domainWidth));
-   const Vector3< real_t > force(real_c(0), real_c(0), real_c(0));
+   // dummy collision model (LBM not simulated in this benchmark)
+   const CollisionModel_T collisionModel = CollisionModel_T(real_c(1));
 
-   const bool enableWetting  = physicsParameters.getParameter< bool >("enableWetting");
-   const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle");
+   const real_t angularVelocity = real_c(2) * math::pi / real_c(timestepsFullRotation);
 
-   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(timestepsFullRotation);
    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(angularVelocity);
-   WALBERLA_LOG_DEVEL_ON_ROOT("Timesteps for full rotation " << real_c(2) * math::pi / angularVelocity);
 
    // 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 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(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");
 
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
 
    // 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 forceDensityFieldID =
-      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(forceDensityFieldID));
+   const LatticeModel_T latticeModel = LatticeModel_T(collisionModel);
 
    // 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)
+   // add fill level field (initialized with 1, i.e., liquid everywhere)
    const BlockDataID fillFieldID =
       field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2));
 
@@ -212,10 +203,9 @@ int main(int argc, char** argv)
    // initialize the velocity profile
    for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
    {
-      PdfField_T* const pdfField   = blockIt->getData< PdfField_T >(pdfFieldID);
-      FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
+      PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID);
 
-      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
+      WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, {
          //  cell in block-local coordinates
          const Cell localCell = pdfFieldIt.cell();
 
@@ -252,66 +242,57 @@ int main(int argc, char** argv)
    freeSurfaceBoundaryHandling->initFlagsFromFillLevel();
 
    // communication after initialization
-   Communication_T communication(blockForest, flagFieldID, fillFieldID, forceDensityFieldID);
+   Communication_T communication(blockForest, flagFieldID, fillFieldID);
    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< LatticeModelStencil_T > > bubbleModelDerived =
-         std::make_shared< bubble_model::BubbleModel< LatticeModelStencil_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)); }
+   const ConstBlockDataID initialFillFieldID =
+      field::addCloneToStorage< ScalarField_T >(blockForest, fillFieldID, "Initial fill level field");
 
-   // 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);
+   // add bubble model
+   const std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel =
+      std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1));
 
    // 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);
+      blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, false, false, real_c(0));
 
    geometryHandler.addSweeps(timeloop);
 
    // get fields created by surface geometry handler
-   const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID();
-   const ConstBlockDataID normalFieldID    = geometryHandler.getConstNormalFieldID();
+   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, forceDensityFieldID, normalFieldID, curvatureFieldID,
-      freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel,
-      relaxationRate, force, surfaceTension, useSimpleMassExchange, cellConversionThreshold,
+   const AdvectionDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler(
+      blockForest, pdfFieldID, flagFieldID, fillFieldID, normalFieldID, freeSurfaceBoundaryHandling, bubbleModel,
+      pdfReconstructionModel, excessMassDistributionModel, 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 evaluator for geometrical
+   const std::shared_ptr< real_t > geometricalError = std::make_shared< real_t >(real_c(0));
+   const GeometricalErrorEvaluator< FreeSurfaceBoundaryHandling_T, FlagField_T, ScalarField_T >
+      geometricalErrorEvaluator(blockForest, freeSurfaceBoundaryHandling, initialFillFieldID, fillFieldID,
+                                evaluationFrequency, geometricalError);
+   timeloop.addFuncAfterTimeStep(geometricalErrorEvaluator, "Evaluator: geometrical errors");
+
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
 
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
-      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
+      blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, BlockDataID(),
       geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(),
       geometryHandler.getObstNormalFieldID());
 
@@ -332,6 +313,12 @@ int main(int argc, char** argv)
    {
       timeloop.singleStep(timingPool, true);
 
+      if (t % evaluationFrequency == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass << "\n\t\texcess mass = "
+                                                   << *excessMass << "\n\t\tgeometrical error = " << *geometricalError);
+      }
+
       // set the constant velocity profile
       for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
       {
@@ -339,22 +326,20 @@ int main(int argc, char** argv)
          FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID);
 
          WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, {
-            if (flagInfo.isInterface(flagFieldIt) || flagInfo.isLiquid(flagFieldIt))
-            {
-               //  cell in block-local coordinates
-               const Cell localCell = pdfFieldIt.cell();
-
-               // get cell in global coordinates
-               Cell globalCell = pdfFieldIt.cell();
-               blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
-
-               // set velocity profile
-               const Vector3< real_t > initialVelocity = velocityProfile(angularVelocity, globalCell, domainCenter);
-               pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1));
-            }
+            const Cell localCell = pdfFieldIt.cell();
+
+            // get cell in global coordinates
+            Cell globalCell = pdfFieldIt.cell();
+            blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell);
+
+            // set velocity profile
+            const Vector3< real_t > initialVelocity = velocityProfile(angularVelocity, globalCell, domainCenter);
+            pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1));
          }) // WALBERLA_FOR_ALL_CELLS
       }
 
+      pdfCommunication();
+
       if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
    }
 
diff --git a/apps/showcases/FreeSurface/ZalesakDisk.prm b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.prm
similarity index 63%
rename from apps/showcases/FreeSurface/ZalesakDisk.prm
rename to apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.prm
index b9af1280c96a12bba2f0ae5725675ef6f34e7429..857f1510bcd1d0a9166b43b8e1dc24d227dfb181 100644
--- a/apps/showcases/FreeSurface/ZalesakDisk.prm
+++ b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.prm
@@ -1,53 +1,46 @@
 BlockForestParameters
 {
-   cellsPerBlock                 < 50, 50, 1 >;
-   periodicity                   < 0, 0, 1 >;
-   loadBalancingFrequency        0;
-   printLoadBalancingStatistics  false;
+   cellsPerBlock                 < 100, 100, 1 >;
+   periodicity                   < 0, 0, 0 >;
 }
 
 DomainParameters
 {
-   domainWidth       100;
+   domainWidth       200;
 }
 
 PhysicsParameters
 {
-   reynoldsNumber    100; // Re = angularVelocity * domainWidth * 0.5 * domainWidth / kin. viscosity
-   relaxationRate    1.8;
-   enableWetting     false;
-   contactAngle      0; // only used if enableWetting=true
-   timesteps         1000000;
+   timestepsFullRotation 12570;  // angularVelocity = 2 * pi / timestepsFullRotation
+   timesteps             12571;
+
 }
 
 ModelParameters
 {
    pdfReconstructionModel        OnlyMissing;
-   pdfRefillingModel             EquilibriumRefilling;
    excessMassDistributionModel   EvenlyAllInterface;
    curvatureModel                FiniteDifferenceMethod;
    useSimpleMassExchange         false;
    cellConversionThreshold       1e-2;
    cellConversionForceThreshold  1e-1;
-
-   enableBubbleModel             True;
-   enableBubbleSplits            false; // only used if enableBubbleModel=true
 }
 
 EvaluationParameters
 {
+   evaluationFrequency 12570;
    performanceLogFrequency 10000;
 }
 
 BoundaryParameters
 {
    // X
-   Border { direction W;  walldistance -1; FreeSlip{} }
-   Border { direction E;  walldistance -1; FreeSlip{} }
+   //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{} }
+   //Border { direction N;  walldistance -1; FreeSlip{} }
+   //Border { direction S;  walldistance -1; FreeSlip{} }
 
    // Z
    //Border { direction T;  walldistance -1; FreeSlip{} }
@@ -64,7 +57,7 @@ VTK
 {
    fluid_field
    {
-      writeFrequency       4241;
+      writeFrequency       12570;
       ghostLayers          0;
       baseFolder           vtk-out;
       samplingResolution   1;
@@ -80,7 +73,6 @@ VTK
          //obstacle_normal;
          //pdf;
          //flag;
-         //force_density;
       }
 
       inclusion_filters
diff --git a/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h
new file mode 100644
index 0000000000000000000000000000000000000000..80db158e6b6f341e3f8d637efa511e0a709dfa2b
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h
@@ -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 AdvectSweep.h
+//! \ingroup free_surface
+//! \author Martin Bauer
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Sweep for mass advection and interface cell conversion marking (simplified StreamReconstructAdvectSweep).
+//
+//======================================================================================================================
+
+#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/free_surface/dynamics/PdfReconstructionModel.h"
+#include "lbm/free_surface/dynamics/functionality/AdvectMass.h"
+#include "lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h"
+#include "lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h"
+#include "lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h"
+#include "lbm/sweeps/StreamPull.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 >
+class AdvectSweep
+{
+ public:
+   using flag_t     = typename FlagInfo_T::flag_t;
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+   AdvectSweep(BlockDataID handlingID, BlockDataID fillFieldID, BlockDataID flagFieldID, BlockDataID pdfField,
+               const FlagInfo_T& flagInfo, const PdfReconstructionModel& pdfReconstructionModel,
+               bool useSimpleMassExchange, real_t cellConversionThreshold, real_t cellConversionForceThreshold)
+      : handlingID_(handlingID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), pdfFieldID_(pdfField),
+        flagInfo_(flagInfo), neighborhoodFlagFieldClone_(flagFieldID), fillFieldClone_(fillFieldID),
+        pdfFieldClone_(pdfField), pdfReconstructionModel_(pdfReconstructionModel),
+        useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold),
+        cellConversionForceThreshold_(cellConversionForceThreshold)
+   {}
+
+   void operator()(IBlock* const block);
+
+ protected:
+   BlockDataID handlingID_;
+   BlockDataID fillFieldID_;
+   BlockDataID flagFieldID_;
+   BlockDataID pdfFieldID_;
+
+   FlagInfo_T flagInfo_;
+
+   // efficient clones of fields to provide temporary fields (for writing to)
+   field::FieldClone< FlagField_T, true > neighborhoodFlagFieldClone_;
+   field::FieldClone< ScalarField_T, true > fillFieldClone_;
+   field::FieldClone< PdfField_T, true > pdfFieldClone_;
+
+   PdfReconstructionModel pdfReconstructionModel_;
+   bool useSimpleMassExchange_;
+   real_t cellConversionThreshold_;
+   real_t cellConversionForceThreshold_;
+}; // class AdvectSweep
+
+template< typename LatticeModel_T, typename BoundaryHandling_T, typename FlagField_T, typename FlagInfo_T,
+          typename ScalarField_T, typename VectorField_T >
+void AdvectSweep< LatticeModel_T, BoundaryHandling_T, FlagField_T, FlagInfo_T, ScalarField_T,
+                  VectorField_T >::operator()(IBlock* const block)
+{
+   // fetch data
+   ScalarField_T* const fillSrcField = block->getData< ScalarField_T >(fillFieldID_);
+   PdfField_T* const pdfSrcField     = block->getData< PdfField_T >(pdfFieldID_);
+
+   FlagField_T* const flagField = block->getData< FlagField_T >(flagFieldID_);
+
+   // temporary fields that act as destination fields
+   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(
+      pdfSrcFieldIt, pdfSrcField, fillSrcFieldIt, fillSrcField, fillDstFieldIt, fillDstField, flagFieldIt, flagField,
+      neighborhoodFlagFieldIt, neighborhoodFlagField, omp critical, {
+         if (flagInfo_.isInterface(flagFieldIt))
+         {
+            const real_t rho = lbm::getDensity(pdfSrcField->latticeModel(), pdfSrcFieldIt);
+
+            // 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 + deltaMass / rho;
+         }
+         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)) { *fillDstFieldIt = real_c(1); }
+               else // flag is e.g. obstacle or outflow
+               {
+                  *fillDstFieldIt = *fillSrcFieldIt;
+               }
+            }
+         }
+      }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP
+
+   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/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.h b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..1e48a1683a0bde1ed7392cf3b0e295c5189abf72
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.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 AdvectionDynamicsHandler.h
+//! \ingroup surface_dynamics
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Handles free surface advection (without LBM flow simulation; this is a simplified SurfaceDynamicsHandler).
+//
+//======================================================================================================================
+
+#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/free_surface/dynamics/CellConversionSweep.h"
+#include "lbm/free_surface/dynamics/ConversionFlagsResetSweep.h"
+#include "lbm/free_surface/dynamics/ExcessMassDistributionModel.h"
+#include "lbm/free_surface/dynamics/ExcessMassDistributionSweep.h"
+#include "lbm/free_surface/dynamics/PdfReconstructionModel.h"
+#include "lbm/sweeps/CellwiseSweep.h"
+#include "lbm/sweeps/SweepWrappers.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "AdvectSweep.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T >
+class AdvectionDynamicsHandler
+{
+ 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:
+   AdvectionDynamicsHandler(const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID,
+                            BlockDataID flagFieldID, BlockDataID fillFieldID, ConstBlockDataID normalFieldID,
+                            const std::shared_ptr< FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                            const std::shared_ptr< BubbleModelBase >& bubbleModel,
+                            const std::string& pdfReconstructionModel, const std::string& excessMassDistributionModel,
+                            bool useSimpleMassExchange, real_t cellConversionThreshold,
+                            real_t cellConversionForceThreshold)
+      : blockForest_(blockForest), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID),
+        normalFieldID_(normalFieldID), bubbleModel_(bubbleModel),
+        freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfReconstructionModel_(pdfReconstructionModel),
+        excessMassDistributionModel_({ excessMassDistributionModel }), useSimpleMassExchange_(useSimpleMassExchange),
+        cellConversionThreshold_(cellConversionThreshold), cellConversionForceThreshold_(cellConversionForceThreshold)
+   {
+      WALBERLA_CHECK(LatticeModel_T::compressible,
+                     "The free surface lattice Boltzmann extension works only with compressible LBM models.");
+
+      if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType())
+      {
+         // add additional field for storing excess mass in liquid cells
+         excessMassFieldID_ =
+            field::addToStorage< ScalarField_T >(blockForest_, "Excess mass", real_c(0), field::fzyx, uint_c(1));
+      }
+   }
+
+   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*) {};
+
+      // 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 AdvectSweep< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, FlagField_T,
+                         typename FreeSurfaceBoundaryHandling_T::FlagInfo_T, ScalarField_T, VectorField_T >
+         advectSweep(freeSurfaceBoundaryHandling_->getHandlingID(), fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo,
+                     pdfReconstructionModel_, useSimpleMassExchange_, cellConversionThreshold_,
+                     cellConversionForceThreshold_);
+      // sweep acts only on blocks with at least one interface cell (due to StateSweep::fullFreeSurface)
+      timeloop.add()
+         << Sweep(advectSweep, "Sweep: Advect", StateSweep::fullFreeSurface)
+         << Sweep(emptySweep, "Empty sweep: Advect")
+         // 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 Advect sweep")
+         << AfterFunction(blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_),
+                          "Second ghost layer update: after Advect sweep (fill level field)")
+         << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_),
+                          "Second ghost layer update: after Advect sweep (flag field)");
+
+      // 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)");
+
+      // distribute excess mass:
+      // - excess mass: mass that is free after conversion from interface to gas/liquid cells
+      // - update the bubble model
+      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_.isEvenlyAllInterfaceFallbackLiquidType())
+            {
+               const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T,
+                                                                    VectorField_T >
+                  distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo,
+                                      excessMassFieldID_);
+               timeloop.add()
+                  // perform this sweep also on "onlyLBM" blocks because liquid cells also exchange excess mass here
+                  << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface)
+                  << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::onlyLBM)
+                  << Sweep(emptySweep, "Empty sweep: distribute excess mass")
+                  << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, excessMassFieldID_),
+                                   "Communication: after excess mass distribution sweep")
+                  << AfterFunction(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_;
+
+   ConstBlockDataID normalFieldID_;
+
+   std::shared_ptr< BubbleModelBase > bubbleModel_;
+   std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+
+   PdfReconstructionModel pdfReconstructionModel_;
+   ExcessMassDistributionModel excessMassDistributionModel_;
+
+   bool useSimpleMassExchange_;
+   real_t cellConversionThreshold_;
+   real_t cellConversionForceThreshold_;
+
+   BlockDataID excessMassFieldID_ = BlockDataID();
+}; // class AdvectionDynamicsHandler
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h b/apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h
new file mode 100644
index 0000000000000000000000000000000000000000..b0a5d7bbae749142f0a25c108a35b274ef146038
--- /dev/null
+++ b/apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h
@@ -0,0 +1,139 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file GeometricalErrorEvaluator.h
+//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de>
+//! \brief Compute the geometrical error in free-surface advection test cases.
+//======================================================================================================================
+
+#include "blockforest/StructuredBlockForest.h"
+
+#include "core/DataTypes.h"
+
+#include "domain_decomposition/BlockDataID.h"
+
+#include "field/iterators/IteratorMacros.h"
+
+namespace walberla
+{
+namespace free_surface
+{
+template< typename FreeSurfaceBoundaryHandling_T, typename FlagField_T, typename ScalarField_T >
+class GeometricalErrorEvaluator
+{
+ public:
+   GeometricalErrorEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest,
+                             const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling,
+                             const ConstBlockDataID& initialfillFieldID, const ConstBlockDataID& fillFieldID,
+                             uint_t frequency, const std::shared_ptr< real_t >& geometricalError)
+      : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling),
+        initialFillFieldID_(initialfillFieldID), fillFieldID_(fillFieldID), frequency_(frequency),
+        geometricalError_(geometricalError), 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))
+      {
+         computeInitialFillLevelSum(blockForest, freeSurfaceBoundaryHandling);
+         computeError(blockForest, freeSurfaceBoundaryHandling);
+      }
+      else
+      {
+         // only evaluate in given frequencies
+         if (executionCounter_ % frequency_ == uint_c(0)) { computeError(blockForest, freeSurfaceBoundaryHandling); }
+      }
+
+      ++executionCounter_;
+   }
+
+   void computeInitialFillLevelSum(
+      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();
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         const ScalarField_T* const initialfillField = blockIt->getData< const ScalarField_T >(initialFillFieldID_);
+         const FlagField_T* const flagField          = blockIt->getData< const FlagField_T >(flagFieldID);
+
+         // avoid OpenMP here because initialFillLevelSum_ is a class member and not a regular variable
+         WALBERLA_FOR_ALL_CELLS_OMP(initialfillFieldIt, initialfillField, flagFieldIt, flagField, omp critical, {
+            if (flagInfo.isInterface(flagFieldIt) || flagInfo.isLiquid(flagFieldIt))
+            {
+               initialFillLevelSum_ += *initialfillFieldIt;
+            }
+         }) // WALBERLA_FOR_ALL_CELLS
+      }
+
+      mpi::allReduceInplace< real_t >(initialFillLevelSum_, mpi::SUM);
+   }
+
+   void computeError(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 geometricalError = real_c(0);
+      real_t fillLevelSum     = real_c(0);
+
+      for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
+      {
+         const ScalarField_T* const initialfillField = blockIt->getData< const ScalarField_T >(initialFillFieldID_);
+         const ScalarField_T* const fillField        = blockIt->getData< const ScalarField_T >(fillFieldID_);
+         const FlagField_T* const flagField          = blockIt->getData< const FlagField_T >(flagFieldID);
+
+         WALBERLA_FOR_ALL_CELLS_OMP(initialfillFieldIt, initialfillField, fillFieldIt, fillField, flagFieldIt,
+                                    flagField, omp parallel for schedule(static) reduction(+:geometricalError)
+                                                                                 reduction(+:fillLevelSum), {
+            if (flagInfo.isInterface(flagFieldIt) || flagInfo.isLiquid(flagFieldIt))
+            {
+               geometricalError += real_c(std::abs(*initialfillFieldIt - *fillFieldIt));
+               fillLevelSum += *fillFieldIt;
+            }
+         }) // WALBERLA_FOR_ALL_CELLS
+      }
+
+      mpi::allReduceInplace< real_t >(geometricalError, mpi::SUM);
+
+      // compute L1 norms
+      *geometricalError_ = geometricalError / initialFillLevelSum_;
+   }
+
+ private:
+   std::weak_ptr< StructuredBlockForest > blockForest_;
+   std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_;
+   ConstBlockDataID initialFillFieldID_;
+   ConstBlockDataID fillFieldID_;
+   uint_t frequency_;
+   std::shared_ptr< real_t > geometricalError_;
+
+   uint_t executionCounter_;
+   real_t initialFillLevelSum_ = real_c(0);
+}; // class GeometricalErrorEvaluator
+
+} // namespace free_surface
+} // namespace walberla
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.cpp b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp
index e27edf893862b3a6569b631484d507e662c22328..e54117c159c3ac0dc4de3f507e642ead83f474af 100644
--- a/apps/showcases/FreeSurface/BubblyPoiseuille.cpp
+++ b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp
@@ -28,6 +28,7 @@
 #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"
@@ -195,8 +196,10 @@ int main(int argc, char** argv)
    // 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");
 
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
 
    // create non-uniform block forest (non-uniformity required for load balancing)
    const std::shared_ptr< StructuredBlockForest > blockForest =
@@ -327,6 +330,14 @@ int main(int argc, char** argv)
       loadBalancingFrequency, printLoadBalancingStatistics);
    timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -348,11 +359,13 @@ int main(int argc, char** argv)
 
    for (uint_t t = uint_c(0); t != timesteps; ++t)
    {
-      if (t % uint_c(real_c(timesteps / 100)) == uint_c(0))
+      timeloop.singleStep(timingPool, true);
+
+      if (t % evaluationFrequency == uint_c(0))
       {
-         WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t);
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass
+                                                   << "\n\t\texcess mass = " << *excessMass);
       }
-      timeloop.singleStep(timingPool, true);
 
       if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
    }
diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.prm b/apps/showcases/FreeSurface/BubblyPoiseuille.prm
index e4fb01b3df536afc8512ee9bb5b59711a8aee4fa..5b3c7728fe3a3b21f29fcf66edc19cfad9a4a4c3 100644
--- a/apps/showcases/FreeSurface/BubblyPoiseuille.prm
+++ b/apps/showcases/FreeSurface/BubblyPoiseuille.prm
@@ -39,6 +39,7 @@ ModelParameters
 
 EvaluationParameters
 {
+   evaluationFrequency 5000;
    performanceLogFrequency 5000;
 }
 
diff --git a/apps/showcases/FreeSurface/CMakeLists.txt b/apps/showcases/FreeSurface/CMakeLists.txt
index b0dd586089fea55b83e95e1163772371a6b030cf..34bed9ecd86323ffd5f914a757ca1d4453d4fbe7 100644
--- a/apps/showcases/FreeSurface/CMakeLists.txt
+++ b/apps/showcases/FreeSurface/CMakeLists.txt
@@ -39,14 +39,14 @@ if( WALBERLA_BUILD_WITH_CODEGEN )
                                    GravityWaveLatticeModelGeneration)
 endif()
 
+waLBerla_add_executable(NAME    MovingDrop
+                        FILES   MovingDrop.cpp
+                        DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
+
 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)
-
-waLBerla_add_executable(NAME    ZalesakDisk
-                        FILES   ZalesakDisk.cpp
                         DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk)
\ No newline at end of file
diff --git a/apps/showcases/FreeSurface/CapillaryWave.cpp b/apps/showcases/FreeSurface/CapillaryWave.cpp
index 64148c5b313fbb4f1c9e31eed5b58b004d49d6ac..376a8d9cb0810f76a58cc1485bf7cb4bedf67c55 100644
--- a/apps/showcases/FreeSurface/CapillaryWave.cpp
+++ b/apps/showcases/FreeSurface/CapillaryWave.cpp
@@ -391,6 +391,14 @@ int main(int argc, char** argv)
 
    dynamicsHandler.addSweeps(timeloop);
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add load balancing
    LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer(
       blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5),
@@ -436,9 +444,10 @@ int main(int argc, char** argv)
          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);
+            WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\ttNonDimensional = " << tNonDimensional
+                                              << "\n\t\tpositionNonDimensional = " << positionNonDimensional
+                                              << "\n\t\ttotal mass = " << *totalMass
+                                              << "\n\t\texcess mass = " << *excessMass);
             writeVectorToFile(resultVector, filename);
          }
       }
diff --git a/apps/showcases/FreeSurface/DamBreakCylindrical.cpp b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp
index f48e23232d26c404a9ffb70350d307ee591e1b19..0cf75946683d43aec338c23545e4239558257398 100644
--- a/apps/showcases/FreeSurface/DamBreakCylindrical.cpp
+++ b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp
@@ -32,6 +32,7 @@
 #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"
@@ -527,6 +528,14 @@ int main(int argc, char** argv)
       evaluationFrequency, columnRadiusSample);
    timeloop.addFuncAfterTimeStep(columnRadiusEvaluator, "Evaluator: radius");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -571,13 +580,14 @@ int main(int argc, char** argv)
             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()
          {
+            WALBERLA_LOG_DEVEL("time step =" << t);
+            WALBERLA_LOG_DEVEL("\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 << "\n\t\ttotal mass = " << *totalMass
+                                          << "\n\t\texcess mass = " << *excessMass);
+
             const std::vector< real_t > resultVector{ T, Z_mean, Z_max, Z_min, Z_stdDeviation, H };
             writeNumberVector(resultVector, t, filename);
          }
diff --git a/apps/showcases/FreeSurface/DamBreakRectangular.cpp b/apps/showcases/FreeSurface/DamBreakRectangular.cpp
index e7abd62e6b9d605db803f8f680173c599cce61d8..fba4e95798adb5973e0f0aaa842ecdc45cf60b47 100644
--- a/apps/showcases/FreeSurface/DamBreakRectangular.cpp
+++ b/apps/showcases/FreeSurface/DamBreakRectangular.cpp
@@ -512,6 +512,14 @@ int main(int argc, char** argv)
       blockForest, freeSurfaceBoundaryHandling, domainSize, evaluationFrequency, currentColumnWidth);
    timeloop.addFuncAfterTimeStep(widthEvaluator, "Evaluator: column width");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -543,9 +551,14 @@ int main(int argc, char** argv)
          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); }
+         WALBERLA_ROOT_SECTION()
+         {
+            WALBERLA_LOG_DEVEL("time step =" << t);
+            WALBERLA_LOG_DEVEL("\t\tT = " << T << "\n\t\tZ = " << Z << "\n\t\tH = " << H << "\n\t\ttotal mass = "
+                                          << *totalMass << "\n\t\texcess mass = " << *excessMass);
+
+            writeNumberVector(resultVector, t, filename);
+         }
 
          // simulation is considered converged
          if (Z >= real_c(domainSize[0]) / columnWidth - real_c(0.5))
diff --git a/apps/showcases/FreeSurface/DropImpact.cpp b/apps/showcases/FreeSurface/DropImpact.cpp
index 896c5b098b6a0e44edf6e6983b650aa25cdb4c90..61da9474b18682bf4e26e075d29bc917172df934 100644
--- a/apps/showcases/FreeSurface/DropImpact.cpp
+++ b/apps/showcases/FreeSurface/DropImpact.cpp
@@ -29,6 +29,7 @@
 #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"
@@ -202,8 +203,10 @@ int main(int argc, char** argv)
    // 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");
 
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
 
    // create non-uniform block forest (non-uniformity required for load balancing)
    const std::shared_ptr< StructuredBlockForest > blockForest =
@@ -325,6 +328,14 @@ int main(int argc, char** argv)
       loadBalancingFrequency, printLoadBalancingStatistics);
    timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -346,11 +357,13 @@ int main(int argc, char** argv)
 
    for (uint_t t = uint_c(0); t != timesteps; ++t)
    {
-      if (t % uint_c(real_c(timesteps / 100)) == uint_c(0))
+      timeloop.singleStep(timingPool, true);
+
+      if (t % evaluationFrequency == uint_c(0))
       {
-         WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t);
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass
+                                                   << "\n\t\texcess mass = " << *excessMass);
       }
-      timeloop.singleStep(timingPool, true);
 
       if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
    }
diff --git a/apps/showcases/FreeSurface/DropImpact.prm b/apps/showcases/FreeSurface/DropImpact.prm
index 8437db98ebca2c88b4c2e22d52d3e72d057f7d29..1790dffb48d4d8d5ff3d762e95f4ecbacaad89a1 100644
--- a/apps/showcases/FreeSurface/DropImpact.prm
+++ b/apps/showcases/FreeSurface/DropImpact.prm
@@ -41,6 +41,7 @@ ModelParameters
 
 EvaluationParameters
 {
+   evaluationFrequency 1000;
    performanceLogFrequency 3000;
 }
 
diff --git a/apps/showcases/FreeSurface/DropWetting.cpp b/apps/showcases/FreeSurface/DropWetting.cpp
index bdba5908629d95c20904d5ab157a19b90b37ebca..a55ad88083021fa3cc3f86caa3b7d8248a7e199d 100644
--- a/apps/showcases/FreeSurface/DropWetting.cpp
+++ b/apps/showcases/FreeSurface/DropWetting.cpp
@@ -29,6 +29,7 @@
 #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"
@@ -368,6 +369,14 @@ int main(int argc, char** argv)
       blockForest, freeSurfaceBoundaryHandling, fillFieldID, dropHeight, evaluationFrequency);
    timeloop.addFuncAfterTimeStep(dropHeightEvaluator, "Evaluator: drop height");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -395,8 +404,7 @@ int main(int argc, char** argv)
       // 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)
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\tdrop height = " << *dropHeight)
          if (std::abs(formerDropHeight - *dropHeight) / *dropHeight < convergenceThreshold)
          {
             WALBERLA_LOG_DEVEL_ON_ROOT("Final converged drop height=" << *dropHeight);
diff --git a/apps/showcases/FreeSurface/GravityWave.cpp b/apps/showcases/FreeSurface/GravityWave.cpp
index acca70e45fd4bb0fa75e8808cd639ece5353a59e..702682bcf0aa37a77ec8e54dea1ba2155810c66c 100644
--- a/apps/showcases/FreeSurface/GravityWave.cpp
+++ b/apps/showcases/FreeSurface/GravityWave.cpp
@@ -554,6 +554,14 @@ int main(int argc, char** argv)
                                                                evaluationFrequency, symmetryNorm);
    timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -586,10 +594,11 @@ int main(int argc, char** argv)
          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);
+            WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\ttNonDimensional = " << tNonDimensional
+                                              << "\n\t\tpositionNonDimensional = " << positionNonDimensional
+                                              << "\n\t\tsymmetryNorm = " << *symmetryNorm << "\n\t\ttotal mass = "
+                                              << *totalMass << "\n\t\texcess mass = " << *excessMass);
+
             writeVectorToFile(resultVector, filename);
          }
       }
diff --git a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
index dd603d62c4a1552ad525d2fdceaf35b29ab98880..65d18594b5b2c9f024ef5347b871ff2b9b79501a 100644
--- a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
+++ b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp
@@ -516,6 +516,14 @@ int main(int argc, char** argv)
                                                                evaluationFrequency, symmetryNorm);
    timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T,
                  true, VectorFieldFlattened_T >(
@@ -549,10 +557,11 @@ int main(int argc, char** argv)
          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);
+            WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\ttNonDimensional = " << tNonDimensional
+                                              << "\n\t\tpositionNonDimensional = " << positionNonDimensional
+                                              << "\n\t\tsymmetryNorm = " << *symmetryNorm << "\n\t\ttotal mass = "
+                                              << *totalMass << "\n\t\texcess mass = " << *excessMass);
+
             writeVectorToFile(resultVector, filename);
          }
       }
diff --git a/apps/showcases/FreeSurface/MovingDrop.cpp b/apps/showcases/FreeSurface/MovingDrop.cpp
index 352a85c72cc7239d2608c62567677f7db3d5c324..4fb4c49d34f3031f74c480795d9873c90a7cc91b 100644
--- a/apps/showcases/FreeSurface/MovingDrop.cpp
+++ b/apps/showcases/FreeSurface/MovingDrop.cpp
@@ -27,6 +27,7 @@
 #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"
@@ -189,8 +190,10 @@ int main(int argc, char** argv)
    // 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");
 
    WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency);
+   WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency);
 
    // create non-uniform block forest (non-uniformity required for load balancing)
    const std::shared_ptr< StructuredBlockForest > blockForest =
@@ -286,6 +289,14 @@ int main(int argc, char** argv)
       loadBalancingFrequency, printLoadBalancingStatistics);
    timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing");
 
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
+   const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
+   timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
+
    // add VTK output
    addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >(
       blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceDensityFieldID,
@@ -307,9 +318,14 @@ int main(int argc, char** argv)
 
    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 % evaluationFrequency == uint_c(0))
+      {
+         WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass
+                                                   << "\n\t\texcess mass = " << *excessMass);
+      }
+
       if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); }
    }
 
diff --git a/apps/showcases/FreeSurface/MovingDrop.prm b/apps/showcases/FreeSurface/MovingDrop.prm
index dceed84e71501ad90e5d8020059792531f9a8d13..79718c8c060feceff66ce13e972a605283a07c7a 100644
--- a/apps/showcases/FreeSurface/MovingDrop.prm
+++ b/apps/showcases/FreeSurface/MovingDrop.prm
@@ -1,17 +1,17 @@
 BlockForestParameters
 {
-   cellsPerBlock                 < 10, 10, 10 >;
+   cellsPerBlock                 < 20, 20, 20 >;
    periodicity                   < 1, 1, 1 >;
    loadBalancingFrequency        0;
-   printLoadBalancingStatistics  true;
+   printLoadBalancingStatistics  false;
 }
 
 DomainParameters
 {
-   dropDiameter      50;
+   dropDiameter      20;
    dropCenterFactor  < 1, 1, 1 >;    // values multiplied with dropDiameter
-   poolHeightFactor  0;                // value multiplied with dropDiameter
-   domainSizeFactor  < 2, 2, 4 >;   // values multiplied with dropDiameter
+   poolHeightFactor  0;              // value multiplied with dropDiameter
+   domainSizeFactor  < 2, 2, 4 >;    // values multiplied with dropDiameter
 }
 
 PhysicsParameters
@@ -31,14 +31,16 @@ ModelParameters
    excessMassDistributionModel   EvenlyAllInterface;
    curvatureModel                FiniteDifferenceMethod;
    useSimpleMassExchange         false;
-   enableBubbleModel             false;
-   enableBubbleSplits            false; // only used if enableBubbleModel=true
    cellConversionThreshold       1e-2;
    cellConversionForceThreshold  1e-1;
+
+   enableBubbleModel             false;
+   enableBubbleSplits            false; // only used if enableBubbleModel=true
 }
 
 EvaluationParameters
 {
+   evaluationFrequency 100;
    performanceLogFrequency 10000;
 }
 
@@ -59,7 +61,7 @@ BoundaryParameters
 
 MeshOutputParameters
 {
-   writeFrequency 10000;
+   writeFrequency 1000;
    baseFolder mesh-out;
 }
 
@@ -67,7 +69,7 @@ VTK
 {
    fluid_field
    {
-      writeFrequency 10000;
+      writeFrequency 1000;
       ghostLayers 0;
       baseFolder vtk-out;
       samplingResolution 1;
diff --git a/apps/showcases/FreeSurface/RisingBubble.cpp b/apps/showcases/FreeSurface/RisingBubble.cpp
index 8fc7738761357e34d9cea02a73399d4572dddcec..6699fdf1eac647bc87f87bf3e1e9bbf8150679ee 100644
--- a/apps/showcases/FreeSurface/RisingBubble.cpp
+++ b/apps/showcases/FreeSurface/RisingBubble.cpp
@@ -376,10 +376,12 @@ int main(int argc, char** argv)
       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));
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
    const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
-      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, evaluationFrequency, totalMass);
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
    timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
 
    // add VTK output
@@ -419,10 +421,10 @@ int main(int argc, char** argv)
             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);
+            WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\tcenterOfMass = " << *centerOfMass
+                                              << "\n\t\triseVelocity = " << riseVelocity
+                                              << "\n\t\tdragForce = " << dragForce << "\n\t\ttotalMass = " << *totalMass
+                                              << "\n\t\texcessMass = " << *excessMass);
 
             const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce };
 
diff --git a/apps/showcases/FreeSurface/TaylorBubble.cpp b/apps/showcases/FreeSurface/TaylorBubble.cpp
index a045cd6acca3b405e60168dea64489812984259a..ce2a45302d06494fb588819d1ecb43cbedec9afb 100644
--- a/apps/showcases/FreeSurface/TaylorBubble.cpp
+++ b/apps/showcases/FreeSurface/TaylorBubble.cpp
@@ -405,9 +405,12 @@ int main(int argc, char** argv)
       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));
+   // add evaluator for total and excessive mass (mass that is currently undistributed)
+   const std::shared_ptr< real_t > totalMass  = std::make_shared< real_t >(real_c(0));
+   const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0));
    const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer(
-      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, evaluationFrequency, totalMass);
+      blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(),
+      evaluationFrequency, totalMass, excessMass);
    timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass");
 
    // add VTK output
@@ -447,20 +450,20 @@ int main(int argc, char** argv)
             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);
+            WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\tcenterOfMass = " << *centerOfMass
+                                              << "\n\t\triseVelocity = " << riseVelocity
+                                              << "\n\t\tdragForce = " << dragForce << "\n\t\ttotalMass = " << *totalMass
+                                              << "\n\t\texcessMass = " << *excessMass);
 
             const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce };
 
             writeVectorToFile(resultVector, t, filename);
          }
-
-         timestepOld     = t;
-         centerOfMassOld = *centerOfMass;
       }
 
+      timestepOld     = t;
+      centerOfMassOld = *centerOfMass;
+
       // stop simulation before bubble hits the top wall
       if ((*centerOfMass)[2] > stoppingHeight) { break; }
 
diff --git a/src/lbm/free_surface/TotalMassComputer.h b/src/lbm/free_surface/TotalMassComputer.h
index 192ec875594b98dc87bff0e1418c200544965590..7b3fda3e83162cc3657501ecca5fa833649a7ffc 100644
--- a/src/lbm/free_surface/TotalMassComputer.h
+++ b/src/lbm/free_surface/TotalMassComputer.h
@@ -50,10 +50,10 @@ class TotalMassComputer
                      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)
+                     const std::shared_ptr< real_t >& totalMass, const std::shared_ptr< real_t >& excessMass)
       : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID),
-        fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass), frequency_(frequency),
-        executionCounter_(uint_c(0))
+        fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass),
+        excessMass_(excessMass), frequency_(frequency), executionCounter_(uint_c(0))
    {}
 
    void operator()()
@@ -66,11 +66,10 @@ class TotalMassComputer
       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) || executionCounter_ == uint_c(0))
       {
-         // only evaluate in given frequencies
-         if (executionCounter_ % frequency_ == uint_c(0)) { computeMass(blockForest, freeSurfaceBoundaryHandling); }
+         computeMass(blockForest, freeSurfaceBoundaryHandling);
       }
 
       ++executionCounter_;
@@ -82,7 +81,8 @@ class TotalMassComputer
       const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID();
       const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo();
 
-      real_t mass = real_c(0);
+      real_t mass       = real_c(0);
+      real_t excessMass = real_c(0);
 
       for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt)
       {
@@ -104,6 +104,8 @@ class TotalMassComputer
                {
                   const real_t density = pdfField->getDensity(pdfFieldIt.cell());
                   mass += *fillFieldIt * density + *excessMassFieldIt;
+
+                  if (excessMass_ != nullptr) { excessMass += *excessMassFieldIt; }
                }
             }) // WALBERLA_FOR_ALL_CELLS_OMP
          }
@@ -122,8 +124,13 @@ class TotalMassComputer
       }
 
       mpi::allReduceInplace< real_t >(mass, mpi::SUM);
-
       *totalMass_ = mass;
+
+      if (excessMass_ != nullptr)
+      {
+         mpi::allReduceInplace< real_t >(excessMass, mpi::SUM);
+         *excessMass_ = excessMass;
+      }
    };
 
  private:
@@ -135,6 +142,7 @@ class TotalMassComputer
    const ConstBlockDataID excessMassFieldID_ = ConstBlockDataID();
 
    std::shared_ptr< real_t > totalMass_;
+   std::shared_ptr< real_t > excessMass_ = nullptr; // mass stored in the excessMassField
 
    uint_t frequency_;
    uint_t executionCounter_;
diff --git a/src/lbm/free_surface/VtkWriter.h b/src/lbm/free_surface/VtkWriter.h
index a27103510f6831a96bf7612391518626c6f45985..726e0ac735ef4495d234532a7e3eb3e52c0372a8 100644
--- a/src/lbm/free_surface/VtkWriter.h
+++ b/src/lbm/free_surface/VtkWriter.h
@@ -56,7 +56,7 @@ void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
                   const BlockDataID& forceDensityFieldID, const BlockDataID& curvatureFieldID,
                   const BlockDataID& normalFieldID, const BlockDataID& obstacleNormalFieldID)
 {
-   using value_type = typename FlagField_T::value_type;
+   using value_type       = typename FlagField_T::value_type;
    const auto blockForest = blockForestPtr.lock();
    WALBERLA_CHECK_NOT_NULLPTR(blockForest);
 
@@ -89,27 +89,34 @@ void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr,
          std::make_shared< VTKWriter< VectorField_T, float > >(obstacleNormalFieldID, "obstacle_normal"));
       if constexpr (useCodegen)
       {
-         writers.push_back(
-            std::make_shared< VTKWriter< VectorFieldFlattened_T, float > >(forceDensityFieldID, "force_density"));
+         if (forceDensityFieldID != BlockDataID())
+         {
+            writers.push_back(
+               std::make_shared< VTKWriter< VectorFieldFlattened_T, float > >(forceDensityFieldID, "force_density"));
+         }
       }
       else
       {
-         writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(forceDensityFieldID, "force_density"));
+         if (forceDensityFieldID != BlockDataID())
+         {
+            writers.push_back(
+               std::make_shared< VTKWriter< VectorField_T, float > >(forceDensityFieldID, "force_density"));
+         }
       }
 
       // map flagIDs to integer values
       const auto flagMapper =
          std::make_shared< field::FlagFieldMapping< FlagField_T, value_type > >(flagFieldID, "mapped_flag");
-      flagMapper->addMapping(flagIDs::liquidFlagID, numeric_cast<value_type>(1));
-      flagMapper->addMapping(flagIDs::interfaceFlagID, numeric_cast<value_type>(2));
-      flagMapper->addMapping(flagIDs::gasFlagID, numeric_cast<value_type>(3));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, numeric_cast<value_type>(4));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, numeric_cast<value_type>(6));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, numeric_cast<value_type>(6));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, numeric_cast<value_type>(7));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, numeric_cast<value_type>(8));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, numeric_cast<value_type>(9));
-      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, numeric_cast<value_type>(10));
+      flagMapper->addMapping(flagIDs::liquidFlagID, numeric_cast< value_type >(1));
+      flagMapper->addMapping(flagIDs::interfaceFlagID, numeric_cast< value_type >(2));
+      flagMapper->addMapping(flagIDs::gasFlagID, numeric_cast< value_type >(3));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, numeric_cast< value_type >(4));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, numeric_cast< value_type >(6));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, numeric_cast< value_type >(6));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, numeric_cast< value_type >(7));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, numeric_cast< value_type >(8));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, numeric_cast< value_type >(9));
+      flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, numeric_cast< value_type >(10));
 
       writers.push_back(flagMapper);
 
diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h
index 2e4d0b798ca3fb8bf6e325f38abfaa30119291fc..a4b86c43c64ca2c22b7326b12a779be846da5b86 100644
--- a/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h
+++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h
@@ -63,15 +63,21 @@ namespace free_surface
  *       cells, i.e., cells that are non-newly converted to interface. Falls back to WeightedAllInterface if not
  * applicable.
  *
- *  - EvenlyLiquidAndAllInterface:
+ *  - EvenlyAllInterfaceAndLiquid:
  *      Excess mass is distributed evenly among all neighboring interface and liquid cells (see p.47 in master thesis of
  *      M. Lehmann, 2019). The excess mass distributed to liquid cells does neither modify the cell's density nor fill
  *      level. Instead, it is stored in an additional excess mass field. Therefore, not only the converted interface
  *      cells' excess mass is distributed, but also the excess mass of liquid cells stored in this additional field.
  *
- *  - EvenlyLiquidAndAllInterfacePreferInterface:
- *      Similar to EvenlyLiquidAndAllInterface, however, excess mass is preferably distributed to interface cells. It is
+ *  - EvenlyAllInterfaceFallbackLiquid:
+ *      Similar to EvenlyAllInterfaceAndLiquid, however, excess mass is preferably distributed to interface cells. It is
  *      distributed to liquid cells only if there are no neighboring interface cells available.
+ *
+ *  - EvenlyNewInterfaceFallbackLiquid:
+ *      Similar to EvenlyAllInterfaceFallbackLiquid, however, excess mass is preferably distributed to newly
+ *      converted interface cells. If there are no newly converted interface cells, the excess mass is distributed to
+ *      old interface cells. The excess mass is distributed to neighboring liquid cells only if there are no neighboring
+ *      interface cells available.
  * ********************************************************************************************************************/
 class ExcessMassDistributionModel
 {
@@ -83,8 +89,9 @@ class ExcessMassDistributionModel
       WeightedAllInterface,
       WeightedNewInterface,
       WeightedOldInterface,
-      EvenlyLiquidAndAllInterface,
-      EvenlyLiquidAndAllInterfacePreferInterface
+      EvenlyAllInterfaceAndLiquid,
+      EvenlyAllInterfaceFallbackLiquid,
+      EvenlyNewInterfaceFallbackLiquid
    };
 
    ExcessMassDistributionModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName))
@@ -107,9 +114,11 @@ class ExcessMassDistributionModel
          break;
       case ExcessMassModel::WeightedOldInterface:
          break;
-      case ExcessMassModel::EvenlyLiquidAndAllInterface:
+      case ExcessMassModel::EvenlyAllInterfaceAndLiquid:
+         break;
+      case ExcessMassModel::EvenlyAllInterfaceFallbackLiquid:
          break;
-      case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface:
+      case ExcessMassModel::EvenlyNewInterfaceFallbackLiquid:
          break;
       }
    }
@@ -130,10 +139,12 @@ class ExcessMassDistributionModel
              modelType_ == ExcessMassModel::WeightedNewInterface || modelType_ == ExcessMassModel::WeightedOldInterface;
    }
 
-   inline bool isEvenlyLiquidAndAllInterfacePreferInterfaceType() const
+   inline bool isEvenlyAllInterfaceFallbackLiquidType() const
    {
-      return modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterface ||
-             modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface;
+      return modelType_ == ExcessMassModel::EvenlyAllInterfaceAndLiquid ||
+             modelType_ == ExcessMassModel::EvenlyAllInterfaceFallbackLiquid ||
+             modelType_ == ExcessMassModel::EvenlyNewInterfaceFallbackLiquid;
+      ;
    }
 
    static inline std::initializer_list< const ExcessMassModel > getTypeIterator() { return listOfAllEnums; }
@@ -153,14 +164,19 @@ class ExcessMassDistributionModel
 
       if (!string_icompare(modelName, "WeightedOldInterface")) { return ExcessMassModel::WeightedOldInterface; }
 
-      if (!string_icompare(modelName, "EvenlyLiquidAndAllInterface"))
+      if (!string_icompare(modelName, "EvenlyAllInterfaceAndLiquid"))
+      {
+         return ExcessMassModel::EvenlyAllInterfaceAndLiquid;
+      }
+
+      if (!string_icompare(modelName, "EvenlyAllInterfaceFallbackLiquid"))
       {
-         return ExcessMassModel::EvenlyLiquidAndAllInterface;
+         return ExcessMassModel::EvenlyAllInterfaceFallbackLiquid;
       }
 
-      if (!string_icompare(modelName, "EvenlyLiquidAndAllInterfacePreferInterface"))
+      if (!string_icompare(modelName, "EvenlyNewInterfaceFallbackLiquid"))
       {
-         return ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface;
+         return ExcessMassModel::EvenlyNewInterfaceFallbackLiquid;
       }
 
       WALBERLA_ABORT("The specified PDF reinitialization model " << modelName << " is not available.");
@@ -190,11 +206,14 @@ class ExcessMassDistributionModel
          modelName = "WeightedOldInterface";
          break;
 
-      case ExcessMassModel::EvenlyLiquidAndAllInterface:
-         modelName = "EvenlyLiquidAndAllInterface";
+      case ExcessMassModel::EvenlyAllInterfaceAndLiquid:
+         modelName = "EvenlyAllInterfaceAndLiquid";
+         break;
+      case ExcessMassModel::EvenlyAllInterfaceFallbackLiquid:
+         modelName = "EvenlyAllInterfaceFallbackLiquid";
          break;
-      case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface:
-         modelName = "EvenlyLiquidAndAllInterfacePreferInterface";
+      case ExcessMassModel::EvenlyNewInterfaceFallbackLiquid:
+         modelName = "EvenlyNewInterfaceFallbackLiquid";
          break;
       }
       return modelName;
@@ -203,10 +222,15 @@ class ExcessMassDistributionModel
    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
+      ExcessMassModel::EvenlyAllInterface,
+      ExcessMassModel::EvenlyNewInterface,
+      ExcessMassModel::EvenlyOldInterface,
+      ExcessMassModel::WeightedAllInterface,
+      ExcessMassModel::WeightedNewInterface,
+      ExcessMassModel::WeightedOldInterface,
+      ExcessMassModel::EvenlyAllInterfaceAndLiquid,
+      ExcessMassModel::EvenlyAllInterfaceFallbackLiquid,
+      ExcessMassModel::EvenlyNewInterfaceFallbackLiquid
    };
 
 }; // class ExcessMassDistributionModel
diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h
index ab9efe397121b33391eceedefc407811aec16ac8..34ebe1b2055bbe04a09700c0a7b983b1fe9485e8 100644
--- a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h
+++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h
@@ -71,9 +71,8 @@ class ExcessMassDistributionSweepBase
    /********************************************************************************************************************
     * 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);
+   void getNumberOfLiquidAndInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& liquidNeighbors,
+                                               uint_t& interfaceNeighbors, uint_t& newInterfaceNeighbors);
 
    ExcessMassDistributionModel excessMassDistributionModel_;
    BlockDataID fillFieldID_;
@@ -171,6 +170,8 @@ class ExcessMassDistributionSweepInterfaceWeighted
  * Distribute the excess mass evenly among
  *  - all neighboring liquid and interface cells (see p. 47 in master thesis of M. Lehmann, 2019)
  *  - all neighboring interface cells and only to liquid cells if there exists no neighboring interface cell
+ *  - new neighboring interface cells, if not available to old interface cells and only to liquid cells if there exists
+ *    no neighboring interface cell
  *
  * Neither the fill level, nor the density of liquid cells is modified. Instead, the excess mass is stored in an
  * additional field.
diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
index ba30c8445d19276136859641d97673fc8faff98c..12ffe03179912214a89b1f1110004d4e3cd331ce 100644
--- a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
+++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h
@@ -79,18 +79,23 @@ void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, Sc
 
 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)
+   getNumberOfLiquidAndInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& liquidNeighbors,
+                                          uint_t& interfaceNeighbors, uint_t& newInterfaceNeighbors)
 {
-   interfaceNeighbors = uint_c(0);
-   liquidNeighbors    = uint_c(0);
+   newInterfaceNeighbors = uint_c(0);
+   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; }
+      if (isFlagSet(neighborFlags, flagInfo_.interfaceFlag))
+      {
+         ++interfaceNeighbors;
+         if (isFlagSet(neighborFlags, flagInfo_.convertedFlag)) { ++newInterfaceNeighbors; }
+      }
       else
       {
          if (isFlagSet(neighborFlags, flagInfo_.liquidFlag)) { ++liquidNeighbors; }
@@ -147,7 +152,8 @@ void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, Sc
 
    if (interfaceNeighbors == uint_c(0))
    {
-      WALBERLA_LOG_WARNING("No interface cell is in the neighborhood to distribute excess mass to. Mass is lost.");
+      WALBERLA_LOG_WARNING(
+         "No interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained.");
       return;
    }
 
@@ -285,7 +291,8 @@ void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T,
 
    if (interfaceNeighbors == uint_c(0))
    {
-      WALBERLA_LOG_WARNING("No interface cell is in the neighborhood to distribute excess mass to. Mass is lost.");
+      WALBERLA_LOG_WARNING(
+         "No interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained.");
       return;
    }
 
@@ -505,7 +512,9 @@ void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T,
             // 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
+            // store excess mass such that it can be distributed below (no += here because cell was an interface cell
+            // that can not have an excess mass stored in the field; any excess mass is added to the interface cell's
+            // fill level)
             srcExcessMassField->get(cell) = excessFill * pdfField->getDensity(cell);
 
             if (newGas) { fillField->get(cell) = real_c(0.0); }
@@ -533,28 +542,41 @@ void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T,
    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;
+   uint_t liquidNeighbors       = uint_c(0);
+   uint_t interfaceNeighbors    = uint_c(0);
+   uint_t newInterfaceNeighbors = uint_c(0);
+   Base_T::getNumberOfLiquidAndInterfaceNeighbors(flagField, cell, liquidNeighbors, interfaceNeighbors,
+                                                  newInterfaceNeighbors);
+   const uint_t liquidAndInterfaceNeighbors = liquidNeighbors + interfaceNeighbors;
 
-   if (EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors == uint_c(0))
+   if (liquidAndInterfaceNeighbors == uint_c(0))
    {
       WALBERLA_LOG_WARNING(
-         "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost.");
+         "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained.");
       return;
    }
 
-   const bool preferInterface =
-      Base_T::excessMassDistributionModel_.getModelType() ==
-         ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface &&
-      interfaceNeighbors > uint_c(0);
+   // check if there are neighboring new interface cells
+   const bool preferNewInterface = Base_T::excessMassDistributionModel_.getModelType() ==
+                                      ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterfaceFallbackLiquid &&
+                                   newInterfaceNeighbors > uint_c(0);
+
+   // check if there are neighboring interface cells
+   const bool preferInterface = (Base_T::excessMassDistributionModel_.getModelType() ==
+                                    ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceFallbackLiquid ||
+                                 !preferNewInterface) &&
+                                interfaceNeighbors > uint_c(0) &&
+                                Base_T::excessMassDistributionModel_.getModelType() !=
+                                   ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceAndLiquid;
 
    // compute mass to be distributed to neighboring cells
    real_t deltaMass;
-   if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); }
-   else { deltaMass = excessMass / real_c(EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors); }
+   if (preferNewInterface) { deltaMass = excessMass / real_c(newInterfaceNeighbors); }
+   else
+   {
+      if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); }
+      else { deltaMass = excessMass / real_c(liquidAndInterfaceNeighbors); }
+   }
 
    // distribute the excess mass
    for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_T::Stencil::end(); ++pushDir)
@@ -571,20 +593,35 @@ void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T,
          continue;
       }
 
-      if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag))
+      // distribute excess mass to newly converted interface cell
+      if (flagField->isMaskSet(neighborCell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.interfaceFlag))
       {
          // get density of neighboring interface cell
          const real_t neighborDensity = pdfField->getDensity(neighborCell);
 
-         // add excess mass directly to fill level for neighboring interface cells
+         // add excess mass directly to fill level for newly converted neighboring interface cells
          fillField->get(neighborCell) += deltaMass / neighborDensity;
       }
       else
       {
-         if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface)
+         // distribute excess mass to old interface cell
+         if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag) && !preferNewInterface)
          {
-            // add excess mass to excessMassField for neighboring liquid cells
-            dstExcessMassField->get(neighborCell) += deltaMass;
+            // 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
+         {
+            // distribute excess mass to liquid cell
+            if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface &&
+                !preferNewInterface)
+            {
+               // add excess mass to excessMassField for neighboring liquid cells
+               dstExcessMassField->get(neighborCell) += deltaMass;
+            }
          }
       }
    }
diff --git a/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h
index d654699409faadba97dc6f654035b8f0d22b0d17..7180ec57c255c30fb605f3cc9bd5fdb950b4ff9a 100644
--- a/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h
+++ b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h
@@ -103,7 +103,7 @@ class SurfaceDynamicsHandler
                         "generate the Smagorinsky model directly into the kernel.");
       }
 
-      if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType())
+      if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType())
       {
          // add additional field for storing excess mass in liquid cells
          excessMassFieldID_ =
@@ -389,14 +389,16 @@ class SurfaceDynamicsHandler
          }
          else
          {
-            if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType())
+            if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType())
             {
                const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T,
                                                                     VectorField_T >
                   distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo,
                                       excessMassFieldID_);
                timeloop.add()
+                  // perform this sweep also on "onlyLBM" blocks because liquid cells also exchange excess mass here
                   << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface)
+                  << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::onlyLBM)
                   << Sweep(emptySweep, "Empty sweep: distribute excess mass")
                   << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, excessMassFieldID_),
                                    "Communication: after excess mass distribution sweep")
diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
index 8c99cf8726ecd46fff29da30d9706edebb45ffcc..3bda9fd5762f118b3ab3e4ee97f0438bc309f2d7 100644
--- a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
+++ b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp
@@ -275,7 +275,7 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode
       }
       else
       {
-         if (excessMassDistributionModel.isEvenlyLiquidAndAllInterfacePreferInterfaceType())
+         if (excessMassDistributionModel.isEvenlyAllInterfaceFallbackLiquidType())
          {
             const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T,
                                                                  VectorField_T >
@@ -876,7 +876,7 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode
          }
 
          if (excessMassDistributionModel.getModelType() ==
-             ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterface)
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceAndLiquid)
          {
             // left block
             if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
@@ -990,7 +990,7 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode
          }
 
          if (excessMassDistributionModel.getModelType() ==
-             ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface)
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceFallbackLiquid)
          {
             // left block
             if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)))
@@ -1102,6 +1102,120 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode
                WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4));
             }
          }
+
+         if (excessMassDistributionModel.getModelType() ==
+             ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterfaceFallbackLiquid)
+         {
+            // 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.7), 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.5), 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.5), 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.590909090909091), 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.5), 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.595), 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.615277777777778), 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.605952380952381), 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.5), 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.573958333333333), 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
    }
 
@@ -1137,11 +1251,15 @@ int main(int argc, char** argv)
    WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
    runSimulation(model);
 
-   model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterface");
+   model = ExcessMassDistributionModel("EvenlyAllInterfaceAndLiquid");
+   WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
+   runSimulation(model);
+
+   model = ExcessMassDistributionModel("EvenlyAllInterfaceFallbackLiquid");
    WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
    runSimulation(model);
 
-   model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterfacePreferInterface");
+   model = ExcessMassDistributionModel("EvenlyNewInterfaceFallbackLiquid");
    WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification());
    runSimulation(model);
 
diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods
index fa159c7b1b01c41933c16c886af01b6344a26bf9..8f0210d1c68a269eb7401a58947b64ebbc5bab05 100644
Binary files a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods and b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods differ