diff --git a/lbmpy/creationfunctions.py b/lbmpy/creationfunctions.py
index 479504565cf07220883e559038680db553169d44..79050dbbd39685496c60e6d82436ab61e0c0f6a4 100644
--- a/lbmpy/creationfunctions.py
+++ b/lbmpy/creationfunctions.py
@@ -52,6 +52,8 @@ For example, to modify the AST one can run::
     func = create_lb_function(ast=ast, ...)
 
 """
+import copy
+
 from dataclasses import dataclass, field, replace
 from typing import Union, List, Tuple, Any, Type, Iterable
 from warnings import warn, filterwarnings
@@ -65,6 +67,7 @@ from lbmpy.enums import Stencil, Method, ForceModel, CollisionSpace
 import lbmpy.forcemodels as forcemodels
 from lbmpy.fieldaccess import CollideOnlyInplaceAccessor, PdfFieldAccessor, PeriodicTwoFieldsAccessor
 from lbmpy.fluctuatinglb import add_fluctuations_to_collision_rule
+from lbmpy.partially_saturated_cells import add_psm_to_collision_rule, PSMConfig
 from lbmpy.non_newtonian_models import add_cassons_model, CassonsParameters
 from lbmpy.methods import (create_mrt_orthogonal, create_mrt_raw, create_central_moment,
                            create_srt, create_trt, create_trt_kbc)
@@ -79,7 +82,9 @@ from lbmpy.turbulence_models import add_smagorinsky_model
 from lbmpy.updatekernels import create_lbm_kernel, create_stream_pull_with_output_kernel
 from lbmpy.advanced_streaming.utility import Timestep, get_accessor
 from pystencils import CreateKernelConfig, create_kernel
+from pystencils.astnodes import Conditional, Block
 from pystencils.cache import disk_cache_no_fallback
+from pystencils.node_collection import NodeCollection
 from pystencils.typing import collate_types
 from pystencils.field import Field
 from pystencils.simp import sympy_cse, SimplificationStrategy
@@ -257,6 +262,12 @@ class LBMConfig:
     Temperature for fluctuating lattice Boltzmann methods.
     """
 
+    psm_config: PSMConfig = None
+    """
+    If a PSM config is specified, (1 - fractionField) is added to the relaxation rates of the collision 
+    and to the potential force term, and a solid collision is build and added to the main assignments.
+    """
+
     output: dict = field(default_factory=dict)
     """
     A dictionary mapping macroscopic quantites e.g. the strings 'density' and 'velocity' to pystencils
@@ -446,6 +457,9 @@ class LBMConfig:
             'centralmoment': forcemodels.CentralMoment
         }
 
+        if self.psm_config is not None and self.psm_config.fraction_field is not None:
+            self.force = [(1.0 - self.psm_config.fraction_field.center) * f for f in self.force]
+
         if isinstance(self.force_model, str):
             new_force_model = ForceModel[self.force_model.upper()]
             warn(f'ForceModel "{self.force_model}" as str is deprecated. Use {new_force_model} instead or '
@@ -657,6 +671,11 @@ def create_lb_collision_rule(lb_method=None, lbm_config=None, lbm_optimisation=N
     else:
         collision_rule = lb_method.get_collision_rule(pre_simplification=pre_simplification)
 
+    if lbm_config.psm_config is not None:
+        if lbm_config.psm_config.fraction_field is None or lbm_config.psm_config.object_velocity_field is None:
+            raise ValueError("Specify a fraction and object velocity field in the PSM Config")
+        collision_rule = add_psm_to_collision_rule(collision_rule, lbm_config.psm_config)
+
     if lbm_config.galilean_correction:
         from lbmpy.methods.cumulantbased import add_galilean_correction
         collision_rule = add_galilean_correction(collision_rule)
@@ -686,6 +705,7 @@ def create_lb_collision_rule(lb_method=None, lbm_config=None, lbm_optimisation=N
                                                              omega_output_field=lbm_config.omega_output_field)
         else:
             collision_rule = add_entropy_condition(collision_rule, omega_output_field=lbm_config.omega_output_field)
+
     elif lbm_config.smagorinsky:
         if lbm_config.cassons:
             raise ValueError("Cassons model can not be combined with Smagorinsky model")
@@ -738,6 +758,11 @@ def create_lb_method(lbm_config=None, **params):
     if isinstance(lbm_config.force, Field):
         lbm_config.force = tuple(lbm_config.force(i) for i in range(dim))
 
+    if lbm_config.psm_config is None:
+        fraction_field = None
+    else:
+        fraction_field = lbm_config.psm_config.fraction_field
+
     common_params = {
         'compressible': lbm_config.compressible,
         'zero_centered': lbm_config.zero_centered,
@@ -747,6 +772,7 @@ def create_lb_method(lbm_config=None, **params):
         'continuous_equilibrium': lbm_config.continuous_equilibrium,
         'c_s_sq': lbm_config.c_s_sq,
         'collision_space_info': lbm_config.collision_space_info,
+        'fraction_field': fraction_field,
     }
 
     cumulant_params = {
@@ -754,6 +780,7 @@ def create_lb_method(lbm_config=None, **params):
         'force_model': lbm_config.force_model,
         'c_s_sq': lbm_config.c_s_sq,
         'collision_space_info': lbm_config.collision_space_info,
+        'fraction_field': fraction_field,
     }
 
     if lbm_config.method == Method.SRT:
@@ -815,6 +842,54 @@ def create_lb_method(lbm_config=None, **params):
     return method
 
 
+def create_psm_update_rule(lbm_config, lbm_optimisation):
+    node_collection = []
+
+    # Use regular lb update rule for no overlapping particles
+    config_without_psm = copy.deepcopy(lbm_config)
+    config_without_psm.psm_config = None
+    # TODO: the force is still multiplied by (1.0 - self.psm_config.fraction_field.center)
+    #  (should not harm if memory bound since self.psm_config.fraction_field.center should always be 0.0)
+    lb_update_rule = create_lb_update_rule(
+        lbm_config=config_without_psm, lbm_optimisation=lbm_optimisation
+    )
+    node_collection.append(
+        Conditional(
+            lbm_config.psm_config.fraction_field.center(0) <= 0.0,
+            Block(lb_update_rule.all_assignments),
+        )
+    )
+
+    # Only one particle, i.e., no individual_fraction_field is provided
+    if lbm_config.psm_config.individual_fraction_field is None:
+        assert lbm_config.psm_config.MaxParticlesPerCell == 1
+        psm_update_rule = create_lb_update_rule(
+            lbm_config=lbm_config, lbm_optimisation=lbm_optimisation
+        )
+        node_collection.append(
+            Conditional(
+                lbm_config.psm_config.fraction_field.center(0) > 0.0,
+                Block(psm_update_rule.all_assignments),
+            )
+        )
+    else:
+        for p in range(lbm_config.psm_config.MaxParticlesPerCell):
+            # Add psm update rule for p overlapping particles
+            config_with_p_particles = copy.deepcopy(lbm_config)
+            config_with_p_particles.psm_config.MaxParticlesPerCell = p + 1
+            psm_update_rule = create_lb_update_rule(
+                lbm_config=config_with_p_particles, lbm_optimisation=lbm_optimisation
+            )
+            node_collection.append(
+                Conditional(
+                    lbm_config.psm_config.individual_fraction_field.center(p) > 0.0,
+                    Block(psm_update_rule.all_assignments),
+                )
+            )
+
+    return NodeCollection(node_collection)
+
+
 # ----------------------------------------------------------------------------------------------------------------------
 def update_with_default_parameters(params, opt_params=None, lbm_config=None, lbm_optimisation=None, config=None):
     # Fix CreateKernelConfig params
diff --git a/lbmpy/methods/abstractlbmethod.py b/lbmpy/methods/abstractlbmethod.py
index 096a3dd6f264a0a15b12b813a24a251d2abd1514..7bc4f5c920e0c8e3474a052a8842c1a70a040042 100644
--- a/lbmpy/methods/abstractlbmethod.py
+++ b/lbmpy/methods/abstractlbmethod.py
@@ -99,7 +99,7 @@ class AbstractLbMethod(abc.ABC):
 
     # -------------------------------- Helper Functions ----------------------------------------------------------------
 
-    def _generate_symbolic_relaxation_matrix(self, relaxation_rates=None):
+    def _generate_symbolic_relaxation_matrix(self, relaxation_rates=None, relaxation_rates_modifier=None):
         """
         This function replaces the numbers in the relaxation matrix with symbols in this case, and returns also
         the subexpressions, that assign the number to the newly introduced symbol
@@ -120,5 +120,7 @@ class AbstractLbMethod(abc.ABC):
 
         new_rr = [subexpressions[sp.sympify(e)] if sp.sympify(e) in subexpressions else e for e in rr]
         substitutions = [Assignment(e[1], e[0]) for e in subexpressions.items()]
+        if relaxation_rates_modifier is not None:
+            new_rr = [r * relaxation_rates_modifier for r in new_rr]
 
         return substitutions, sp.diag(*new_rr)
diff --git a/lbmpy/methods/creationfunctions.py b/lbmpy/methods/creationfunctions.py
index 78f35192b95d1fe718b02d75b830f40be6c82f65..e7440bd82a81b68c457db4b298ed404e9728c891 100644
--- a/lbmpy/methods/creationfunctions.py
+++ b/lbmpy/methods/creationfunctions.py
@@ -162,7 +162,7 @@ def create_with_continuous_maxwellian_equilibrium(stencil, moment_to_relaxation_
 def create_from_equilibrium(stencil, equilibrium, conserved_quantity_computation,
                             moment_to_relaxation_rate_dict,
                             collision_space_info=CollisionSpaceInfo(CollisionSpace.POPULATIONS),
-                            zero_centered=False, force_model=None):
+                            zero_centered=False, force_model=None, fraction_field=None):
     r"""
     Creates a lattice Boltzmann method in either population, moment, or central moment space, from a given
     discrete velocity set and equilibrium distribution. 
@@ -201,11 +201,11 @@ def create_from_equilibrium(stencil, equilibrium, conserved_quantity_computation
 
     if cspace.collision_space == CollisionSpace.POPULATIONS:
         return MomentBasedLbMethod(stencil, equilibrium, mom_to_rr_dict, conserved_quantity_computation=cqc,
-                                   force_model=force_model, zero_centered=zero_centered,
+                                   force_model=force_model, zero_centered=zero_centered, fraction_field=fraction_field,
                                    moment_transform_class=None)
     elif cspace.collision_space == CollisionSpace.RAW_MOMENTS:
         return MomentBasedLbMethod(stencil, equilibrium, mom_to_rr_dict, conserved_quantity_computation=cqc,
-                                   force_model=force_model, zero_centered=zero_centered,
+                                   force_model=force_model, zero_centered=zero_centered, fraction_field=fraction_field,
                                    moment_transform_class=cspace.raw_moment_transform_class)
     elif cspace.collision_space == CollisionSpace.CENTRAL_MOMENTS:
         return CentralMomentBasedLbMethod(stencil, equilibrium, mom_to_rr_dict, conserved_quantity_computation=cqc,
@@ -333,7 +333,7 @@ def create_mrt_raw(stencil, relaxation_rates, continuous_equilibrium=True, **kwa
 
 
 def create_central_moment(stencil, relaxation_rates, nested_moments=None,
-                          continuous_equilibrium=True, **kwargs):
+                          continuous_equilibrium=True, fraction_field=None, **kwargs):
     r"""
     Creates moment based LB method where the collision takes place in the central moment space.
 
@@ -368,6 +368,10 @@ def create_central_moment(stencil, relaxation_rates, nested_moments=None,
         nested_moments = cascaded_moment_sets_literature(stencil)
 
     rr_dict = _get_relaxation_info_dict(relaxation_rates, nested_moments, stencil.D)
+    if fraction_field is not None:
+        relaxation_rates_modifier = (1.0 - fraction_field.center)
+        rr_dict = _get_relaxation_info_dict(relaxation_rates, nested_moments, stencil.D,
+                                            relaxation_rates_modifier=relaxation_rates_modifier)
 
     if continuous_equilibrium:
         return create_with_continuous_maxwellian_equilibrium(stencil, rr_dict, **kwargs)
@@ -519,7 +523,7 @@ def create_mrt_orthogonal(stencil, relaxation_rates, continuous_equilibrium=True
 
 # ----------------------------------------- Cumulant method creators ---------------------------------------------------
 
-def create_cumulant(stencil, relaxation_rates, cumulant_groups, **kwargs):
+def create_cumulant(stencil, relaxation_rates, cumulant_groups, fraction_field=None, **kwargs):
     r"""Creates a cumulant-based lattice Boltzmann method.
 
     Args:
@@ -537,6 +541,12 @@ def create_cumulant(stencil, relaxation_rates, cumulant_groups, **kwargs):
         :class:`lbmpy.methods.cumulantbased.CumulantBasedLbMethod` instance
     """
     cumulant_to_rr_dict = _get_relaxation_info_dict(relaxation_rates, cumulant_groups, stencil.D)
+
+    if fraction_field is not None:
+        relaxation_rates_modifier = (1.0 - fraction_field.center)
+        cumulant_to_rr_dict = _get_relaxation_info_dict(relaxation_rates, cumulant_groups, stencil.D,
+                                                        relaxation_rates_modifier=relaxation_rates_modifier)
+
     kwargs.setdefault('collision_space_info', CollisionSpaceInfo(CollisionSpace.CUMULANTS))
 
     if kwargs['collision_space_info'].collision_space != CollisionSpace.CUMULANTS:
@@ -581,7 +591,7 @@ def create_with_default_polynomial_cumulants(stencil, relaxation_rates, **kwargs
     return create_cumulant(stencil, relaxation_rates, cumulant_groups, **kwargs)
 
 
-def _get_relaxation_info_dict(relaxation_rates, nested_moments, dim):
+def _get_relaxation_info_dict(relaxation_rates, nested_moments, dim, relaxation_rates_modifier=None):
     r"""Creates a dictionary where each moment is mapped to a relaxation rate.
 
     Args:
@@ -599,7 +609,9 @@ def _get_relaxation_info_dict(relaxation_rates, nested_moments, dim):
             rr = iter(relaxation_rates(group))
             for moment in group:
                 result[moment] = next(rr)
-
+        if relaxation_rates_modifier is not None:
+            for key in result:
+                result[key] *= relaxation_rates_modifier
         return result
 
     number_of_moments = 0
@@ -664,6 +676,9 @@ def _get_relaxation_info_dict(relaxation_rates, nested_moments, dim):
                              "moment. In this case, conserved moments are also "
                              "relaxed with 0. The last possibility is to specify a relaxation rate for each moment, "
                              "including conserved moments")
+    if relaxation_rates_modifier is not None:
+        for key in result:
+            result[key] *= relaxation_rates_modifier
     return result
 
 
diff --git a/lbmpy/methods/momentbased/momentbasedmethod.py b/lbmpy/methods/momentbased/momentbasedmethod.py
index 79aee5c813b71c358e93964b52b84b6b1532269f..740834072a0210772d407a7e441b9e1ef46d0f7a 100644
--- a/lbmpy/methods/momentbased/momentbasedmethod.py
+++ b/lbmpy/methods/momentbased/momentbasedmethod.py
@@ -39,7 +39,7 @@ class MomentBasedLbMethod(AbstractLbMethod):
 
     def __init__(self, stencil, equilibrium, relaxation_dict,
                  conserved_quantity_computation=None, force_model=None, zero_centered=False,
-                 moment_transform_class=PdfsToMomentsByChimeraTransform):
+                 fraction_field=None, moment_transform_class=PdfsToMomentsByChimeraTransform):
         assert isinstance(conserved_quantity_computation, AbstractConservedQuantityComputation)
         super(MomentBasedLbMethod, self).__init__(stencil)
 
@@ -48,6 +48,7 @@ class MomentBasedLbMethod(AbstractLbMethod):
         self._cqc = conserved_quantity_computation
         self._force_model = force_model
         self._zero_centered = zero_centered
+        self.fraction_field = fraction_field
         self._weights = None
         self._moment_transform_class = moment_transform_class
 
@@ -174,7 +175,14 @@ class MomentBasedLbMethod(AbstractLbMethod):
 
     def get_collision_rule(self, conserved_quantity_equations: AssignmentCollection = None,
                            pre_simplification: bool = True) -> LbmCollisionRule:
-        rr_sub_expressions, d = self._generate_symbolic_relaxation_matrix()
+
+        if self.fraction_field is not None:
+            relaxation_rates_modifier = (1.0 - self.fraction_field.center)
+            rr_sub_expressions, d = self._generate_symbolic_relaxation_matrix(
+                relaxation_rates_modifier=relaxation_rates_modifier)
+        else:
+            rr_sub_expressions, d = self._generate_symbolic_relaxation_matrix()
+
         ac = self._collision_rule_with_relaxation_matrix(d=d,
                                                          additional_subexpressions=rr_sub_expressions,
                                                          include_force_terms=True,
diff --git a/lbmpy/partially_saturated_cells.py b/lbmpy/partially_saturated_cells.py
new file mode 100644
index 0000000000000000000000000000000000000000..0798ac69501b46ffc22251ff13e7f5ecc036d3b9
--- /dev/null
+++ b/lbmpy/partially_saturated_cells.py
@@ -0,0 +1,115 @@
+import sympy as sp
+from dataclasses import dataclass
+
+from lbmpy.methods.abstractlbmethod import LbmCollisionRule
+from pystencils import Assignment, AssignmentCollection
+from pystencils.field import Field
+
+
+@dataclass
+class PSMConfig:
+    fraction_field: Field = None
+    """
+    Fraction field for PSM 
+    """
+
+    object_velocity_field: Field = None
+    """
+    Object velocity field for PSM 
+    """
+
+    SC: int = 1
+    """
+    Solid collision option for PSM
+    """
+
+    MaxParticlesPerCell: int = 1
+    """
+    Maximum number of particles overlapping with a cell 
+    """
+
+    individual_fraction_field: Field = None
+    """
+    Fraction field for each overlapping particle in PSM 
+    """
+
+    particle_force_field: Field = None
+    """
+    Force field for each overlapping particle in PSM 
+    """
+
+
+def add_psm_to_collision_rule(collision_rule, psm_config):
+    if psm_config.individual_fraction_field is None:
+        psm_config.individual_fraction_field = psm_config.fraction_field
+
+    method = collision_rule.method
+    pre_collision_pdf_symbols = method.pre_collision_pdf_symbols
+    stencil = method.stencil
+
+    # Get equilibrium from object velocity for solid collision
+    forces_rhs = [0] * psm_config.MaxParticlesPerCell * stencil.D
+    solid_collisions = [0] * stencil.Q
+    for p in range(psm_config.MaxParticlesPerCell):
+        equilibrium_fluid = method.get_equilibrium_terms()
+        equilibrium_solid = []
+        for eq in equilibrium_fluid:
+            eq_sol = eq
+            for i in range(stencil.D):
+                eq_sol = eq_sol.subs(sp.Symbol("u_" + str(i)),
+                                     psm_config.object_velocity_field.center(p * stencil.D + i), )
+            equilibrium_solid.append(eq_sol)
+
+        # Build solid collision
+        for i, (eqFluid, eqSolid, f, offset) in enumerate(
+                zip(equilibrium_fluid, equilibrium_solid, pre_collision_pdf_symbols, stencil)):
+            inverse_direction_index = stencil.stencil_entries.index(stencil.inverse_stencil_entries[i])
+            if psm_config.SC == 1:
+                solid_collision = psm_config.individual_fraction_field.center(p) * (
+                    (
+                        pre_collision_pdf_symbols[inverse_direction_index]
+                        - equilibrium_fluid[inverse_direction_index]
+                    )
+                    - (f - eqSolid)
+                )
+            elif psm_config.SC == 2:
+                # TODO get relaxation rate vector from method and use the right relaxation rate [i]
+                solid_collision = psm_config.individual_fraction_field.center(p) * (
+                    (eqSolid - f) + (1.0 - method.relaxation_rates[0]) * (f - eqFluid)
+                )
+            elif psm_config.SC == 3:
+                solid_collision = psm_config.individual_fraction_field.center(p) * (
+                    (
+                        pre_collision_pdf_symbols[inverse_direction_index]
+                        - equilibrium_solid[inverse_direction_index]
+                    )
+                    - (f - eqSolid)
+                )
+            else:
+                raise ValueError("Only SC=1, SC=2 and SC=3 are supported.")
+            solid_collisions[i] += solid_collision
+            for j in range(stencil.D):
+                forces_rhs[p * stencil.D + j] -= solid_collision * int(offset[j])
+
+    # Add solid collision to main assignments of collision rule
+    collision_assignments = []
+    for main, sc in zip(collision_rule.main_assignments, solid_collisions):
+        collision_assignments.append(Assignment(main.lhs, main.rhs + sc))
+
+    # Add hydrodynamic force calculations to collision assignments if two-way coupling is used
+    # (i.e., force field is not None)
+    if psm_config.particle_force_field is not None:
+        for p in range(psm_config.MaxParticlesPerCell):
+            for i in range(stencil.D):
+                collision_assignments.append(
+                    Assignment(
+                        psm_config.particle_force_field.center(p * stencil.D + i),
+                        forces_rhs[p * stencil.D + i],
+                    )
+                )
+
+    collision_assignments = AssignmentCollection(collision_assignments)
+    ac = LbmCollisionRule(method, collision_assignments, collision_rule.subexpressions,
+                          collision_rule.simplification_hints)
+    ac.topological_sort()
+    return ac