From 3a0e87f7cc8bb4a39aa119d8488fcf3ce01d2f9d Mon Sep 17 00:00:00 2001
From: Helen Schottenhamml <helen.schottenhamml@fau.de>
Date: Thu, 2 May 2024 17:37:38 +0200
Subject: [PATCH] Wall function bounce

---
 doc/sphinx/lbmpy.bib                         |  26 ++
 src/lbmpy/boundaries/__init__.py             |   8 +-
 src/lbmpy/boundaries/boundaryconditions.py   | 237 +++++++++++++++++++
 src/lbmpy/boundaries/wall_function_models.py | 173 ++++++++++++++
 src/lbmpy/flow_statistics.py                 |  82 +++++++
 tests/test_wall_function_bounce.py           | 116 +++++++++
 6 files changed, 639 insertions(+), 3 deletions(-)
 create mode 100644 src/lbmpy/boundaries/wall_function_models.py
 create mode 100644 src/lbmpy/flow_statistics.py
 create mode 100644 tests/test_wall_function_bounce.py

diff --git a/doc/sphinx/lbmpy.bib b/doc/sphinx/lbmpy.bib
index efce2c1a..c9d012c1 100644
--- a/doc/sphinx/lbmpy.bib
+++ b/doc/sphinx/lbmpy.bib
@@ -288,4 +288,30 @@ journal = {Communications in Computational Physics}
   journal   = {Physics of Fluids}
 }
 
+@article{Han2021,
+   doi = {10.1088/1873-7005/ac1782},
+   url = {https://dx.doi.org/10.1088/1873-7005/ac1782}                    ,
+   year = {2021},
+   month = {aug},
+   publisher = {IOP Publishing},
+   volume = {53},
+   number = {4},
+   pages = {045506},
+   author = {Mengtao Han and Ryozo Ooka and Hideki Kikumoto},
+   title = {A wall function approach in lattice Boltzmann method: algorithm and validation using turbulent channel flow},
+   journal = {Fluid Dynamics Research}
+}
+
+@article{Maronga2020,
+ author = {Maronga, Bj{\"o}rn and Knigge, Christoph and Raasch, Siegfried},
+ year = {2020},
+ title = {{An Improved Surface Boundary Condition for Large-Eddy Simulations Based on Monin--Obukhov Similarity Theory: Evaluation and Consequences for Grid Convergence in Neutral and Stable Conditions}},
+ pages = {297--325},
+ volume = {174},
+ number = {2},
+ issn = {0006-8314},
+ journal = {{Boundary-layer meteorology}},
+ doi = {10.1007/s10546-019-00485-w}
+}
+
 @Comment{jabref-meta: databaseType:bibtex;}
diff --git a/src/lbmpy/boundaries/__init__.py b/src/lbmpy/boundaries/__init__.py
index 5520f318..e522740a 100644
--- a/src/lbmpy/boundaries/__init__.py
+++ b/src/lbmpy/boundaries/__init__.py
@@ -1,10 +1,12 @@
 from lbmpy.boundaries.boundaryconditions import (
-    UBB, FixedDensity, DiffusionDirichlet, SimpleExtrapolationOutflow,
+    UBB, FixedDensity, DiffusionDirichlet, SimpleExtrapolationOutflow, WallFunctionBounce,
     ExtrapolationOutflow, NeumannByCopy, NoSlip, NoSlipLinearBouzidi, QuadraticBounceBack, StreamInConstant, FreeSlip)
 from lbmpy.boundaries.boundaryhandling import LatticeBoltzmannBoundaryHandling
+from lbmpy.boundaries.wall_function_models import MoninObukhovSimilarityTheory, LogLaw, MuskerLaw, SpaldingsLaw
 
-__all__ = ['NoSlip', 'NoSlipLinearBouzidi', 'QuadraticBounceBack', 'FreeSlip',
+__all__ = ['NoSlip', 'NoSlipLinearBouzidi', 'QuadraticBounceBack', 'FreeSlip', 'WallFunctionBounce',
            'UBB', 'FixedDensity',
            'SimpleExtrapolationOutflow', 'ExtrapolationOutflow',
            'DiffusionDirichlet', 'NeumannByCopy', 'StreamInConstant',
-           'LatticeBoltzmannBoundaryHandling']
+           'LatticeBoltzmannBoundaryHandling',
+           'MoninObukhovSimilarityTheory', 'LogLaw', 'MuskerLaw', 'SpaldingsLaw']
diff --git a/src/lbmpy/boundaries/boundaryconditions.py b/src/lbmpy/boundaries/boundaryconditions.py
index 6dcf9d45..3d52af04 100644
--- a/src/lbmpy/boundaries/boundaryconditions.py
+++ b/src/lbmpy/boundaries/boundaryconditions.py
@@ -1,4 +1,5 @@
 import abc
+from enum import Enum, auto
 from warnings import warn
 
 from pystencils import Assignment, Field
@@ -14,6 +15,7 @@ from lbmpy.maxwellian_equilibrium import discrete_equilibrium
 from lbmpy.simplificationfactory import create_simplification_strategy
 
 import sympy as sp
+import numpy as np
 
 
 class LbBoundary(abc.ABC):
@@ -444,6 +446,241 @@ class FreeSlip(LbBoundary):
 # end class FreeSlip
 
 
+class WallFunctionBounce(LbBoundary):
+    """
+    Wall function based on the bounce back idea, cf. :cite:`Han2021`. Its implementation is extended to the D3Q27
+    stencil, whereas different weights of the drag distribution are proposed.
+
+    Args:
+        lb_method: LB method which is used for the simulation
+        pdfs: Symbolic representation of the particle distribution functions.
+        normal_direction: Normal direction of the wall. Currently, only straight and axis-aligned walls are supported.
+        wall_function_model: Wall function that is used to retrieve the wall stress :math:`tau_w` during the simulation.
+                             See :class:`lbmpy.boundaries.wall_treatment.WallFunctionModel` for more details
+        mean_velocity: Optional field or field access for the mean velocity. As wall functions are typically defined
+                       in terms of the mean velocity, it is recommended to provide this variable. Per default, the
+                       instantaneous velocity obtained from pdfs is used for the wall function.
+        sampling_shift: Optional sampling shift for the velocity sampling. Can be provided as symbolic variable or
+                        integer. In both cases, the user must assure that the sampling shift is at least 1, as sampling
+                        in boundary cells is not physical. Per default, a sampling shift of 1 is employed which
+                        corresponds to a sampling in the first fluid cell normal to the wall. For lower friction
+                        Reynolds numbers, choosing a sampling shift >1 has shown to improve the results for higher
+                        resolutions.
+                        Mutually exclusive with the Maronga sampling shift.
+        maronga_sampling_shift: Optionally, apply a correction factor to the wall shear stress proposed by Maronga et
+                                al. :cite:`Maronga2020`. Has only been tested and validated for the MOST wall function.
+                                No guarantee is given that it also works with other wall functions.
+                                Mutually exclusive with the standard sampling shift.
+        dt: time discretisation. Usually one in LB units
+        dy: space discretisation. Usually one in LB units
+        y: distance from the wall
+        target_friction_velocity: A target friction velocity can be given if an estimate is known a priori. This target
+                                  friction velocity will be used as initial guess for implicit wall functions to ensure
+                                  convergence of the Newton algorithm.
+        weight_method: The extension of the WFB to a D3Q27 stencil is non-unique. Different weights can be chosen to
+                       define the drag distribution onto the pdfs. Per default, weights corresponding to the weights
+                       in the D3Q27 stencil are chosen.
+        name: Optional name of the boundary.
+        data_type: Floating-point precision. Per default, double.
+    """
+
+    class WeightMethod(Enum):
+        LATTICE_WEIGHT = auto(),
+        GEOMETRIC_WEIGHT = auto()
+
+    def __init__(self, lb_method, pdfs, normal_direction, wall_function_model,
+                 mean_velocity=None, sampling_shift=1, maronga_sampling_shift=None,
+                 dt=1, dy=1, y=0.5,
+                 target_friction_velocity=None,
+                 weight_method=WeightMethod.LATTICE_WEIGHT,
+                 name=None, data_type='double'):
+        """Set an optional name here, to mark boundaries, for example for force evaluations"""
+        self.stencil = lb_method.stencil
+        if not (self.stencil.Q == 19 or self.stencil.Q == 27):
+            raise ValueError("WFB boundary is currently only defined for D3Q19 and D3Q27 stencils.")
+        self.pdfs = pdfs
+
+        self.wall_function_model = wall_function_model
+
+        if mean_velocity:
+            if isinstance(mean_velocity, Field):
+                self.mean_velocity = mean_velocity.center_vector
+            elif isinstance(mean_velocity, Field.Access):
+                self.mean_velocity = mean_velocity.field.neighbor_vector(mean_velocity.offsets)
+            else:
+                raise ValueError("Mean velocity field has to be a pystencils Field or Field.Access")
+        else:
+            self.mean_velocity = None
+
+        if not isinstance(sampling_shift, int):
+            self.sampling_shift = TypedSymbol(sampling_shift.name, np.uint32)
+        else:
+            assert sampling_shift >= 1, "The sampling shift must be greater than 1."
+            self.sampling_shift = sampling_shift
+
+        if maronga_sampling_shift:
+            assert self.mean_velocity, "Mean velocity field must be provided when using the Maronga correction"
+            if not isinstance(maronga_sampling_shift, int):
+                self.maronga_sampling_shift = TypedSymbol(maronga_sampling_shift.name, np.uint32)
+            else:
+                assert maronga_sampling_shift >= 1, "The Maronga sampling shift must be greater than 1."
+                self.maronga_sampling_shift = maronga_sampling_shift
+        else:
+            self.maronga_sampling_shift = None
+
+        if (self.sampling_shift != 1) and self.maronga_sampling_shift:
+            raise ValueError("Both sampling shift and Maronga offset are set. This is currently not supported.")
+
+        self.dt = dt
+        self.dy = dy
+        self.y = y
+        self.data_type = data_type
+
+        self.target_friction_velocity = target_friction_velocity
+
+        self.weight_method = weight_method
+
+        if len(normal_direction) - normal_direction.count(0) != 1:
+            raise ValueError("Only normal directions for straight walls are supported for example (0, 1, 0) for "
+                             "a WallFunctionBounce applied to the southern boundary of the domain")
+
+        self.mirror_axis = normal_direction.index(*[direction for direction in normal_direction if direction != 0])
+
+        self.normal_direction = normal_direction
+        assert all([n in [-1, 0, 1] for n in self.normal_direction]), \
+            "Only -1, 0 and 1 allowed for defining the normal direction"
+        tangential_component = [int(not n) for n in self.normal_direction]
+        self.normal_axis = tangential_component.index(0)
+        self.tangential_axis = [0, 1, 2]
+        self.tangential_axis.remove(self.normal_axis)
+
+        self.dim = self.stencil.D
+
+        if name is None:
+            name = f"WFB : {offset_to_direction_string([-x for x in normal_direction])}"
+
+        super(WallFunctionBounce, self).__init__(name)
+
+    def get_additional_code_nodes(self, lb_method):
+        return [MirroredStencilDirections(self.stencil, self.mirror_axis),
+                NeighbourOffsetArrays(lb_method.stencil)]
+
+    def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field):
+        # needed symbols for offsets and indices
+        # neighbour offset symbols are basically the stencil directions defined in stencils.py:L130ff.
+        neighbor_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
+        tangential_offset = tuple(offset + normal for offset, normal in zip(neighbor_offset, self.normal_direction))
+        mirrored_stencil_symbol = MirroredStencilDirections._mirrored_symbol(self.mirror_axis)
+        mirrored_direction = inv_dir[sp.IndexedBase(mirrored_stencil_symbol, shape=(1,))[dir_symbol]]
+
+        name_base = "f_in_inv_offsets_"
+        offset_array_symbols = [TypedSymbol(name_base + d, mirrored_stencil_symbol.dtype) for d in ['x', 'y', 'z']]
+        mirrored_offset = sp.IndexedBase(mirrored_stencil_symbol, shape=(1,))[dir_symbol]
+        offsets = tuple(sp.IndexedBase(s, shape=(1,))[mirrored_offset] for s in offset_array_symbols)
+
+        # needed symbols in the Assignments
+        u_m = sp.Symbol("u_m")
+        tau_w = sp.Symbol("tau_w")
+        wall_stress = sp.symbols("tau_w_x tau_w_y tau_w_z")
+
+        # if the mean velocity field is not given, or the Maronga correction is applied, density and velocity values
+        # will be calculated from pdfs
+        cqc = lb_method.conserved_quantity_computation
+
+        result = []
+        if (not self.mean_velocity) or self.maronga_sampling_shift:
+            pdf_center_vector = sp.Matrix([0] * self.stencil.Q)
+
+            for i in range(self.stencil.Q):
+                pdf_center_vector[i] = self.pdfs[offsets[0] + self.normal_direction[0],
+                                                 offsets[1] + self.normal_direction[1],
+                                                 offsets[2] + self.normal_direction[2]](i)
+
+            eq_equations = cqc.equilibrium_input_equations_from_pdfs(pdf_center_vector)
+            result.append(eq_equations.all_assignments)
+
+        # sample velocity which will be used in the wall stress calculation
+        if self.mean_velocity:
+            if self.maronga_sampling_shift:
+                u_for_tau_wall = tuple(u_mean_i.get_shifted(
+                    self.maronga_sampling_shift * self.normal_direction[0],
+                    self.maronga_sampling_shift * self.normal_direction[1],
+                    self.maronga_sampling_shift * self.normal_direction[2]
+                ) for u_mean_i in self.mean_velocity)
+            else:
+                u_for_tau_wall = tuple(u_mean_i.get_shifted(
+                    self.sampling_shift * self.normal_direction[0],
+                    self.sampling_shift * self.normal_direction[1],
+                    self.sampling_shift * self.normal_direction[2]
+                ) for u_mean_i in self.mean_velocity)
+
+            rho_for_tau_wall = sp.Float(1)
+        else:
+            rho_for_tau_wall = cqc.density_symbol
+            u_for_tau_wall = cqc.velocity_symbols
+
+        # calculate Maronga factor in case of correction
+        maronga_fix = sp.Symbol("maronga_fix")
+        if self.maronga_sampling_shift:
+            inst_first_cell_vel = cqc.velocity_symbols
+            mean_first_cell_vel = tuple(u_mean_i.get_shifted(*self.normal_direction) for u_mean_i in self.mean_velocity)
+
+            mag_inst_vel_first_cell = sp.sqrt(sum([inst_first_cell_vel[i] ** 2 for i in self.tangential_axis]))
+            mag_mean_vel_first_cell = sp.sqrt(sum([mean_first_cell_vel[i] ** 2 for i in self.tangential_axis]))
+
+            result.append(Assignment(maronga_fix, mag_inst_vel_first_cell / mag_mean_vel_first_cell))
+        else:
+            maronga_fix = 1
+
+        # store which direction is tangential component (only those are used for the wall shear stress)
+        red_u_mag = sp.sqrt(sum([u_for_tau_wall[i]**2 for i in self.tangential_axis]))
+
+        u_mag = Assignment(u_m, red_u_mag)
+        result.append(u_mag)
+
+        wall_distance = self.maronga_sampling_shift if self.maronga_sampling_shift else self.sampling_shift
+
+        # using wall function model
+        wall_law_assignments = self.wall_function_model.shear_stress_assignments(
+            density_symbol=rho_for_tau_wall, velocity_symbol=u_m, shear_stress_symbol=tau_w,
+            wall_distance=(wall_distance - sp.Rational(1, 2) * self.dy),
+            u_tau_target=self.target_friction_velocity)
+        result.append(wall_law_assignments)
+
+        # calculate wall stress components and use them to calculate the drag
+        for i in self.tangential_axis:
+            result.append(Assignment(wall_stress[i], - u_for_tau_wall[i] / u_m * tau_w * maronga_fix))
+
+        weight, inv_weight_sq = sp.symbols("wfb_weight inverse_weight_squared")
+
+        if self.stencil.Q == 19:
+            result.append(Assignment(weight, sp.Rational(1, 2)))
+        elif self.stencil.Q == 27:
+            result.append(Assignment(inv_weight_sq, sum([neighbor_offset[i]**2 for i in self.tangential_axis])))
+            a, b = sp.symbols("wfb_a wfb_b")
+
+            if self.weight_method == self.WeightMethod.LATTICE_WEIGHT:
+                res_ab = sp.solve([2 * a + 4 * b - 1, a - 4 * b], [a, b])  # lattice weight scaling
+            elif self.weight_method == self.WeightMethod.GEOMETRIC_WEIGHT:
+                res_ab = sp.solve([2 * a + 4 * b - 1, a - sp.sqrt(2) * b], [a, b])  # geometric scaling
+            else:
+                raise ValueError("Unknown weighting method for the WFB D3Q27 extension. Currently, only lattice "
+                                 "weights and geometric weights are supported.")
+
+            result.append(Assignment(weight, sp.Piecewise((sp.Float(0), sp.Equality(inv_weight_sq, 0)),
+                                                          (res_ab[a], sp.Equality(inv_weight_sq, 1)),
+                                                          (res_ab[b], True))))
+
+        factor = self.dt / self.dy * weight
+        drag = sum([neighbor_offset[i] * factor * wall_stress[i] for i in self.tangential_axis])
+
+        result.append(Assignment(f_in.center(inv_dir[dir_symbol]), f_out[tangential_offset](mirrored_direction) - drag))
+
+        return result
+
+# end class WallFunctionBounce
+
+
 class UBB(LbBoundary):
     r"""Velocity bounce back boundary condition, enforcing specified velocity at obstacle. Furthermore, a density
         at the wall can be implied. The boundary condition is implemented with the following formula:
diff --git a/src/lbmpy/boundaries/wall_function_models.py b/src/lbmpy/boundaries/wall_function_models.py
new file mode 100644
index 00000000..4f627787
--- /dev/null
+++ b/src/lbmpy/boundaries/wall_function_models.py
@@ -0,0 +1,173 @@
+import sympy as sp
+from abc import ABC, abstractmethod
+
+from pystencils import Assignment
+
+
+class WallFunctionModel(ABC):
+    def __init__(self, name):
+        self._name = name
+
+    @abstractmethod
+    def shear_stress_assignments(self, density_symbol: sp.Symbol, shear_stress_symbol: sp.Symbol,
+                                 velocity_symbol: sp.Symbol, wall_distance, u_tau_target):
+        """
+        Computes a symbolic representation for the log law.
+
+        Args:
+            density_symbol: symbol density, should be provided by the LB method's conserved quantity computation
+            shear_stress_symbol: symbolic wall shear stress to which the calculated shear stress will be assigned
+            velocity_symbol: symbolic velocity that is taken as a reference in the wall functions
+            wall_distance: distance to the wall, equals to 0.5 in standard cell-centered LBM
+            u_tau_target: in implicit wall functions, a target friction velocity can be provided which will be used as
+                          initial guess in the Newton iteration. This target friction velocity can be obtained, e.g.,
+                          from the target friction Reynolds number
+        """
+        pass
+
+
+# end class WallFunctionModel
+
+
+class ExplicitWallFunctionModel(WallFunctionModel, ABC):
+    """
+    Abstract base class for explicit wall functions that can be solved directly for the wall shear stress.
+    """
+
+    def __init__(self, name):
+        super(ExplicitWallFunctionModel, self).__init__(name=name)
+
+
+class MoninObukhovSimilarityTheory(ExplicitWallFunctionModel):
+    def __init__(self, z0, kappa=0.41, phi=0, name="MOST"):
+        self.z0 = z0
+        self.kappa = kappa
+        self.phi = phi
+
+        super(MoninObukhovSimilarityTheory, self).__init__(name=name)
+
+    def shear_stress_assignments(self, density_symbol: sp.Symbol, shear_stress_symbol: sp.Symbol,
+                                 velocity_symbol: sp.Symbol, wall_distance, u_tau_target=None):
+        u_tau = velocity_symbol * self.kappa / sp.ln(wall_distance / self.z0 + self.phi)
+        return [Assignment(shear_stress_symbol, u_tau ** 2 * density_symbol)]
+
+
+class ImplicitWallFunctionModel(WallFunctionModel, ABC):
+    """
+    Abstract base class for implicit wall functions that require a Newton procedure to solve for the wall shear stress.
+    """
+
+    def __init__(self, name, newton_steps, viscosity):
+        self.newton_steps = newton_steps
+        self.u_tau = sp.symbols(f"wall_function_u_tau_:{self.newton_steps + 1}")
+        self.delta = sp.symbols(f"wall_function_delta_:{self.newton_steps}")
+
+        self.viscosity = viscosity
+
+        super(ImplicitWallFunctionModel, self).__init__(name=name)
+
+    def newton_iteration(self, wall_law):
+        m = -wall_law / wall_law.diff(self.u_tau[0])
+
+        assignments = []
+        for i in range(self.newton_steps):
+            assignments.append(Assignment(self.delta[i], m.subs({self.u_tau[0]: self.u_tau[i]})))
+            assignments.append(Assignment(self.u_tau[i + 1], self.u_tau[i] + self.delta[i]))
+
+        return assignments
+
+
+class LogLaw(ImplicitWallFunctionModel):
+    """
+    Analytical model for the velocity profile inside the boundary layer, obtained from the mean velocity gradient.
+    Only valid in the log-law region.
+    """
+
+    def __init__(self, viscosity, newton_steps=5, kappa=0.41, b=5.2, name="LogLaw"):
+        self.kappa = kappa
+        self.b = b
+
+        super(LogLaw, self).__init__(name=name, newton_steps=newton_steps, viscosity=viscosity)
+
+    def shear_stress_assignments(self, density_symbol: sp.Symbol, shear_stress_symbol: sp.Symbol,
+                                 velocity_symbol: sp.Symbol, wall_distance, u_tau_target=None):
+        def law(u_p, y_p):
+            return 1 / self.kappa * sp.ln(y_p) + self.b - u_p
+
+        u_plus = velocity_symbol / self.u_tau[0]
+        y_plus = (wall_distance * self.u_tau[0]) / self.viscosity
+
+        u_tau_init = u_tau_target if u_tau_target else velocity_symbol / sp.Float(100)
+
+        wall_law = law(u_plus, y_plus)
+        assignments = [Assignment(self.u_tau[0], u_tau_init),  # initial guess
+                       *self.newton_iteration(wall_law),  # newton iterations
+                       Assignment(shear_stress_symbol, self.u_tau[-1] ** 2 * density_symbol)]  # final result
+
+        return assignments
+
+
+class SpaldingsLaw(ImplicitWallFunctionModel):
+    """
+    Single formula for the velocity profile inside the boundary layer, proposed by Spalding :cite:`spalding1961`.
+    Valid in the inner and the outer layer.
+    """
+
+    def __init__(self, viscosity, newton_steps=5, kappa=0.41, b=5.5, name="Spalding"):
+        self.kappa = kappa
+        self.b = b
+
+        super(SpaldingsLaw, self).__init__(name=name, newton_steps=newton_steps, viscosity=viscosity)
+
+    def shear_stress_assignments(self, density_symbol: sp.Symbol, shear_stress_symbol: sp.Symbol,
+                                 velocity_symbol: sp.Symbol, wall_distance, u_tau_target=None):
+        def law(u_p, y_p):
+            k_times_u = self.kappa * u_p
+            frac_1 = (k_times_u ** 2) / sp.Float(2)
+            frac_2 = (k_times_u ** 3) / sp.Float(6)
+            return (u_p + sp.exp(-self.kappa * self.b) * (sp.exp(k_times_u) - sp.Float(1) - k_times_u - frac_1 - frac_2)
+                    - y_p)
+
+        u_plus = velocity_symbol / self.u_tau[0]
+        y_plus = (wall_distance * self.u_tau[0]) / self.viscosity
+
+        u_tau_init = u_tau_target if u_tau_target else velocity_symbol / sp.Float(100)
+
+        wall_law = law(u_plus, y_plus)
+        assignments = [Assignment(self.u_tau[0], u_tau_init),  # initial guess
+                       *self.newton_iteration(wall_law),  # newton iterations
+                       Assignment(shear_stress_symbol, self.u_tau[-1] ** 2 * density_symbol)]  # final result
+
+        return assignments
+
+
+class MuskerLaw(ImplicitWallFunctionModel):
+    """
+    Quasi-analytical model for the velocity profile inside the boundary layer, proposed by Musker. Valid in the inner
+    and the outer layer.
+    Formulation taken from :cite:`malaspinas2015`, Equation (59).
+    """
+
+    def __init__(self, viscosity, newton_steps=5, name="Musker"):
+
+        super(MuskerLaw, self).__init__(name=name, newton_steps=newton_steps, viscosity=viscosity)
+
+    def shear_stress_assignments(self, density_symbol: sp.Symbol, shear_stress_symbol: sp.Symbol,
+                                 velocity_symbol: sp.Symbol, wall_distance, u_tau_target=None):
+        def law(u_p, y_p):
+            arctan = sp.Float(5.424) * sp.atan(sp.Float(0.119760479041916168) * y_p - sp.Float(0.488023952095808383))
+            logarithm = (sp.Float(0.434) * sp.log((y_p + sp.Float(10.6)) ** sp.Float(9.6)
+                                                  / (y_p ** 2 - sp.Float(8.15) * y_p + sp.Float(86)) ** 2, 10))
+            return (arctan + logarithm - sp.Float(3.50727901936264842)) - u_p
+
+        u_plus = velocity_symbol / self.u_tau[0]
+        y_plus = (wall_distance * self.u_tau[0]) / self.viscosity
+
+        u_tau_init = u_tau_target if u_tau_target else velocity_symbol / sp.Float(100)
+
+        wall_law = law(u_plus, y_plus)
+        assignments = [Assignment(self.u_tau[0], u_tau_init),  # initial guess
+                       *self.newton_iteration(wall_law),  # newton iterations
+                       Assignment(shear_stress_symbol, self.u_tau[-1] ** 2 * density_symbol)]  # final result
+
+        return assignments
diff --git a/src/lbmpy/flow_statistics.py b/src/lbmpy/flow_statistics.py
new file mode 100644
index 00000000..3e5b5f5f
--- /dev/null
+++ b/src/lbmpy/flow_statistics.py
@@ -0,0 +1,82 @@
+import sympy as sp
+import pystencils as ps
+
+from pystencils.field import Field
+
+
+def welford_assignments(field, mean_field, sum_of_products_field=None):
+    r"""
+    Creates the assignments for the kernel creation of Welford's algorithm
+    (https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm).
+    This algorithm is an online algorithm for the mean and variance calculation of sample data, here implemented for
+    the execution on vector fields, e.g., velocity.
+    The calculation of the variance is optional and only performed if a field for the sum of products is given.
+
+    The mean value is directly updated in the mean vector field.
+    The variance must be retrieved in a post-processing step. Let :math `M_{2,n}` denote the value of the sum of
+    products after the first :math `n` samples. According to Welford the biased sample variance
+    :math `\sigma_n^2` and the unbiased sample variance :math `s_n^2` are given by
+
+    .. math ::
+        \sigma_n^2 = \frac{M_{2,n}}{n}
+        s_n^2 = \frac{M_{2,n}}{n-1},
+
+    respectively.
+    """
+
+    if isinstance(field, Field):
+        dim = field.values_per_cell()
+        welford_field = field.center
+    elif isinstance(field, Field.Access):
+        dim = field.field.values_per_cell()
+        welford_field = field
+    else:
+        raise ValueError("Vector field has to be a pystencils Field or Field.Access")
+
+    if isinstance(mean_field, Field):
+        welford_mean_field = mean_field.center
+    elif isinstance(mean_field, Field.Access):
+        welford_mean_field = mean_field
+    else:
+        raise ValueError("Mean vector field has to be a pystencils Field or Field.Access")
+
+    if sum_of_products_field is None:
+        # sum of products will not be calculated, i.e., the variance is not retrievable
+        welford_sum_of_products_field = None
+    else:
+        if isinstance(sum_of_products_field, Field):
+            welford_sum_of_products_field = sum_of_products_field.center
+            assert sum_of_products_field.values_per_cell() == dim**2, \
+                (f"Sum of products field does not have the right layout. "
+                 f"Index dimension: {sum_of_products_field.values_per_cell()}, expected: {dim**2}")
+        elif isinstance(sum_of_products_field, Field.Access):
+            welford_sum_of_products_field = sum_of_products_field
+            assert sum_of_products_field.field.values_per_cell() == dim**2, \
+                (f"Sum of products field does not have the right layout. "
+                 f"Index dimension: {sum_of_products_field.field.values_per_cell()}, expected: {dim**2}")
+        else:
+            raise ValueError("Variance vector field has to be a pystencils Field or Field.Access")
+
+    counter = sp.Symbol('counter')
+    delta = sp.symbols(f"delta_:{dim}")
+
+    main_assignments = list()
+
+    for i in range(dim):
+        main_assignments.append(ps.Assignment(delta[i], welford_field.at_index(i) - welford_mean_field.at_index(i)))
+        main_assignments.append(ps.Assignment(welford_mean_field.at_index(i),
+                                              welford_mean_field.at_index(i) + delta[i] / counter))
+
+    if sum_of_products_field is not None:
+        delta2 = sp.symbols(f"delta2_:{dim}")
+        for i in range(dim):
+            main_assignments.append(
+                ps.Assignment(delta2[i], welford_field.at_index(i) - welford_mean_field.at_index(i)))
+        for i in range(dim):
+            for j in range(dim):
+                idx = i * dim + j
+                main_assignments.append(ps.Assignment(
+                    welford_sum_of_products_field.at_index(idx),
+                    welford_sum_of_products_field.at_index(idx) + delta[i] * delta2[j]))
+
+    return main_assignments
diff --git a/tests/test_wall_function_bounce.py b/tests/test_wall_function_bounce.py
new file mode 100644
index 00000000..9eeeb1f7
--- /dev/null
+++ b/tests/test_wall_function_bounce.py
@@ -0,0 +1,116 @@
+import pytest
+
+import pystencils as ps
+from lbmpy import Stencil, LBStencil, LBMConfig, Method, lattice_viscosity_from_relaxation_rate, \
+    LatticeBoltzmannStep, pdf_initialization_assignments, ForceModel
+from lbmpy.boundaries.boundaryconditions import UBB, WallFunctionBounce, NoSlip, FreeSlip
+from lbmpy.boundaries.wall_function_models import SpaldingsLaw, LogLaw, MoninObukhovSimilarityTheory, MuskerLaw
+from pystencils.slicing import slice_from_direction
+
+
+@pytest.mark.parametrize('stencil', [Stencil.D3Q19, Stencil.D3Q27])
+@pytest.mark.parametrize('wfb_type', ['wfb_i', 'wfb_ii', 'wfb_iii', 'wfb_iv'])
+def test_wfb(stencil, wfb_type):
+    stencil = LBStencil(stencil)
+    periodicity = (True, False, True)
+    domain_size = (30, 20, 15)
+    dim = len(domain_size)
+
+    omega = 1.1
+    nu = lattice_viscosity_from_relaxation_rate(omega)
+
+    # pressure gradient for laminar channel flow with u_max = 0.1
+    ext_force_density = 0.1 * 2 * nu / ((domain_size[1] - 2) / 2) ** 2
+
+    lbm_config = LBMConfig(stencil=stencil,
+                           method=Method.SRT,
+                           force_model=ForceModel.GUO,
+                           force=(ext_force_density, 0, 0),
+                           relaxation_rate=omega,
+                           compressible=True)
+
+    wall_north = NoSlip()
+    normal = (0, 1, 0)
+
+    # NO-SLIP
+
+    lb_step_noslip = LatticeBoltzmannStep(domain_size=domain_size, periodicity=periodicity,
+                                          lbm_config=lbm_config, compute_velocity_in_every_step=True)
+
+    noslip = NoSlip()
+
+    lb_step_noslip.boundary_handling.set_boundary(noslip, slice_from_direction('S', dim))
+    lb_step_noslip.boundary_handling.set_boundary(wall_north, slice_from_direction('N', dim))
+
+    # FREE-SLIP
+
+    lb_step_freeslip = LatticeBoltzmannStep(domain_size=domain_size, periodicity=periodicity,
+                                            lbm_config=lbm_config, compute_velocity_in_every_step=True)
+
+    freeslip = FreeSlip(stencil=stencil, normal_direction=normal)
+
+    lb_step_freeslip.boundary_handling.set_boundary(freeslip, slice_from_direction('S', dim))
+    lb_step_freeslip.boundary_handling.set_boundary(wall_north, slice_from_direction('N', dim))
+
+    # WFB
+
+    lb_step_wfb = LatticeBoltzmannStep(domain_size=domain_size, periodicity=periodicity,
+                                       lbm_config=lbm_config, compute_velocity_in_every_step=True)
+
+    # pdf initialisation
+
+    init = pdf_initialization_assignments(lb_step_wfb.method, 1.0, (0.025, 0, 0),
+                                          lb_step_wfb.data_handling.fields[lb_step_wfb._pdf_arr_name].center_vector)
+
+    config = ps.CreateKernelConfig(target=lb_step_wfb.data_handling.default_target, cpu_openmp=False)
+    ast_init = ps.create_kernel(init, config=config)
+    kernel_init = ast_init.compile()
+
+    lb_step_wfb.data_handling.run_kernel(kernel_init)
+
+    # potential mean velocity field
+
+    mean_vel_field = lb_step_wfb.data_handling.fields[lb_step_wfb.velocity_data_name]
+    # mean_vel_field = lb_step_wfb.data_handling.add_array('mean_velocity_field', values_per_cell=stencil.D)
+    # lb_step_wfb.data_handling.fill('mean_velocity_field', 0.005, value_idx=0, ghost_layers=True)
+    lb_step_wfb.data_handling.fill(lb_step_wfb.velocity_data_name, 0.025, value_idx=0, ghost_layers=True)
+
+    # wfb arguments
+    wfb_args = {
+        'wfb_i': {'wall_function_model': SpaldingsLaw(viscosity=nu),
+                  'weight_method': WallFunctionBounce.WeightMethod.GEOMETRIC_WEIGHT,
+                  'name': "wall"},
+        'wfb_ii': {'wall_function_model': MuskerLaw(viscosity=nu),
+                   'weight_method': WallFunctionBounce.WeightMethod.GEOMETRIC_WEIGHT,
+                   'mean_velocity': mean_vel_field,
+                   'name': "wall"},
+        'wfb_iii': {'wall_function_model': LogLaw(viscosity=nu),
+                    'weight_method': WallFunctionBounce.WeightMethod.LATTICE_WEIGHT,
+                    'mean_velocity': mean_vel_field.center,
+                    'sampling_shift': 2},
+        'wfb_iv': {'wall_function_model': MoninObukhovSimilarityTheory(z0=1e-2),
+                   'weight_method': WallFunctionBounce.WeightMethod.LATTICE_WEIGHT,
+                   'mean_velocity': mean_vel_field,
+                   'maronga_sampling_shift': 2}
+    }
+
+    wall = WallFunctionBounce(lb_method=lb_step_wfb.method, normal_direction=normal,
+                              pdfs=lb_step_wfb.data_handling.fields[lb_step_wfb._pdf_arr_name],
+                              **wfb_args[wfb_type])
+
+    lb_step_wfb.boundary_handling.set_boundary(wall, slice_from_direction('S', dim))
+    lb_step_wfb.boundary_handling.set_boundary(wall_north, slice_from_direction('N', dim))
+
+    # rum cases
+
+    timesteps = 4000
+    lb_step_noslip.run(timesteps)
+    lb_step_freeslip.run(timesteps)
+    lb_step_wfb.run(timesteps)
+
+    noslip_velocity = lb_step_noslip.velocity[domain_size[0] // 2, :, domain_size[2] // 2, 0]
+    freeslip_velocity = lb_step_freeslip.velocity[domain_size[0] // 2, :, domain_size[2] // 2, 0]
+    wfb_velocity = lb_step_wfb.velocity[domain_size[0] // 2, :, domain_size[2] // 2, 0]
+
+    assert wfb_velocity[0] > noslip_velocity[0], f"WFB enforced velocity below no-slip velocity"
+    assert wfb_velocity[0] < freeslip_velocity[0], f"WFB enforced velocity above free-slip velocity"
-- 
GitLab