From c5d065bb2debc4921fcee464fbf709b814aad7cb Mon Sep 17 00:00:00 2001
From: Samuel Kemmler <samuel.kemmler@fau.de>
Date: Wed, 1 Jun 2022 14:07:04 +0200
Subject: [PATCH] Add the PSM to lbm_mesapd_coupling

---
 .clang-format                                 |  28 +-
 src/geometry/bodies/DynamicBody.h             |   8 +-
 src/lbm_mesapd_coupling/CMakeLists.txt        |   3 +-
 src/lbm_mesapd_coupling/DataTypes.h           |  29 +-
 .../overlapping/OverlapFraction.h             | 117 +++
 .../overlapping/shapes/BoxWithOverlap.h       |  62 ++
 .../shapes/CylindricalBoundaryWithOverlap.h   |  64 ++
 .../overlapping/shapes/EllipsoidWithOverlap.h |  63 ++
 .../overlapping/shapes/HalfSpaceWithOverlap.h |  62 ++
 .../overlapping/shapes/SphereWithOverlap.h    |  61 ++
 .../CMakeLists.txt                            |   6 +
 .../PSMSweep.h                                | 686 ++++++++++++++
 .../PSMUtility.h                              | 159 ++++
 .../ParticleAndVolumeFractionMapping.h        | 124 +++
 tests/lbm_mesapd_coupling/CMakeLists.txt      |  25 +
 .../DragForceSphere.cpp                       | 503 ++++++++++
 .../ParticleAndVolumeFractionMapping.cpp      | 212 +++++
 .../SettlingSphere.cpp                        | 877 ++++++++++++++++++
 .../TorqueSphere.cpp                          | 561 +++++++++++
 .../Utility.h                                 |  85 ++
 20 files changed, 3710 insertions(+), 25 deletions(-)
 create mode 100644 src/lbm_mesapd_coupling/overlapping/OverlapFraction.h
 create mode 100644 src/lbm_mesapd_coupling/overlapping/shapes/BoxWithOverlap.h
 create mode 100644 src/lbm_mesapd_coupling/overlapping/shapes/CylindricalBoundaryWithOverlap.h
 create mode 100644 src/lbm_mesapd_coupling/overlapping/shapes/EllipsoidWithOverlap.h
 create mode 100644 src/lbm_mesapd_coupling/overlapping/shapes/HalfSpaceWithOverlap.h
 create mode 100644 src/lbm_mesapd_coupling/overlapping/shapes/SphereWithOverlap.h
 create mode 100644 src/lbm_mesapd_coupling/partially_saturated_cells_method/CMakeLists.txt
 create mode 100644 src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h
 create mode 100644 src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h
 create mode 100644 src/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h
 create mode 100644 tests/lbm_mesapd_coupling/partially_saturated_cells_method/DragForceSphere.cpp
 create mode 100644 tests/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.cpp
 create mode 100644 tests/lbm_mesapd_coupling/partially_saturated_cells_method/SettlingSphere.cpp
 create mode 100644 tests/lbm_mesapd_coupling/partially_saturated_cells_method/TorqueSphere.cpp
 create mode 100644 tests/lbm_mesapd_coupling/partially_saturated_cells_method/Utility.h

diff --git a/.clang-format b/.clang-format
index 3750f7b5b..5f0ff6558 100644
--- a/.clang-format
+++ b/.clang-format
@@ -79,30 +79,34 @@ IncludeCategories:
     Priority:        12
   - Regex:           '^"lbm/'
     Priority:        13
-  - Regex:           '^"mesh/'
+  - Regex:           '^"lbm_mesapd_coupling/'
     Priority:        14
-  - Regex:           '^"pde/'
+  - Regex:           '^"mesh/'
     Priority:        15
-  - Regex:           '^"pe/'
+  - Regex:           '^"mesa_pd/'
     Priority:        16
-  - Regex:           '^"pe_coupling/'
+  - Regex:           '^"pde/'
     Priority:        17
-  - Regex:           '^"postprocessing/'
+  - Regex:           '^"pe/'
     Priority:        18
-  - Regex:           '^"python_coupling/'
+  - Regex:           '^"pe_coupling/'
     Priority:        19
-  - Regex:           '^"simd/'
+  - Regex:           '^"postprocessing/'
     Priority:        20
-  - Regex:           '^"stencil/'
+  - Regex:           '^"python_coupling/'
     Priority:        21
-  - Regex:           '^"timeloop/'
+  - Regex:           '^"simd/'
     Priority:        22
-  - Regex:           '^"vtk/'
+  - Regex:           '^"stencil/'
     Priority:        23
-  - Regex:           '^<boost/'
+  - Regex:           '^"timeloop/'
     Priority:        24
-  - Regex:           '^<'
+  - Regex:           '^"vtk/'
     Priority:        25
+  - Regex:           '^<boost/'
+    Priority:        26
+  - Regex:           '^<'
+    Priority:        27
 IndentCaseLabels: false
 IndentPPDirectives: AfterHash
 IndentWidth: 3
diff --git a/src/geometry/bodies/DynamicBody.h b/src/geometry/bodies/DynamicBody.h
index 1ebd1670e..bb656cd69 100644
--- a/src/geometry/bodies/DynamicBody.h
+++ b/src/geometry/bodies/DynamicBody.h
@@ -32,8 +32,12 @@ class AbstractBody {
 public:
    virtual ~AbstractBody() = default;
    virtual bool contains (const Vector3<real_t> & point ) const = 0;
-   virtual FastOverlapResult fastOverlapCheck ( const Vector3<real_t> & cellMidpoint, const Vector3<real_t> & dx ) const = 0;
-   virtual FastOverlapResult fastOverlapCheck ( const AABB & box ) const = 0;
+   virtual FastOverlapResult fastOverlapCheck(const Vector3< real_t >& /*cellMidpoint*/,
+                                              const Vector3< real_t >& /*dx*/) const
+   {
+      return geometry::DONT_KNOW;
+   }
+   virtual FastOverlapResult fastOverlapCheck(const AABB& /*box*/) const { return geometry::DONT_KNOW; }
 };
 
 // this is an adaptor from static to dynamic polymorphism
diff --git a/src/lbm_mesapd_coupling/CMakeLists.txt b/src/lbm_mesapd_coupling/CMakeLists.txt
index 76f78cca8..bde22da3f 100644
--- a/src/lbm_mesapd_coupling/CMakeLists.txt
+++ b/src/lbm_mesapd_coupling/CMakeLists.txt
@@ -14,5 +14,6 @@ target_sources( lbm_mesapd_coupling
 
 add_subdirectory( amr )
 add_subdirectory( momentum_exchange_method )
+add_subdirectory( partially_saturated_cells_method )
 add_subdirectory( utility )
-add_subdirectory( mapping )
\ No newline at end of file
+add_subdirectory( mapping )
diff --git a/src/lbm_mesapd_coupling/DataTypes.h b/src/lbm_mesapd_coupling/DataTypes.h
index a71680613..f543a1496 100644
--- a/src/lbm_mesapd_coupling/DataTypes.h
+++ b/src/lbm_mesapd_coupling/DataTypes.h
@@ -1,15 +1,15 @@
 //======================================================================================================================
 //
-//  This file is part of waLBerla. waLBerla is free software: you can 
+//  This file is part of waLBerla. waLBerla is free software: you can
 //  redistribute it and/or modify it under the terms of the GNU General Public
-//  License as published by the Free Software Foundation, either version 3 of 
+//  License as published by the Free Software Foundation, either version 3 of
 //  the License, or (at your option) any later version.
-//  
-//  waLBerla is distributed in the hope that it will be useful, but WITHOUT 
-//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
-//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License 
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 //  for more details.
-//  
+//
 //  You should have received a copy of the GNU General Public License along
 //  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
 //
@@ -21,17 +21,26 @@
 
 #pragma once
 
-#include "field/GhostLayerField.h"
 #include "core/DataTypes.h"
 
-namespace walberla {
-namespace lbm_mesapd_coupling {
+#include "field/GhostLayerField.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
 
 /*
  * Typedefs specific to the lbm - mesa_pd coupling
  */
 using ParticleField_T = walberla::GhostLayerField< walberla::id_t, 1 >;
 
+namespace psm
+{
+// store the particle uid together with the overlap fraction
+using ParticleAndVolumeFraction_T      = std::pair< id_t, real_t >;
+using ParticleAndVolumeFractionField_T = GhostLayerField< std::vector< ParticleAndVolumeFraction_T >, 1 >;
 
+} // namespace psm
 } // namespace lbm_mesapd_coupling
 } // namespace walberla
diff --git a/src/lbm_mesapd_coupling/overlapping/OverlapFraction.h b/src/lbm_mesapd_coupling/overlapping/OverlapFraction.h
new file mode 100644
index 000000000..3dbf68e03
--- /dev/null
+++ b/src/lbm_mesapd_coupling/overlapping/OverlapFraction.h
@@ -0,0 +1,117 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file OverlapFraction.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \brief Functor that provides overlap fraction computations for different MESA-PD shapes (used for SingleCast)
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/DataTypes.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+
+#include "mesa_pd/common/Contains.h"
+#include "mesa_pd/data/DataTypes.h"
+#include "mesa_pd/data/shape/Sphere.h"
+
+#include "shapes/BoxWithOverlap.h"
+#include "shapes/CylindricalBoundaryWithOverlap.h"
+#include "shapes/EllipsoidWithOverlap.h"
+#include "shapes/HalfSpaceWithOverlap.h"
+#include "shapes/SphereWithOverlap.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+struct OverlapFractionFunctor
+{
+   template< typename ParticleAccessor_T, typename Shape_T >
+   real_t operator()(const size_t /*particleIdx*/, const Shape_T& /*shape*/,
+                     const shared_ptr< ParticleAccessor_T >& /*ac*/, const Vector3< real_t >& /*point*/,
+                     const Vector3< real_t >& /*dxVec*/, uint_t /*superSamplingDepth*/)
+   {
+      WALBERLA_ABORT("OverlapFraction not implemented!");
+   }
+
+   template< typename ParticleAccessor_T >
+   real_t operator()(const size_t particleIdx, const mesa_pd::data::Sphere& sphere,
+                     const shared_ptr< ParticleAccessor_T >& ac, const Vector3< real_t >& point,
+                     const Vector3< real_t >& dxVec, uint_t superSamplingDepth)
+   {
+      WALBERLA_STATIC_ASSERT((std::is_base_of< mesa_pd::data::IAccessor, ParticleAccessor_T >::value));
+
+      SphereWithOverlap sphereWithOverlap(particleIdx, ac, sphere);
+      return geometry::overlapFraction< geometry::AbstractBody >(sphereWithOverlap, point, dxVec, superSamplingDepth);
+   }
+
+   template< typename ParticleAccessor_T >
+   real_t operator()(const size_t particleIdx, const mesa_pd::data::HalfSpace& halfSphere,
+                     const shared_ptr< ParticleAccessor_T >& ac, const Vector3< real_t >& point, real_t dxVec,
+                     uint_t superSamplingDepth)
+   {
+      WALBERLA_STATIC_ASSERT((std::is_base_of< mesa_pd::data::IAccessor, ParticleAccessor_T >::value));
+
+      HalfSpaceWithOverlap halfSpaceWithOverlap(particleIdx, ac, halfSphere);
+      return geometry::overlapFraction< geometry::AbstractBody >(halfSpaceWithOverlap, point, dxVec,
+                                                                 superSamplingDepth);
+   }
+
+   template< typename ParticleAccessor_T >
+   real_t operator()(const size_t particleIdx, const mesa_pd::data::CylindricalBoundary& cylindricalBoundary,
+                     const shared_ptr< ParticleAccessor_T >& ac, const Vector3< real_t >& point, real_t dxVec,
+                     uint_t superSamplingDepth)
+   {
+      WALBERLA_STATIC_ASSERT((std::is_base_of< mesa_pd::data::IAccessor, ParticleAccessor_T >::value));
+
+      CylindricalBoundaryWithOverlap cylindricalBoundaryWithOverlap(particleIdx, ac, cylindricalBoundary);
+      return geometry::overlapFraction< geometry::AbstractBody >(cylindricalBoundaryWithOverlap, point, dxVec,
+                                                                 superSamplingDepth);
+   }
+
+   template< typename ParticleAccessor_T >
+   real_t operator()(const size_t particleIdx, const mesa_pd::data::Box& box,
+                     const shared_ptr< ParticleAccessor_T >& ac, const Vector3< real_t >& point, real_t dxVec,
+                     uint_t superSamplingDepth)
+   {
+      WALBERLA_STATIC_ASSERT((std::is_base_of< mesa_pd::data::IAccessor, ParticleAccessor_T >::value));
+
+      BoxWithOverlap boxWithOverlap(particleIdx, ac, box);
+      return geometry::overlapFraction< geometry::AbstractBody >(boxWithOverlap, point, dxVec, superSamplingDepth);
+   }
+
+   template< typename ParticleAccessor_T >
+   real_t operator()(const size_t particleIdx, const mesa_pd::data::Ellipsoid& ellipsoid,
+                     const shared_ptr< ParticleAccessor_T >& ac, const Vector3< real_t >& point, real_t dxVec,
+                     uint_t superSamplingDepth)
+   {
+      WALBERLA_STATIC_ASSERT((std::is_base_of< mesa_pd::data::IAccessor, ParticleAccessor_T >::value));
+
+      EllipsoidWithOverlap ellipsoidWithOverlap(particleIdx, ac, ellipsoid);
+      return geometry::overlapFraction< geometry::AbstractBody >(ellipsoidWithOverlap, point, dxVec,
+                                                                 superSamplingDepth);
+   }
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/overlapping/shapes/BoxWithOverlap.h b/src/lbm_mesapd_coupling/overlapping/shapes/BoxWithOverlap.h
new file mode 100644
index 000000000..6bf00ca38
--- /dev/null
+++ b/src/lbm_mesapd_coupling/overlapping/shapes/BoxWithOverlap.h
@@ -0,0 +1,62 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file BoxWithOverlap.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \brief Wrapper class that provides a "contains" function for MESA-PD boxes
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+#include "geometry/bodies/DynamicBody.h"
+
+#include "mesa_pd/common/Contains.h"
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/shape/Box.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+template< typename ParticleAccessor_T >
+class BoxWithOverlap : public geometry::AbstractBody
+{
+ public:
+   BoxWithOverlap(const size_t idx, const shared_ptr< ParticleAccessor_T >& ac, const mesa_pd::data::Box& box)
+      : idx_(idx), ac_(ac), box_(box)
+   {}
+
+   bool contains(const Vector3< real_t >& point) const
+   {
+      return mesa_pd::isPointInsideBoxBF(mesa_pd::transformPositionFromWFtoBF(idx_, ac_, point), box_.getEdgeLength());
+   }
+
+ private:
+   size_t idx_;
+   shared_ptr< ParticleAccessor_T > ac_;
+   mesa_pd::data::Box box_;
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/overlapping/shapes/CylindricalBoundaryWithOverlap.h b/src/lbm_mesapd_coupling/overlapping/shapes/CylindricalBoundaryWithOverlap.h
new file mode 100644
index 000000000..56a14bfec
--- /dev/null
+++ b/src/lbm_mesapd_coupling/overlapping/shapes/CylindricalBoundaryWithOverlap.h
@@ -0,0 +1,64 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file CylindricalBoundaryWithOverlap.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \brief Wrapper class that provides a "contains" function for MESA-PD cylindrical boundaries
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+#include "geometry/bodies/DynamicBody.h"
+
+#include "mesa_pd/common/Contains.h"
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/shape/CylindricalBoundary.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+template< typename ParticleAccessor_T >
+class CylindricalBoundaryWithOverlap : public geometry::AbstractBody
+{
+ public:
+   CylindricalBoundaryWithOverlap(const size_t idx, const shared_ptr< ParticleAccessor_T >& ac,
+                                  const mesa_pd::data::CylindricalBoundary& cylindricalBoundary)
+      : idx_(idx), ac_(ac), cylindricalBoundary_(cylindricalBoundary)
+   {}
+
+   bool contains(const Vector3< real_t >& point) const
+   {
+      return mesa_pd::isPointInsideCylindricalBoundary(point, ac_->getPosition(idx_), cylindricalBoundary_.getRadius(),
+                                                       cylindricalBoundary_.getAxis());
+   }
+
+ private:
+   size_t idx_;
+   shared_ptr< ParticleAccessor_T > ac_;
+   mesa_pd::data::CylindricalBoundary cylindricalBoundary_;
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/overlapping/shapes/EllipsoidWithOverlap.h b/src/lbm_mesapd_coupling/overlapping/shapes/EllipsoidWithOverlap.h
new file mode 100644
index 000000000..3c8ffe185
--- /dev/null
+++ b/src/lbm_mesapd_coupling/overlapping/shapes/EllipsoidWithOverlap.h
@@ -0,0 +1,63 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file EllipsoidWithOverlap.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \brief Wrapper class that provides a "contains" function for MESA-PD ellipsoids
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+
+#include "mesa_pd/common/Contains.h"
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/shape/Ellipsoid.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+template< typename ParticleAccessor_T >
+class EllipsoidWithOverlap : public geometry::AbstractBody
+{
+ public:
+   EllipsoidWithOverlap(const size_t idx, const shared_ptr< ParticleAccessor_T >& ac,
+                        const mesa_pd::data::Ellipsoid& ellipsoid)
+      : idx_(idx), ac_(ac), ellipsoid_(ellipsoid)
+   {}
+
+   bool contains(const Vector3< real_t >& point) const
+   {
+      return mesa_pd::isPointInsideEllipsoidBF(mesa_pd::transformPositionFromWFtoBF(idx_, ac_, point),
+                                               ellipsoid_.getSemiAxes());
+   }
+
+ private:
+   size_t idx_;
+   shared_ptr< ParticleAccessor_T > ac_;
+   mesa_pd::data::Ellipsoid ellipsoid_;
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/overlapping/shapes/HalfSpaceWithOverlap.h b/src/lbm_mesapd_coupling/overlapping/shapes/HalfSpaceWithOverlap.h
new file mode 100644
index 000000000..9f50d6d37
--- /dev/null
+++ b/src/lbm_mesapd_coupling/overlapping/shapes/HalfSpaceWithOverlap.h
@@ -0,0 +1,62 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file HalfSpaceWithOverlap.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \brief Wrapper class that provides a "contains" function for MESA-PD half spaces
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+
+#include "mesa_pd/common/Contains.h"
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/shape/HalfSpace.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+template< typename ParticleAccessor_T >
+class HalfSpaceWithOverlap : public geometry::AbstractBody
+{
+ public:
+   HalfSpaceWithOverlap(const size_t idx, const shared_ptr< ParticleAccessor_T >& ac,
+                        const mesa_pd::data::HalfSpace& halfSpace)
+      : idx_(idx), ac_(ac), halfSpace_(halfSpace)
+   {}
+
+   bool contains(const Vector3< real_t >& point) const
+   {
+      return mesa_pd::isPointInsideHalfSpace(point, ac_->getPosition(idx_), halfSpace_.getNormal());
+   }
+
+ private:
+   size_t idx_;
+   shared_ptr< ParticleAccessor_T > ac_;
+   mesa_pd::data::HalfSpace halfSpace_;
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/overlapping/shapes/SphereWithOverlap.h b/src/lbm_mesapd_coupling/overlapping/shapes/SphereWithOverlap.h
new file mode 100644
index 000000000..1db48699c
--- /dev/null
+++ b/src/lbm_mesapd_coupling/overlapping/shapes/SphereWithOverlap.h
@@ -0,0 +1,61 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SphereWithOverlap.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \brief Wrapper class that provides a "contains" function for MESA-PD spheres
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "core/math/Vector3.h"
+
+#include "geometry/bodies/BodyOverlapFunctions.h"
+
+#include "mesa_pd/common/Contains.h"
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/shape/Sphere.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+template< typename ParticleAccessor_T >
+class SphereWithOverlap : public geometry::AbstractBody
+{
+ public:
+   SphereWithOverlap(const size_t idx, const shared_ptr< ParticleAccessor_T >& ac, const mesa_pd::data::Sphere& sphere)
+      : idx_(idx), ac_(ac), sphere_(sphere)
+   {}
+
+   bool contains(const Vector3< real_t >& point) const
+   {
+      return mesa_pd::isPointInsideSphere(point, ac_->getPosition(idx_), sphere_.getRadius());
+   }
+
+ private:
+   size_t idx_;
+   shared_ptr< ParticleAccessor_T > ac_;
+   mesa_pd::data::Sphere sphere_;
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/partially_saturated_cells_method/CMakeLists.txt b/src/lbm_mesapd_coupling/partially_saturated_cells_method/CMakeLists.txt
new file mode 100644
index 000000000..65b1c468c
--- /dev/null
+++ b/src/lbm_mesapd_coupling/partially_saturated_cells_method/CMakeLists.txt
@@ -0,0 +1,6 @@
+target_sources( lbm_mesapd_coupling
+    PRIVATE
+    PSMSweep.h
+    ParticleAndVolumeFractionMapping.h
+    PSMUtility.h
+    )
diff --git a/src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h b/src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h
new file mode 100644
index 000000000..8ad996ba8
--- /dev/null
+++ b/src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h
@@ -0,0 +1,686 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file PSMSweep.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \author Christoph Rettinger <christoph.rettinger@fau.de>
+//! \brief Modification of pe_coupling/partially_saturated_cells_method/PSMSweep.h
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/GhostLayerField.h"
+
+#include "lbm/lattice_model/all.h"
+#include "lbm/sweeps/StreamPull.h"
+#include "lbm/sweeps/SweepBase.h"
+
+#include "lbm_mesapd_coupling/DataTypes.h"
+#include "lbm_mesapd_coupling/utility/ParticleFunctions.h"
+
+#include "mesa_pd/common/ParticleFunctions.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+// functions to calculate the cell coverage weighting B
+template< int Weighting_T >
+real_t calculateWeighting(const real_t& /*epsilon*/, const real_t& /*tau*/)
+{
+   WALBERLA_STATIC_ASSERT(Weighting_T == 1 || Weighting_T == 2);
+   exit(EXIT_FAILURE);
+}
+template<>
+inline real_t calculateWeighting< 1 >(const real_t& epsilon, const real_t& /*tau*/)
+{
+   return epsilon;
+}
+template<>
+inline real_t calculateWeighting< 2 >(const real_t& epsilon, const real_t& tau)
+{
+   return epsilon * (tau - real_c(0.5)) / ((real_c(1) - epsilon) + (tau - real_c(0.5)));
+}
+
+/*!\brief LBM sweep for the partially saturated cells method
+ *
+ * Literature:
+ * Original idea:  Noble et al. - A lattice-Boltzmann method for partially saturated computational cells, 1998
+ * Overview paper: Owen et al. - An efficient framework for fluid-structure interaction using the lattice Boltzmann
+ * method and immersed moving boundaries, 2010
+ *
+ * Based on the overview paper, at least three different versions for the calculation of the solid collision term
+ * (omega_S) and two different versions for the cell coverage weighting function (B) are available. They can be selected
+ * via the template parameters SolidCollision_T ( = {1,2,3} ) and Weighting_T ( = {1,2} ). SolidCollision_T = 1: Eq. 28
+ * Weighting_T = 1: B = solid volume fraction SolidCollision_T = 2: Eq. 29                     Weighting_T = 2: Eq. 30
+ * SolidCollision_T = 3: Eq. 33
+ *
+ * For the calculation of the force acting on the particle, an additional minus sign has to be added compared to Eqs. 31
+ * and 32 in the paper.
+ *
+ */
+template< typename LatticeModel_T, typename Filter_T, typename DensityVelocityIn_T, typename DensityVelocityOut_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+class PSMSweep : public lbm::SweepBase< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T >
+{
+ public:
+   using PdfField_T =
+      typename lbm::SweepBase< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T >::PdfField_T;
+   using Stencil_T = typename LatticeModel_T::Stencil;
+
+   PSMSweep(const BlockDataID& pdfFieldID, const BlockDataID& particleAndVolumeFractionFieldID,
+            const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+            const Filter_T& _filter                         = walberla::field::DefaultEvaluationFilter(),
+            const DensityVelocityIn_T& _densityVelocityIn   = lbm::DefaultDensityEquilibriumVelocityCalculation(),
+            const DensityVelocityOut_T& _densityVelocityOut = lbm::DefaultDensityVelocityCallback())
+      : lbm::SweepBase< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T >(
+           pdfFieldID, _filter, _densityVelocityIn, _densityVelocityOut),
+        particleAndVolumeFractionFieldID_(particleAndVolumeFractionFieldID), blockStorage_(blockStorage), accessor_(ac)
+   {}
+
+   PSMSweep(const BlockDataID& src, const BlockDataID& dst, const BlockDataID& particleAndVolumeFractionFieldID,
+            const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+            const Filter_T& _filter                         = walberla::field::DefaultEvaluationFilter(),
+            const DensityVelocityIn_T& _densityVelocityIn   = lbm::DefaultDensityEquilibriumVelocityCalculation(),
+            const DensityVelocityOut_T& _densityVelocityOut = lbm::DefaultDensityVelocityCallback())
+      : lbm::SweepBase< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T >(
+           src, dst, _filter, _densityVelocityIn, _densityVelocityOut),
+        particleAndVolumeFractionFieldID_(particleAndVolumeFractionFieldID), blockStorage_(blockStorage), accessor_(ac)
+   {}
+
+   void operator()(IBlock* const block, const uint_t numberOfGhostLayersToInclude = uint_t(0))
+   {
+      streamCollide(block, numberOfGhostLayersToInclude);
+   }
+
+   void streamCollide(IBlock* const block, const uint_t numberOfGhostLayersToInclude = uint_t(0));
+
+   void stream(IBlock* const block, const uint_t numberOfGhostLayersToInclude = uint_t(0));
+   void collide(IBlock* const block, const uint_t numberOfGhostLayersToInclude = uint_t(0));
+
+   inline ParticleAndVolumeFractionField_T* getParticleAndVolumeFractionField(IBlock* const block) const
+   {
+      WALBERLA_ASSERT_NOT_NULLPTR(block);
+      ParticleAndVolumeFractionField_T* particleAndVolumeFractionField =
+         block->getData< ParticleAndVolumeFractionField_T >(particleAndVolumeFractionFieldID_);
+      WALBERLA_ASSERT_NOT_NULLPTR(particleAndVolumeFractionField);
+      return particleAndVolumeFractionField;
+   }
+
+   inline void getFields(IBlock* const block, PdfField_T*& src, PdfField_T*& dst,
+                         ParticleAndVolumeFractionField_T*& particleAndVolumeFractionField)
+   {
+      WALBERLA_ASSERT_NOT_NULLPTR(block);
+
+      src                            = this->getSrcField(block);
+      dst                            = this->getDstField(block, src);
+      particleAndVolumeFractionField = getParticleAndVolumeFractionField(block);
+
+      WALBERLA_ASSERT_NOT_NULLPTR(src);
+      WALBERLA_ASSERT_NOT_NULLPTR(dst);
+      WALBERLA_ASSERT_NOT_NULLPTR(particleAndVolumeFractionField);
+
+      WALBERLA_ASSERT_EQUAL(src->xyzSize(), dst->xyzSize());
+      WALBERLA_ASSERT_EQUAL(src->xyzSize(), particleAndVolumeFractionField->xyzSize());
+   }
+
+ private:
+   const BlockDataID particleAndVolumeFractionFieldID_;
+   shared_ptr< StructuredBlockStorage > blockStorage_;
+   const shared_ptr< ParticleAccessor_T > accessor_;
+};
+
+template< typename LatticeModel_T, typename Filter_T, typename DensityVelocityIn_T, typename DensityVelocityOut_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+void PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T, Weighting_T,
+               ParticleAccessor_T >::streamCollide(IBlock* const block, const uint_t numberOfGhostLayersToInclude)
+{
+   PdfField_T* src(nullptr);
+   PdfField_T* dst(nullptr);
+   ParticleAndVolumeFractionField_T* particleAndVolumeFractionField(nullptr);
+
+   getFields(block, src, dst, particleAndVolumeFractionField);
+
+   const real_t dxCurrentLevel      = blockStorage_->dx(blockStorage_->getLevel(*block));
+   const real_t lengthScalingFactor = dxCurrentLevel;
+   const real_t forceScalingFactor  = lengthScalingFactor * lengthScalingFactor;
+
+   WALBERLA_ASSERT_GREATER(src->nrOfGhostLayers(), numberOfGhostLayersToInclude);
+   WALBERLA_ASSERT_GREATER_EQUAL(dst->nrOfGhostLayers(), numberOfGhostLayersToInclude);
+
+   const auto& lm = src->latticeModel();
+   dst->resetLatticeModel(
+      lm); /* required so that member functions for getting density and equilibrium velocity can be called for dst! */
+
+   this->filter(*block);
+   this->densityVelocityIn(*block);
+   this->densityVelocityOut(*block);
+
+   const auto& collisionModel = lm.collisionModel();
+
+   const real_t omega = collisionModel.omega();
+   const real_t tau   = real_c(1) / omega;
+
+   const cell_idx_t xSize = cell_idx_c(src->xSize());
+   const cell_idx_t ySize = cell_idx_c(src->ySize());
+   const cell_idx_t zSize = cell_idx_c(src->zSize());
+   const cell_idx_t gl    = cell_idx_c(numberOfGhostLayersToInclude);
+   for (cell_idx_t z = -gl; z < (zSize + gl); ++z)
+   {
+      for (cell_idx_t y = -gl; y < (ySize + gl); ++y)
+      {
+         for (cell_idx_t x = -gl; x < (xSize + gl); ++x)
+         {
+            if (this->filter(x, y, z))
+            {
+               using namespace stencil;
+
+               real_t pdfs[Stencil_T::Size];
+
+               // stream pull & temporal storage of PDFs
+               for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+               {
+                  dst->get(x, y, z, d.toIdx()) = src->get(x - d.cx(), y - d.cy(), z - d.cz(), d.toIdx());
+                  pdfs[d.toIdx()]              = dst->get(x, y, z, d.toIdx());
+               }
+
+               Vector3< real_t > velocity;
+               real_t rho = this->densityVelocityIn(velocity, dst, x, y, z);
+
+               this->densityVelocityOut(x, y, z, lm, velocity, rho);
+
+               // equilibrium distributions
+               auto pdfs_equ = lbm::EquilibriumDistribution< LatticeModel_T >::get(velocity, rho);
+
+               // possible external forcing on fluid
+               const auto commonForceTerms = lm.forceModel().template directionIndependentTerms< LatticeModel_T >(
+                  x, y, z, velocity, rho, omega, collisionModel.omega_bulk(), collisionModel.omega_odd());
+
+               // check if particle is present
+               if (particleAndVolumeFractionField->get(x, y, z).size() != size_t(0))
+               {
+                  // total coverage ratio in the cell
+                  real_t Bn = real_t(0);
+
+                  // averaged solid collision operator for all intersecting bodies s
+                  // = \sum_s B_s * \Omega_s_i
+                  std::vector< real_t > omega_n(Stencil_T::Size, real_t(0));
+
+                  // get center of cell
+                  Vector3< real_t > cellCenter = blockStorage_->getBlockLocalCellCenter(*block, Cell(x, y, z));
+
+                  for (auto particleFracIt = particleAndVolumeFractionField->get(x, y, z).begin();
+                       particleFracIt != particleAndVolumeFractionField->get(x, y, z).end(); ++particleFracIt)
+                  {
+                     real_t omega_s(real_c(0));
+                     Vector3< real_t > forceOnParticle(real_c(0));
+
+                     real_t Bs = calculateWeighting< Weighting_T >((*particleFracIt).second, tau);
+                     Bn += Bs;
+
+                     // particle velocity at cell center
+                     const size_t idx = accessor_->uidToIdx(particleFracIt->first);
+                     WALBERLA_ASSERT_UNEQUAL(idx, accessor_->getInvalidIdx(), "Index of particle is invalid!");
+                     const auto particleVelocity = mesa_pd::getVelocityAtWFPoint(idx, *accessor_, cellCenter);
+
+                     // equilibrium distributions with solid velocity
+                     auto pdfs_equ_solid = lbm::EquilibriumDistribution< LatticeModel_T >::get(particleVelocity, rho);
+
+                     for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+                     {
+                        // Different solid collision operators available
+                        if (SolidCollision_T == 1)
+                        {
+                           omega_s =
+                              pdfs[d.toInvIdx()] - pdfs_equ[d.toInvIdx()] + pdfs_equ_solid[d.toIdx()] - pdfs[d.toIdx()];
+                        }
+                        else if (SolidCollision_T == 2)
+                        {
+                           omega_s = pdfs_equ_solid[d.toIdx()] - pdfs[d.toIdx()] +
+                                     (real_c(1) - omega) * (pdfs[d.toIdx()] - pdfs_equ[d.toIdx()]);
+                        }
+                        else if (SolidCollision_T == 3)
+                        {
+                           omega_s = pdfs[d.toInvIdx()] - pdfs_equ_solid[d.toInvIdx()] + pdfs_equ_solid[d.toIdx()] -
+                                     pdfs[d.toIdx()];
+                        }
+                        else
+                        {
+                           WALBERLA_STATIC_ASSERT(SolidCollision_T >= 1 && SolidCollision_T <= 3);
+                           exit(EXIT_FAILURE);
+                        }
+                        real_t BsOmegaS = Bs * omega_s;
+
+                        omega_n[d.toIdx()] += BsOmegaS;
+
+                        forceOnParticle[0] -= BsOmegaS * real_c(d.cx());
+                        forceOnParticle[1] -= BsOmegaS * real_c(d.cy());
+                        forceOnParticle[2] -= BsOmegaS * real_c(d.cz());
+                     }
+
+                     // scale force when using refinement with (dx)^3 / dt
+                     forceOnParticle *= forceScalingFactor;
+
+                     // only if cell inside inner domain
+                     if (dst->isInInnerPart(Cell(x, y, z)))
+                     {
+                        // apply force (and automatically torque) on particle
+                        lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic(idx, *accessor_, forceOnParticle,
+                                                                               cellCenter);
+                     }
+                  }
+
+                  // collide step
+                  for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+                  {
+                     // external forcing
+                     const real_t forceTerm = lm.forceModel().template forceTerm< LatticeModel_T >(
+                        x, y, z, velocity, rho, commonForceTerms, LatticeModel_T::w[d.toIdx()], real_c(d.cx()),
+                        real_c(d.cy()), real_c(d.cz()), omega, collisionModel.omega_bulk(), collisionModel.omega_odd());
+
+                     dst->get(x, y, z, d.toIdx()) =
+                        pdfs[d.toIdx()] - omega * (real_c(1) - Bn) * (pdfs[d.toIdx()] - pdfs_equ[d.toIdx()]) // SRT
+                        + omega_n[d.toIdx()] + (real_c(1) - Bn) * forceTerm;
+                  }
+               }
+               else
+               {
+                  // SRT collide step
+                  for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+                  {
+                     // external forcing
+                     const real_t forceTerm = lm.forceModel().template forceTerm< LatticeModel_T >(
+                        x, y, z, velocity, rho, commonForceTerms, LatticeModel_T::w[d.toIdx()], real_c(d.cx()),
+                        real_c(d.cy()), real_c(d.cz()), omega, collisionModel.omega_bulk(), collisionModel.omega_odd());
+
+                     dst->get(x, y, z, d.toIdx()) =
+                        pdfs[d.toIdx()] - omega * (pdfs[d.toIdx()] - pdfs_equ[d.toIdx()]) + forceTerm;
+                  }
+               }
+            }
+         }
+      }
+   }
+   src->swapDataPointers(dst);
+}
+
+template< typename LatticeModel_T, typename Filter_T, typename DensityVelocityIn_T, typename DensityVelocityOut_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+void PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T, Weighting_T,
+               ParticleAccessor_T >::stream(IBlock* const block, const uint_t numberOfGhostLayersToInclude)
+{
+   PdfField_T* src(nullptr);
+   PdfField_T* dst(nullptr);
+   lbm::SweepBase< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T >::getFields(block, src, dst);
+   lbm::StreamPull< LatticeModel_T >::execute(src, dst, block, this->filter_, numberOfGhostLayersToInclude);
+}
+
+template< typename LatticeModel_T, typename Filter_T, typename DensityVelocityIn_T, typename DensityVelocityOut_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+void PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T, Weighting_T,
+               ParticleAccessor_T >::collide(IBlock* const block, const uint_t numberOfGhostLayersToInclude)
+{
+   PdfField_T* src                                                  = this->getSrcField(block);
+   ParticleAndVolumeFractionField_T* particleAndVolumeFractionField = getParticleAndVolumeFractionField(block);
+
+   WALBERLA_ASSERT_GREATER(src->nrOfGhostLayers(), numberOfGhostLayersToInclude);
+
+   const real_t dxCurrentLevel      = blockStorage_->dx(blockStorage_->getLevel(*block));
+   const real_t lengthScalingFactor = dxCurrentLevel;
+   const real_t forceScalingFactor  = lengthScalingFactor * lengthScalingFactor;
+
+   this->filter(*block);
+   this->densityVelocityIn(*block);
+   this->densityVelocityOut(*block);
+
+   const auto& lm             = src->latticeModel();
+   const auto& collisionModel = lm.collisionModel();
+
+   const real_t omega = collisionModel.omega();
+   const real_t tau   = real_c(1) / omega;
+
+   const cell_idx_t xSize = cell_idx_c(src->xSize());
+   const cell_idx_t ySize = cell_idx_c(src->ySize());
+   const cell_idx_t zSize = cell_idx_c(src->zSize());
+   const cell_idx_t gl    = cell_idx_c(numberOfGhostLayersToInclude);
+   for (cell_idx_t z = -gl; z < (zSize + gl); ++z)
+   {
+      for (cell_idx_t y = -gl; y < (ySize + gl); ++y)
+      {
+         for (cell_idx_t x = -gl; x < (xSize + gl); ++x)
+         {
+            if (this->filter(x, y, z))
+            {
+               using namespace stencil;
+
+               real_t pdfs[Stencil_T::Size];
+
+               // temporal storage of PDFs
+               for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+               {
+                  pdfs[d.toIdx()] = src->get(x, y, z, d.toIdx());
+               }
+
+               Vector3< real_t > velocity;
+               real_t rho = this->densityVelocityIn(velocity, src, x, y, z);
+
+               this->densityVelocityOut(x, y, z, lm, velocity, rho);
+
+               // equilibrium distributions
+               auto pdfs_equ = lbm::EquilibriumDistribution< LatticeModel_T >::get(velocity, rho);
+
+               // possible external forcing on fluid
+               const auto commonForceTerms = lm.forceModel().template directionIndependentTerms< LatticeModel_T >(
+                  x, y, z, velocity, rho, omega, collisionModel.omega_bulk(), collisionModel.omega_odd());
+
+               // check if a particle is present
+               if (particleAndVolumeFractionField->get(x, y, z).size() != size_t(0))
+               {
+                  // total coverage ratio in the cell
+                  real_t Bn = real_t(0);
+
+                  // averaged solid collision operator for all intersecting bodies s
+                  // = \sum_s B_s * \Omega_s_i
+                  std::vector< real_t > omega_n(Stencil_T::Size, real_t(0));
+
+                  // get center of cell
+                  Vector3< real_t > cellCenter = blockStorage_->getBlockLocalCellCenter(*block, Cell(x, y, z));
+
+                  for (auto particleFracIt = particleAndVolumeFractionField->get(x, y, z).begin();
+                       particleFracIt != particleAndVolumeFractionField->get(x, y, z).end(); ++particleFracIt)
+                  {
+                     real_t omega_s(real_c(0));
+                     Vector3< real_t > forceOnParticle(real_c(0));
+
+                     real_t Bs = calculateWeighting< Weighting_T >((*particleFracIt).second, tau);
+                     Bn += Bs;
+
+                     // particle velocity at cell center
+                     const size_t idx = accessor_->uidToIdx(particleFracIt->first);
+                     WALBERLA_ASSERT_UNEQUAL(idx, accessor_->getInvalidIdx(), "Index of particle is invalid!");
+                     const auto particleVelocity = mesa_pd::getVelocityAtWFPoint(idx, *accessor_, cellCenter);
+
+                     // equilibrium distributions with solid velocity
+                     auto pdfs_equ_solid = lbm::EquilibriumDistribution< LatticeModel_T >::get(particleVelocity, rho);
+
+                     for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+                     {
+                        // Different solid collision operators available
+                        if (SolidCollision_T == 1)
+                        {
+                           omega_s =
+                              pdfs[d.toInvIdx()] - pdfs_equ[d.toInvIdx()] + pdfs_equ_solid[d.toIdx()] - pdfs[d.toIdx()];
+                        }
+                        else if (SolidCollision_T == 2)
+                        {
+                           omega_s = pdfs_equ_solid[d.toIdx()] - pdfs[d.toIdx()] +
+                                     (real_c(1) - omega) * (pdfs[d.toIdx()] - pdfs_equ[d.toIdx()]);
+                        }
+                        else if (SolidCollision_T == 3)
+                        {
+                           omega_s = pdfs[d.toInvIdx()] - pdfs_equ_solid[d.toInvIdx()] + pdfs_equ_solid[d.toIdx()] -
+                                     pdfs[d.toIdx()];
+                        }
+                        else
+                        {
+                           WALBERLA_STATIC_ASSERT(SolidCollision_T >= 1 && SolidCollision_T <= 3);
+                           exit(EXIT_FAILURE);
+                        }
+                        real_t BsOmegaS = Bs * omega_s;
+
+                        omega_n[d.toIdx()] += BsOmegaS;
+
+                        forceOnParticle[0] -= BsOmegaS * real_c(d.cx());
+                        forceOnParticle[1] -= BsOmegaS * real_c(d.cy());
+                        forceOnParticle[2] -= BsOmegaS * real_c(d.cz());
+                     }
+
+                     // scale force when using refinement with (dx)^3 / dt
+                     forceOnParticle *= forceScalingFactor;
+
+                     // only if cell inside inner domain
+                     if (src->isInInnerPart(Cell(x, y, z)))
+                     {
+                        // apply force (and automatically torque) on particle
+                        lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic(idx, *accessor_, forceOnParticle,
+                                                                               cellCenter);
+                     }
+                  }
+
+                  // collide step
+                  for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+                  {
+                     // external forcing
+                     const real_t forceTerm = lm.forceModel().template forceTerm< LatticeModel_T >(
+                        x, y, z, velocity, rho, commonForceTerms, LatticeModel_T::w[d.toIdx()], real_c(d.cx()),
+                        real_c(d.cy()), real_c(d.cz()), omega, collisionModel.omega_bulk(), collisionModel.omega_odd());
+
+                     src->get(x, y, z, d.toIdx()) =
+                        pdfs[d.toIdx()] - omega * (real_c(1) - Bn) * (pdfs[d.toIdx()] - pdfs_equ[d.toIdx()]) // SRT
+                        + omega_n[d.toIdx()] + (real_c(1) - Bn) * forceTerm;
+                  }
+               }
+               else
+               {
+                  // SRT collide step
+                  for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d)
+                  {
+                     // external forcing
+                     const real_t forceTerm = lm.forceModel().template forceTerm< LatticeModel_T >(
+                        x, y, z, velocity, rho, commonForceTerms, LatticeModel_T::w[d.toIdx()], real_c(d.cx()),
+                        real_c(d.cy()), real_c(d.cz()), omega, collisionModel.omega_bulk(), collisionModel.omega_odd());
+
+                     src->get(x, y, z, d.toIdx()) =
+                        pdfs[d.toIdx()] - omega * (pdfs[d.toIdx()] - pdfs_equ[d.toIdx()]) + forceTerm;
+                  }
+               }
+            }
+         }
+      }
+   }
+}
+
+/////////////////////////////
+// makePSMSweep FUNCTIONS //
+////////////////////////////
+
+template< typename LatticeModel_T, typename Filter_T, typename DensityVelocityIn_T, typename DensityVelocityOut_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T,
+                      Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& pdfFieldID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const Filter_T& filter, const DensityVelocityIn_T& densityVelocityIn,
+                const DensityVelocityOut_T& densityVelocityOut)
+{
+   using PSMS_T = PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T,
+                            Weighting_T, ParticleAccessor_T >;
+   return shared_ptr< PSMS_T >(new PSMS_T(pdfFieldID, particleAndVolumeFractionFieldID, blockStorage, ac, filter,
+                                          densityVelocityIn, densityVelocityOut));
+}
+
+template< typename LatticeModel_T, typename Filter_T, typename DensityVelocityIn_T, typename DensityVelocityOut_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T,
+                      Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& srcID, const BlockDataID& dstID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const Filter_T& filter, const DensityVelocityIn_T& densityVelocityIn,
+                const DensityVelocityOut_T& densityVelocityOut)
+{
+   using PSMS_T = PSMSweep< LatticeModel_T, Filter_T, DensityVelocityIn_T, DensityVelocityOut_T, SolidCollision_T,
+                            Weighting_T, ParticleAccessor_T >;
+   return shared_ptr< PSMS_T >(new PSMS_T(srcID, dstID, particleAndVolumeFractionFieldID, blockStorage, ac, filter,
+                                          densityVelocityIn, densityVelocityOut));
+}
+
+// only block data IDs of PDF data
+
+template< typename LatticeModel_T, int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::DefaultEvaluationFilter,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                      SolidCollision_T, Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& pdfFieldID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::DefaultEvaluationFilter,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                        SolidCollision_T, Weighting_T >(
+      pdfFieldID, particleAndVolumeFractionFieldID, blockStorage, ac, walberla::field::DefaultEvaluationFilter(),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(), lbm::DefaultDensityVelocityCallback());
+}
+
+template< typename LatticeModel_T, int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::DefaultEvaluationFilter,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                      SolidCollision_T, Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& srcID, const BlockDataID& dstID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::DefaultEvaluationFilter,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                        SolidCollision_T, Weighting_T >(
+      srcID, dstID, particleAndVolumeFractionFieldID, blockStorage, ac, walberla::field::DefaultEvaluationFilter(),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(), lbm::DefaultDensityVelocityCallback());
+}
+
+// block data IDs of PDF data + flag field as filter
+
+template< typename LatticeModel_T, typename FlagField_T, int SolidCollision_T, int Weighting_T,
+          typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                      SolidCollision_T, Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& pdfFieldID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const ConstBlockDataID& flagFieldID, const Set< FlagUID >& cellsToEvaluate)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                        SolidCollision_T, Weighting_T >(
+      pdfFieldID, particleAndVolumeFractionFieldID, blockStorage, ac,
+      walberla::field::FlagFieldEvaluationFilter< FlagField_T >(flagFieldID, cellsToEvaluate),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(), lbm::DefaultDensityVelocityCallback());
+}
+
+template< typename LatticeModel_T, typename FlagField_T, int SolidCollision_T, int Weighting_T,
+          typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                      SolidCollision_T, Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& srcID, const BlockDataID& dstID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const ConstBlockDataID& flagFieldID, const Set< FlagUID >& cellsToEvaluate)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::DefaultDensityVelocityCallback,
+                        SolidCollision_T, Weighting_T >(
+      srcID, dstID, particleAndVolumeFractionFieldID, blockStorage, ac,
+      walberla::field::FlagFieldEvaluationFilter< FlagField_T >(flagFieldID, cellsToEvaluate),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(), lbm::DefaultDensityVelocityCallback());
+}
+
+// block data IDs of PDF data + flag field as filter + block data ID of velocity field (out)
+
+template< typename LatticeModel_T, typename FlagField_T, typename VelocityField_T, int SolidCollision_T,
+          int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::VelocityCallback< VelocityField_T >,
+                      SolidCollision_T, Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& pdfFieldID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const ConstBlockDataID& flagFieldID, const Set< FlagUID >& cellsToEvaluate,
+                const BlockDataID& velocityFieldID)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::VelocityCallback< VelocityField_T >,
+                        SolidCollision_T, Weighting_T >(
+      pdfFieldID, particleAndVolumeFractionFieldID, blockStorage, ac,
+      walberla::field::FlagFieldEvaluationFilter< FlagField_T >(flagFieldID, cellsToEvaluate),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(), lbm::VelocityCallback< VelocityField_T >(velocityFieldID));
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename VelocityField_T, int SolidCollision_T,
+          int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::VelocityCallback< VelocityField_T >,
+                      SolidCollision_T, Weighting_T, ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& srcID, const BlockDataID& dstID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const ConstBlockDataID& flagFieldID, const Set< FlagUID >& cellsToEvaluate,
+                const BlockDataID& velocityFieldID)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation, lbm::VelocityCallback< VelocityField_T >,
+                        SolidCollision_T, Weighting_T >(
+      srcID, dstID, particleAndVolumeFractionFieldID, blockStorage, ac,
+      walberla::field::FlagFieldEvaluationFilter< FlagField_T >(flagFieldID, cellsToEvaluate),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(), lbm::VelocityCallback< VelocityField_T >(velocityFieldID));
+}
+
+// block data IDs of PDF data + flag field as filter + block data IDs of velocity and density field (out)
+
+template< typename LatticeModel_T, typename FlagField_T, typename VelocityField_T, typename DensityField_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation,
+                      lbm::DensityVelocityCallback< VelocityField_T, DensityField_T >, SolidCollision_T, Weighting_T,
+                      ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& pdfFieldID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const ConstBlockDataID& flagFieldID, const Set< FlagUID >& cellsToEvaluate,
+                const BlockDataID& velocityFieldID, const BlockDataID& densityFieldID)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation,
+                        lbm::DensityVelocityCallback< VelocityField_T, DensityField_T >, SolidCollision_T,
+                        Weighting_T >(
+      pdfFieldID, particleAndVolumeFractionFieldID, blockStorage, ac,
+      walberla::field::FlagFieldEvaluationFilter< FlagField_T >(flagFieldID, cellsToEvaluate),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(),
+      lbm::DensityVelocityCallback< VelocityField_T, DensityField_T >(velocityFieldID, densityFieldID));
+}
+
+template< typename LatticeModel_T, typename FlagField_T, typename VelocityField_T, typename DensityField_T,
+          int SolidCollision_T, int Weighting_T, typename ParticleAccessor_T >
+shared_ptr< PSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                      lbm::DefaultDensityEquilibriumVelocityCalculation,
+                      lbm::DensityVelocityCallback< VelocityField_T, DensityField_T >, SolidCollision_T, Weighting_T,
+                      ParticleAccessor_T > >
+   makePSMSweep(const BlockDataID& srcID, const BlockDataID& dstID, const BlockDataID& particleAndVolumeFractionFieldID,
+                const shared_ptr< StructuredBlockStorage >& blockStorage, const shared_ptr< ParticleAccessor_T >& ac,
+                const ConstBlockDataID& flagFieldID, const Set< FlagUID >& cellsToEvaluate,
+                const BlockDataID& velocityFieldID, const BlockDataID& densityFieldID)
+{
+   return makePSMSweep< LatticeModel_T, walberla::field::FlagFieldEvaluationFilter< FlagField_T >,
+                        lbm::DefaultDensityEquilibriumVelocityCalculation,
+                        lbm::DensityVelocityCallback< VelocityField_T, DensityField_T >, SolidCollision_T,
+                        Weighting_T >(
+      srcID, dstID, particleAndVolumeFractionFieldID, blockStorage, ac,
+      walberla::field::FlagFieldEvaluationFilter< FlagField_T >(flagFieldID, cellsToEvaluate),
+      lbm::DefaultDensityEquilibriumVelocityCalculation(),
+      lbm::DensityVelocityCallback< VelocityField_T, DensityField_T >(velocityFieldID, densityFieldID));
+}
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h b/src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h
new file mode 100644
index 000000000..854d27f99
--- /dev/null
+++ b/src/lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h
@@ -0,0 +1,159 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file PSMUtility.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \author Christoph Rettinger <christoph.rettinger@fau.de>
+//! \brief Modification of pe_coupling/partially_saturated_cells_method/PSMUtility.h
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/Field.h"
+#include "field/GhostLayerField.h"
+
+#include "lbm/field/PdfField.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+/*!\brief Evaluates the macroscopic velocity for a given cell when using the PSM method.
+ *
+ * returns the cell local macroscopic velocity for the PSM method
+ * cell local velocity = \f$ (1-\sum_s B_s) * u + \sum_s B_s * v_s \f$
+ * u = fluid velocity
+ * v_s = velocity of object s at cell center
+ *
+ * lbm::PdfField< LatticeModel_T > is typically typedef'ed as PdfField_T;
+ * GhostLayerField< std::vector< std::pair< walberla::id_t, real_t > >, 1 > is typically typedef'ed as
+ * ParticleAndVolumeFractionField_T;
+ *
+ * Weighting_T is like in the PSMSweep.
+ */
+template< typename LatticeModel_T, int Weighting_T, typename ParticleAccessor_T >
+Vector3< real_t > getPSMMacroscopicVelocity(
+   const IBlock& block, lbm::PdfField< LatticeModel_T >* pdfField,
+   GhostLayerField< std::vector< std::pair< walberla::size_t, real_t > >, 1 >* particleAndVolumeFractionField,
+   StructuredBlockStorage& blockStorage, const Cell& cell, const ParticleAccessor_T& ac)
+{
+   static_assert(LatticeModel_T::compressible == false, "Only works with incompressible models!");
+   WALBERLA_ASSERT_NOT_NULLPTR(pdfField);
+   WALBERLA_ASSERT_NOT_NULLPTR(particleAndVolumeFractionField);
+
+   const real_t ralaxationTime =
+      real_c(1) / pdfField->latticeModel().collisionModel().omega(cell.x(), cell.y(), cell.z());
+
+   Vector3< real_t > velocity(real_t(0));
+   real_t totalSolidWeightingInCell = real_t(0);
+
+   for (auto particleFracIt = particleAndVolumeFractionField->get(cell).begin();
+        particleFracIt != particleAndVolumeFractionField->get(cell).end(); ++particleFracIt)
+   {
+      const Vector3< real_t > coordsCellCenter = blockStorage.getBlockLocalCellCenter(block, cell);
+
+      const real_t eps = (*particleFracIt).second;
+
+      const real_t Bs = calculateWeighting< Weighting_T >(eps, ralaxationTime);
+
+      const size_t idx = ac.uidToIdx(particleFracIt->first);
+      WALBERLA_ASSERT_UNEQUAL(idx, ac.getInvalidIdx(), "Index of particle is invalid!");
+      velocity += Bs * mesa_pd::getVelocityAtWFPoint(idx, ac, coordsCellCenter);
+
+      totalSolidWeightingInCell += Bs;
+   }
+
+   velocity += pdfField->getVelocity(cell) * (real_c(1) - totalSolidWeightingInCell);
+
+   return velocity;
+}
+
+/*!\brief Initializes the PDF field inside the bodies according to the velocities of the bodies.
+ *
+ * As the Partially Saturated Cells method relies on executing the LBM sweep also inside the bodies, it is good practice
+ * (and for some PSM variants also required) to initialize the PDF field ( i.e. the velocity ) in agreement with
+ * possible initial velocities of the bodies. This is also the case in the presence of external forces acting on the
+ * fluid, as these will often shift the macroscopic velocities during the initialization of the PDF field.
+ *
+ * Note, that the ParticleAndVolumeFractionMapping for PSM has to be called first to have a valid field.
+ *
+ * Only the velocity of cells intersecting with bodies is set, pure fluid cells remain unchanged.
+ */
+template< typename LatticeModel_T, int Weighting_T, typename ParticleAccessor_T >
+void initializeDomainForPSM(StructuredBlockStorage& blockStorage, const BlockDataID& pdfFieldID,
+                            const BlockDataID& particleAndVolumeFractionFieldID, const ParticleAccessor_T& ac)
+{
+   using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+   // iterate all blocks with an iterator 'block'
+   for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt)
+   {
+      // get the field data out of the block
+      PdfField_T* pdfField = blockIt->getData< PdfField_T >(pdfFieldID);
+      ParticleAndVolumeFractionField_T* particleAndVolumeFractionField =
+         blockIt->getData< ParticleAndVolumeFractionField_T >(particleAndVolumeFractionFieldID);
+
+      auto xyzFieldSize = pdfField->xyzSize();
+      for (auto fieldIt = xyzFieldSize.begin(); fieldIt != xyzFieldSize.end(); ++fieldIt)
+      {
+         Cell cell(*fieldIt);
+
+         const real_t ralaxationTime =
+            real_c(1) / pdfField->latticeModel().collisionModel().omega(cell.x(), cell.y(), cell.z());
+
+         Vector3< real_t > weightedAverageParticleVelocityInCell(real_t(0));
+         real_t totalSolidWeightingInCell = real_t(0);
+
+         for (auto particleFracIt = particleAndVolumeFractionField->get(cell).begin();
+              particleFracIt != particleAndVolumeFractionField->get(cell).end(); ++particleFracIt)
+         {
+            const Vector3< real_t > coordsCellCenter = blockStorage.getBlockLocalCellCenter(*blockIt, cell);
+
+            const real_t eps = (*particleFracIt).second;
+
+            const real_t Bs = calculateWeighting< Weighting_T >(eps, ralaxationTime);
+
+            const size_t idx = ac.uidToIdx(particleFracIt->first);
+            WALBERLA_ASSERT_UNEQUAL(idx, ac.getInvalidIdx(), "Index of particle is invalid!");
+            weightedAverageParticleVelocityInCell += Bs * mesa_pd::getVelocityAtWFPoint(idx, ac, coordsCellCenter);
+
+            totalSolidWeightingInCell += Bs;
+         }
+
+         if (totalSolidWeightingInCell > real_t(0))
+         {
+            Vector3< real_t > fluidVelocityInCell(real_t(0));
+            const real_t rho = pdfField->getDensityAndVelocity(fluidVelocityInCell, cell);
+
+            // set the PDFs to equilibrium with the density rho and the average velocity of all intersecting bodies
+            pdfField->setToEquilibrium(cell,
+                                       fluidVelocityInCell * (real_c(1) - totalSolidWeightingInCell) +
+                                          weightedAverageParticleVelocityInCell,
+                                       rho);
+         }
+      }
+   }
+}
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/src/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h b/src/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h
new file mode 100644
index 000000000..021d3d176
--- /dev/null
+++ b/src/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h
@@ -0,0 +1,124 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ParticleAndVolumeFractionMapping.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "domain_decomposition/StructuredBlockStorage.h"
+
+#include "field/GhostLayerField.h"
+
+#include "lbm_mesapd_coupling/DataTypes.h"
+#include "lbm_mesapd_coupling/mapping/ParticleBoundingBox.h"
+#include "lbm_mesapd_coupling/overlapping/OverlapFraction.h"
+#include "lbm_mesapd_coupling/utility/ParticleSelector.h"
+
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/ParticleStorage.h"
+#include "mesa_pd/kernel/SingleCast.h"
+
+#include <functional>
+#include <mesa_pd/data/ParticleStorage.h>
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+namespace psm
+{
+
+template< typename ParticleAccessor_T, typename ParticleSelector_T >
+class ParticleAndVolumeFractionMapping
+{
+ public:
+   ParticleAndVolumeFractionMapping(const shared_ptr< StructuredBlockStorage >& blockStorage,
+                                    const shared_ptr< ParticleAccessor_T >& ac,
+                                    const ParticleSelector_T& mappingParticleSelector,
+                                    const BlockDataID& particleAndVolumeFractionFieldID,
+                                    const uint_t superSamplingDepth = uint_t(4))
+      : blockStorage_(blockStorage), ac_(ac), mappingParticleSelector_(mappingParticleSelector),
+        particleAndVolumeFractionFieldID_(particleAndVolumeFractionFieldID), superSamplingDepth_(superSamplingDepth)
+   {
+      static_assert(std::is_base_of< mesa_pd::data::IAccessor, ParticleAccessor_T >::value,
+                    "Provide a valid accessor as template");
+   }
+
+   void operator()()
+   {
+      // clear the field
+      for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+      {
+         ParticleAndVolumeFractionField_T* particleAndVolumeFractionField =
+            blockIt->getData< ParticleAndVolumeFractionField_T >(particleAndVolumeFractionFieldID_);
+
+         WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(particleAndVolumeFractionField,
+                                                          particleAndVolumeFractionField->nrOfGhostLayers(),
+                                                          (particleAndVolumeFractionField->get(x, y, z)).clear();)
+      }
+
+      for (size_t idx = 0; idx < ac_->size(); ++idx)
+      {
+         if (mappingParticleSelector_(idx, *ac_)) { update(idx); }
+      }
+   }
+
+ private:
+   void update(const size_t idx)
+   {
+      for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+      {
+         ParticleAndVolumeFractionField_T* particleAndVolumeFractionField =
+            blockIt->getData< ParticleAndVolumeFractionField_T >(particleAndVolumeFractionFieldID_);
+
+         CellInterval cellBB =
+            getParticleCellBB(idx, *ac_, *blockIt, *blockStorage_, particleAndVolumeFractionField->nrOfGhostLayers());
+
+         uint_t level = blockStorage_->getLevel(*blockIt);
+         Vector3< real_t > dxVec(blockStorage_->dx(level), blockStorage_->dy(level), blockStorage_->dz(level));
+
+         // compute the overlap fraction for each cell that has to be considered and save the information
+         for (auto cellIt = cellBB.begin(); cellIt != cellBB.end(); ++cellIt)
+         {
+            Cell cell(*cellIt);
+
+            Vector3< real_t > cellCenter;
+            cellCenter = blockStorage_->getBlockLocalCellCenter(*blockIt, cell);
+
+            real_t fraction = singleCast_(idx, *ac_, overlapFractionFctr_, ac_, cellCenter, dxVec, superSamplingDepth_);
+
+            id_t particleUid = ac_->getUid(idx);
+            if (fraction > real_t(0)) { particleAndVolumeFractionField->get(cell).emplace_back(particleUid, fraction); }
+         }
+      }
+   }
+
+   shared_ptr< StructuredBlockStorage > blockStorage_;
+   const shared_ptr< ParticleAccessor_T > ac_;
+   ParticleSelector_T mappingParticleSelector_;
+   const BlockDataID particleAndVolumeFractionFieldID_;
+   const uint_t superSamplingDepth_;
+
+   mesa_pd::kernel::SingleCast singleCast_;
+   OverlapFractionFunctor overlapFractionFctr_;
+};
+
+} // namespace psm
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
diff --git a/tests/lbm_mesapd_coupling/CMakeLists.txt b/tests/lbm_mesapd_coupling/CMakeLists.txt
index eeb208530..94438a2d9 100644
--- a/tests/lbm_mesapd_coupling/CMakeLists.txt
+++ b/tests/lbm_mesapd_coupling/CMakeLists.txt
@@ -47,3 +47,28 @@ waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_UTIL_HydForceMultBlocks_VVAvg CO
 waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_UTIL_HydForceMultBlocks_EulerNoAvg COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_UTIL_HydForceMultBlocks> --noForceAveraging PROCESSES 4 )
 waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_UTIL_HydForceMultBlocks_VVNoAvg COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_UTIL_HydForceMultBlocks> --noForceAveraging --useVV PROCESSES 4 )
 
+waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_PSM_BodyAndVolumeFractionMapping FILES partially_saturated_cells_method/ParticleAndVolumeFractionMapping.cpp DEPENDS blockforest boundary core field lbm_mesapd_coupling stencil mesa_pd )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_BodyAndVolumeFractionMapping PROCESSES 27 )
+
+waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_PSM_DragForceSphere FILES partially_saturated_cells_method/DragForceSphere.cpp DEPENDS blockforest boundary core field lbm lbm_mesapd_coupling timeloop mesa_pd )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_DragForceSphereFuncTest COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_DragForceSphere> --funcTest PROCESSES 8 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_DragForceSphere COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_DragForceSphere> PROCESSES 8 LABELS longrun CONFIGURATIONS Release RelWithDbgInfo)
+
+waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_PSM_SettlingSphere FILES partially_saturated_cells_method/SettlingSphere.cpp DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling timeloop mesa_pd )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_SettlingSphereFuncTest COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_SettlingSphere> --funcTest PROCESSES 4 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_SettlingSphere COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_SettlingSphere> --resolution 70 PROCESSES 4 LABELS longrun CONFIGURATIONS Release RelWithDbgInfo )
+
+waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphere FILES partially_saturated_cells_method/TorqueSphere.cpp DEPENDS blockforest boundary core domain_decomposition field lbm stencil timeloop )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC1W1FuncTest     COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --funcTest --SC1W1   PROCESSES 1 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC1W1SingleTest   COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC1W1              PROCESSES 1 LABELS longrun     CONFIGURATIONS Release RelWithDbgInfo )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC1W1ParallelTest COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC1W1              PROCESSES 8 LABELS verylongrun CONFIGURATIONS Release RelWithDbgInfo )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC2W1FuncTest     COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --funcTest --SC2W1   PROCESSES 1 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC2W1SingleTest   COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC2W1              PROCESSES 1 LABELS longrun     CONFIGURATIONS Release RelWithDbgInfo )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC3W1FuncTest     COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --funcTest --SC3W1   PROCESSES 1 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC3W1SingleTest   COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC3W1              PROCESSES 1 LABELS longrun     CONFIGURATIONS Release RelWithDbgInfo )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC1W2FuncTest     COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --funcTest --SC1W2   PROCESSES 1 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC1W2SingleTest   COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC1W2              PROCESSES 1 LABELS longrun     CONFIGURATIONS Release RelWithDbgInfo )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC2W2FuncTest     COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --funcTest --SC2W2   PROCESSES 1 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC2W2SingleTest   COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC2W2              PROCESSES 1 LABELS longrun     CONFIGURATIONS Release RelWithDbgInfo )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC3W2FuncTest     COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --funcTest --SC3W2   PROCESSES 1 )
+waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_PSM_TorqueSphereSC3W2SingleTest   COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_PSM_TorqueSphere> --SC3W2              PROCESSES 1 LABELS longrun     CONFIGURATIONS Release RelWithDbgInfo )
diff --git a/tests/lbm_mesapd_coupling/partially_saturated_cells_method/DragForceSphere.cpp b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/DragForceSphere.cpp
new file mode 100644
index 000000000..fc7f56dd3
--- /dev/null
+++ b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/DragForceSphere.cpp
@@ -0,0 +1,503 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file DragForceSphere.cpp
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \author Christoph Rettinger <christoph.rettinger@fau.de>
+//! \brief Modification of momentum_exchange_method/DragForceSphere.cpp
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "boundary/all.h"
+
+#include "core/DataTypes.h"
+#include "core/Environment.h"
+#include "core/SharedFunctor.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/mpi/MPIManager.h"
+#include "core/mpi/Reduce.h"
+#include "core/timing/RemainingTimeLogger.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm/communication/PdfFieldPackInfo.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/ForceModel.h"
+#include "lbm/sweeps/SweepWrappers.h"
+
+#include "lbm_mesapd_coupling/DataTypes.h"
+#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h"
+#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h"
+#include "lbm_mesapd_coupling/utility/ParticleSelector.h"
+#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h"
+
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/ParticleStorage.h"
+#include "mesa_pd/data/ShapeStorage.h"
+#include "mesa_pd/domain/BlockForestDomain.h"
+#include "mesa_pd/kernel/ParticleSelector.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <iostream>
+#include <vector>
+
+#include "Utility.h"
+
+namespace drag_force_sphere_psm
+{
+
+///////////
+// USING //
+///////////
+
+using namespace walberla;
+using walberla::uint_t;
+
+using ForceModel_T   = lbm::force_model::LuoConstant;
+using LatticeModel_T = lbm::D3Q19< lbm::collision_model::TRT, false, ForceModel_T >;
+
+using Stencil_T  = LatticeModel_T::Stencil;
+using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+////////////////
+// PARAMETERS //
+////////////////
+
+struct Setup
+{
+   uint_t checkFrequency;
+   real_t visc;
+   real_t tau;
+   real_t radius;
+   uint_t length;
+   real_t chi;
+   real_t extForce;
+   real_t analyticalDrag;
+};
+
+template< typename ParticleAccessor_T >
+class DragForceEvaluator
+{
+ public:
+   DragForceEvaluator(SweepTimeloop* timeloop, Setup* setup, const shared_ptr< StructuredBlockStorage >& blocks,
+                      const BlockDataID& pdfFieldID, const shared_ptr< ParticleAccessor_T >& ac,
+                      walberla::id_t sphereID)
+      : timeloop_(timeloop), setup_(setup), blocks_(blocks), pdfFieldID_(pdfFieldID), ac_(ac), sphereID_(sphereID),
+        normalizedDragOld_(0.0), normalizedDragNew_(0.0)
+   {
+      // calculate the analytical drag force value based on the series expansion of chi
+      // see also Sangani - Slow flow through a periodic array of spheres, IJMF 1982. Eq. 60 and Table 1
+      real_t analyticalDrag = real_c(0);
+      real_t tempChiPowS    = real_c(1);
+
+      // coefficients to calculate the drag in a series expansion
+      real_t dragCoefficients[31] = { real_c(1.000000),  real_c(1.418649),  real_c(2.012564),   real_c(2.331523),
+                                      real_c(2.564809),  real_c(2.584787),  real_c(2.873609),   real_c(3.340163),
+                                      real_c(3.536763),  real_c(3.504092),  real_c(3.253622),   real_c(2.689757),
+                                      real_c(2.037769),  real_c(1.809341),  real_c(1.877347),   real_c(1.534685),
+                                      real_c(0.9034708), real_c(0.2857896), real_c(-0.5512626), real_c(-1.278724),
+                                      real_c(1.013350),  real_c(5.492491),  real_c(4.615388),   real_c(-0.5736023),
+                                      real_c(-2.865924), real_c(-4.709215), real_c(-6.870076),  real_c(0.1455304),
+                                      real_c(12.51891),  real_c(9.742811),  real_c(-5.566269) };
+
+      for (uint_t s = 0; s <= uint_t(30); ++s)
+      {
+         analyticalDrag += dragCoefficients[s] * tempChiPowS;
+         tempChiPowS *= setup->chi;
+      }
+      setup_->analyticalDrag = analyticalDrag;
+   }
+
+   // evaluate the acting drag force
+   void operator()()
+   {
+      const uint_t timestep(timeloop_->getCurrentTimeStep() + 1);
+
+      if (timestep % setup_->checkFrequency != 0) return;
+
+      // get force in x-direction acting on the sphere
+      real_t forceX = computeDragForce();
+      // get average volumetric flowrate in the domain
+      real_t uBar = computeAverageVel();
+
+      // f_total = f_drag + f_buoyancy
+      real_t totalForce =
+         forceX + real_c(4.0 / 3.0) * math::pi * setup_->radius * setup_->radius * setup_->radius * setup_->extForce;
+
+      real_t normalizedDragForce = totalForce / real_c(6.0 * math::pi * setup_->visc * setup_->radius * uBar);
+
+      // update drag force values
+      normalizedDragOld_ = normalizedDragNew_;
+      normalizedDragNew_ = normalizedDragForce;
+   }
+
+   // return the relative temporal change in the normalized drag
+   real_t getDragForceDiff() const { return std::fabs((normalizedDragNew_ - normalizedDragOld_) / normalizedDragNew_); }
+
+   // return the drag force
+   real_t getDragForce() const { return normalizedDragNew_; }
+
+   void logResultToFile(const std::string& filename) const
+   {
+      // write to file if desired
+      // format: length tau viscosity simulatedDrag analyticalDrag\n
+      WALBERLA_ROOT_SECTION()
+      {
+         std::ofstream file;
+         file.open(filename.c_str(), std::ofstream::app);
+         file.precision(8);
+         file << setup_->length << " " << setup_->tau << " " << setup_->visc << " " << normalizedDragNew_ << " "
+              << setup_->analyticalDrag << "\n";
+         file.close();
+      }
+   }
+
+ private:
+   // obtain the drag force acting on the sphere by summing up all the process local parts of fX
+   real_t computeDragForce()
+   {
+      size_t idx = ac_->uidToIdx(sphereID_);
+      WALBERLA_ASSERT_UNEQUAL(idx, ac_->getInvalidIdx(), "Index of particle is invalid!");
+      real_t force = real_t(0);
+      if (idx != ac_->getInvalidIdx()) { force = ac_->getHydrodynamicForce(idx)[0]; }
+
+      WALBERLA_MPI_SECTION() { mpi::allReduceInplace(force, mpi::SUM); }
+
+      return force;
+   }
+
+   // calculate the average velocity in forcing direction (here: x) inside the domain (assuming dx=1)
+   real_t computeAverageVel()
+   {
+      auto velocity_sum = real_t(0);
+      // iterate all blocks stored locally on this process
+      for (auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt)
+      {
+         // retrieve the pdf field and the flag field from the block
+         PdfField_T* pdfField = blockIt->getData< PdfField_T >(pdfFieldID_);
+
+         // get the flag that marks a cell as being fluid
+
+         auto xyzField = pdfField->xyzSize();
+         for (auto cell : xyzField)
+         {
+            velocity_sum += pdfField->getVelocity(cell)[0];
+         }
+      }
+
+      WALBERLA_MPI_SECTION() { mpi::allReduceInplace(velocity_sum, mpi::SUM); }
+
+      return velocity_sum / real_c(setup_->length * setup_->length * setup_->length);
+   }
+
+   SweepTimeloop* timeloop_;
+
+   Setup* setup_;
+
+   shared_ptr< StructuredBlockStorage > blocks_;
+   const BlockDataID pdfFieldID_;
+
+   shared_ptr< ParticleAccessor_T > ac_;
+   const walberla::id_t sphereID_;
+
+   // drag coefficient
+   real_t normalizedDragOld_;
+   real_t normalizedDragNew_;
+};
+
+//////////
+// MAIN //
+//////////
+
+//*******************************************************************************************************************
+/*!\brief Testcase that checks the drag force acting on a fixed sphere in the center of a cubic domain in Stokes flow
+ *
+ * The drag force for this problem (often denoted as Simple Cubic setup) is given by a semi-analytical series expansion.
+ * The cubic domain is periodic in all directions, making it a physically infinite periodic array of spheres.
+   \verbatim
+           _______________
+        ->|               |->
+        ->|      ___      |->
+      W ->|     /   \     |-> E
+      E ->|    |  x  |    |-> A
+      S ->|     \___/     |-> S
+      T ->|               |-> T
+        ->|_______________|->
+
+   \endverbatim
+ *
+ * The collision model used for the LBM is TRT with a relaxation parameter tau=1.5 and the magic parameter 3/16.
+ * The Stokes approximation of the equilibrium PDFs is used.
+ * The flow is driven by a constant particle force of 1e-5.
+ * The domain is 32x32x32, and the sphere has a diameter of 16 cells ( chi * domainlength )
+ * The simulation is run until the relative change in the dragforce between 100 time steps is less than 1e-5.
+ * The RPD is not used since the sphere is kept fixed and the force is explicitly reset after each time step.
+ * To avoid periodicity constrain problems, the sphere is set as global.
+ *
+ */
+//*******************************************************************************************************************
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+
+   mpi::Environment env(argc, argv);
+
+   auto processes = MPIManager::instance()->numProcesses();
+
+   if (processes != 1 && processes != 2 && processes != 4 && processes != 8)
+   {
+      std::cerr << "Number of processes must be equal to either 1, 2, 4, or 8!" << std::endl;
+      return EXIT_FAILURE;
+   }
+
+   ///////////////////
+   // Customization //
+   ///////////////////
+
+   bool shortrun = false;
+   bool funcTest = false;
+   bool logging  = false;
+   real_t tau    = real_c(1.5);
+   uint_t length = uint_c(32);
+
+   for (int i = 1; i < argc; ++i)
+   {
+      if (std::strcmp(argv[i], "--shortrun") == 0)
+      {
+         shortrun = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--funcTest") == 0)
+      {
+         funcTest = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--logging") == 0)
+      {
+         logging = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--tau") == 0)
+      {
+         tau = real_c(std::atof(argv[++i]));
+         continue;
+      }
+      if (std::strcmp(argv[i], "--length") == 0)
+      {
+         length = uint_c(std::atof(argv[++i]));
+         continue;
+      }
+      WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]);
+   }
+
+   ///////////////////////////
+   // SIMULATION PROPERTIES //
+   ///////////////////////////
+
+   Setup setup;
+
+   setup.length                  = length;       // length of the cubic domain in lattice cells
+   setup.chi                     = real_c(0.5);  // porosity parameter: diameter / length
+   setup.tau                     = tau;          // relaxation time
+   setup.extForce                = real_c(1e-7); // constant particle force in lattice units
+   setup.checkFrequency          = uint_t(100);  // evaluate the drag force only every checkFrequency time steps
+   setup.radius                  = real_c(0.5) * setup.chi * real_c(setup.length); // sphere radius
+   setup.visc                    = (setup.tau - real_c(0.5)) / real_c(3);          // viscosity in lattice units
+   const real_t omega            = real_c(1) / setup.tau;                          // relaxation rate
+   const real_t dx               = real_c(1);                                      // lattice dx
+   const real_t convergenceLimit = real_c(1e-7); // tolerance for relative change in drag force
+   const uint_t timesteps =
+      funcTest ? 1 : (shortrun ? uint_c(150) : uint_c(50000)); // maximum number of time steps for the whole simulation
+
+   ///////////////////////////
+   // BLOCK STRUCTURE SETUP //
+   ///////////////////////////
+
+   const uint_t XBlocks = (processes >= 2) ? uint_t(2) : uint_t(1);
+   const uint_t YBlocks = (processes >= 4) ? uint_t(2) : uint_t(1);
+   const uint_t ZBlocks = (processes == 8) ? uint_t(2) : uint_t(1);
+   const uint_t XCells  = setup.length / XBlocks;
+   const uint_t YCells  = setup.length / YBlocks;
+   const uint_t ZCells  = setup.length / ZBlocks;
+
+   // create fully periodic domain
+   auto blocks = blockforest::createUniformBlockGrid(XBlocks, YBlocks, ZBlocks, XCells, YCells, ZCells, dx, true, true,
+                                                     true, true);
+
+   /////////
+   // RPD //
+   /////////
+
+   mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer());
+
+   // init data structures
+   auto ps                  = std::make_shared< mesa_pd::data::ParticleStorage >(1);
+   auto ss                  = std::make_shared< mesa_pd::data::ShapeStorage >();
+   using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape;
+   auto accessor            = make_shared< ParticleAccessor_T >(ps, ss);
+   auto sphereShape         = ss->create< mesa_pd::data::Sphere >(setup.radius);
+
+   //////////////////
+   // RPD COUPLING //
+   //////////////////
+
+   // connect to pe
+   const real_t overlap = real_t(1.5) * dx;
+
+   if (setup.radius > real_c(setup.length) * real_t(0.5) - overlap)
+   {
+      std::cerr << "Periodic sphere is too large and would lead to incorrect mapping!" << std::endl;
+      // solution: create the periodic copies explicitly
+      return EXIT_FAILURE;
+   }
+
+   // create the sphere in the middle of the domain
+   // it is global and thus present on all processes
+   Vector3< real_t > position(real_c(setup.length) * real_c(0.5));
+   walberla::id_t sphereID;
+   {
+      mesa_pd::data::Particle&& p = *ps->create(true);
+      p.setPosition(position);
+      p.setInteractionRadius(setup.radius);
+      p.setOwner(mpi::MPIManager::instance()->rank());
+      p.setShapeID(sphereShape);
+      sphereID = p.getUid();
+   }
+
+   ///////////////////////
+   // ADD DATA TO BLOCKS //
+   ////////////////////////
+
+   // create the lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::TRT::constructWithMagicNumber(omega),
+                                                ForceModel_T(Vector3< real_t >(setup.extForce, 0, 0)));
+
+   // add PDF field
+   BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >(
+      blocks, "pdf field (zyxf)", latticeModel, Vector3< real_t >(real_t(0)), real_t(1), uint_t(1), field::zyxf);
+
+   ///////////////
+   // TIME LOOP //
+   ///////////////
+
+   // create the timeloop
+   SweepTimeloop timeloop(blocks->getBlockStorage(), timesteps);
+
+   // setup of the LBM communication for synchronizing the pdf field between neighboring blocks
+   blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme(blocks);
+   optimizedPDFCommunicationScheme.addPackInfo(
+      make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >(pdfFieldID)); // optimized sync
+
+   // initially map particles into the LBM simulation
+   BlockDataID particleAndVolumeFractionFieldID =
+      field::addToStorage< lbm_mesapd_coupling::psm::ParticleAndVolumeFractionField_T >(
+         blocks, "particle and volume fraction field",
+         std::vector< lbm_mesapd_coupling::psm::ParticleAndVolumeFraction_T >(), field::zyxf, 0);
+   lbm_mesapd_coupling::psm::ParticleAndVolumeFractionMapping particleMapping(
+      blocks, accessor, lbm_mesapd_coupling::GlobalParticlesSelector(), particleAndVolumeFractionFieldID, 4);
+   particleMapping();
+
+   lbm_mesapd_coupling::psm::initializeDomainForPSM< LatticeModel_T, 1 >(*blocks, pdfFieldID,
+                                                                         particleAndVolumeFractionFieldID, *accessor);
+
+   walberla::lbm_mesapd_coupling::FractionFieldSum fractionFieldSum(blocks, particleAndVolumeFractionFieldID);
+   // check that the sum of all fractions is roughly the volume of the particle
+   WALBERLA_CHECK_LESS(
+      std::fabs(4.0 / 3.0 * math::pi * setup.radius * setup.radius * setup.radius - fractionFieldSum()), real_c(1.0));
+
+   // since external forcing is applied, the evaluation of the velocity has to be carried out directly after the
+   // streaming step however, the default sweep is a  stream - collide step, i.e. after the sweep, the velocity
+   // evaluation is not correct solution: split the sweep explicitly into collide and stream
+   auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 1, 1 >(
+      pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+
+   // collision sweep
+   timeloop.add() << Sweep(lbm::makeCollideSweep(sweep), "cell-wise LB sweep (collide)");
+
+   // add LBM communication function and streaming & force evaluation
+   using DragForceEval_T = DragForceEvaluator< ParticleAccessor_T >;
+   auto forceEval        = make_shared< DragForceEval_T >(&timeloop, &setup, blocks, pdfFieldID, accessor, sphereID);
+   timeloop.add() << BeforeFunction(optimizedPDFCommunicationScheme, "LBM Communication")
+                  << Sweep(lbm::makeStreamSweep(sweep), "cell-wise LB sweep (stream)")
+                  << AfterFunction(SharedFunctor< DragForceEval_T >(forceEval), "drag force evaluation");
+
+   // resetting force
+   timeloop.addFuncAfterTimeStep(
+      [ps, accessor]() {
+         ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor,
+                             lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel(), *accessor);
+      },
+      "reset force on sphere");
+
+   timeloop.addFuncAfterTimeStep(RemainingTimeLogger(timeloop.getNrOfTimeSteps()), "Remaining Time Logger");
+
+   ////////////////////////
+   // EXECUTE SIMULATION //
+   ////////////////////////
+
+   WcTimingPool timeloopTiming;
+
+   // time loop
+   for (uint_t i = 0; i < timesteps; ++i)
+   {
+      // perform a single simulation step
+      timeloop.singleStep(timeloopTiming);
+
+      // check if the relative change in the normalized drag force is below the specified convergence criterion
+      if (i > setup.checkFrequency && forceEval->getDragForceDiff() < convergenceLimit)
+      {
+         // if simulation has converged, terminate simulation
+         break;
+      }
+   }
+
+   timeloopTiming.logResultOnRoot();
+
+   if (!funcTest && !shortrun)
+   {
+      // check the result
+      real_t relErr = std::fabs((setup.analyticalDrag - forceEval->getDragForce()) / setup.analyticalDrag);
+      if (logging)
+      {
+         WALBERLA_ROOT_SECTION()
+         {
+            std::cout << "Analytical drag: " << setup.analyticalDrag << "\n"
+                      << "Simulated drag: " << forceEval->getDragForce() << "\n"
+                      << "Relative error: " << relErr << "\n";
+         }
+         forceEval->logResultToFile("log_DragForceSphere.txt");
+      }
+      // the relative error has to be below 10%
+      WALBERLA_CHECK_LESS(relErr, real_c(0.1));
+   }
+
+   return 0;
+}
+
+} // namespace drag_force_sphere_psm
+
+int main(int argc, char** argv) { drag_force_sphere_psm::main(argc, argv); }
diff --git a/tests/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.cpp b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.cpp
new file mode 100644
index 000000000..eb03d3537
--- /dev/null
+++ b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.cpp
@@ -0,0 +1,212 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file ParticleAndVolumeFractionMapping.cpp
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+
+#include "core/DataTypes.h"
+#include "core/Environment.h"
+#include "core/debug/TestSubsystem.h"
+
+#include "field/AddToStorage.h"
+
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h"
+#include "lbm_mesapd_coupling/utility/ParticleSelector.h"
+
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/ParticleStorage.h"
+#include "mesa_pd/data/ShapeStorage.h"
+#include "mesa_pd/domain/BlockForestDomain.h"
+#include "mesa_pd/kernel/SemiImplicitEuler.h"
+#include "mesa_pd/mpi/SyncNextNeighbors.h"
+
+#include <functional>
+#include <memory>
+
+#include "Utility.h"
+
+namespace particle_volume_fraction_check
+{
+
+///////////
+// USING //
+///////////
+
+using namespace walberla;
+using walberla::uint_t;
+using namespace lbm_mesapd_coupling;
+
+////////////////
+// Parameters //
+////////////////
+
+struct Setup
+{
+   // domain size (in lattice cells) in x, y and z direction
+   uint_t xlength;
+   uint_t ylength;
+   uint_t zlength;
+
+   // number of block in x, y and z, direction
+   Vector3< uint_t > nBlocks;
+
+   // cells per block in x, y and z direction
+   Vector3< uint_t > cellsPerBlock;
+
+   real_t sphereDiam;
+
+   uint_t timesteps;
+};
+
+//////////
+// MAIN //
+//////////
+
+//*******************************************************************************************************************
+/*!\brief Testcase that checks if ParticleAndVolumeFractionMapping.h works as intended
+ *
+ * A sphere particle is placed inside the domain and is moving with a constant velocity. The overlap fraction is
+ * computed for all cells in each time step. If the mapping is correct, the sum over all fractions should be roughly
+ * equivalent to the volume of the sphere.
+ *
+ */
+//*******************************************************************************************************************
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+
+   mpi::Environment env(argc, argv);
+
+   auto processes = MPIManager::instance()->numProcesses();
+
+   if (processes != 27)
+   {
+      std::cerr << "Number of processes must be 27!" << std::endl;
+      return EXIT_FAILURE;
+   }
+
+   ///////////////////////////
+   // SIMULATION PROPERTIES //
+   ///////////////////////////
+
+   Setup setup;
+
+   setup.sphereDiam = real_c(12);
+   setup.zlength    = uint_c(4 * setup.sphereDiam);
+   setup.xlength    = setup.zlength;
+   setup.ylength    = setup.zlength;
+
+   const real_t sphereRadius = real_c(0.5) * setup.sphereDiam;
+   const real_t dx           = real_c(1);
+
+   setup.timesteps = 100;
+
+   ///////////////////////////
+   // BLOCK STRUCTURE SETUP //
+   ///////////////////////////
+
+   setup.nBlocks[0]       = uint_c(3);
+   setup.nBlocks[1]       = uint_c(3);
+   setup.nBlocks[2]       = uint_c(3);
+   setup.cellsPerBlock[0] = setup.xlength / setup.nBlocks[0];
+   setup.cellsPerBlock[1] = setup.ylength / setup.nBlocks[1];
+   setup.cellsPerBlock[2] = setup.zlength / setup.nBlocks[2];
+
+   auto blocks =
+      blockforest::createUniformBlockGrid(setup.nBlocks[0], setup.nBlocks[1], setup.nBlocks[2], setup.cellsPerBlock[0],
+                                          setup.cellsPerBlock[1], setup.cellsPerBlock[2], dx, true, true, true, true);
+
+   ////////////
+   // MesaPD //
+   ////////////
+
+   auto mesapdDomain        = std::make_shared< mesa_pd::domain::BlockForestDomain >(blocks->getBlockForestPointer());
+   auto ps                  = std::make_shared< mesa_pd::data::ParticleStorage >(1);
+   auto ss                  = std::make_shared< mesa_pd::data::ShapeStorage >();
+   using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape;
+   auto accessor            = walberla::make_shared< ParticleAccessor_T >(ps, ss);
+
+   // set up synchronization
+   std::function< void(void) > syncCall = [&]() {
+      const real_t overlap = real_t(1.5) * dx;
+      mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc;
+      syncNextNeighborFunc(*ps, *mesapdDomain, overlap);
+   };
+
+   // add the sphere in the center of the domain
+   Vector3< real_t > position(real_c(setup.xlength) * real_c(0.5), real_c(setup.ylength) * real_c(0.5),
+                              real_c(setup.zlength) * real_c(0.5));
+   Vector3< real_t > velocity(real_c(0.1), real_c(0.1), real_c(0.1));
+   auto sphereShape = ss->create< mesa_pd::data::Sphere >(sphereRadius);
+
+   if (mesapdDomain->isContainedInProcessSubdomain(uint_c(walberla::mpi::MPIManager::instance()->rank()), position))
+   {
+      auto sphereParticle = ps->create();
+
+      sphereParticle->setShapeID(sphereShape);
+      sphereParticle->setType(0);
+      sphereParticle->setPosition(position);
+      sphereParticle->setLinearVelocity(velocity);
+      sphereParticle->setOwner(walberla::MPIManager::instance()->rank());
+      sphereParticle->setInteractionRadius(sphereRadius);
+   }
+
+   syncCall();
+
+   ///////////////////////
+   // ADD DATA TO BLOCKS //
+   ////////////////////////
+
+   // add particle and volume fraction field (needed for the PSM)
+   BlockDataID particleAndVolumeFractionFieldID =
+      field::addToStorage< lbm_mesapd_coupling::psm::ParticleAndVolumeFractionField_T >(
+         blocks, "particle and volume fraction field",
+         std::vector< lbm_mesapd_coupling::psm::ParticleAndVolumeFraction_T >(), field::zyxf, 0);
+
+   // calculate fraction
+   lbm_mesapd_coupling::psm::ParticleAndVolumeFractionMapping particleMapping(
+      blocks, accessor, lbm_mesapd_coupling::RegularParticlesSelector(), particleAndVolumeFractionFieldID, 4);
+   particleMapping();
+
+   FractionFieldSum fractionFieldSum(blocks, particleAndVolumeFractionFieldID);
+   auto selector = mesa_pd::kernel::SelectMaster();
+   mesa_pd::kernel::SemiImplicitEuler particleIntegration(1.0);
+
+   for (uint_t i = 0; i < setup.timesteps; ++i)
+   {
+      // check that the sum over all fractions is roughly the volume of the sphere
+      real_t sum = fractionFieldSum();
+      WALBERLA_CHECK_LESS(std::fabs(4.0 / 3.0 * math::pi * sphereRadius * sphereRadius * sphereRadius - sum),
+                          real_c(1.0));
+
+      // update position
+      ps->forEachParticle(false, selector, *accessor, particleIntegration, *accessor);
+      syncCall();
+      // update fraction mapping
+      particleMapping();
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace particle_volume_fraction_check
+
+int main(int argc, char** argv) { particle_volume_fraction_check::main(argc, argv); }
diff --git a/tests/lbm_mesapd_coupling/partially_saturated_cells_method/SettlingSphere.cpp b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/SettlingSphere.cpp
new file mode 100644
index 000000000..7fb9d96b4
--- /dev/null
+++ b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/SettlingSphere.cpp
@@ -0,0 +1,877 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file SettlingSphere.cpp
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \author Christoph Rettinger <christoph.rettinger@fau.de>
+//! \brief Modification of momentum_exchange_method/SettlingSphere.cpp
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "boundary/all.h"
+
+#include "core/DataTypes.h"
+#include "core/Environment.h"
+#include "core/debug/Debug.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/all.h"
+#include "core/math/all.h"
+#include "core/timing/RemainingTimeLogger.h"
+
+#include "domain_decomposition/SharedSweep.h"
+
+#include "field/AddToStorage.h"
+#include "field/communication/PackInfo.h"
+#include "field/vtk/all.h"
+
+#include "lbm/boundary/all.h"
+#include "lbm/communication/PdfFieldPackInfo.h"
+#include "lbm/field/AddToStorage.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/sweeps/SweepWrappers.h"
+#include "lbm/vtk/all.h"
+
+#include "lbm_mesapd_coupling/DataTypes.h"
+#include "lbm_mesapd_coupling/mapping/ParticleMapping.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h"
+#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h"
+#include "lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h"
+#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h"
+#include "lbm_mesapd_coupling/utility/InitializeHydrodynamicForceTorqueForAveragingKernel.h"
+#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h"
+#include "lbm_mesapd_coupling/utility/ParticleSelector.h"
+#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h"
+
+#include "mesa_pd/collision_detection/AnalyticContactDetection.h"
+#include "mesa_pd/data/DataTypes.h"
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/ParticleStorage.h"
+#include "mesa_pd/data/ShapeStorage.h"
+#include "mesa_pd/data/shape/HalfSpace.h"
+#include "mesa_pd/data/shape/Sphere.h"
+#include "mesa_pd/domain/BlockForestDomain.h"
+#include "mesa_pd/kernel/DoubleCast.h"
+#include "mesa_pd/kernel/ExplicitEuler.h"
+#include "mesa_pd/kernel/ParticleSelector.h"
+#include "mesa_pd/kernel/SpringDashpot.h"
+#include "mesa_pd/kernel/VelocityVerlet.h"
+#include "mesa_pd/mpi/ContactFilter.h"
+#include "mesa_pd/mpi/ReduceProperty.h"
+#include "mesa_pd/mpi/SyncNextNeighbors.h"
+#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h"
+#include "mesa_pd/mpi/notifications/HydrodynamicForceTorqueNotification.h"
+#include "mesa_pd/vtk/ParticleVtkOutput.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include "vtk/all.h"
+
+#include <functional>
+
+#include "Utility.h"
+
+namespace settling_sphere
+{
+
+///////////
+// USING //
+///////////
+
+using namespace walberla;
+using walberla::uint_t;
+
+using LatticeModel_T = lbm::D3Q19< lbm::collision_model::TRT >;
+
+using Stencil_T  = LatticeModel_T::Stencil;
+using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+using flag_t      = walberla::uint8_t;
+using FlagField_T = FlagField< flag_t >;
+
+const uint_t FieldGhostLayers = 1;
+
+///////////
+// FLAGS //
+///////////
+
+const FlagUID Fluid_Flag("fluid");
+const FlagUID NoSlip_Flag("no slip");
+
+/////////////////////////////////////
+// BOUNDARY HANDLING CUSTOMIZATION //
+/////////////////////////////////////
+template< typename ParticleAccessor_T >
+class MyBoundaryHandling
+{
+ public:
+   using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >;
+   using Type     = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T >;
+
+   MyBoundaryHandling(const BlockDataID& flagFieldID, const BlockDataID& pdfFieldID,
+                      const shared_ptr< ParticleAccessor_T >& ac)
+      : flagFieldID_(flagFieldID), pdfFieldID_(pdfFieldID), ac_(ac)
+   {}
+
+   Type* operator()(IBlock* const block, const StructuredBlockStorage* const /*storage*/) const
+   {
+      WALBERLA_ASSERT_NOT_NULLPTR(block);
+
+      auto* flagField = block->getData< FlagField_T >(flagFieldID_);
+      auto* pdfField  = block->getData< PdfField_T >(pdfFieldID_);
+
+      const auto fluid =
+         flagField->flagExists(Fluid_Flag) ? flagField->getFlag(Fluid_Flag) : flagField->registerFlag(Fluid_Flag);
+
+      Type* handling =
+         new Type("moving obstacle boundary handling", flagField, fluid, NoSlip_T("NoSlip", NoSlip_Flag, pdfField));
+
+      handling->fillWithDomain(FieldGhostLayers);
+
+      return handling;
+   }
+
+ private:
+   const BlockDataID flagFieldID_;
+   const BlockDataID pdfFieldID_;
+
+   shared_ptr< ParticleAccessor_T > ac_;
+};
+//*******************************************************************************************************************
+
+//*******************************************************************************************************************
+/*!\brief Evaluating the position and velocity of the sphere
+ *
+ */
+//*******************************************************************************************************************
+template< typename ParticleAccessor_T >
+class SpherePropertyLogger
+{
+ public:
+   SpherePropertyLogger(const shared_ptr< ParticleAccessor_T >& ac, walberla::id_t sphereUid,
+                        const std::string& fileName, bool fileIO, real_t dx_SI, real_t dt_SI, real_t diameter,
+                        real_t gravitationalForceMag)
+      : ac_(ac), sphereUid_(sphereUid), fileName_(fileName), fileIO_(fileIO), dx_SI_(dx_SI), dt_SI_(dt_SI),
+        diameter_(diameter), gravitationalForceMag_(gravitationalForceMag), position_(real_t(0)),
+        maxVelocity_(real_t(0))
+   {
+      if (fileIO_)
+      {
+         WALBERLA_ROOT_SECTION()
+         {
+            std::ofstream file;
+            file.open(fileName_.c_str());
+            file << "#\t t\t posX\t posY\t gapZ\t velX\t velY\t velZ\n";
+            file.close();
+         }
+      }
+   }
+
+   void operator()(const uint_t timestep)
+   {
+      Vector3< real_t > pos(real_t(0));
+      Vector3< real_t > transVel(real_t(0));
+      Vector3< real_t > hydForce(real_t(0));
+
+      size_t idx = ac_->uidToIdx(sphereUid_);
+      if (idx != ac_->getInvalidIdx())
+      {
+         if (!mesa_pd::data::particle_flags::isSet(ac_->getFlags(idx), mesa_pd::data::particle_flags::GHOST))
+         {
+            pos      = ac_->getPosition(idx);
+            transVel = ac_->getLinearVelocity(idx);
+            hydForce = ac_->getHydrodynamicForce(idx);
+         }
+      }
+
+      WALBERLA_MPI_SECTION()
+      {
+         mpi::allReduceInplace(pos[0], mpi::SUM);
+         mpi::allReduceInplace(pos[1], mpi::SUM);
+         mpi::allReduceInplace(pos[2], mpi::SUM);
+
+         mpi::allReduceInplace(transVel[0], mpi::SUM);
+         mpi::allReduceInplace(transVel[1], mpi::SUM);
+         mpi::allReduceInplace(transVel[2], mpi::SUM);
+
+         mpi::allReduceInplace(hydForce[0], mpi::SUM);
+         mpi::allReduceInplace(hydForce[1], mpi::SUM);
+         mpi::allReduceInplace(hydForce[2], mpi::SUM);
+      }
+
+      position_    = pos[2];
+      maxVelocity_ = std::max(maxVelocity_, -transVel[2]);
+
+      if (fileIO_) writeToFile(timestep, pos, transVel, hydForce);
+   }
+
+   real_t getPosition() const { return position_; }
+
+   real_t getMaxVelocity() const { return maxVelocity_; }
+
+ private:
+   void writeToFile(const uint_t timestep, const Vector3< real_t >& position, const Vector3< real_t >& velocity,
+                    const Vector3< real_t >& hydForce)
+   {
+      WALBERLA_ROOT_SECTION()
+      {
+         std::ofstream file;
+         file.open(fileName_.c_str(), std::ofstream::app);
+
+         auto scaledPosition     = position / diameter_;
+         auto velocity_SI        = velocity * dx_SI_ / dt_SI_;
+         auto normalizedHydForce = hydForce / gravitationalForceMag_;
+
+         file << timestep << "\t" << real_c(timestep) * dt_SI_ << "\t"
+              << "\t" << scaledPosition[0] << "\t" << scaledPosition[1] << "\t" << scaledPosition[2] - real_t(0.5)
+              << "\t" << velocity_SI[0] << "\t" << velocity_SI[1] << "\t" << velocity_SI[2] << "\t"
+              << normalizedHydForce[0] << "\t" << normalizedHydForce[1] << "\t" << normalizedHydForce[2] << "\n";
+         file.close();
+      }
+   }
+
+   shared_ptr< ParticleAccessor_T > ac_;
+   const walberla::id_t sphereUid_;
+   std::string fileName_;
+   bool fileIO_;
+   real_t dx_SI_, dt_SI_, diameter_, gravitationalForceMag_;
+
+   real_t position_;
+   real_t maxVelocity_;
+};
+
+void createPlaneSetup(const shared_ptr< mesa_pd::data::ParticleStorage >& ps,
+                      const shared_ptr< mesa_pd::data::ShapeStorage >& ss, const math::AABB& simulationDomain)
+{
+   // create bounding planes
+   mesa_pd::data::Particle p0 = *ps->create(true);
+   p0.setPosition(simulationDomain.minCorner());
+   p0.setInteractionRadius(std::numeric_limits< real_t >::infinity());
+   p0.setShapeID(ss->create< mesa_pd::data::HalfSpace >(Vector3< real_t >(0, 0, 1)));
+   p0.setOwner(mpi::MPIManager::instance()->rank());
+   p0.setType(0);
+   mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE);
+   mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED);
+
+   mesa_pd::data::Particle p1 = *ps->create(true);
+   p1.setPosition(simulationDomain.maxCorner());
+   p1.setInteractionRadius(std::numeric_limits< real_t >::infinity());
+   p1.setShapeID(ss->create< mesa_pd::data::HalfSpace >(Vector3< real_t >(0, 0, -1)));
+   p1.setOwner(mpi::MPIManager::instance()->rank());
+   p1.setType(0);
+   mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE);
+   mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED);
+
+   mesa_pd::data::Particle p2 = *ps->create(true);
+   p2.setPosition(simulationDomain.minCorner());
+   p2.setInteractionRadius(std::numeric_limits< real_t >::infinity());
+   p2.setShapeID(ss->create< mesa_pd::data::HalfSpace >(Vector3< real_t >(1, 0, 0)));
+   p2.setOwner(mpi::MPIManager::instance()->rank());
+   p2.setType(0);
+   mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE);
+   mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::FIXED);
+
+   mesa_pd::data::Particle p3 = *ps->create(true);
+   p3.setPosition(simulationDomain.maxCorner());
+   p3.setInteractionRadius(std::numeric_limits< real_t >::infinity());
+   p3.setShapeID(ss->create< mesa_pd::data::HalfSpace >(Vector3< real_t >(-1, 0, 0)));
+   p3.setOwner(mpi::MPIManager::instance()->rank());
+   p3.setType(0);
+   mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE);
+   mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::FIXED);
+
+   mesa_pd::data::Particle p4 = *ps->create(true);
+   p4.setPosition(simulationDomain.minCorner());
+   p4.setInteractionRadius(std::numeric_limits< real_t >::infinity());
+   p4.setShapeID(ss->create< mesa_pd::data::HalfSpace >(Vector3< real_t >(0, 1, 0)));
+   p4.setOwner(mpi::MPIManager::instance()->rank());
+   p4.setType(0);
+   mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE);
+   mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::FIXED);
+
+   mesa_pd::data::Particle p5 = *ps->create(true);
+   p5.setPosition(simulationDomain.maxCorner());
+   p5.setInteractionRadius(std::numeric_limits< real_t >::infinity());
+   p5.setShapeID(ss->create< mesa_pd::data::HalfSpace >(Vector3< real_t >(0, -1, 0)));
+   p5.setOwner(mpi::MPIManager::instance()->rank());
+   p5.setType(0);
+   mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE);
+   mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::FIXED);
+}
+
+//////////
+// MAIN //
+//////////
+
+//*******************************************************************************************************************
+/*!\brief Testcase that simulates the settling of a sphere inside a rectangular column filled with viscous fluid
+ *
+ * see: ten Cate, Nieuwstad, Derksen, Van den Akker - "Particle imaging velocimetry experiments and lattice-Boltzmann
+ * simulations on a single sphere settling under gravity" (2002), Physics of Fluids, doi: 10.1063/1.1512918
+ */
+//*******************************************************************************************************************
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+
+   mpi::Environment env(argc, argv);
+
+   ///////////////////
+   // Customization //
+   ///////////////////
+
+   // simulation control
+   bool shortrun          = false;
+   bool funcTest          = false;
+   bool fileIO            = false;
+   uint_t vtkIOFreq       = 0;
+   std::string baseFolder = "vtk_out_SettlingSphere";
+
+   // physical setup
+   uint_t fluidType = 1;
+
+   // numerical parameters
+   uint_t numberOfCellsInHorizontalDirection = uint_t(135);
+   bool averageForceTorqueOverTwoTimeSteps   = true;
+   uint_t numRPDSubCycles                    = uint_t(1);
+   bool useVelocityVerlet                    = false;
+
+   for (int i = 1; i < argc; ++i)
+   {
+      if (std::strcmp(argv[i], "--shortrun") == 0)
+      {
+         shortrun = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--funcTest") == 0)
+      {
+         funcTest = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--fileIO") == 0)
+      {
+         fileIO = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--vtkIOFreq") == 0)
+      {
+         vtkIOFreq = uint_c(std::atof(argv[++i]));
+         continue;
+      }
+      if (std::strcmp(argv[i], "--fluidType") == 0)
+      {
+         fluidType = uint_c(std::atof(argv[++i]));
+         continue;
+      }
+      if (std::strcmp(argv[i], "--numRPDSubCycles") == 0)
+      {
+         numRPDSubCycles = uint_c(std::atof(argv[++i]));
+         continue;
+      }
+      if (std::strcmp(argv[i], "--resolution") == 0)
+      {
+         numberOfCellsInHorizontalDirection = uint_c(std::atof(argv[++i]));
+         continue;
+      }
+      if (std::strcmp(argv[i], "--noForceAveraging") == 0)
+      {
+         averageForceTorqueOverTwoTimeSteps = false;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--baseFolder") == 0)
+      {
+         baseFolder = argv[++i];
+         continue;
+      }
+      if (std::strcmp(argv[i], "--useVV") == 0)
+      {
+         useVelocityVerlet = true;
+         continue;
+      }
+      WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]);
+   }
+
+   if (funcTest) { walberla::logging::Logging::instance()->setLogLevel(logging::Logging::LogLevel::WARNING); }
+
+   if (fileIO)
+   {
+      // create base directory if it does not yet exist
+      filesystem::path tpath(baseFolder);
+      if (!filesystem::exists(tpath)) filesystem::create_directory(tpath);
+   }
+
+   //////////////////////////////////////
+   // SIMULATION PROPERTIES in SI units//
+   //////////////////////////////////////
+
+   // values are mainly taken from the reference paper
+   const real_t diameter_SI      = real_t(15e-3);
+   const real_t densitySphere_SI = real_t(1120);
+
+   real_t densityFluid_SI, dynamicViscosityFluid_SI;
+   real_t expectedSettlingVelocity_SI;
+   switch (fluidType)
+   {
+   case 1:
+      // Re_p around 1.5
+      densityFluid_SI             = real_t(970);
+      dynamicViscosityFluid_SI    = real_t(373e-3);
+      expectedSettlingVelocity_SI = real_t(0.035986);
+      break;
+   case 2:
+      // Re_p around 4.1
+      densityFluid_SI             = real_t(965);
+      dynamicViscosityFluid_SI    = real_t(212e-3);
+      expectedSettlingVelocity_SI = real_t(0.05718);
+      break;
+   case 3:
+      // Re_p around 11.6
+      densityFluid_SI             = real_t(962);
+      dynamicViscosityFluid_SI    = real_t(113e-3);
+      expectedSettlingVelocity_SI = real_t(0.087269);
+      break;
+   case 4:
+      // Re_p around 31.9
+      densityFluid_SI             = real_t(960);
+      dynamicViscosityFluid_SI    = real_t(58e-3);
+      expectedSettlingVelocity_SI = real_t(0.12224);
+      break;
+   default:
+      WALBERLA_ABORT("Only four different fluids are supported! Choose type between 1 and 4.");
+   }
+   const real_t kinematicViscosityFluid_SI = dynamicViscosityFluid_SI / densityFluid_SI;
+
+   const real_t gravitationalAcceleration_SI = real_t(9.81);
+   Vector3< real_t > domainSize_SI(real_t(100e-3), real_t(100e-3), real_t(160e-3));
+   // shift starting gap a bit upwards to match the reported (plotted) values
+   const real_t startingGapSize_SI = real_t(120e-3) + real_t(0.25) * diameter_SI;
+
+   WALBERLA_LOG_INFO_ON_ROOT("Setup (in SI units):");
+   WALBERLA_LOG_INFO_ON_ROOT(" - fluid type = " << fluidType);
+   WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize_SI);
+   WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter_SI << ", density = " << densitySphere_SI
+                                                      << ", starting gap size = " << startingGapSize_SI);
+   WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid_SI << ", dyn. visc = " << dynamicViscosityFluid_SI
+                                                    << ", kin. visc = " << kinematicViscosityFluid_SI);
+   WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = "
+                             << expectedSettlingVelocity_SI << " --> Re_p = "
+                             << expectedSettlingVelocity_SI * diameter_SI / kinematicViscosityFluid_SI);
+
+   //////////////////////////
+   // NUMERICAL PARAMETERS //
+   //////////////////////////
+
+   const real_t dx_SI = domainSize_SI[0] / real_c(numberOfCellsInHorizontalDirection);
+   const Vector3< uint_t > domainSize(uint_c(floor(domainSize_SI[0] / dx_SI + real_t(0.5))),
+                                      uint_c(floor(domainSize_SI[1] / dx_SI + real_t(0.5))),
+                                      uint_c(floor(domainSize_SI[2] / dx_SI + real_t(0.5))));
+   const real_t diameter     = diameter_SI / dx_SI;
+   const real_t sphereVolume = math::pi / real_t(6) * diameter * diameter * diameter;
+
+   const real_t expectedSettlingVelocity = real_t(0.01);
+   const real_t dt_SI                    = expectedSettlingVelocity / expectedSettlingVelocity_SI * dx_SI;
+
+   const real_t viscosity      = kinematicViscosityFluid_SI * dt_SI / (dx_SI * dx_SI);
+   const real_t relaxationTime = real_t(1) / lbm::collision_model::omegaFromViscosity(viscosity);
+
+   const real_t gravitationalAcceleration = gravitationalAcceleration_SI * dt_SI * dt_SI / dx_SI;
+
+   const real_t densityFluid  = real_t(1);
+   const real_t densitySphere = densityFluid * densitySphere_SI / densityFluid_SI;
+
+   const real_t dx = real_t(1);
+
+   const uint_t timesteps = funcTest ? 1 : (shortrun ? uint_t(200) : uint_t(250000));
+
+   WALBERLA_LOG_INFO_ON_ROOT(" - dx_SI = " << dx_SI << ", dt_SI = " << dt_SI);
+   WALBERLA_LOG_INFO_ON_ROOT("Setup (in simulation, i.e. lattice, units):");
+   WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize);
+   WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter << ", density = " << densitySphere);
+   WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid << ", relaxation time (tau) = " << relaxationTime
+                                                    << ", kin. visc = " << viscosity);
+   WALBERLA_LOG_INFO_ON_ROOT(" - gravitational acceleration = " << gravitationalAcceleration);
+   WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = " << expectedSettlingVelocity << " --> Re_p = "
+                                                                << expectedSettlingVelocity * diameter / viscosity);
+   WALBERLA_LOG_INFO_ON_ROOT(" - integrator = " << (useVelocityVerlet ? "Velocity Verlet" : "Explicit Euler"));
+
+   if (vtkIOFreq > 0)
+   {
+      WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files to folder \"" << baseFolder << "\" with frequency " << vtkIOFreq);
+   }
+
+   ///////////////////////////
+   // BLOCK STRUCTURE SETUP //
+   ///////////////////////////
+
+   Vector3< uint_t > numberOfBlocksPerDirection(uint_t(1), uint_t(1), uint_t(4));
+   Vector3< uint_t > cellsPerBlockPerDirection(domainSize[0] / numberOfBlocksPerDirection[0],
+                                               domainSize[1] / numberOfBlocksPerDirection[1],
+                                               domainSize[2] / numberOfBlocksPerDirection[2]);
+
+   for (uint_t i = 0; i < 3; ++i)
+   {
+      WALBERLA_CHECK_EQUAL(cellsPerBlockPerDirection[i] * numberOfBlocksPerDirection[i], domainSize[i],
+                           "Unmatching domain decomposition in direction " << i << "!");
+   }
+
+   auto blocks = blockforest::createUniformBlockGrid(numberOfBlocksPerDirection[0], numberOfBlocksPerDirection[1],
+                                                     numberOfBlocksPerDirection[2], cellsPerBlockPerDirection[0],
+                                                     cellsPerBlockPerDirection[1], cellsPerBlockPerDirection[2], dx, 0,
+                                                     false, false, false, false, false, // periodicity
+                                                     false);
+
+   WALBERLA_LOG_INFO_ON_ROOT("Domain decomposition:");
+   WALBERLA_LOG_INFO_ON_ROOT(" - blocks per direction = " << numberOfBlocksPerDirection);
+   WALBERLA_LOG_INFO_ON_ROOT(" - cells per block = " << cellsPerBlockPerDirection);
+
+   // write domain decomposition to file
+   if (vtkIOFreq > 0) { vtk::writeDomainDecomposition(blocks, "initial_domain_decomposition", baseFolder); }
+
+   //////////////////
+   // RPD COUPLING //
+   //////////////////
+
+   auto rpdDomain = std::make_shared< mesa_pd::domain::BlockForestDomain >(blocks->getBlockForestPointer());
+
+   // init data structures
+   auto ps                  = walberla::make_shared< mesa_pd::data::ParticleStorage >(1);
+   auto ss                  = walberla::make_shared< mesa_pd::data::ShapeStorage >();
+   using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape;
+   auto accessor            = walberla::make_shared< ParticleAccessor_T >(ps, ss);
+
+   // bounding planes
+   createPlaneSetup(ps, ss, blocks->getDomain());
+
+   // create sphere and store Uid
+   Vector3< real_t > initialPosition(real_t(0.5) * real_c(domainSize[0]), real_t(0.5) * real_c(domainSize[1]),
+                                     startingGapSize_SI / dx_SI + real_t(0.5) * diameter);
+   auto sphereShape = ss->create< mesa_pd::data::Sphere >(diameter * real_t(0.5));
+   ss->shapes[sphereShape]->updateMassAndInertia(densitySphere);
+
+   walberla::id_t sphereUid = 0;
+   if (rpdDomain->isContainedInProcessSubdomain(uint_c(mpi::MPIManager::instance()->rank()), initialPosition))
+   {
+      mesa_pd::data::Particle&& p = *ps->create();
+      p.setPosition(initialPosition);
+      p.setInteractionRadius(diameter * real_t(0.5));
+      p.setOwner(mpi::MPIManager::instance()->rank());
+      p.setShapeID(sphereShape);
+      sphereUid = p.getUid();
+   }
+   mpi::allReduceInplace(sphereUid, mpi::SUM);
+
+   ///////////////////////
+   // ADD DATA TO BLOCKS //
+   ////////////////////////
+
+   // create the lattice model
+   LatticeModel_T latticeModel =
+      LatticeModel_T(lbm::collision_model::TRT::constructWithMagicNumber(real_t(1) / relaxationTime));
+
+   // add PDF field
+   BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >(
+      blocks, "pdf field (zyxf)", latticeModel, Vector3< real_t >(real_t(0)), real_t(1), uint_t(1), field::zyxf);
+   // add flag field
+   BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >(blocks, "flag field");
+
+   // add boundary handling
+   using BoundaryHandling_T       = MyBoundaryHandling< ParticleAccessor_T >::Type;
+   BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(
+      MyBoundaryHandling< ParticleAccessor_T >(flagFieldID, pdfFieldID, accessor), "boundary handling");
+
+   // set up RPD functionality
+   std::function< void(void) > syncCall = [ps, rpdDomain]() {
+      const real_t overlap = real_t(1.5);
+      mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc;
+      syncNextNeighborFunc(*ps, *rpdDomain, overlap);
+   };
+
+   syncCall();
+
+   mesa_pd::kernel::ExplicitEuler explEulerIntegrator(real_t(1) / real_t(numRPDSubCycles));
+   mesa_pd::kernel::VelocityVerletPreForceUpdate vvIntegratorPreForce(real_t(1) / real_t(numRPDSubCycles));
+   mesa_pd::kernel::VelocityVerletPostForceUpdate vvIntegratorPostForce(real_t(1) / real_t(numRPDSubCycles));
+
+   mesa_pd::kernel::SpringDashpot collisionResponse(1);
+   mesa_pd::mpi::ReduceProperty reduceProperty;
+
+   // set up coupling functionality
+   lbm_mesapd_coupling::RegularParticlesSelector sphereSelector;
+   Vector3< real_t > gravitationalForce(real_t(0), real_t(0),
+                                        -(densitySphere - densityFluid) * gravitationalAcceleration * sphereVolume);
+   lbm_mesapd_coupling::AddForceOnParticlesKernel addGravitationalForce(gravitationalForce);
+   lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction;
+   lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque;
+   lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque;
+   lbm_mesapd_coupling::LubricationCorrectionKernel lubricationCorrectionKernel(
+      viscosity, [](real_t r) { return real_t(0.0016) * r; });
+   lbm_mesapd_coupling::ParticleMappingKernel< BoundaryHandling_T > particleMappingKernel(blocks, boundaryHandlingID);
+
+   ///////////////
+   // TIME LOOP //
+   ///////////////
+
+   // map planes into the LBM simulation -> act as no-slip boundaries
+   ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel,
+                       *accessor, NoSlip_Flag);
+
+   // map particles into the LBM simulation
+   BlockDataID particleAndVolumeFractionFieldID =
+      field::addToStorage< lbm_mesapd_coupling::psm::ParticleAndVolumeFractionField_T >(
+         blocks, "particle and volume fraction field",
+         std::vector< lbm_mesapd_coupling::psm::ParticleAndVolumeFraction_T >(), field::zyxf, 0);
+   lbm_mesapd_coupling::psm::ParticleAndVolumeFractionMapping particleMapping(blocks, accessor, sphereSelector,
+                                                                              particleAndVolumeFractionFieldID, 4);
+   particleMapping();
+
+   lbm_mesapd_coupling::psm::initializeDomainForPSM< LatticeModel_T, 1 >(*blocks, pdfFieldID,
+                                                                         particleAndVolumeFractionFieldID, *accessor);
+
+   walberla::lbm_mesapd_coupling::FractionFieldSum fractionFieldSum(blocks, particleAndVolumeFractionFieldID);
+   // check that the sum of all fractions is roughly the volume of the particle
+   real_t particleRadius = diameter * real_t(0.5);
+   WALBERLA_CHECK_LESS(
+      std::fabs(4.0 / 3.0 * math::pi * particleRadius * particleRadius * particleRadius - fractionFieldSum()),
+      real_c(1.0));
+
+   // setup of the LBM communication for synchronizing the pdf field between neighboring blocks
+   std::function< void() > commFunction;
+   blockforest::communication::UniformBufferedScheme< Stencil_T > scheme(blocks);
+   scheme.addPackInfo(make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >(pdfFieldID));
+   commFunction = scheme;
+
+   // create the timeloop
+   SweepTimeloop timeloop(blocks->getBlockStorage(), timesteps);
+
+   auto bhSweep  = BoundaryHandling_T::getBlockSweep(boundaryHandlingID);
+   auto lbmSweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, FlagField_T, 1, 1 >(
+      pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor, flagFieldID, Fluid_Flag);
+
+   timeloop.addFuncBeforeTimeStep(RemainingTimeLogger(timeloop.getNrOfTimeSteps()), "Remaining Time Logger");
+
+   // vtk output
+   if (vtkIOFreq != uint_t(0))
+   {
+      // spheres
+      auto particleVtkOutput = make_shared< mesa_pd::vtk::ParticleVtkOutput >(ps);
+      particleVtkOutput->addOutput< mesa_pd::data::SelectParticleOwner >("owner");
+      particleVtkOutput->addOutput< mesa_pd::data::SelectParticleLinearVelocity >("velocity");
+      auto particleVtkWriter =
+         vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step");
+      timeloop.addFuncBeforeTimeStep(vtk::writeFiles(particleVtkWriter), "VTK (sphere data)");
+
+      // flag field (written only once in the first time step, ghost layers are also written)
+      // auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", timesteps, FieldGhostLayers, false,
+      // baseFolder ); flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID,
+      // "FlagField" ) ); timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" );
+
+      // pdf field
+      auto pdfFieldVTK = vtk::createVTKOutput_BlockData(blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder);
+
+      blockforest::communication::UniformBufferedScheme< stencil::D3Q27 > pdfGhostLayerSync(blocks);
+      pdfGhostLayerSync.addPackInfo(make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID));
+      pdfFieldVTK->addBeforeFunction(pdfGhostLayerSync);
+
+      field::FlagFieldCellFilter< FlagField_T > fluidFilter(flagFieldID);
+      fluidFilter.addFlag(Fluid_Flag);
+      pdfFieldVTK->addCellInclusionFilter(fluidFilter);
+
+      pdfFieldVTK->addCellDataWriter(
+         make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >(pdfFieldID, "VelocityFromPDF"));
+      pdfFieldVTK->addCellDataWriter(
+         make_shared< lbm::DensityVTKWriter< LatticeModel_T, float > >(pdfFieldID, "DensityFromPDF"));
+
+      timeloop.addFuncBeforeTimeStep(vtk::writeFiles(pdfFieldVTK), "VTK (fluid field data)");
+   }
+
+   bool useStreamCollide = true;
+
+   if (useStreamCollide)
+   {
+      // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip
+      // treatment)
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication") << Sweep(bhSweep, "Boundary Handling");
+
+      // stream + collide LBM step
+      timeloop.add() << Sweep(makeSharedSweep(lbmSweep), "cell-wise LB sweep");
+   }
+   else
+   {
+      // collide LBM step
+      timeloop.add() << Sweep(makeCollideSweep(lbmSweep), "cell-wise collide LB sweep");
+
+      // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip
+      // treatment)
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(BoundaryHandling_T::getBlockSweep(boundaryHandlingID), "Boundary Handling");
+
+      // stream LBM step
+      timeloop.add() << Sweep(makeStreamSweep(lbmSweep), "cell-wise stream LB sweep");
+   }
+
+   // evaluation functionality
+   std::string loggingFileName(baseFolder + "/LoggingSettlingSphere_");
+   loggingFileName += std::to_string(fluidType);
+   loggingFileName += ".txt";
+   if (fileIO) { WALBERLA_LOG_INFO_ON_ROOT(" - writing logging output to file \"" << loggingFileName << "\""); }
+   SpherePropertyLogger< ParticleAccessor_T > logger(accessor, sphereUid, loggingFileName, fileIO, dx_SI, dt_SI,
+                                                     diameter, -gravitationalForce[2]);
+
+   ////////////////////////
+   // EXECUTE SIMULATION //
+   ////////////////////////
+
+   WcTimingPool timeloopTiming;
+
+   real_t terminationPosition = real_t(0.51) * diameter; // right before sphere touches the bottom wall
+
+   const bool useOpenMP = false;
+
+   // time loop
+   for (uint_t i = 0; i < timesteps; ++i)
+   {
+      // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions
+      timeloop.singleStep(timeloopTiming);
+
+      timeloopTiming["RPD"].start();
+
+      // -> full hydrodynamic force/torque info is available on local particle
+      reduceProperty.operator()< mesa_pd::HydrodynamicForceTorqueNotification >(*ps);
+
+      if (averageForceTorqueOverTwoTimeSteps)
+      {
+         if (i == 0)
+         {
+            lbm_mesapd_coupling::InitializeHydrodynamicForceTorqueForAveragingKernel
+               initializeHydrodynamicForceTorqueForAveragingKernel;
+            ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor,
+                                initializeHydrodynamicForceTorqueForAveragingKernel, *accessor);
+         }
+         ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, averageHydrodynamicForceTorque,
+                             *accessor);
+      }
+
+      for (auto subCycle = uint_t(0); subCycle < numRPDSubCycles; ++subCycle)
+      {
+         if (useVelocityVerlet)
+         {
+            ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPreForce, *accessor);
+            syncCall();
+         }
+
+         ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, addHydrodynamicInteraction,
+                             *accessor);
+         ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, addGravitationalForce, *accessor);
+
+         // lubrication correction
+         ps->forEachParticlePairHalf(
+            useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor,
+            [&lubricationCorrectionKernel, rpdDomain](const size_t idx1, const size_t idx2, auto& ac) {
+               // TODO change this to storing copy, not reference
+               mesa_pd::collision_detection::AnalyticContactDetection acd;
+               acd.getContactThreshold() = lubricationCorrectionKernel.getNormalCutOffDistance();
+               mesa_pd::kernel::DoubleCast double_cast;
+               mesa_pd::mpi::ContactFilter contact_filter;
+               if (double_cast(idx1, idx2, ac, acd, ac))
+               {
+                  if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain))
+                  {
+                     double_cast(idx1, idx2, ac, lubricationCorrectionKernel, ac, acd.getContactNormal(),
+                                 acd.getPenetrationDepth());
+                  }
+               }
+            },
+            *accessor);
+
+         // one could add linked cells here
+
+         // collision response
+         ps->forEachParticlePairHalf(
+            useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor,
+            [collisionResponse, rpdDomain](const size_t idx1, const size_t idx2, auto& ac) {
+               mesa_pd::collision_detection::AnalyticContactDetection acd;
+               mesa_pd::kernel::DoubleCast double_cast;
+               mesa_pd::mpi::ContactFilter contact_filter;
+               if (double_cast(idx1, idx2, ac, acd, ac))
+               {
+                  if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain))
+                  {
+                     collisionResponse(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), acd.getContactNormal(),
+                                       acd.getPenetrationDepth());
+                  }
+               }
+            },
+            *accessor);
+
+         reduceProperty.operator()< mesa_pd::ForceTorqueNotification >(*ps);
+
+         if (useVelocityVerlet)
+            ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPostForce, *accessor);
+         else
+            ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, explEulerIntegrator, *accessor);
+
+         syncCall();
+         particleMapping();
+         WALBERLA_CHECK_LESS(
+            std::fabs(4.0 / 3.0 * math::pi * particleRadius * particleRadius * particleRadius - fractionFieldSum()),
+            real_c(1.0));
+      }
+
+      timeloopTiming["RPD"].end();
+
+      // evaluation
+      timeloopTiming["Logging"].start();
+      logger(i);
+      timeloopTiming["Logging"].end();
+
+      // reset after logging here
+      ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor);
+
+      // check for termination
+      if (logger.getPosition() < terminationPosition)
+      {
+         WALBERLA_LOG_INFO_ON_ROOT("Sphere reached terminal position " << logger.getPosition() << " after " << i
+                                                                       << " timesteps!");
+         break;
+      }
+   }
+
+   timeloopTiming.logResultOnRoot();
+
+   // check the result
+   if (!funcTest && !shortrun)
+   {
+      real_t relErr = std::fabs(expectedSettlingVelocity - logger.getMaxVelocity()) / expectedSettlingVelocity;
+      WALBERLA_LOG_INFO_ON_ROOT("Expected maximum settling velocity: " << expectedSettlingVelocity);
+      WALBERLA_LOG_INFO_ON_ROOT("Simulated maximum settling velocity: " << logger.getMaxVelocity());
+      WALBERLA_LOG_INFO_ON_ROOT("Relative error: " << relErr);
+
+      // the relative error has to be below 10%
+      WALBERLA_CHECK_LESS(relErr, real_t(0.1));
+   }
+
+   return EXIT_SUCCESS;
+}
+
+} // namespace settling_sphere
+
+int main(int argc, char** argv) { settling_sphere::main(argc, argv); }
diff --git a/tests/lbm_mesapd_coupling/partially_saturated_cells_method/TorqueSphere.cpp b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/TorqueSphere.cpp
new file mode 100644
index 000000000..9b965c164
--- /dev/null
+++ b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/TorqueSphere.cpp
@@ -0,0 +1,561 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file TorqueSphere.cpp
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//! \author Christoph Rettinger <christoph.rettinger@fau.de>
+//! \brief Modification of pe_coupling/partially_saturated_cells_method/TorqueSpherePSM.cpp
+//
+//======================================================================================================================
+
+#include "blockforest/Initialization.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "boundary/all.h"
+
+#include "core/DataTypes.h"
+#include "core/Environment.h"
+#include "core/SharedFunctor.h"
+#include "core/debug/TestSubsystem.h"
+#include "core/logging/Logging.h"
+#include "core/mpi/MPIManager.h"
+#include "core/mpi/Reduce.h"
+#include "core/timing/RemainingTimeLogger.h"
+
+#include "domain_decomposition/SharedSweep.h"
+
+#include "field/AddToStorage.h"
+#include "field/communication/PackInfo.h"
+
+#include "lbm/field/AddToStorage.h"
+#include "lbm/field/PdfField.h"
+#include "lbm/lattice_model/D3Q19.h"
+#include "lbm/lattice_model/ForceModel.h"
+
+#include "lbm_mesapd_coupling/DataTypes.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/PSMSweep.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/PSMUtility.h"
+#include "lbm_mesapd_coupling/partially_saturated_cells_method/ParticleAndVolumeFractionMapping.h"
+#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h"
+
+#include "mesa_pd/data/ParticleAccessorWithShape.h"
+#include "mesa_pd/data/ParticleStorage.h"
+#include "mesa_pd/data/ShapeStorage.h"
+#include "mesa_pd/domain/BlockForestDomain.h"
+#include "mesa_pd/mpi/SyncNextNeighbors.h"
+
+#include "stencil/D3Q27.h"
+
+#include "timeloop/SweepTimeloop.h"
+
+#include <iostream>
+#include <vector>
+
+namespace torque_sphere_psm
+{
+
+///////////
+// USING //
+///////////
+
+using namespace walberla;
+using walberla::uint_t;
+
+// PDF field, flag field & particle field
+typedef lbm::D3Q19< lbm::collision_model::SRT, false > LatticeModel_T;
+
+using Stencil_T  = LatticeModel_T::Stencil;
+using PdfField_T = lbm::PdfField< LatticeModel_T >;
+
+using flag_t      = walberla::uint8_t;
+using FlagField_T = FlagField< flag_t >;
+
+///////////
+// FLAGS //
+///////////
+
+const FlagUID Fluid_Flag("fluid");
+
+////////////////
+// PARAMETERS //
+////////////////
+
+struct Setup
+{
+   uint_t checkFrequency;
+   real_t visc;
+   real_t tau;
+   real_t radius;
+   uint_t length;
+   real_t phi;
+   real_t angularVel;
+   real_t analyticalTorque;
+};
+
+//*******************************************************************************************************************
+/*!\brief Evaluating the torque on a sphere, rotating with a constant angular velocity
+ */
+//*******************************************************************************************************************
+template< typename ParticleAccessor_T >
+class TorqueEval
+{
+ public:
+   TorqueEval(SweepTimeloop* timeloop, Setup* setup, const shared_ptr< StructuredBlockStorage >& blocks,
+              const shared_ptr< ParticleAccessor_T >& ac, bool fileIO)
+      : timeloop_(timeloop), setup_(setup), blocks_(blocks), ac_(ac), fileIO_(fileIO), torqueOld_(0.0), torqueNew_(0.0)
+   {
+      // calculate the (semi)analytical torque value
+      // see also Hofmann et al. - Hydrodynamic interactions in colloidal crystals:(II). Application to dense cubic and
+      // tetragonal arrays (1999), Eqs. 5.1 and 5.5
+      const real_t S = real_c(1.95708);
+      setup_->analyticalTorque =
+         -setup_->visc *
+         (real_c(6) * setup_->phi / (real_c(1) - setup_->phi - S * std::pow(setup_->phi, real_c(10. / 3.)))) *
+         setup_->angularVel * real_c(setup_->length * setup_->length * setup_->length);
+
+      if (fileIO_)
+      {
+         std::ofstream file;
+         filename_ = "TorqueSpherePSM.txt";
+         WALBERLA_ROOT_SECTION()
+         {
+            file.open(filename_.c_str());
+            file << "#\t torqueSim\t torqueAnaly\n";
+            file.close();
+         }
+      }
+   }
+
+   // evaluate the acting torque
+   void operator()()
+   {
+      const uint_t timestep(timeloop_->getCurrentTimeStep() + 1);
+
+      if (timestep % setup_->checkFrequency != 0) return;
+
+      // update torque values
+      torqueOld_ = torqueNew_;
+      torqueNew_ = calculateTorque();
+
+      // write to file if desired
+      WALBERLA_ROOT_SECTION()
+      {
+         if (fileIO_)
+         {
+            std::ofstream file;
+            file.open(filename_.c_str(), std::ofstream::app);
+            file.setf(std::ios::unitbuf);
+            file.precision(15);
+            file << timestep << " " << torqueNew_ << " " << setup_->analyticalTorque << "\n";
+            file.close();
+         }
+      }
+   }
+
+   // obtain the torque acting on the sphere by summing up all the process local parts
+   real_t calculateTorque()
+   {
+      real_t torque = real_c(0);
+      for (auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt)
+      {
+         for (size_t idx = 0; idx < ac_->size(); ++idx)
+         {
+            torque += ac_->getHydrodynamicTorque(idx)[1];
+         }
+      }
+
+      WALBERLA_MPI_SECTION() { mpi::allReduceInplace(torque, mpi::SUM); }
+      return torque;
+   }
+
+   // return the relative temporal change in the torque
+   real_t getTorqueDiff() const { return std::fabs((torqueNew_ - torqueOld_) / torqueNew_); }
+
+   // return the torque
+   real_t getTorque() const { return torqueNew_; }
+
+ private:
+   SweepTimeloop* timeloop_;
+
+   Setup* setup_;
+
+   shared_ptr< StructuredBlockStorage > blocks_;
+   shared_ptr< ParticleAccessor_T > ac_;
+
+   bool fileIO_;
+   std::string filename_;
+
+   real_t torqueOld_;
+   real_t torqueNew_;
+};
+
+//////////
+// MAIN //
+//////////
+
+//*******************************************************************************************************************
+/*!\brief Testcase that checks the torque acting on a constantly rotating sphere in the center of a cubic domain
+ *
+ * The torque for this problem (often denoted as Simple Cubic setup) is given by a semi-analytical formula.
+ * The cubic domain is periodic in all directions, making it a physically infinite periodic array of spheres.
+   \verbatim
+         _______________
+        |       <-      |
+        |      ___      |
+        |     /   \     |
+        |    |  x  |    |
+        |     \___/     |
+        |      ->       |
+        |_______________|
+
+   \endverbatim
+ *
+ * The collision model used for the LBM is TRT with a relaxation parameter tau=1.5 and the magic parameter 3/16.
+ * The Stokes approximation of the equilibrium PDFs is used.
+ * The sphere rotates with a angular velocity of 1e-5.
+ * The domain is 32x32x32, and the sphere has a diameter of around 27 cells ( chi * domainlength )
+ * The simulation is run until the relative change in the torque between 100 time steps is less than 1e-5.
+ * The pe is not used since the angular velocity is kept constant and the force is explicitly reset after each time
+ step.
+ *
+ */
+//*******************************************************************************************************************
+
+int main(int argc, char** argv)
+{
+   debug::enterTestMode();
+
+   mpi::Environment env(argc, argv);
+
+   auto processes = MPIManager::instance()->numProcesses();
+
+   if (processes != 1 && processes != 2 && processes != 4 && processes != 8)
+   {
+      std::cerr << "Number of processes must be equal to either 1, 2, 4, or 8!" << std::endl;
+      return EXIT_FAILURE;
+   }
+
+   ///////////////////
+   // Customization //
+   ///////////////////
+
+   bool shortrun = false;
+   bool funcTest = false;
+   bool fileIO   = false;
+   bool SC1W1    = false;
+   bool SC2W1    = false;
+   bool SC3W1    = false;
+   bool SC1W2    = false;
+   bool SC2W2    = false;
+   bool SC3W2    = false;
+
+   for (int i = 1; i < argc; ++i)
+   {
+      if (std::strcmp(argv[i], "--shortrun") == 0)
+      {
+         shortrun = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--funcTest") == 0)
+      {
+         funcTest = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--fileIO") == 0)
+      {
+         fileIO = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--SC1W1") == 0)
+      {
+         SC1W1 = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--SC2W1") == 0)
+      {
+         SC2W1 = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--SC3W1") == 0)
+      {
+         SC3W1 = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--SC1W2") == 0)
+      {
+         SC1W2 = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--SC2W2") == 0)
+      {
+         SC2W2 = true;
+         continue;
+      }
+      if (std::strcmp(argv[i], "--SC3W2") == 0)
+      {
+         SC3W2 = true;
+         continue;
+      }
+      WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]);
+   }
+
+   if (!SC1W1 && !SC2W1 && !SC3W1 && !SC1W2 && !SC2W2 && !SC3W2)
+   {
+      std::cerr << "Specify the model (--SC_W_) you want to use for the partially saturated cells method!" << std::endl;
+      return EXIT_FAILURE;
+   }
+
+   ///////////////////////////
+   // SIMULATION PROPERTIES //
+   ///////////////////////////
+
+   Setup setup;
+
+   setup.length         = uint_t(32);   // length of the cubic domain in lattice cells
+   const real_t chi     = real_c(0.85); // porosity parameter: diameter / length
+   setup.tau            = real_c(1.5);  // relaxation time
+   setup.angularVel     = real_c(1e-5); // angular velocity of the sphere
+   setup.checkFrequency = uint_t(100);  // evaluate the torque only every checkFrequency time steps
+   setup.radius         = real_c(0.5) * chi * real_c(setup.length); // sphere radius
+   setup.visc           = (setup.tau - real_c(0.5)) / real_c(3);    // viscosity in lattice units
+   setup.phi            = real_c(4.0 / 3.0) * math::pi * setup.radius * setup.radius * setup.radius /
+               (real_c(setup.length * setup.length * setup.length)); // solid volume fraction
+   const real_t omega            = real_c(1) / setup.tau;            // relaxation rate
+   const real_t dx               = real_c(1);                        // lattice dx
+   const real_t convergenceLimit = real_c(1e-5);                     // tolerance for relative change in torque
+   const uint_t timesteps =
+      funcTest ? 1 : (shortrun ? uint_c(150) : uint_c(5000)); // maximum number of time steps for the whole simulation
+
+   ///////////////////////////
+   // BLOCK STRUCTURE SETUP //
+   ///////////////////////////
+
+   const uint_t XBlocks = (processes >= 2) ? uint_t(2) : uint_t(1);
+   const uint_t YBlocks = (processes >= 4) ? uint_t(2) : uint_t(1);
+   const uint_t ZBlocks = (processes == 8) ? uint_t(2) : uint_t(1);
+   const uint_t XCells  = setup.length / XBlocks;
+   const uint_t YCells  = setup.length / YBlocks;
+   const uint_t ZCells  = setup.length / ZBlocks;
+
+   // create fully periodic domain
+   auto blocks = blockforest::createUniformBlockGrid(XBlocks, YBlocks, ZBlocks, XCells, YCells, ZCells, dx, true, true,
+                                                     true, true);
+
+   ////////
+   // PE //
+   ////////
+
+   auto mesapdDomain        = std::make_shared< mesa_pd::domain::BlockForestDomain >(blocks->getBlockForestPointer());
+   auto ps                  = std::make_shared< mesa_pd::data::ParticleStorage >(1);
+   auto ss                  = std::make_shared< mesa_pd::data::ShapeStorage >();
+   using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape;
+   auto accessor            = walberla::make_shared< ParticleAccessor_T >(ps, ss);
+
+   /////////////////
+   // PE COUPLING //
+   /////////////////
+
+   // connect to pe
+   const real_t overlap = real_c(1.5) * dx;
+
+   if (setup.radius > real_c(setup.length) * real_c(0.5) - overlap)
+   {
+      std::cerr << "Periodic sphere is too large!" << std::endl;
+      return EXIT_FAILURE;
+   }
+
+   // create the sphere in the middle of the domain
+   Vector3< real_t > position(real_c(setup.length) * real_c(0.5));
+   auto sphereShape = ss->create< mesa_pd::data::Sphere >(setup.radius);
+
+   if (mesapdDomain->isContainedInProcessSubdomain(uint_c(walberla::mpi::MPIManager::instance()->rank()), position))
+   {
+      auto sphereParticle = ps->create();
+      sphereParticle->setShapeID(sphereShape);
+      sphereParticle->setType(0);
+      sphereParticle->setPosition(position);
+      sphereParticle->setAngularVelocity(Vector3(real_c(0), setup.angularVel, real_c(0)));
+      sphereParticle->setOwner(walberla::MPIManager::instance()->rank());
+      sphereParticle->setInteractionRadius(setup.radius);
+   }
+
+   // synchronize often enough for large bodies
+   std::function< void(void) > syncCall = [&]() {
+      // const real_t overlap = real_t(1.5) * dx;
+      mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc;
+      syncNextNeighborFunc(*ps, *mesapdDomain, overlap);
+   };
+
+   syncCall();
+
+   ///////////////////////
+   // ADD DATA TO BLOCKS //
+   ////////////////////////
+
+   // create the lattice model
+   LatticeModel_T latticeModel = LatticeModel_T(omega);
+
+   // add PDF field ( uInit = <0,0,0>, rhoInit = 1 )
+   BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >(
+      blocks, "pdf field (zyxf)", latticeModel, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), real_c(1),
+      uint_t(1), field::zyxf);
+
+   // add particle and volume fraction field
+   BlockDataID particleAndVolumeFractionFieldID =
+      field::addToStorage< lbm_mesapd_coupling::psm::ParticleAndVolumeFractionField_T >(
+         blocks, "particle and volume fraction field",
+         std::vector< lbm_mesapd_coupling::psm::ParticleAndVolumeFraction_T >(), field::zyxf, 0);
+   // map bodies and calculate solid volume fraction initially
+   lbm_mesapd_coupling::psm::ParticleAndVolumeFractionMapping particleMapping(
+      blocks, accessor, lbm_mesapd_coupling::RegularParticlesSelector(), particleAndVolumeFractionFieldID, 4);
+   particleMapping();
+
+   // initialize the PDF field for PSM
+   if (SC1W1 || SC2W1 || SC3W1)
+   {
+      lbm_mesapd_coupling::psm::initializeDomainForPSM< LatticeModel_T, 1 >(
+         *blocks, pdfFieldID, particleAndVolumeFractionFieldID, *accessor);
+   }
+   else
+   {
+      lbm_mesapd_coupling::psm::initializeDomainForPSM< LatticeModel_T, 2 >(
+         *blocks, pdfFieldID, particleAndVolumeFractionFieldID, *accessor);
+   }
+
+   ///////////////
+   // TIME LOOP //
+   ///////////////
+
+   // create the timeloop
+   SweepTimeloop timeloop(blocks->getBlockStorage(), timesteps);
+
+   // setup of the LBM communication for synchronizing the pdf field between neighboring blocks
+   std::function< void() > commFunction;
+
+   blockforest::communication::UniformBufferedScheme< stencil::D3Q27 > scheme(blocks);
+   scheme.addPackInfo(make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID));
+   commFunction = scheme;
+
+   using TorqueEval_T                    = TorqueEval< ParticleAccessor_T >;
+   shared_ptr< TorqueEval_T > torqueEval = make_shared< TorqueEval_T >(&timeloop, &setup, blocks, accessor, fileIO);
+
+   if (SC1W1)
+   {
+      auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 1, 1 >(
+         pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+      // communication, streaming and force evaluation
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(makeSharedSweep(sweep), "cell-wise LB sweep")
+                     << AfterFunction(SharedFunctor< TorqueEval_T >(torqueEval), "torque evaluation");
+   }
+   else if (SC2W1)
+   {
+      auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 2, 1 >(
+         pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+      // communication, streaming and force evaluation
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(makeSharedSweep(sweep), "cell-wise LB sweep")
+                     << AfterFunction(SharedFunctor< TorqueEval_T >(torqueEval), "torque evaluation");
+   }
+   else if (SC3W1)
+   {
+      auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 3, 1 >(
+         pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+      // communication, streaming and force evaluation
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(makeSharedSweep(sweep), "cell-wise LB sweep")
+                     << AfterFunction(SharedFunctor< TorqueEval_T >(torqueEval), "torque evaluation");
+   }
+   else if (SC1W2)
+   {
+      auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 1, 2 >(
+         pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+      // communication, streaming and force evaluation
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(makeSharedSweep(sweep), "cell-wise LB sweep")
+                     << AfterFunction(SharedFunctor< TorqueEval_T >(torqueEval), "torque evaluation");
+   }
+   else if (SC2W2)
+   {
+      auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 2, 2 >(
+         pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+      // communication, streaming and force evaluation
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(makeSharedSweep(sweep), "cell-wise LB sweep")
+                     << AfterFunction(SharedFunctor< TorqueEval_T >(torqueEval), "torque evaluation");
+   }
+   else if (SC3W2)
+   {
+      auto sweep = lbm_mesapd_coupling::psm::makePSMSweep< LatticeModel_T, 3, 2 >(
+         pdfFieldID, particleAndVolumeFractionFieldID, blocks, accessor);
+      // communication, streaming and force evaluation
+      timeloop.add() << BeforeFunction(commFunction, "LBM Communication")
+                     << Sweep(makeSharedSweep(sweep), "cell-wise LB sweep")
+                     << AfterFunction(SharedFunctor< TorqueEval_T >(torqueEval), "torque evaluation");
+   }
+
+   lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque;
+
+   timeloop.addFuncAfterTimeStep(RemainingTimeLogger(timeloop.getNrOfTimeSteps()), "Remaining Time Logger");
+
+   ////////////////////////
+   // EXECUTE SIMULATION //
+   ////////////////////////
+
+   WcTimingPool timeloopTiming;
+
+   // time loop
+   for (uint_t i = 0; i < timesteps; ++i)
+   {
+      // perform a single simulation step
+      timeloop.singleStep(timeloopTiming);
+
+      // resetting force
+      ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor);
+
+      // check if the relative change in the torque is below the specified convergence criterion
+      if (i > setup.checkFrequency && torqueEval->getTorqueDiff() < convergenceLimit)
+      {
+         // if simulation has converged, terminate simulation
+         break;
+      }
+   }
+
+   timeloopTiming.logResultOnRoot();
+
+   // check the result
+   if (!funcTest && !shortrun)
+   {
+      real_t relErr = std::fabs((setup.analyticalTorque - torqueEval->getTorque()) / setup.analyticalTorque);
+      if (fileIO)
+      {
+         WALBERLA_ROOT_SECTION()
+         {
+            std::cout << "Analytical torque: " << setup.analyticalTorque << "\n"
+                      << "Simulated torque: " << torqueEval->getTorque() << "\n"
+                      << "Relative error: " << relErr << "\n";
+         }
+      }
+      // the relative error has to be below 10% (25% for SC2)
+      WALBERLA_CHECK_LESS(relErr, (SC2W1 || SC2W2) ? real_c(0.25) : real_c(0.1));
+   }
+
+   return 0;
+}
+
+} // namespace torque_sphere_psm
+
+int main(int argc, char** argv) { torque_sphere_psm::main(argc, argv); }
diff --git a/tests/lbm_mesapd_coupling/partially_saturated_cells_method/Utility.h b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/Utility.h
new file mode 100644
index 000000000..f24a4332a
--- /dev/null
+++ b/tests/lbm_mesapd_coupling/partially_saturated_cells_method/Utility.h
@@ -0,0 +1,85 @@
+//======================================================================================================================
+//
+//  This file is part of waLBerla. waLBerla is free software: you can
+//  redistribute it and/or modify it under the terms of the GNU General Public
+//  License as published by the Free Software Foundation, either version 3 of
+//  the License, or (at your option) any later version.
+//
+//  waLBerla is distributed in the hope that it will be useful, but WITHOUT
+//  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+//  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+//  for more details.
+//
+//  You should have received a copy of the GNU General Public License along
+//  with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>.
+//
+//! \file Utility.h
+//! \ingroup lbm_mesapd_coupling
+//! \author Samuel Kemmler <samuel.kemmler@fau.de>
+//
+//======================================================================================================================
+
+#pragma once
+
+#include "mesa_pd/data/ParticleStorage.h"
+#include "mesa_pd/data/ShapeStorage.h"
+
+namespace walberla
+{
+namespace lbm_mesapd_coupling
+{
+
+//*******************************************************************************************************************
+/*!\brief Calculating the sum over all fraction values. This can be used as a sanity check since it has to be roughly
+ * equal to the volume of all particles.
+ *
+ */
+//*******************************************************************************************************************
+class FractionFieldSum
+{
+ public:
+   FractionFieldSum(const shared_ptr< StructuredBlockStorage >& blockStorage,
+                    BlockDataID particleAndVolumeFractionFieldID)
+      : blockStorage_(blockStorage), particleAndVolumeFractionFieldID_(particleAndVolumeFractionFieldID)
+   {}
+
+   real_t operator()()
+   {
+      real_t sum = 0.0;
+
+      for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt)
+      {
+         psm::ParticleAndVolumeFractionField_T* particleAndVolumeFractionField =
+            blockIt->getData< psm::ParticleAndVolumeFractionField_T >(particleAndVolumeFractionFieldID_);
+
+         const cell_idx_t xSize = cell_idx_c(particleAndVolumeFractionField->xSize());
+         const cell_idx_t ySize = cell_idx_c(particleAndVolumeFractionField->ySize());
+         const cell_idx_t zSize = cell_idx_c(particleAndVolumeFractionField->zSize());
+
+         for (cell_idx_t z = 0; z < zSize; ++z)
+         {
+            for (cell_idx_t y = 0; y < ySize; ++y)
+            {
+               for (cell_idx_t x = 0; x < xSize; ++x)
+               {
+                  for (auto particleAndVolumeFraction : particleAndVolumeFractionField->get(x, y, z))
+                  {
+                     sum += particleAndVolumeFraction.second;
+                  }
+               }
+            }
+         }
+      }
+
+      WALBERLA_MPI_SECTION() { mpi::allReduceInplace(sum, mpi::SUM); }
+
+      return sum;
+   }
+
+ private:
+   shared_ptr< StructuredBlockStorage > blockStorage_;
+   BlockDataID particleAndVolumeFractionFieldID_;
+};
+
+} // namespace lbm_mesapd_coupling
+} // namespace walberla
-- 
GitLab