From 962f1134dd7eeb87236d362e1a6c634c3897c790 Mon Sep 17 00:00:00 2001
From: brendan-waters <brendan.waters@sydney.edu.au>
Date: Tue, 12 Nov 2024 17:14:59 +1100
Subject: [PATCH] iMEM progress

---
 src/lbmpy/boundaries/__init__.py             |   8 +-
 src/lbmpy/boundaries/boundaryconditions.py   | 351 +++++++++++++++++--
 src/lbmpy/boundaries/wall_function_models.py |  59 ++++
 3 files changed, 393 insertions(+), 25 deletions(-)

diff --git a/src/lbmpy/boundaries/__init__.py b/src/lbmpy/boundaries/__init__.py
index e522740..8814eed 100644
--- a/src/lbmpy/boundaries/__init__.py
+++ b/src/lbmpy/boundaries/__init__.py
@@ -1,12 +1,12 @@
 from lbmpy.boundaries.boundaryconditions import (
-    UBB, FixedDensity, DiffusionDirichlet, SimpleExtrapolationOutflow, WallFunctionBounce,
+    UBB, FixedDensity, DiffusionDirichlet, SimpleExtrapolationOutflow, WallFunctionBounce, iMEMBounceBack,
     ExtrapolationOutflow, NeumannByCopy, NoSlip, NoSlipLinearBouzidi, QuadraticBounceBack, StreamInConstant, FreeSlip)
 from lbmpy.boundaries.boundaryhandling import LatticeBoltzmannBoundaryHandling
-from lbmpy.boundaries.wall_function_models import MoninObukhovSimilarityTheory, LogLaw, MuskerLaw, SpaldingsLaw
+from lbmpy.boundaries.wall_function_models import MoninObukhovSimilarityTheory, LogLaw, MuskerLaw, ExplicitMuskerLaw, SpaldingsLaw
 
-__all__ = ['NoSlip', 'NoSlipLinearBouzidi', 'QuadraticBounceBack', 'FreeSlip', 'WallFunctionBounce',
+__all__ = ['NoSlip', 'NoSlipLinearBouzidi', 'QuadraticBounceBack', 'FreeSlip', 'WallFunctionBounce','iMEMBounceBack',
            'UBB', 'FixedDensity',
            'SimpleExtrapolationOutflow', 'ExtrapolationOutflow',
            'DiffusionDirichlet', 'NeumannByCopy', 'StreamInConstant',
            'LatticeBoltzmannBoundaryHandling',
-           'MoninObukhovSimilarityTheory', 'LogLaw', 'MuskerLaw', 'SpaldingsLaw']
+           'MoninObukhovSimilarityTheory', 'LogLaw', 'MuskerLaw', 'ExplicitMuskerLaw', 'SpaldingsLaw']
diff --git a/src/lbmpy/boundaries/boundaryconditions.py b/src/lbmpy/boundaries/boundaryconditions.py
index d1c67eb..457aa51 100644
--- a/src/lbmpy/boundaries/boundaryconditions.py
+++ b/src/lbmpy/boundaries/boundaryconditions.py
@@ -240,7 +240,7 @@ class QuadraticBounceBack(LbBoundary):
         self.init_wall_distance = init_wall_distance
         self.equilibrium_values_name = "f_eq"
         self.inv_dir_symbol = TypedSymbol("inv_dir", create_type("int32"))
-
+        self.unique_id = "_out" # Needed for iMEM has no effect on QBB 
         super(QuadraticBounceBack, self).__init__(name, calculate_force_on_boundary)
 
     @property
@@ -299,31 +299,24 @@ class QuadraticBounceBack(LbBoundary):
         pdf_field_accesses = [f_out(i) for i in range(len(lb_method.stencil))]
 
         pdf_symbols = [sp.Symbol(f"pdf_{i}") for i in range(len(lb_method.stencil))]
-        f_xf = sp.Symbol("f_xf")
-        f_xf_inv = sp.Symbol("f_xf_inv")
-        q = sp.Symbol("q")
-        feq = sp.Symbol("f_eq")
-        weight = sp.Symbol("w")
-        weight_inv = sp.Symbol("w_inv")
-        v = [TypedSymbol(f"c_{i}", self.data_type) for i in range(lb_method.stencil.D)]
-        v_inv = [TypedSymbol(f"c_inv_{i}", self.data_type) for i in range(lb_method.stencil.D)]
+        
+        f_xf = f_out(dir_symbol)
+        f_xf_inv = f_out(inv_dir[dir_symbol])
+        
+        q = sp.Symbol(f"q")
+        feq = sp.Symbol(f"f_eq{self.unique_id}")
+        
+        weight = weight_of_direction(dir_symbol, lb_method)
+        weight_inv = weight_of_direction(inv, lb_method)
+
         one = sp.Float(1.0)
         half = sp.Rational(1, 2)
 
         subexpressions = [Assignment(pdf_symbols[i], pdf) for i, pdf in enumerate(pdf_field_accesses)]
-        subexpressions.append(Assignment(f_xf, f_out(dir_symbol)))
-        subexpressions.append(Assignment(f_xf_inv, f_out(inv_dir[dir_symbol])))
         subexpressions.append(Assignment(q, index_field[0]('q')))
-        subexpressions.append(Assignment(weight, weight_of_direction(dir_symbol, lb_method)))
-        subexpressions.append(Assignment(weight_inv, weight_of_direction(inv, lb_method)))
 
-        for i in range(lb_method.stencil.D):
-            offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
-            subexpressions.append(Assignment(v[i], offset[i]))
-
-        for i in range(lb_method.stencil.D):
-            offset = NeighbourOffsetArrays.neighbour_offset(inv, lb_method.stencil)
-            subexpressions.append(Assignment(v_inv[i], offset[i]))
+        v =  NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
+        v_inv = NeighbourOffsetArrays.neighbour_offset(inv, lb_method.stencil)
 
         cqc = lb_method.conserved_quantity_computation
         rho = cqc.density_symbol
@@ -683,7 +676,7 @@ class WallFunctionBounce(LbBoundary):
         # 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),
+            wall_distance=(wall_distance + sp.Rational(1, 2) * self.dy),
             u_tau_target=self.target_friction_velocity)
         result.append(wall_law_assignments)
 
@@ -721,6 +714,322 @@ class WallFunctionBounce(LbBoundary):
 # end class WallFunctionBounce
 
 
+class iMEMBounceBack(LbBoundary):
+    """
+    Wall function based on the bounce back idea, cf. :cite:`Asmuth2021`.
+
+    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.
+        dt: time discretisation.  Usually one in LB units
+        dx: space discretisation. Usually one in LB units (assumes constant in x,y,z i.e. dx=dy=dz)
+
+        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.
+        name: Optional name of the boundary.
+        data_type: Floating-point precision. Per default, double.
+    """
+
+    def __init__(self, lb_method, pdfs, normal_direction, wall_function_model,
+                 inst_velocity=None, clipped_flag=None, mean_velocity=None, sampling_shift=1, dt=1, dx=1,
+                 target_friction_velocity=None, BBObject=None,
+                 name=None, data_type='double'):
+        
+        self.stencil = lb_method.stencil
+
+        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 isinstance(inst_velocity, Field):
+            self.inst_velocity = inst_velocity.center_vector
+        elif isinstance(inst_velocity, Field.Access):
+            self.inst_velocity = inst_velocity.field.neighbor_vector(inst_velocity.offsets)
+
+        if isinstance(clipped_flag, Field):
+            self.clipped_flag = clipped_flag.center_vector
+        elif isinstance(clipped_flag, Field.Access):
+            self.clipped_flag = clipped_flag.field.neighbor_vector(clipped_flag.offsets)
+
+        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
+        
+        self.dt = dt
+        self.dx = dx
+
+        self.data_type = data_type
+
+        self.target_friction_velocity = target_friction_velocity
+
+        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.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 = list(range(len(self.normal_direction)))
+
+        self.tangential_axis.remove(self.normal_axis)
+
+        self.dim = self.stencil.D
+
+        self.inv_dir_symbol = TypedSymbol("inv_dir", create_type("int32"))
+
+        self.BounceBack = BBObject if BBObject else NoSlip()
+
+        if name is None:
+            name = f"iMEM : {offset_to_direction_string([-x for x in normal_direction])}"
+
+        super(iMEMBounceBack, self).__init__(name, calculate_force_on_boundary=False)
+
+    @property
+    def additional_data(self):
+        """Used internally only. For the NoSlipLinearBouzidi boundary the distance to the obstacle of every
+        direction is needed. This information is stored in the index vector."""
+        return [('q', create_type(self.data_type))]
+
+    def get_additional_code_nodes(self, lb_method):
+        inv_directions = [str(self.stencil.index(inverse_direction(direction))) for direction in self.stencil]
+        dtype = self.inv_dir_symbol.dtype
+        name = self.inv_dir_symbol.name
+  
+        return [NeighbourOffsetArrays(self.stencil),
+                LbmWeightInfo(lb_method, self.data_type),
+                TranslationArraysNode([(dtype, name, inv_directions), ], {self.inv_dir_symbol})]
+
+    def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
+    ## Step 0: Needed symbols for offsets and indices
+        # neighbour offset symbols are the stencil directions defined in stencils.py:L130ff.
+        neighbor_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, self.stencil)
+
+        weight_info = LbmWeightInfo(lb_method, data_type=self.data_type)
+        weight_of_direction = weight_info.weight_of_direction
+
+        u_m = sp.Symbol("u_m")
+        u_m_w = sp.Symbol("u_m_w")
+        tau_w = sp.Symbol("tau_w")
+        wall_stress = sp.symbols("tau_w_x tau_w_y tau_w_z")
+
+        cqc = lb_method.conserved_quantity_computation
+        
+        result = []
+
+     
+    # Step 2: Compute Bounce-Back pdf's
+        c_f_ijk = self.stencil.stencil_entries
+
+        pdfs_in_wall_dir = []
+        for f in range(self.stencil.Q):
+            if c_f_ijk[f][self.normal_axis] == - self.normal_direction[self.normal_axis]:
+                pdfs_in_wall_dir.append(f)
+                print(f, c_f_ijk[f], c_f_ijk[f][self.normal_axis])
+
+        pdf_symbols        = [sp.Symbol(f"pdf_{i}") for i in range(len(lb_method.stencil))]
+        pdf_field_accesses = [f_out(i) for i in range(len(lb_method.stencil))]
+        result += [Assignment(pdf_symbols[i], pdf) for i, pdf in enumerate(pdf_field_accesses)]
+        
+        f_ijk_field_accesses = [pdf_symbols[i] for i in pdfs_in_wall_dir]
+        
+        f_ijk_bb_field_accesses = [sp.Symbol(f"f_bb_{i}") for i in pdfs_in_wall_dir]
+
+        f_ijk_bb_mainexpressions = []
+        f_ijk_bb_subexpressions = []
+
+        unique_lhs = set( pdf_symbols )
+
+        for f_bb, i in zip(f_ijk_bb_field_accesses, pdfs_in_wall_dir):
+            self.BounceBack.unique_id = i
+            bb_rule = self.BounceBack(f_out, f_in, i, inv_dir, lb_method, index_field, None)
+            f_ijk_bb_mainexpressions.append( Assignment(f_bb, bb_rule.main_assignments[0].rhs ))
+            
+            for subexpressions in bb_rule.subexpressions:
+                if subexpressions.lhs not in unique_lhs:
+                    unique_lhs.add( subexpressions.lhs )
+                    f_ijk_bb_subexpressions.append(subexpressions)
+        
+        result += f_ijk_bb_subexpressions
+        result += f_ijk_bb_mainexpressions
+        
+        print(f_ijk_field_accesses)
+        print(f_ijk_bb_field_accesses)
+
+    # Step 1: Compute u_tau using reference point
+        # Adjusted normal direction for 2D case
+        normal_direction = (list(self.normal_direction) + [0])[:3]
+
+        if (not self.mean_velocity):
+            pdf_center_vector = sp.Matrix([0] * self.stencil.Q)
+
+            for i in range(self.stencil.Q):
+                pdf_center_vector[i] = self.pdfs[neighbor_offset[0] + normal_direction[0],
+                                                 neighbor_offset[1] + normal_direction[1],
+                                                 neighbor_offset[2] + normal_direction[2]](i)
+
+            eq_equations = cqc.equilibrium_input_equations_from_pdfs(pdf_center_vector)
+            result.append(eq_equations.all_assignments)
+
+        # reference velocity for wall stress calculation
+        if self.mean_velocity:
+            u_for_tau_wall = tuple(u_mean_i.get_shifted(
+                self.sampling_shift * normal_direction[0],
+                self.sampling_shift * normal_direction[1],
+                self.sampling_shift * 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
+
+        u_mag = Assignment(u_m, sp.sqrt(sum([u_for_tau_wall[i]**2 for i in self.tangential_axis])))
+        result.append(u_mag)
+
+        # Assumes wall is at distace 0.5 from the fluid cell center
+        wall_distance = (self.sampling_shift + sp.Rational(1, 2) * self.dx)
+
+        # Call wall model instance to get shear stress
+        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,
+            u_tau_target=self.target_friction_velocity)
+        result += wall_law_assignments
+
+        # for i in self.tangential_axis:
+        #     result.append(Assignment(wall_stress[i], u_for_tau_wall[i] / u_m * tau_w ))
+
+        # Could also be:
+        u_norm = sp.Symbol("u_norm")
+        u_mag = Assignment(u_norm, sp.sqrt(sum([self.mean_velocity[i]**2 for i in self.tangential_axis])))
+        result.append(u_mag)
+        for i in self.tangential_axis:
+            result.append(Assignment(wall_stress[i], cqc.velocity_symbols[i] / u_norm * tau_w ))
+
+    # Step 3: Compute F_f and F_wm eqs. 24 and 27
+        # F_fluid
+        F_f = sp.symbols("F_fx F_fy F_fz")
+
+        # Δx^2 for 2D or Δx^3 for 3D, see Kruger, 2016, 10.1007/978-3-319-44649-3 Eqs. 5.79-5.80
+        coeff = (self.dx ** self.stencil.D) / self.dt 
+        
+        # Eq. 24 in Asmuth2021:
+        result += [ Assignment( F_f[j], coeff * sum( c_f_ijk[i][j] * 
+                                                        (f_ijk_field_accesses[idx] + f_ijk_bb_field_accesses[idx]) 
+                                                        for idx, i in enumerate(pdfs_in_wall_dir) )
+                              ) for j in self.tangential_axis
+                  ]
+
+        # F_wm
+        # Eq. 27 in Asmuth2021:
+        F_wm = sp.symbols("F_wm_x F_wm_y F_wm_z")
+        result += [Assignment( F_wm[i], wall_stress[i] * self.dx ** 2 ) for i in self.tangential_axis]
+
+        # F_uw 
+        # Eq. 26 in Asmuth2021:
+        F_uw = sp.symbols("F_uvw_x F_uvw_y F_uvw_z")
+        result += [Assignment( F_uw[i], F_wm[i] - F_f[i] ) for i in self.tangential_axis]
+
+    # Step 4: Compute u_w 
+        wi = lb_method.weights
+        wall_velocity = sp.symbols("u_wx u_wy u_wz")
+
+        c_s_sq = sp.Rational(1, 3)
+
+        dx4_on_dt2 = coeff * ( self.dx / self.dt )
+
+        rho = cqc.density_symbol if cqc.compressible else 1
+
+        # rho = 1
+        velocity_expression = [ dx4_on_dt2 * sum( c_f_ijk[i][k] * 
+                                        (- 2 * wi[i] * rho * sum( wall_velocity[j] * c_f_ijk[i][j] for j in range(self.stencil.D)
+                                                              ) / c_s_sq) for i in pdfs_in_wall_dir
+                                    ) for k in range(self.stencil.D) ]
+
+        result += [ Assignment( wall_velocity[i], sp.solve(velocity_expression[i] - F_uw[i], wall_velocity[i])[0] ) for i in self.tangential_axis]
+        result.append(Assignment( wall_velocity[self.normal_axis], 0 )) 
+        
+        clipped_velocity = sp.symbols("clip_u_wx clip_u_wy clip_u_wz")
+
+        u_for_clipping = tuple( 0.5 * u_mean_i.get_shifted( normal_direction[0], normal_direction[1], normal_direction[2]
+            ) for u_mean_i in self.inst_velocity)
+
+        result += [ Assignment( clipped_velocity[i],  
+                                    sp.Piecewise((sp.Min(u_for_clipping[i], sp.Max(-u_for_clipping[i], wall_velocity[i])), sp.Gt(u_for_clipping[i], -u_for_clipping[i])),
+                                                 (sp.Max(u_for_clipping[i], sp.Min(-u_for_clipping[i], wall_velocity[i])), True))
+                              ) for i in self.tangential_axis]  
+        result.append(Assignment( clipped_velocity[self.normal_axis], wall_velocity[self.normal_axis] )) 
+
+        #! NOTE: For debugging only ... so we can see if clipping is being used or not
+        result += [ Assignment( self.clipped_flag[i],  clipped_velocity[i] - wall_velocity[i]) for i in range(self.stencil.D)]           
+     
+    # # Step 5: Add f_ijk_uw and f_ijk_bb 
+        # Standard NoSlip Bounce-back
+        self.BounceBack.unique_id = "_out"
+        bb_rule = self.BounceBack(f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, None)
+        
+        for subexpressions in bb_rule.subexpressions:
+            if subexpressions.lhs not in unique_lhs:
+                unique_lhs.add( subexpressions.lhs )
+                result.append(subexpressions)
+        
+        f_ijk_bb = bb_rule.main_assignments[0].rhs
+
+        # velocity = [v_i for v_i in wall_velocity]
+        velocity = [v_i for v_i in clipped_velocity]
+        shifted_vel_eqs = cqc.equilibrium_input_equations_from_init_values(velocity=velocity)
+        shifted_vel_eqs = shifted_vel_eqs.new_without_subexpressions()
+        velocity = [eq.rhs for eq in shifted_vel_eqs.new_filtered(cqc.velocity_symbols).main_assignments]
+
+        weight_info = LbmWeightInfo(lb_method, data_type=self.data_type)
+        weight_of_direction = weight_info.weight_of_direction
+        f_ijk_uw = 2 / c_s_sq * rho * sum([d_i * v_i for d_i, v_i in zip(neighbor_offset, velocity)]) * weight_of_direction(
+            dir_symbol, lb_method)
+
+    # Step 6: Return Assignment
+        # Eq. 20 in Asmuth2021: Note, eq. 20 should not have the 2x on LHS and should be - not +
+        result.append(Assignment(f_in(inv_dir[dir_symbol]),  f_ijk_bb - f_ijk_uw) )
+
+        # for ass in result:
+        #     print("")
+        #     print(ass)
+
+        return result
+
+# end class iMEMBounceBack
+
+
+
 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
index 4f62778..44ef439 100644
--- a/src/lbmpy/boundaries/wall_function_models.py
+++ b/src/lbmpy/boundaries/wall_function_models.py
@@ -52,6 +52,65 @@ class MoninObukhovSimilarityTheory(ExplicitWallFunctionModel):
         return [Assignment(shear_stress_symbol, u_tau ** 2 * density_symbol)]
 
 
+class ExplicitMuskerLaw(ExplicitWallFunctionModel):
+    def __init__(self, viscosity, kappa=0.41, name="Explicit Musker"):
+        self.viscosity = viscosity
+        self.kappa = kappa
+        
+        super(ExplicitMuskerLaw, 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):
+        
+        # Constants
+        c = 0.23
+        s = 8.347e-4
+        
+        one_third = sp.Float(1/3)
+
+        l, a, alpha, beta, beta_sq, u = sp.symbols('l a alpha beta beta_sq u')
+        u_tau = sp.Symbol("eval_utau")
+
+        u_tau_init = u_tau_target if u_tau_target else velocity_symbol / sp.Float(100)
+
+        u_plus = velocity_symbol / u_tau_init
+        y_plus = (wall_distance * u_tau_init) / self.viscosity
+
+        r_plus = u_plus * y_plus
+
+        sqrt_r_plus = sp.sqrt( r_plus )
+        
+        log_arg = sp.Symbol("log_arg")
+        log_law = (1/self.kappa) * sp.ln( ( sqrt_r_plus + a ) / a )
+        
+        coeff = sp.Symbol("coeff")
+        coeff_exp = alpha / ( a + 4 * alpha ) 
+        
+        first_arg = sp.Symbol("first_arg")
+        first_arg_exp = ( a - 4 * alpha ) * sp.ln( ( a * ((sqrt_r_plus - alpha) ** 2 + beta_sq )) / ( 2 * alpha * ( sqrt_r_plus + a ) **2 ))
+        
+        second_arg = sp.Symbol("second_arg")
+        second_arg_exp = ( 2 * alpha * (5*a - 4*alpha) / beta ) * ( sp.atan( (sqrt_r_plus - alpha)/beta ) + sp.atan( alpha/beta ) ) 
+
+        u_p = log_arg + coeff * ( first_arg + second_arg_exp )
+
+        assignments = [Assignment(l, (( 0.5 * sp.sqrt( ( 4*s + 27*c**3) / (27*c) ) / (c*s) ) + \
+                                      (( 2*s + 27*c**3 ) / ( 54*s*c**3)))**one_third ),
+                       Assignment(a,          l + (1 / (9 * l * c**2)) + (1 / (3*c)) ),
+                       Assignment(alpha,      0.5*(a - 1/c )),
+                       Assignment(beta_sq,    2*a*alpha - alpha**2  ),
+                       Assignment(beta,       sp.sqrt( beta_sq )  ),
+                       Assignment(log_arg,    log_law ),
+                       Assignment(coeff,      coeff_exp ),
+                       Assignment(first_arg,  first_arg_exp ),
+                       Assignment(second_arg, second_arg_exp ),
+                       Assignment(u, u_p ),
+                       Assignment(u_tau, velocity_symbol / u ),
+                       Assignment(shear_stress_symbol, u_tau ** 2 * density_symbol)]  
+
+        return assignments
+
+
 class ImplicitWallFunctionModel(WallFunctionModel, ABC):
     """
     Abstract base class for implicit wall functions that require a Newton procedure to solve for the wall shear stress.
-- 
GitLab