Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Sparse
  • WallLaw
  • improved_comm
  • master
  • mr_refactor_wfb
  • suffa/cumulantfourth_order_correction_with_psm
  • release/0.2.1
  • release/0.2.10
  • release/0.2.11
  • release/0.2.12
  • release/0.2.13
  • release/0.2.14
  • release/0.2.15
  • release/0.2.2
  • release/0.2.3
  • release/0.2.4
  • release/0.2.5
  • release/0.2.6
  • release/0.2.7
  • release/0.2.8
  • release/0.2.9
  • release/0.3.0
  • release/0.3.1
  • release/0.3.2
  • release/0.3.3
  • release/0.3.4
  • release/0.4.0
  • release/0.4.1
  • release/0.4.2
  • release/0.4.3
  • release/0.4.4
  • release/1.0
  • release/1.0.1
  • release/1.1
  • release/1.1.1
  • release/1.2
  • release/1.3
  • release/1.3.1
  • release/1.3.2
  • release/1.3.3
  • release/1.3.4
  • release/1.3.5
  • release/1.3.6
  • release/1.3.7
44 results

Target

Select target project
  • ravi.k.ayyala/lbmpy
  • brendan-waters/lbmpy
  • anirudh.jonnalagadda/lbmpy
  • jbadwaik/lbmpy
  • alexander.reinauer/lbmpy
  • itischler/lbmpy
  • he66coqe/lbmpy
  • ev81oxyl/lbmpy
  • Bindgen/lbmpy
  • da15siwa/lbmpy
  • holzer/lbmpy
  • RudolfWeeber/lbmpy
  • pycodegen/lbmpy
13 results
Select Git revision
Show changes
Showing
with 3836 additions and 81 deletions
import sympy as sp import sympy as sp
from lbmpy.boundaries.boundaryhandling import LbmWeightInfo from .._compat import IS_PYSTENCILS_2
from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing
from lbmpy.advanced_streaming.utility import Timestep, get_accessor from lbmpy.advanced_streaming.utility import Timestep, get_accessor
from pystencils.boundaries.boundaryhandling import BoundaryOffsetInfo from pystencils.boundaries.boundaryhandling import BoundaryOffsetInfo
from pystencils.assignment import Assignment from pystencils import Assignment
from pystencils.astnodes import Block, Conditional, LoopOverCoordinate, SympyAssignment from pystencils.simp import AssignmentCollection, sympy_cse_on_assignment_list
from pystencils.data_types import type_all_numbers
from pystencils.simp.assignment_collection import AssignmentCollection
from pystencils.simp.simplifications import sympy_cse_on_assignment_list
from pystencils.stencil import inverse_direction from pystencils.stencil import inverse_direction
from pystencils.sympyextensions import fast_subs from pystencils.sympyextensions import fast_subs
if IS_PYSTENCILS_2:
from lbmpy.lookup_tables import LbmWeightInfo
else:
from lbmpy.custom_code_nodes import LbmWeightInfo
from pystencils.astnodes import Block, Conditional, LoopOverCoordinate, SympyAssignment # TODO replace
def direction_indices_in_direction(direction, stencil): def direction_indices_in_direction(direction, stencil):
for i, offset in enumerate(stencil): for i, offset in enumerate(stencil):
...@@ -48,17 +52,20 @@ def border_conditions(direction, field, ghost_layers=1): ...@@ -48,17 +52,20 @@ def border_conditions(direction, field, ghost_layers=1):
border_condition = sp.Eq(loop_ctr, gl if val < 0 else field.shape[idx] - gl - 1) border_condition = sp.Eq(loop_ctr, gl if val < 0 else field.shape[idx] - gl - 1)
if ghost_layers == 0: if ghost_layers == 0:
return type_all_numbers(border_condition, loop_ctr.dtype) return border_condition
else: else:
other_min = [sp.Ge(c, gl) other_min = [sp.Ge(c, gl)
for c in loop_ctrs if c != loop_ctr] for c in loop_ctrs if c != loop_ctr]
other_max = [sp.Lt(c, field.shape[i] - gl) other_max = [sp.Lt(c, field.shape[i] - gl)
for i, c in enumerate(loop_ctrs) if c != loop_ctr] for i, c in enumerate(loop_ctrs) if c != loop_ctr]
result = sp.And(border_condition, *other_min, *other_max) result = sp.And(border_condition, *other_min, *other_max)
return type_all_numbers(result, loop_ctr.dtype) return result
def boundary_conditional(boundary, direction, streaming_pattern, prev_timestep, lb_method, output_field, cse=False): def boundary_conditional(boundary, direction, streaming_pattern, prev_timestep, lb_method, output_field, cse=False):
if IS_PYSTENCILS_2:
raise NotImplementedError("In-Kernel Boundaries are not yet available on pystencils 2.0")
stencil = lb_method.stencil stencil = lb_method.stencil
dir_indices = direction_indices_in_direction(direction, stencil) dir_indices = direction_indices_in_direction(direction, stencil)
...@@ -68,7 +75,7 @@ def boundary_conditional(boundary, direction, streaming_pattern, prev_timestep, ...@@ -68,7 +75,7 @@ def boundary_conditional(boundary, direction, streaming_pattern, prev_timestep,
assignments = [] assignments = []
for direction_idx in dir_indices: for direction_idx in dir_indices:
rule = boundary(f_out, f_in, direction_idx, inv_dir, lb_method, index_field=None) rule = boundary(f_out, f_in, direction_idx, inv_dir, lb_method, index_field=None, force_vector=None)
# rhs: replace f_out by post collision symbols. # rhs: replace f_out by post collision symbols.
rhs_substitutions = {f_out(i): sym for i, sym in enumerate(lb_method.post_collision_pdf_symbols)} rhs_substitutions = {f_out(i): sym for i, sym in enumerate(lb_method.post_collision_pdf_symbols)}
......
import abc
from enum import Enum, auto
from warnings import warn
from pystencils import Assignment, AssignmentCollection, Field, TypedSymbol
from pystencils.stencil import offset_to_direction_string, direction_string_to_offset, inverse_direction
from pystencils.sympyextensions import get_symmetric_part, simplify_by_equality, scalar_product
from lbmpy.advanced_streaming.utility import AccessPdfValues, Timestep from lbmpy.advanced_streaming.utility import AccessPdfValues, Timestep
from pystencils.simp.assignment_collection import AssignmentCollection from lbmpy.maxwellian_equilibrium import discrete_equilibrium
from pystencils import Assignment, Field
from lbmpy.boundaries.boundaryhandling import LbmWeightInfo
from pystencils.data_types import create_type
from pystencils.sympyextensions import get_symmetric_part
from lbmpy.simplificationfactory import create_simplification_strategy from lbmpy.simplificationfactory import create_simplification_strategy
from lbmpy.advanced_streaming.indexing import NeighbourOffsetArrays
from pystencils.stencil import offset_to_direction_string, direction_string_to_offset, inverse_direction
import sympy as sp import sympy as sp
import numpy as np
class LbBoundary: from .._compat import IS_PYSTENCILS_2
if IS_PYSTENCILS_2:
from pystencils import create_type
from pystencils.sympyextensions.typed_sympy import CastFunc
from pystencils.types.quick import Arr
from lbmpy.lookup_tables import (
NeighbourOffsetArrays,
MirroredStencilDirections,
LbmWeightInfo,
TranslationArraysNode
)
else:
from pystencils.typing import create_type, CastFunc
from lbmpy.custom_code_nodes import (
NeighbourOffsetArrays,
MirroredStencilDirections,
LbmWeightInfo,
TranslationArraysNode
)
class LbBoundary(abc.ABC):
"""Base class that all boundaries should derive from. """Base class that all boundaries should derive from.
Args: Args:
...@@ -21,10 +45,11 @@ class LbBoundary: ...@@ -21,10 +45,11 @@ class LbBoundary:
inner_or_boundary = True inner_or_boundary = True
single_link = False single_link = False
def __init__(self, name=None): def __init__(self, name=None, calculate_force_on_boundary=False):
self._name = name self._name = name
self.calculate_force_on_boundary = calculate_force_on_boundary
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
""" """
This function defines the boundary behavior and must therefore be implemented by all boundaries. This function defines the boundary behavior and must therefore be implemented by all boundaries.
The boundary is defined through a list of sympy equations from which a boundary kernel is generated. The boundary is defined through a list of sympy equations from which a boundary kernel is generated.
...@@ -41,6 +66,8 @@ class LbBoundary: ...@@ -41,6 +66,8 @@ class LbBoundary:
lb_method: an instance of the LB method used. Use this to adapt the boundary to the method lb_method: an instance of the LB method used. Use this to adapt the boundary to the method
(e.g. compressibility) (e.g. compressibility)
index_field: the boundary index field that can be used to retrieve and update boundary data index_field: the boundary index field that can be used to retrieve and update boundary data
force_vector: vector to store the force on the boundary. Has the same size as the index field and
D-entries per cell
Returns: Returns:
list of pystencils assignments, or pystencils.AssignmentCollection list of pystencils assignments, or pystencils.AssignmentCollection
...@@ -92,31 +119,674 @@ class LbBoundary: ...@@ -92,31 +119,674 @@ class LbBoundary:
class NoSlip(LbBoundary): class NoSlip(LbBoundary):
""" r"""
No-Slip, (half-way) simple bounce back boundary condition, enforcing zero velocity at obstacle. No-Slip, (half-way) simple bounce back boundary condition, enforcing zero velocity at obstacle.
Extended for use with any streaming pattern. Populations leaving the boundary node :math:`\mathbf{x}_b` at time :math:`t` are reflected
back with :math:`\mathbf{c}_{\overline{i}} = -\mathbf{c}_{i}`
.. math ::
f_{\overline{i}}(\mathbf{x}_b, t + \Delta t) = f^{\star}_{i}(\mathbf{x}_b, t)
Args:
name: optional name of the boundary.
calculate_force_on_boundary: stores the force for each PDF at the boundary in a force vector
"""
def __init__(self, name=None, calculate_force_on_boundary=False):
"""Set an optional name here, to mark boundaries, for example for force evaluations"""
super(NoSlip, self).__init__(name, calculate_force_on_boundary)
def get_additional_code_nodes(self, lb_method):
if self.calculate_force_on_boundary:
return [NeighbourOffsetArrays(lb_method.stencil)]
else:
return []
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
if self.calculate_force_on_boundary:
force = sp.Symbol("f")
subexpressions = [Assignment(force, sp.Float(2.0) * f_out(dir_symbol))]
offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
if IS_PYSTENCILS_2:
offset = [CastFunc.as_numeric(o) for o in offset]
for i in range(lb_method.stencil.D):
subexpressions.append(Assignment(force_vector[0](f'F_{i}'), force * offset[i]))
else:
subexpressions = []
boundary_assignments = [Assignment(f_in(inv_dir[dir_symbol]), f_out(dir_symbol))]
return AssignmentCollection(boundary_assignments, subexpressions=subexpressions)
class NoSlipLinearBouzidi(LbBoundary):
"""
No-Slip, (half-way) simple bounce back boundary condition with interpolation
to increase accuracy: :cite:`BouzidiBC`. In order to make the boundary condition work properly a
Python callback function needs to be provided to calculate the distance from the wall for each cell near to the
boundary. If this is not done the boundary condition will fall back to a simple NoSlip boundary.
Furthermore, for this boundary condition a second fluid cell away from the wall is needed. If the second fluid
cell is not available (e.g. because it is marked as boundary as well), the boundary condition should fall back to
a NoSlip boundary as well.
Args:
name: optional name of the boundary.
init_wall_distance: Python callback function to calculate the wall distance for each cell near to the boundary
data_type: data type of the wall distance q
"""
def __init__(self, name=None, init_wall_distance=None, data_type='double', calculate_force_on_boundary=False):
self.data_type = data_type
self.init_wall_distance = init_wall_distance
super(NoSlipLinearBouzidi, self).__init__(name, calculate_force_on_boundary)
@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):
if self.calculate_force_on_boundary:
return [NeighbourOffsetArrays(lb_method.stencil)]
else:
return []
@property
def additional_data_init_callback(self):
def default_callback(boundary_data, **_):
for cell in boundary_data.index_array:
cell['q'] = -1
if self.init_wall_distance:
return self.init_wall_distance
else:
warn("No callback function provided to initialise the wall distance for each cell "
"(init_wall_distance=None). The boundary condition will fall back to a simple NoSlip BC")
return default_callback
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
f_xf = sp.Symbol("f_xf")
f_xf_inv = sp.Symbol("f_xf_inv")
d_x2f = sp.Symbol("d_x2f")
q = sp.Symbol("q")
one = sp.Float(1.0)
two = sp.Float(2.0)
half = sp.Rational(1, 2)
subexpressions = [Assignment(f_xf, f_out(dir_symbol)),
Assignment(f_xf_inv, f_out(inv_dir[dir_symbol])),
Assignment(d_x2f, f_in(dir_symbol)),
Assignment(q, index_field[0]('q'))]
case_one = (half * (f_xf + f_xf_inv * (two * q - one))) / q
case_two = two * q * f_xf + (one - two * q) * d_x2f
case_three = f_xf
rhs = sp.Piecewise((case_one, sp.Ge(q, 0.5)),
(case_two, sp.And(sp.Gt(q, 0), sp.Lt(q, 0.5))),
(case_three, True))
if self.calculate_force_on_boundary:
force = sp.Symbol("f")
subexpressions.append(Assignment(force, f_xf + rhs))
offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
if IS_PYSTENCILS_2:
offset = [CastFunc.as_numeric(o) for o in offset]
for i in range(lb_method.stencil.D):
subexpressions.append(Assignment(force_vector[0](f'F_{i}'), force * offset[i]))
boundary_assignments = [Assignment(f_in(inv_dir[dir_symbol]), rhs)]
return AssignmentCollection(boundary_assignments, subexpressions=subexpressions)
# end class NoSlipLinearBouzidi
class QuadraticBounceBack(LbBoundary):
"""
Second order accurate bounce back boundary condition. Implementation details are provided in a demo notebook here:
https://pycodegen.pages.i10git.cs.fau.de/lbmpy/notebooks/demo_interpolation_boundary_conditions.html
Args: Args:
relaxation_rate: relaxation rate to realise a BGK scheme for recovering the pre collision PDF value.
name: optional name of the boundary. name: optional name of the boundary.
init_wall_distance: Python callback function to calculate the wall distance for each cell near to the boundary
data_type: data type of the wall distance q
"""
def __init__(self, relaxation_rate, name=None, init_wall_distance=None, data_type='double',
calculate_force_on_boundary=False):
self.relaxation_rate = relaxation_rate
self.data_type = data_type
self.init_wall_distance = init_wall_distance
self.equilibrium_values_name = "f_eq"
super(QuadraticBounceBack, self).__init__(name, calculate_force_on_boundary)
def inv_dir_symbol(self, stencil):
if IS_PYSTENCILS_2:
return TypedSymbol("inv_dir", Arr(create_type("int32"), stencil.Q))
else:
return TypedSymbol("inv_dir", create_type("int32"))
@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))]
@property
def additional_data_init_callback(self):
def default_callback(boundary_data, **_):
for cell in boundary_data.index_array:
cell['q'] = 0.5
if self.init_wall_distance:
return self.init_wall_distance
else:
warn("No callback function provided to initialise the wall distance for each cell "
"(init_wall_distance=None). The boundary condition will fall back to a simple NoSlip BC")
return default_callback
def get_additional_code_nodes(self, lb_method):
"""Return a list of code nodes that will be added in the generated code before the index field loop.
Args:
lb_method: Lattice Boltzmann method. See :func:`lbmpy.creationfunctions.create_lb_method`
Returns:
list containing LbmWeightInfo
"""
stencil = lb_method.stencil
inv_directions = [str(stencil.index(inverse_direction(direction))) for direction in stencil]
if IS_PYSTENCILS_2:
inverse_dir_node = TranslationArraysNode([(self.inv_dir_symbol(stencil), inv_directions), ])
else:
inv_dir_symbol = self.inv_dir_symbol(stencil)
dtype = inv_dir_symbol.dtype
name = inv_dir_symbol.name
inverse_dir_node = TranslationArraysNode([(dtype, name, inv_directions), ], {inv_dir_symbol})
return [LbmWeightInfo(lb_method, self.data_type), inverse_dir_node, NeighbourOffsetArrays(lb_method.stencil)]
@staticmethod
def get_equilibrium(v, u, rho, drho, weight, compressible, zero_centered):
rho_background = sp.Integer(1)
result = discrete_equilibrium(v, u, rho, weight,
order=2, c_s_sq=sp.Rational(1, 3), compressible=compressible)
if zero_centered:
shift = discrete_equilibrium(v, [0] * len(u), rho_background, weight,
order=0, c_s_sq=sp.Rational(1, 3), compressible=False)
result = simplify_by_equality(result - shift, rho, drho, rho_background)
return result
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
omega = self.relaxation_rate
inv = sp.IndexedBase(self.inv_dir_symbol(lb_method.stencil), shape=(1,))[dir_symbol]
weight_info = LbmWeightInfo(lb_method, data_type=self.data_type)
weight_of_direction = weight_info.weight_of_direction
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)]
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)))
if IS_PYSTENCILS_2:
cast_offset = CastFunc.as_numeric
else:
def cast_offset(x):
return x
for i in range(lb_method.stencil.D):
offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
subexpressions.append(Assignment(v[i], cast_offset(offset[i])))
for i in range(lb_method.stencil.D):
offset = NeighbourOffsetArrays.neighbour_offset(inv, lb_method.stencil)
subexpressions.append(Assignment(v_inv[i], cast_offset(offset[i])))
cqc = lb_method.conserved_quantity_computation
rho = cqc.density_symbol
drho = cqc.density_deviation_symbol
u = sp.Matrix(cqc.velocity_symbols)
compressible = cqc.compressible
zero_centered = cqc.zero_centered_pdfs
cqe = cqc.equilibrium_input_equations_from_pdfs(pdf_symbols, False)
subexpressions.append(cqe.all_assignments)
eq_dir = self.get_equilibrium(v, u, rho, drho, weight, compressible, zero_centered)
eq_inv = self.get_equilibrium(v_inv, u, rho, drho, weight_inv, compressible, zero_centered)
subexpressions.append(Assignment(feq, eq_dir + eq_inv))
t1 = (f_xf - f_xf_inv + (f_xf + f_xf_inv - feq * omega) / (one - omega))
t2 = (q * (f_xf + f_xf_inv)) / (one + q)
result = (one - q) / (one + q) * t1 * half + t2
if self.calculate_force_on_boundary:
force = sp.Symbol("f")
subexpressions.append(Assignment(force, f_xf + result))
offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
if IS_PYSTENCILS_2:
offset = [CastFunc.as_numeric(o) for o in offset]
for i in range(lb_method.stencil.D):
subexpressions.append(Assignment(force_vector[0](f'F_{i}'), force * offset[i]))
boundary_assignments = [Assignment(f_in(inv_dir[dir_symbol]), result)]
return AssignmentCollection(boundary_assignments, subexpressions=subexpressions)
# end class QuadraticBounceBack
class FreeSlip(LbBoundary):
"""
Free-Slip boundary condition, which enforces a zero normal fluid velocity :math:`u_n = 0` but places no restrictions
on the tangential fluid velocity :math:`u_t`.
Args:
stencil: LBM stencil which is used for the simulation
normal_direction: optional normal direction pointing from wall to fluid.
If the Free slip boundary is applied to a certain side in the domain it is not necessary
to calculate the normal direction since it can be stated for all boundary cells.
This reduces the memory space for the index array significantly.
name: optional name of the boundary.
"""
def __init__(self, stencil, normal_direction=None, name=None):
"""Set an optional name here, to mark boundaries, for example for force evaluations"""
self.stencil = stencil
if normal_direction and len(normal_direction) - normal_direction.count(0) != 1:
raise ValueError("It is only possible to pre specify the normal direction for simple situations."
"This means if the free slip boundary is applied to a straight wall or side in the "
"simulation domain. A possible value for example would be (0, 1, 0) if the "
"free slip boundary is applied to the northern wall. For more complex situations "
"the normal direction has to be calculated for each cell. This is done when "
"the normal direction is not defined for this class")
if normal_direction:
normal_direction = tuple([int(n) for n in normal_direction])
assert all([n in [-1, 0, 1] for n in normal_direction]), \
"Only -1, 0 and 1 allowed for defining the normal direction"
self.mirror_axis = normal_direction.index(*[d for d in normal_direction if d != 0])
self.normal_direction = normal_direction
self.dim = len(stencil[0])
if name is None and normal_direction:
name = f"Free Slip : {offset_to_direction_string([-x for x in normal_direction])}"
super(FreeSlip, self).__init__(name, calculate_force_on_boundary=False)
def init_callback(self, boundary_data, **_):
if len(boundary_data.index_array) > 1e6:
warn(f"The calculation of the normal direction for each cell might take a long time, because "
f"{len(boundary_data.index_array)} cells are marked as Free Slip boundary cells. Consider specifying "
f" the normal direction beforehand, which is possible if it is equal for all cells (e.g. at a wall)")
dim = boundary_data.dim
coords = [coord for coord, _ in zip(['x', 'y', 'z'], range(dim))]
boundary_cells = set()
# get a set containing all boundary cells
for cell in boundary_data.index_array:
fluid_cell = tuple([cell[coord] for coord in coords])
direction = self.stencil[cell['dir']]
boundary_cell = tuple([i + d for i, d in zip(fluid_cell, direction)])
boundary_cells.add(boundary_cell)
for cell in boundary_data.index_array:
fluid_cell = tuple([cell[coord] for coord in coords])
direction = self.stencil[cell['dir']]
ref_direction = direction
normal_direction = [0] * dim
for i in range(dim):
sub_direction = [0] * dim
sub_direction[i] = direction[i]
test_cell = tuple([x + y for x, y in zip(fluid_cell, sub_direction)])
if test_cell in boundary_cells:
normal_direction[i] = direction[i]
ref_direction = MirroredStencilDirections.mirror_stencil(ref_direction, i)
# convex corner special case:
if all(n == 0 for n in normal_direction):
normal_direction = direction
else:
ref_direction = inverse_direction(ref_direction)
for i, cell_name in zip(range(dim), self.additional_data):
cell[cell_name[0]] = -normal_direction[i]
cell['ref_dir'] = self.stencil.index(ref_direction)
@property
def additional_data(self):
"""Used internally only. For the FreeSlip boundary the information of the normal direction for each pdf
direction is needed. This information is stored in the index vector."""
if self.normal_direction:
return []
else:
data_type = create_type('int32')
wnz = [] if self.dim == 2 else [('wnz', data_type)]
data = [('wnx', data_type), ('wny', data_type)] + wnz
return data + [('ref_dir', data_type)]
@property
def additional_data_init_callback(self):
if self.normal_direction:
return None
else:
return self.init_callback
def get_additional_code_nodes(self, lb_method):
if self.normal_direction:
return [MirroredStencilDirections(self.stencil, self.mirror_axis), NeighbourOffsetArrays(lb_method.stencil)]
else:
return [NeighbourOffsetArrays(lb_method.stencil)]
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
neighbor_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
if self.normal_direction:
tangential_offset = tuple(offset + normal for offset, normal in zip(neighbor_offset, self.normal_direction))
mirrored_stencil_symbol = MirroredStencilDirections._mirrored_symbol(self.mirror_axis, self.stencil)
mirrored_direction = inv_dir[sp.IndexedBase(mirrored_stencil_symbol, shape=(1,))[dir_symbol]]
else:
normal_direction = list()
for i, cell_name in zip(range(self.dim), self.additional_data):
normal_direction.append(index_field[0](cell_name[0]))
normal_direction = tuple(normal_direction)
tangential_offset = tuple(offset + normal for offset, normal in zip(neighbor_offset, normal_direction))
mirrored_direction = index_field[0]('ref_dir')
return Assignment(f_in.center(inv_dir[dir_symbol]), f_out[tangential_offset](mirrored_direction))
# 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.
def __init__(self, name=None): 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""" """Set an optional name here, to mark boundaries, for example for force evaluations"""
super(NoSlip, self).__init__(name) 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
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): if mean_velocity:
return Assignment(f_in(inv_dir[dir_symbol]), f_out(dir_symbol)) 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")
# end class NoSlip 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, calculate_force_on_boundary=False)
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, force_vector):
# 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, self.stencil)
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([CastFunc(neighbor_offset[i], self.data_type)**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(
[
CastFunc(neighbor_offset[i], self.data_type) * 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): class UBB(LbBoundary):
"""Velocity bounce back boundary condition, enforcing specified velocity at obstacle 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:
.. math ::
f_{\overline{i}}(\mathbf{x}_b, t + \Delta t) = f^{\star}_{i}(\mathbf{x}_b, t) -
2 w_{i} \rho_{w} \frac{\mathbf{c}_i \cdot \mathbf{u}_w}{c_s^2}
Args: Args:
velocity: can either be a constant, an access into a field, or a callback function. velocity: Prescribe the fluid velocity :math:`\mathbf{u}_w` at the wall.
The callback functions gets a numpy record array with members, 'x','y','z', 'dir' (direction) Can either be a constant, an access into a field, or a callback function.
and 'velocity' which has to be set to the desired velocity of the corresponding link The callback functions gets a numpy record array with members, ``x``, ``y``, ``z``, ``dir``
(direction) and ``velocity`` which has to be set to the desired velocity of the corresponding link
density: Prescribe the fluid density :math:`\rho_{w}` at the wall. If not prescribed the density is
calculated from the PDFs at the wall. The density can only be set constant.
adapt_velocity_to_force: adapts the velocity to the correct equilibrium when the lattice Boltzmann method holds adapt_velocity_to_force: adapts the velocity to the correct equilibrium when the lattice Boltzmann method holds
a forcing term. If no forcing term is set and adapt_velocity_to_force is set to True a forcing term. If no forcing term is set and adapt_velocity_to_force is set to True
it has no effect. it has no effect.
...@@ -124,8 +794,9 @@ class UBB(LbBoundary): ...@@ -124,8 +794,9 @@ class UBB(LbBoundary):
name: optional name of the boundary. name: optional name of the boundary.
""" """
def __init__(self, velocity, adapt_velocity_to_force=False, dim=None, name=None, data_type='double'): def __init__(self, velocity, density=None, adapt_velocity_to_force=False, dim=None, name=None, data_type='double'):
self._velocity = velocity self._velocity = velocity
self._density = density
self._adaptVelocityToForce = adapt_velocity_to_force self._adaptVelocityToForce = adapt_velocity_to_force
if callable(self._velocity) and not dim: if callable(self._velocity) and not dim:
raise ValueError("When using a velocity callback the dimension has to be specified with the dim parameter") raise ValueError("When using a velocity callback the dimension has to be specified with the dim parameter")
...@@ -134,7 +805,7 @@ class UBB(LbBoundary): ...@@ -134,7 +805,7 @@ class UBB(LbBoundary):
self.dim = dim self.dim = dim
self.data_type = data_type self.data_type = data_type
super(UBB, self).__init__(name) super(UBB, self).__init__(name, calculate_force_on_boundary=False)
@property @property
def additional_data(self): def additional_data(self):
...@@ -162,7 +833,7 @@ class UBB(LbBoundary): ...@@ -162,7 +833,7 @@ class UBB(LbBoundary):
Returns: Returns:
list containing LbmWeightInfo and NeighbourOffsetArrays list containing LbmWeightInfo and NeighbourOffsetArrays
""" """
return [LbmWeightInfo(lb_method), NeighbourOffsetArrays(lb_method.stencil)] return [LbmWeightInfo(lb_method, self.data_type), NeighbourOffsetArrays(lb_method.stencil)]
@property @property
def velocity_is_callable(self): def velocity_is_callable(self):
...@@ -170,15 +841,15 @@ class UBB(LbBoundary): ...@@ -170,15 +841,15 @@ class UBB(LbBoundary):
This is useful if the inflow velocity should have a certain profile for instance""" This is useful if the inflow velocity should have a certain profile for instance"""
return callable(self._velocity) return callable(self._velocity)
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
dtype = create_type(self.data_type)
vel_from_idx_field = callable(self._velocity) vel_from_idx_field = callable(self._velocity)
vel = [index_field(f'vel_{i}') for i in range(self.dim)] if vel_from_idx_field else self._velocity vel = [index_field(f'vel_{i}') for i in range(self.dim)] if vel_from_idx_field else self._velocity
direction = dir_symbol
assert self.dim == lb_method.dim, \ assert self.dim == lb_method.dim, \
f"Dimension of UBB ({self.dim}) does not match dimension of method ({lb_method.dim})" f"Dimension of UBB ({self.dim}) does not match dimension of method ({lb_method.dim})"
neighbor_offset = NeighbourOffsetArrays.neighbour_offset(direction, lb_method.stencil) neighbor_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
velocity = tuple(v_i.get_shifted(*neighbor_offset) velocity = tuple(v_i.get_shifted(*neighbor_offset)
if isinstance(v_i, Field.Access) and not vel_from_idx_field if isinstance(v_i, Field.Access) and not vel_from_idx_field
...@@ -188,29 +859,38 @@ class UBB(LbBoundary): ...@@ -188,29 +859,38 @@ class UBB(LbBoundary):
if self._adaptVelocityToForce: if self._adaptVelocityToForce:
cqc = lb_method.conserved_quantity_computation cqc = lb_method.conserved_quantity_computation
shifted_vel_eqs = cqc.equilibrium_input_equations_from_init_values(velocity=velocity) shifted_vel_eqs = cqc.equilibrium_input_equations_from_init_values(velocity=velocity)
velocity = [eq.rhs for eq in shifted_vel_eqs.new_filtered(cqc.first_order_moment_symbols).main_assignments] 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]
c_s_sq = sp.Rational(1, 3) c_s_sq = sp.Rational(1, 3)
weight_of_direction = LbmWeightInfo.weight_of_direction weight_info = LbmWeightInfo(lb_method, data_type=self.data_type)
vel_term = 2 / c_s_sq * sum([d_i * v_i for d_i, v_i in zip(neighbor_offset, velocity)]) * weight_of_direction( weight_of_direction = weight_info.weight_of_direction
direction, lb_method) vel_term = (
2 / c_s_sq
* sum([CastFunc(d_i, dtype) * v_i for d_i, v_i in zip(neighbor_offset, velocity)])
* weight_of_direction(dir_symbol, lb_method)
)
# Better alternative: in conserved value computation # Better alternative: in conserved value computation
# rename what is currently called density to "virtual_density" # rename what is currently called density to "virtual_density"
# provide a new quantity density, which is constant in case of incompressible models # provide a new quantity density, which is constant in case of incompressible models
if not lb_method.conserved_quantity_computation.zero_centered_pdfs: if lb_method.conserved_quantity_computation.compressible:
cqc = lb_method.conserved_quantity_computation cqc = lb_method.conserved_quantity_computation
density_symbol = sp.Symbol("rho") density_symbol = sp.Symbol("rho")
pdf_field_accesses = [f_out(i) for i in range(len(lb_method.stencil))] pdf_field_accesses = [f_out(i) for i in range(len(lb_method.stencil))]
density_equations = cqc.output_equations_from_pdfs(pdf_field_accesses, {'density': density_symbol}) density_equations = cqc.output_equations_from_pdfs(pdf_field_accesses, {'density': density_symbol})
density_symbol = lb_method.conserved_quantity_computation.defined_symbols()['density'] density_symbol = lb_method.conserved_quantity_computation.density_symbol
result = density_equations.all_assignments if self._density:
result += [Assignment(f_in(inv_dir[direction]), result = [Assignment(density_symbol, self._density)]
f_out(direction) - vel_term * density_symbol)] else:
result = density_equations.all_assignments
result += [Assignment(f_in(inv_dir[dir_symbol]),
f_out(dir_symbol) - vel_term * density_symbol)]
return result return result
else: else:
return [Assignment(f_in(inv_dir[direction]), return [Assignment(f_in(inv_dir[dir_symbol]),
f_out(direction) - vel_term)] f_out(dir_symbol) - vel_term)]
# end class UBB # end class UBB
...@@ -238,8 +918,10 @@ class SimpleExtrapolationOutflow(LbBoundary): ...@@ -238,8 +918,10 @@ class SimpleExtrapolationOutflow(LbBoundary):
if name is None: if name is None:
name = f"Simple Outflow: {offset_to_direction_string(normal_direction)}" name = f"Simple Outflow: {offset_to_direction_string(normal_direction)}"
self.normal_direction = normal_direction self.normal_direction = tuple([int(n) for n in normal_direction])
super(SimpleExtrapolationOutflow, self).__init__(name) assert all([n in [-1, 0, 1] for n in self.normal_direction]), \
"Only -1, 0 and 1 allowed for defining the normal direction"
super(SimpleExtrapolationOutflow, self).__init__(name, calculate_force_on_boundary=False)
def get_additional_code_nodes(self, lb_method): def get_additional_code_nodes(self, lb_method):
"""Return a list of code nodes that will be added in the generated code before the index field loop. """Return a list of code nodes that will be added in the generated code before the index field loop.
...@@ -253,12 +935,13 @@ class SimpleExtrapolationOutflow(LbBoundary): ...@@ -253,12 +935,13 @@ class SimpleExtrapolationOutflow(LbBoundary):
""" """
return [NeighbourOffsetArrays(lb_method.stencil)] return [NeighbourOffsetArrays(lb_method.stencil)]
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
neighbor_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil) 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)) tangential_offset = tuple(offset - normal for offset, normal in zip(neighbor_offset, self.normal_direction))
return Assignment(f_in.center(inv_dir[dir_symbol]), f_out[tangential_offset](inv_dir[dir_symbol])) return Assignment(f_in.center(inv_dir[dir_symbol]), f_out[tangential_offset](inv_dir[dir_symbol]))
# end class SimpleExtrapolationOutflow # end class SimpleExtrapolationOutflow
...@@ -278,7 +961,7 @@ class ExtrapolationOutflow(LbBoundary): ...@@ -278,7 +961,7 @@ class ExtrapolationOutflow(LbBoundary):
Args: Args:
normal_direction: direction vector normal to the outflow normal_direction: direction vector normal to the outflow
lb_method: the lattice boltzman method to be used in the simulation lb_method: the lattice Boltzmann method to be used in the simulation
dt: lattice time step size dt: lattice time step size
dx: lattice spacing distance dx: lattice spacing distance
name: optional name of the boundary. name: optional name of the boundary.
...@@ -304,7 +987,9 @@ class ExtrapolationOutflow(LbBoundary): ...@@ -304,7 +987,9 @@ class ExtrapolationOutflow(LbBoundary):
if name is None: if name is None:
name = f"Outflow: {offset_to_direction_string(normal_direction)}" name = f"Outflow: {offset_to_direction_string(normal_direction)}"
self.normal_direction = normal_direction self.normal_direction = tuple([int(n) for n in 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"
self.streaming_pattern = streaming_pattern self.streaming_pattern = streaming_pattern
self.zeroth_timestep = zeroth_timestep self.zeroth_timestep = zeroth_timestep
self.dx = sp.Number(dx) self.dx = sp.Number(dx)
...@@ -329,7 +1014,7 @@ class ExtrapolationOutflow(LbBoundary): ...@@ -329,7 +1014,7 @@ class ExtrapolationOutflow(LbBoundary):
self.equilibrium_calculation = calc_eq_pdfs self.equilibrium_calculation = calc_eq_pdfs
super(ExtrapolationOutflow, self).__init__(name) super(ExtrapolationOutflow, self).__init__(name, calculate_force_on_boundary=False)
def init_callback(self, boundary_data, **_): def init_callback(self, boundary_data, **_):
dim = boundary_data.dim dim = boundary_data.dim
...@@ -382,7 +1067,7 @@ class ExtrapolationOutflow(LbBoundary): ...@@ -382,7 +1067,7 @@ class ExtrapolationOutflow(LbBoundary):
""" """
return [NeighbourOffsetArrays(lb_method.stencil)] return [NeighbourOffsetArrays(lb_method.stencil)]
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
subexpressions = [] subexpressions = []
boundary_assignments = [] boundary_assignments = []
dtdx = sp.Rational(self.dt, self.dx) dtdx = sp.Rational(self.dt, self.dx)
...@@ -406,11 +1091,17 @@ class ExtrapolationOutflow(LbBoundary): ...@@ -406,11 +1091,17 @@ class ExtrapolationOutflow(LbBoundary):
return AssignmentCollection(boundary_assignments, subexpressions=subexpressions) return AssignmentCollection(boundary_assignments, subexpressions=subexpressions)
# end class ExtrapolationOutflow # end class ExtrapolationOutflow
class FixedDensity(LbBoundary): class FixedDensity(LbBoundary):
"""Boundary condition that fixes the density/pressure at the obstacle. r"""Boundary condition for prescribing a density at the wall. Through :math:`p = c_s^2 \rho` this boundary condition
can also function as a pressure boundary condition.
.. math ::
f_{\overline{i}}(\mathbf{x}_b, t + \Delta t) = - f^{\star}_{i}(\mathbf{x}_b, t) +
2 w_{i} \rho_{w} (1 + \frac{(\mathbf{c}_i \cdot \mathbf{u}_w)^2}{2c_s^4} + \frac{\mathbf{u}_w^2}{2c_s^2})
Args: Args:
density: value of the density which should be set. density: value of the density which should be set.
...@@ -422,16 +1113,16 @@ class FixedDensity(LbBoundary): ...@@ -422,16 +1113,16 @@ class FixedDensity(LbBoundary):
name = "Fixed Density " + str(density) name = "Fixed Density " + str(density)
self.density = density self.density = density
super(FixedDensity, self).__init__(name) super(FixedDensity, self).__init__(name, calculate_force_on_boundary=False)
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
def remove_asymmetric_part_of_main_assignments(assignment_collection, degrees_of_freedom): def remove_asymmetric_part_of_main_assignments(assignment_collection, degrees_of_freedom):
new_main_assignments = [Assignment(a.lhs, get_symmetric_part(a.rhs, degrees_of_freedom)) new_main_assignments = [Assignment(a.lhs, get_symmetric_part(a.rhs, degrees_of_freedom))
for a in assignment_collection.main_assignments] for a in assignment_collection.main_assignments]
return assignment_collection.copy(new_main_assignments) return assignment_collection.copy(new_main_assignments)
cqc = lb_method.conserved_quantity_computation cqc = lb_method.conserved_quantity_computation
velocity = cqc.defined_symbols()['velocity'] velocity = cqc.velocity_symbols
symmetric_eq = remove_asymmetric_part_of_main_assignments(lb_method.get_equilibrium(), symmetric_eq = remove_asymmetric_part_of_main_assignments(lb_method.get_equilibrium(),
degrees_of_freedom=velocity) degrees_of_freedom=velocity)
substitutions = {sym: f_out(i) for i, sym in enumerate(lb_method.pre_collision_pdf_symbols)} substitutions = {sym: f_out(i) for i, sym in enumerate(lb_method.pre_collision_pdf_symbols)}
...@@ -440,42 +1131,70 @@ class FixedDensity(LbBoundary): ...@@ -440,42 +1131,70 @@ class FixedDensity(LbBoundary):
simplification = create_simplification_strategy(lb_method) simplification = create_simplification_strategy(lb_method)
symmetric_eq = simplification(symmetric_eq) symmetric_eq = simplification(symmetric_eq)
density_symbol = cqc.defined_symbols()['density']
density = self.density density = self.density
equilibrium_input = cqc.equilibrium_input_equations_from_init_values(density=density) equilibrium_input = cqc.equilibrium_input_equations_from_init_values(density=density)
equilibrium_input = equilibrium_input.new_without_subexpressions() equilibrium_input = equilibrium_input.new_without_subexpressions()
density_eq = equilibrium_input.main_assignments[0] equilibrium_input = equilibrium_input.main_assignments_dict
assert density_eq.lhs == density_symbol
transformed_density = density_eq.rhs subexpressions_dict = symmetric_eq.subexpressions_dict
subexpressions_dict[cqc.density_symbol] = equilibrium_input[cqc.density_symbol]
subexpressions_dict[cqc.density_deviation_symbol] = equilibrium_input[cqc.density_deviation_symbol]
conditions = [(eq_i.rhs, sp.Equality(dir_symbol, i)) conditions = [(eq_i.rhs, sp.Equality(dir_symbol, i))
for i, eq_i in enumerate(symmetric_eq.main_assignments)] + [(0, True)] for i, eq_i in enumerate(symmetric_eq.main_assignments)] + [(0, True)]
eq_component = sp.Piecewise(*conditions) eq_component = sp.Piecewise(*conditions)
subexpressions = [Assignment(eq.lhs, transformed_density if eq.lhs == density_symbol else eq.rhs) main_assignments = [Assignment(f_in(inv_dir[dir_symbol]), 2 * eq_component - f_out(dir_symbol))]
for eq in symmetric_eq.subexpressions]
return subexpressions + [Assignment(f_in(inv_dir[dir_symbol]), ac = AssignmentCollection(main_assignments, subexpressions=subexpressions_dict)
2 * eq_component - f_out(dir_symbol))] ac = ac.new_without_unused_subexpressions()
ac.topological_sort()
return ac
# end class FixedDensity
# end class FixedDensity
class DiffusionDirichlet(LbBoundary): class DiffusionDirichlet(LbBoundary):
"""Boundary condition for advection-diffusion problems that fixes the concentration at the obstacle. """Concentration boundary which is used for concentration or thermal boundary conditions of convection-diffusion
equation Base on https://doi.org/10.1103/PhysRevE.85.016701.
Args: Args:
concentration: value of the concentration which should be set. concentration: can either be a constant, an access into a field, or a callback function.
The callback functions gets a numpy record array with members, ``x``, ``y``, ``z``, ``dir``
(direction) and ``concentration`` which has to be set to the desired
velocity of the corresponding link
velocity_field: if velocity field is given the boundary value is approximated by using the discrete equilibrium.
name: optional name of the boundary. name: optional name of the boundary.
data_type: data type of the concentration value. default is double
""" """
def __init__(self, concentration, name=None): def __init__(self, concentration, velocity_field=None, name=None, data_type='double'):
if name is None: if name is None:
name = "Diffusion Dirichlet " + str(concentration) name = "DiffusionDirichlet"
self.concentration = concentration self.concentration = concentration
self._data_type = data_type
self.concentration_is_callable = callable(self.concentration)
self.velocity_field = velocity_field
super(DiffusionDirichlet, self).__init__(name) super(DiffusionDirichlet, self).__init__(name, calculate_force_on_boundary=False)
@property
def additional_data(self):
""" In case of the UBB boundary additional data is a velocity vector. This vector is added to each cell to
realize velocity profiles for the inlet."""
if self.concentration_is_callable:
return [('concentration', create_type(self._data_type))]
else:
return []
@property
def additional_data_init_callback(self):
"""Initialise additional data of the boundary. For an example see
`tutorial 02 <https://pycodegen.pages.i10git.cs.fau.de/lbmpy/notebooks/02_tutorial_boundary_setup.html>`_
or lbmpy.geometry.add_pipe_inflow_boundary"""
if self.concentration_is_callable:
return self.concentration
def get_additional_code_nodes(self, lb_method): def get_additional_code_nodes(self, lb_method):
"""Return a list of code nodes that will be added in the generated code before the index field loop. """Return a list of code nodes that will be added in the generated code before the index field loop.
...@@ -486,12 +1205,35 @@ class DiffusionDirichlet(LbBoundary): ...@@ -486,12 +1205,35 @@ class DiffusionDirichlet(LbBoundary):
Returns: Returns:
list containing LbmWeightInfo list containing LbmWeightInfo
""" """
return [LbmWeightInfo(lb_method)] if self.velocity_field:
return [LbmWeightInfo(lb_method, self._data_type), NeighbourOffsetArrays(lb_method.stencil)]
else:
return [LbmWeightInfo(lb_method, self._data_type)]
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
assert lb_method.conserved_quantity_computation.zero_centered_pdfs is False, \
"DiffusionDirichlet only works for methods with normal pdfs storage -> set zero_centered=False"
weight_info = LbmWeightInfo(lb_method, self._data_type)
w_dir = weight_info.weight_of_direction(dir_symbol, lb_method)
if self.concentration_is_callable:
concentration = index_field[0]('concentration')
else:
concentration = self.concentration
if self.velocity_field:
neighbour_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
u = self.velocity_field
cs = sp.Rational(1, 3)
equilibrium = (1 + scalar_product(neighbour_offset, u.center_vector)**2 / (2 * cs**4)
- scalar_product(u.center_vector, u.center_vector) / (2 * cs**2))
else:
equilibrium = sp.Rational(1, 1)
result = [Assignment(f_in(inv_dir[dir_symbol]), 2.0 * w_dir * concentration * equilibrium - f_out(dir_symbol))]
return result
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field):
w_dir = LbmWeightInfo.weight_of_direction(dir_symbol, lb_method)
return [Assignment(f_in(inv_dir[dir_symbol]),
2 * w_dir * self.concentration - f_out(dir_symbol))]
# end class DiffusionDirichlet # end class DiffusionDirichlet
...@@ -511,11 +1253,12 @@ class NeumannByCopy(LbBoundary): ...@@ -511,11 +1253,12 @@ class NeumannByCopy(LbBoundary):
""" """
return [NeighbourOffsetArrays(lb_method.stencil)] return [NeighbourOffsetArrays(lb_method.stencil)]
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
neighbour_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil) neighbour_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
return [Assignment(f_in(inv_dir[dir_symbol]), f_out(inv_dir[dir_symbol])), return [Assignment(f_in(inv_dir[dir_symbol]), f_out(inv_dir[dir_symbol])),
Assignment(f_out[neighbour_offset](dir_symbol), f_out(dir_symbol))] Assignment(f_out[neighbour_offset](dir_symbol), f_out(dir_symbol))]
# end class NeumannByCopy # end class NeumannByCopy
...@@ -529,7 +1272,7 @@ class StreamInConstant(LbBoundary): ...@@ -529,7 +1272,7 @@ class StreamInConstant(LbBoundary):
""" """
def __init__(self, constant, name=None): def __init__(self, constant, name=None):
super(StreamInConstant, self).__init__(name) super(StreamInConstant, self).__init__(name, calculate_force_on_boundary=False)
self.constant = constant self.constant = constant
def get_additional_code_nodes(self, lb_method): def get_additional_code_nodes(self, lb_method):
...@@ -543,7 +1286,7 @@ class StreamInConstant(LbBoundary): ...@@ -543,7 +1286,7 @@ class StreamInConstant(LbBoundary):
""" """
return [NeighbourOffsetArrays(lb_method.stencil)] return [NeighbourOffsetArrays(lb_method.stencil)]
def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector):
neighbour_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil) neighbour_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil)
return [Assignment(f_in(inv_dir[dir_symbol]), self.constant), return [Assignment(f_in(inv_dir[dir_symbol]), self.constant),
Assignment(f_out[neighbour_offset](dir_symbol), self.constant)] Assignment(f_out[neighbour_offset](dir_symbol), self.constant)]
......
from dataclasses import replace
import numpy as np import numpy as np
import sympy as sp
from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing from pystencils import Assignment, CreateKernelConfig, create_kernel, Field, Target, FieldType
from lbmpy.advanced_streaming.utility import is_inplace, Timestep, AccessPdfValues
from pystencils import Field, Assignment, TypedSymbol, create_indexed_kernel
from pystencils.stencil import inverse_direction
from pystencils.boundaries import BoundaryHandling from pystencils.boundaries import BoundaryHandling
from pystencils.boundaries.createindexlist import numpy_data_type_for_boundary_object from pystencils.boundaries.createindexlist import numpy_data_type_for_boundary_object
from pystencils.backends.cbackend import CustomCodeNode from pystencils.simp import add_subexpressions_for_field_reads
from pystencils.stencil import inverse_direction
from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing
from lbmpy.advanced_streaming.utility import is_inplace, Timestep, AccessPdfValues
from .._compat import IS_PYSTENCILS_2
if IS_PYSTENCILS_2:
from pystencils.types import PsNumericType
class LatticeBoltzmannBoundaryHandling(BoundaryHandling): class LatticeBoltzmannBoundaryHandling(BoundaryHandling):
""" """
Enables boundary handling for LBM simulations with advanced streaming patterns. Enables boundary handling for LBM simulations with advanced streaming patterns.
For the in-place patterns AA and EsoTwist, two kernels are generated for a boundary For the in-place patterns AA and EsoTwist, two kernels are generated for a boundary
object and the right one selected depending on the time step. object and the right one selected depending on the time step.
""" """
def __init__(self, lb_method, data_handling, pdf_field_name, streaming_pattern='pull', def __init__(self, lb_method, data_handling, pdf_field_name, streaming_pattern='pull',
name="boundary_handling", flag_interface=None, target='cpu', openmp=True): name="boundary_handling", flag_interface=None, target=Target.CPU, openmp=False, **kwargs):
self._lb_method = lb_method self._lb_method = lb_method
self._streaming_pattern = streaming_pattern self._streaming_pattern = streaming_pattern
self._inplace = is_inplace(streaming_pattern) self._inplace = is_inplace(streaming_pattern)
self._prev_timestep = None self._prev_timestep = None
super(LatticeBoltzmannBoundaryHandling, self).__init__(data_handling, pdf_field_name, lb_method.stencil, super(LatticeBoltzmannBoundaryHandling, self).__init__(
name, flag_interface, target, openmp) data_handling, pdf_field_name, lb_method.stencil,
name, flag_interface, target=target, openmp=openmp,
**kwargs
)
# ------------------------- Overridden methods of pystencils.BoundaryHandling ------------------------- # ------------------------- Overridden methods of pystencils.BoundaryHandling -------------------------
...@@ -37,7 +47,7 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): ...@@ -37,7 +47,7 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling):
self._prev_timestep = None self._prev_timestep = None
def add_fixed_steps(self, fixed_loop, **kwargs): def add_fixed_steps(self, fixed_loop, **kwargs):
if self._inplace: # Fixed Loop can't do timestep selection if self._inplace: # Fixed Loop can't do timestep selection
raise NotImplementedError("Adding to fixed loop is currently not supported for inplace kernels") raise NotImplementedError("Adding to fixed loop is currently not supported for inplace kernels")
super(LatticeBoltzmannBoundaryHandling, self).add_fixed_steps(fixed_loop, **kwargs) super(LatticeBoltzmannBoundaryHandling, self).add_fixed_steps(fixed_loop, **kwargs)
...@@ -49,12 +59,14 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): ...@@ -49,12 +59,14 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling):
def _add_inplace_boundary(self, boundary_obj, flag=None): def _add_inplace_boundary(self, boundary_obj, flag=None):
if boundary_obj not in self._boundary_object_to_boundary_info: if boundary_obj not in self._boundary_object_to_boundary_info:
sym_index_field = Field.create_generic('indexField', spatial_dimensions=1, sym_index_field = Field.create_generic('indexField', spatial_dimensions=1, field_type=FieldType.INDEXED,
dtype=numpy_data_type_for_boundary_object(boundary_obj, self.dim)) dtype=numpy_data_type_for_boundary_object(boundary_obj, self.dim))
kernels = [self._create_boundary_kernel(
self._data_handling.fields[self._field_name], sym_index_field, boundary_obj, Timestep.EVEN).compile(), ast_even = self._create_boundary_kernel(self._data_handling.fields[self._field_name], sym_index_field,
self._create_boundary_kernel( boundary_obj, Timestep.EVEN)
self._data_handling.fields[self._field_name], sym_index_field, boundary_obj, Timestep.ODD).compile()] ast_odd = self._create_boundary_kernel(self._data_handling.fields[self._field_name], sym_index_field,
boundary_obj, Timestep.ODD)
kernels = [ast_even.compile(), ast_odd.compile()]
if flag is None: if flag is None:
flag = self.flag_interface.reserve_next_flag() flag = self.flag_interface.reserve_next_flag()
boundary_info = self.InplaceStreamingBoundaryInfo(self, boundary_obj, flag, kernels) boundary_info = self.InplaceStreamingBoundaryInfo(self, boundary_obj, flag, kernels)
...@@ -62,10 +74,15 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): ...@@ -62,10 +74,15 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling):
return self._boundary_object_to_boundary_info[boundary_obj].flag return self._boundary_object_to_boundary_info[boundary_obj].flag
def _create_boundary_kernel(self, symbolic_field, symbolic_index_field, boundary_obj, prev_timestep=Timestep.BOTH): def _create_boundary_kernel(self, symbolic_field, symbolic_index_field, boundary_obj, prev_timestep=Timestep.BOTH):
if IS_PYSTENCILS_2:
additional_args = {"default_dtype": self._default_dtype}
else:
additional_args = dict()
return create_lattice_boltzmann_boundary_kernel( return create_lattice_boltzmann_boundary_kernel(
symbolic_field, symbolic_index_field, self._lb_method, boundary_obj, symbolic_field, symbolic_index_field, self._lb_method, boundary_obj,
prev_timestep=prev_timestep, streaming_pattern=self._streaming_pattern, prev_timestep=prev_timestep, streaming_pattern=self._streaming_pattern,
target=self._target, cpu_openmp=self._openmp) target=self._target, cpu_openmp=self._openmp, **additional_args)
class InplaceStreamingBoundaryInfo(object): class InplaceStreamingBoundaryInfo(object):
...@@ -83,6 +100,7 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): ...@@ -83,6 +100,7 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling):
self.boundary_object = boundary_obj self.boundary_object = boundary_obj
self.flag = flag self.flag = flag
self._kernels = kernels self._kernels = kernels
# end class InplaceStreamingBoundaryInfo # end class InplaceStreamingBoundaryInfo
# ------------------------------ Force On Boundary ------------------------------------------------------------ # ------------------------------ Force On Boundary ------------------------------------------------------------
...@@ -147,55 +165,85 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): ...@@ -147,55 +165,85 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling):
return dh.reduce_float_sequence(list(result), 'sum') return dh.reduce_float_sequence(list(result), 'sum')
# end class LatticeBoltzmannBoundaryHandling # end class LatticeBoltzmannBoundaryHandling
def create_lattice_boltzmann_boundary_kernel(pdf_field, index_field, lb_method, boundary_functor,
prev_timestep=Timestep.BOTH, streaming_pattern='pull',
target=Target.CPU, force_vector=None, **kernel_creation_args):
from .._compat import IS_PYSTENCILS_2
class LbmWeightInfo(CustomCodeNode): indexing = BetweenTimestepsIndexing(
pdf_field, lb_method.stencil, prev_timestep, streaming_pattern, np.int32, np.int32)
# --------------------------- Functions to be used by boundaries -------------------------- dim = lb_method.stencil.D
f_out, f_in = indexing.proxy_fields
dir_symbol = indexing.dir_symbol
inv_dir = indexing.inverse_dir_symbol
@staticmethod if IS_PYSTENCILS_2:
def weight_of_direction(dir_idx, lb_method=None): from pystencils.types.quick import SInt
if isinstance(sp.sympify(dir_idx), sp.Integer): config = CreateKernelConfig(
return lb_method.weights[dir_idx].evalf() index_field=index_field,
else: target=target,
return sp.IndexedBase(LbmWeightInfo.WEIGHTS_SYMBOL, shape=(1,))[dir_idx] index_dtype=SInt(32),
skip_independence_check=True,
**kernel_creation_args
)
# ---------------------------------- Internal --------------------------------------------- default_data_type: PsNumericType = config.get_option("default_dtype")
WEIGHTS_SYMBOL = TypedSymbol("weights", "double") if force_vector is None:
force_vector_type = np.dtype([(f"F_{i}", default_data_type.numpy_dtype) for i in range(dim)], align=True)
force_vector = Field.create_generic('force_vector', spatial_dimensions=1,
dtype=force_vector_type, field_type=FieldType.INDEXED)
def __init__(self, lb_method): boundary_assignments = boundary_functor(f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector)
weights = [str(w.evalf()) for w in lb_method.weights] boundary_assignments = indexing.substitute_proxies(boundary_assignments)
w_sym = LbmWeightInfo.WEIGHTS_SYMBOL
code = "const double %s [] = { %s };\n" % (w_sym.name, ",".join(weights))
super(LbmWeightInfo, self).__init__(code, symbols_read=set(), symbols_defined={w_sym})
# end class LbmWeightInfo
if pdf_field.dtype != default_data_type:
boundary_assignments = add_subexpressions_for_field_reads(boundary_assignments, data_type=default_data_type)
def create_lattice_boltzmann_boundary_kernel(pdf_field, index_field, lb_method, boundary_functor, elements: list[Assignment] = []
prev_timestep=Timestep.BOTH, streaming_pattern='pull',
target='cpu', **kernel_creation_args):
indexing = BetweenTimestepsIndexing( index_arrs_node = indexing.create_code_node()
pdf_field, lb_method.stencil, prev_timestep, streaming_pattern, np.int64, np.int64) elements += index_arrs_node.get_array_declarations()
for node in boundary_functor.get_additional_code_nodes(lb_method)[::-1]:
elements += node.get_array_declarations()
elements += [Assignment(dir_symbol, index_field[0]('dir'))]
elements += boundary_assignments.all_assignments
f_out, f_in = indexing.proxy_fields kernel = create_kernel(elements, config=config)
dir_symbol = indexing.dir_symbol return kernel
inv_dir = indexing.inverse_dir_symbol else:
config = CreateKernelConfig(index_fields=[index_field], target=target, default_number_int="int32",
skip_independence_check=True, **kernel_creation_args)
default_data_type = config.data_type.default_factory()
if force_vector is None:
force_vector_type = np.dtype([(f"F_{i}", default_data_type.c_name) for i in range(dim)], align=True)
force_vector = Field.create_generic('force_vector', spatial_dimensions=1,
dtype=force_vector_type, field_type=FieldType.INDEXED)
config = replace(config, index_fields=[index_field, force_vector])
boundary_assignments = boundary_functor(f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector)
boundary_assignments = indexing.substitute_proxies(boundary_assignments)
boundary_assignments = boundary_functor(f_out, f_in, dir_symbol, inv_dir, lb_method, index_field) if pdf_field.dtype != default_data_type:
boundary_assignments = indexing.substitute_proxies(boundary_assignments) boundary_assignments = add_subexpressions_for_field_reads(boundary_assignments, data_type=default_data_type)
# Code Elements inside the loop elements = [Assignment(dir_symbol, index_field[0]('dir'))]
elements = [Assignment(dir_symbol, index_field[0]('dir'))] elements += boundary_assignments.all_assignments
elements += boundary_assignments.all_assignments
kernel = create_indexed_kernel(elements, [index_field], target=target, **kernel_creation_args) kernel = create_kernel(elements, config=config)
# Code Elements ahead of the loop # Code Elements ahead of the loop
index_arrs_node = indexing.create_code_node() index_arrs_node = indexing.create_code_node()
for node in boundary_functor.get_additional_code_nodes(lb_method)[::-1]: for node in boundary_functor.get_additional_code_nodes(lb_method)[::-1]:
kernel.body.insert_front(node) kernel.body.insert_front(node)
kernel.body.insert_front(index_arrs_node) kernel.body.insert_front(index_arrs_node)
return kernel return kernel
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))
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
...@@ -21,8 +21,8 @@ class ChapmanEnskogAnalysis: ...@@ -21,8 +21,8 @@ class ChapmanEnskogAnalysis:
cqc = method.conserved_quantity_computation cqc = method.conserved_quantity_computation
self._method = method self._method = method
self._moment_cache = LbMethodEqMoments(method) self._moment_cache = LbMethodEqMoments(method)
self.rho = cqc.defined_symbols(order=0)[1] self.rho = cqc.density_symbol
self.u = cqc.defined_symbols(order=1)[1] self.u = cqc.velocity_symbols
self.t = sp.Symbol("t") self.t = sp.Symbol("t")
self.epsilon = sp.Symbol("epsilon") self.epsilon = sp.Symbol("epsilon")
...@@ -370,7 +370,7 @@ def take_moments(eqn, pdf_to_moment_name=(('f', '\\Pi'), ('\\Omega f', '\\Upsilo ...@@ -370,7 +370,7 @@ def take_moments(eqn, pdf_to_moment_name=(('f', '\\Pi'), ('\\Omega f', '\\Upsilo
if new_f_index is None: if new_f_index is None:
rest *= factor rest *= factor
else: else:
assert not(new_f_index and f_index) assert not (new_f_index and f_index)
f_index = new_f_index f_index = new_f_index
moment_tuple = [0] * len(velocity_terms) moment_tuple = [0] * len(velocity_terms)
......
import functools import functools
import numpy as np
import sympy as sp import sympy as sp
from lbmpy.chapman_enskog.chapman_enskog import ( from lbmpy.chapman_enskog.chapman_enskog import (
...@@ -150,7 +151,8 @@ class SteadyStateChapmanEnskogAnalysis: ...@@ -150,7 +151,8 @@ class SteadyStateChapmanEnskogAnalysis:
have_shape = hasattr(arg, 'shape') and hasattr(new_prod, 'shape') have_shape = hasattr(arg, 'shape') and hasattr(new_prod, 'shape')
if have_shape and arg.shape == new_prod.shape and arg.shape[1] == 1: if have_shape and arg.shape == new_prod.shape and arg.shape[1] == 1:
new_prod = sp.matrix_multiply_elementwise(new_prod, arg) # since sympy 1.9 sp.matrix_multiply_elementwise does not work anymore in this case
new_prod = sp.Matrix(np.multiply(new_prod, arg))
else: else:
new_prod = arg * new_prod new_prod = arg * new_prod
if new_prod == 0: if new_prod == 0:
......
...@@ -6,21 +6,23 @@ import sympy as sp ...@@ -6,21 +6,23 @@ import sympy as sp
from lbmpy.moments import polynomial_to_exponent_representation from lbmpy.moments import polynomial_to_exponent_representation
from pystencils.cache import disk_cache, memorycache from pystencils.cache import disk_cache, memorycache
from pystencils.sympyextensions import complete_the_squares_in_exp from pystencils.sympyextensions import complete_the_squares_in_exp, scalar_product
@memorycache() @memorycache()
def moment_generating_function(generating_function, symbols, symbols_in_result): def moment_generating_function(generating_function, symbols, symbols_in_result, velocity=None):
r""" r"""
Computes the moment generating function of a probability distribution. It is defined as: Computes the moment generating function of a probability distribution. It is defined as:
.. math :: .. math ::
F[f(\mathbf{x})](\mathbf{t}) = \int e^{<\mathbf{x}, \mathbf{t}>} f(x)\; dx F[f(\mathbf{x})](t) = \int e^{<\mathbf{x}, t>} f(\mathbf{x})\; dx
Args: Args:
generating_function: sympy expression generating_function: sympy expression
symbols: a sequence of symbols forming the vector x symbols: a sequence of symbols forming the vector :math:`\mathbf{x}`
symbols_in_result: a sequence forming the vector t symbols_in_result: a sequence forming the vector t
velocity: if the generating function generates central moments, the velocity needs to be substracted. Thus the
velocity symbols need to be passed. All generating functions need to have the same parameters.
Returns: Returns:
transformation result F: an expression that depends now on symbols_in_result transformation result F: an expression that depends now on symbols_in_result
...@@ -55,9 +57,27 @@ def moment_generating_function(generating_function, symbols, symbols_in_result): ...@@ -55,9 +57,27 @@ def moment_generating_function(generating_function, symbols, symbols_in_result):
return sp.simplify(result) return sp.simplify(result)
def cumulant_generating_function(func, symbols, symbols_in_result): def central_moment_generating_function(func, symbols, symbols_in_result, velocity=sp.symbols("u_:3")):
r"""
Computes central moment generating func, which is defined as:
.. math ::
K( \mathbf{\Xi} ) = \exp ( - \mathbf{\Xi} \cdot \mathbf{u} ) M( \mathbf{\Xi} ).
For parameter description see :func:`moment_generating_function`.
""" """
Computes cumulant generating func, which is the logarithm of the moment generating func. argument = - scalar_product(symbols_in_result, velocity)
return sp.exp(argument) * moment_generating_function(func, symbols, symbols_in_result)
def cumulant_generating_function(func, symbols, symbols_in_result, velocity=None):
r"""
Computes cumulant generating func, which is the logarithm of the moment generating func:
.. math ::
C(\mathbf{\Xi}) = \log M(\mathbf{\Xi})
For parameter description see :func:`moment_generating_function`. For parameter description see :func:`moment_generating_function`.
""" """
return sp.ln(moment_generating_function(func, symbols, symbols_in_result)) return sp.ln(moment_generating_function(func, symbols, symbols_in_result))
...@@ -93,16 +113,16 @@ def multi_differentiation(generating_function, index, symbols): ...@@ -93,16 +113,16 @@ def multi_differentiation(generating_function, index, symbols):
@memorycache(maxsize=512) @memorycache(maxsize=512)
def __continuous_moment_or_cumulant(func, moment, symbols, generating_function): def __continuous_moment_or_cumulant(func, moment, symbols, generating_function, velocity=sp.symbols("u_:3")):
if type(moment) is tuple and not symbols: if type(moment) is tuple and not symbols:
symbols = sp.symbols("xvar yvar zvar") symbols = sp.symbols("xvar yvar zvar")
dim = len(moment) if type(moment) is tuple else len(symbols) dim = len(moment) if type(moment) is tuple else len(symbols)
# not using sp.Dummy here - since it prohibits caching # not using sp.Dummy here - since it prohibits caching
t = tuple([sp.Symbol("tmpvar_%d" % i, ) for i in range(dim)]) t = sp.symbols(f"tmpvar_:{dim}")
symbols = symbols[:dim] symbols = symbols[:dim]
generating_function = generating_function(func, symbols, t) generating_function = generating_function(func, symbols, t, velocity=velocity)
if type(moment) is tuple: if type(moment) is tuple:
return multi_differentiation(generating_function, moment, t) return multi_differentiation(generating_function, moment, t)
...@@ -128,6 +148,18 @@ def continuous_moment(func, moment, symbols=None): ...@@ -128,6 +148,18 @@ def continuous_moment(func, moment, symbols=None):
return __continuous_moment_or_cumulant(func, moment, symbols, moment_generating_function) return __continuous_moment_or_cumulant(func, moment, symbols, moment_generating_function)
def continuous_central_moment(func, moment, symbols=None, velocity=sp.symbols("u_:3")):
"""Computes central moment of given function.
Args:
func: function to compute moments of
moment: tuple or polynomial describing the moment
symbols: if moment is given as polynomial, pass the moment symbols, i.e. the dof of the polynomial
"""
return __continuous_moment_or_cumulant(func, moment, symbols, central_moment_generating_function,
velocity=velocity)
def continuous_cumulant(func, moment, symbols=None): def continuous_cumulant(func, moment, symbols=None):
"""Computes cumulant of continuous function. """Computes cumulant of continuous function.
......
r"""
Creating LBM kernels and Parameter Specifications
-------------------------------------------------
Kernel functions are created in four/five steps represented by five
python functions: `create_lb_method`, *create_lb_collision_rule/create_lb_update_rule*, `create_lb_ast` and
`create_lb_function` Each of those functions is configured with three data classes.
One dataclass defines the lattice Boltzmann method itself. This class is called `LBMConfig`. It defines, for example,
which collision space or LB stencil should be used.
The second one determines optimisations that are specific to the LBM. Optimisations like the
common subexpression elimination. Most of these optimisations act on the assignment level.
This means they only manipulate the assignments. The config class is called `LBMOptimisation`.
The third data class determines hardware optimisation. This means that contrary to the `LBMOptimisation` class,
it acts on the level of the abstract syntax tree. Thus, it is independent of the assignments and the LBM
and belongs to pystencils, not lbmpy. This can be found in the pystencils module as
'pystencils.kernelcreation.CreateKernelConfig'. With this class, for example, the target (CPU, GPU etc.)
of the generated code is specified.
1. *Method*:
the method defines the collision process. Currently, there are two big categories:
moment and cumulant based methods. A method defines how each moment or cumulant is relaxed by
storing the equilibrium value and the relaxation rate for each moment/cumulant.
2. *Collision/Update Rule*:
Methods can generate a "collision rule" which is an equation collection that define the
post collision values as a function of the pre-collision values. On these equation collection
simplifications are applied to reduce the number of floating point operations.
At this stage an entropic optimisation step can also be added to determine one relaxation rate by an
entropy condition.
Then a streaming rule is added which transforms the collision rule into an update rule.
The streaming step depends on the pdf storage (source/destination, AABB pattern, EsoTwist).
Currently only the simple source/destination pattern is supported.
3. *AST*:
The abstract syntax tree describes the structure of the kernel, including loops and conditionals.
The ast can be modified, e.g., to add OpenMP pragmas, reorder loops or apply other optimisations.
4. *Function*:
This step compiles the AST into an executable function, either for CPU or GPUs. This function
behaves like a normal Python function and runs one LBM time step.
Each stage (apart from *Function*) also adds its result to the given `LBMConfig` object. The `LBMConfig`
thus coalesces all information defining the LBM kernel.
The function :func:`create_lb_function` runs the whole pipeline, the other functions in this module
execute this pipeline only up to a certain step. Each function optionally also takes the result of the previous step.
For example, to modify the AST one can run::
ast = create_lb_ast(...)
# modify ast here
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
from ._compat import IS_PYSTENCILS_2
import sympy as sp
from lbmpy.enums import Stencil, Method, ForceModel, CollisionSpace, SubgridScaleModel
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 (replace_by_psm_collision_rule, PSMConfig,
add_psm_solid_collision_to_collision_rule)
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)
from lbmpy.methods.creationfunctions import CollisionSpaceInfo
from lbmpy.methods.creationfunctions import (
create_with_monomial_cumulants, create_cumulant, create_with_default_polynomial_cumulants)
from lbmpy.methods.momentbased.entropic import add_entropy_condition, add_iterative_entropy_condition
from lbmpy.relaxationrates import relaxation_rate_from_magic_number
from lbmpy.simplificationfactory import create_simplification_strategy
from lbmpy.stencils import LBStencil
from lbmpy.turbulence_models import add_sgs_model
from lbmpy.updatekernels import create_lbm_kernel, create_stream_pull_with_output_kernel
from lbmpy.advanced_streaming.utility import Timestep, get_accessor
from .forcemodels import AbstractForceModel
import pystencils
from pystencils import CreateKernelConfig, create_kernel
from pystencils.cache import disk_cache_no_fallback
from pystencils.field import Field
from pystencils.simp import sympy_cse, SimplificationStrategy
# needed for the docstring
from lbmpy.methods.abstractlbmethod import LbmCollisionRule, AbstractLbMethod
from lbmpy.methods.cumulantbased import CumulantBasedLbMethod
if IS_PYSTENCILS_2:
from pystencils import Kernel as KernelFunction
else:
from pystencils.astnodes import KernelFunction
# Filter out JobLib warnings. They are not useful for use:
# https://github.com/joblib/joblib/issues/683
filterwarnings("ignore", message="Persisting input arguments took")
@dataclass
class LBMConfig:
"""
**Below all parameters for the LBMConfig are explained**
"""
stencil: LBStencil = LBStencil(Stencil.D2Q9)
"""
All stencils are defined in :class:`lbmpy.enums.Stencil`. From that :class:`lbmpy.stencils.LBStencil`
class will be created
"""
method: Method = Method.SRT
"""
Name of lattice Boltzmann method. Defined by :class:`lbmpy.enums.Method`.
This determines the selection and relaxation pattern of moments/cumulants, i.e. which moment/cumulant basis is
chosen, and which of the basis vectors are relaxed together
"""
relaxation_rates: Iterable = None
"""
Sequence of relaxation rates, number depends on selected method. If you specify more rates than
method needs, the additional rates are ignored.
If no relaxation rates are specified, the parameter `relaxation_rate` will be consulted.
"""
relaxation_rate: Union[int, float, Type[sp.Symbol]] = None
"""
The method's primary relaxation rate. In most cases, this is the relaxation rate governing shear viscosity.
For SRT, this is the only relaxation rate.
For TRT, the second relaxation rate is then determined via magic number.
In the case of raw moment, central moment, and cumulant-based MRT methods, all other relaxation rates will be
set to unity.
If neither `relaxation_rate` nor `relaxation_rates` is specified, the behaviour is as if
`relaxation_rate=sp.Symbol('omega')` was set.
"""
compressible: bool = False
"""
Affects the selection of equilibrium moments. Both options approximate the *incompressible*
Navier Stokes Equations. However when chosen as False, the approximation is better, the standard LBM derivation is
compressible.
"""
zero_centered: bool = True
"""
Governs the storage format of populations. If `False`, the discrete particle distribution vector is stored in its
absolute form. If `True`, instead, only the distribution's deviation from its rest state (typically given by the
lattice weights) is stored.
"""
delta_equilibrium: bool = None
"""
Determines whether or not the (continuous or discrete, see `continuous_equilibrium`) Maxwellian equilibrium is
expressed in its absolute form, or only by its deviation from the rest state (typically given by the reference
density and zero velocity). This parameter is only effective if `zero_centered` is set to `True`. Then, if
`delta_equilibrium` is `False`, the rest state must be reintroduced to the populations during collision. Otherwise,
if `delta_equilibrium` is `True`, the collision equations can be derived using only the deviations from the rest
state.
If `None` is passed to `delta_equilibrium`, its value will be chosen automatically, depending on the value of
`zero_centered` and the chosen `method`.
"""
equilibrium_order: int = 2
"""
Order in velocity, at which the equilibrium moment approximation is
truncated. Order 2 is sufficient to approximate Navier-Stokes. This parameter has no effect on cumulant-based
methods, whose equilibrium terms have no contributions above order one.
"""
c_s_sq: sp.Expr = sp.Rational(1, 3)
"""
The squared lattice speed of sound used to derive the LB method. It is very uncommon to use a value different
to 1 / 3.
"""
weighted: bool = True
"""
Affects only orthogonal MRT methods. If set to True a weighted Gram-Schmidt procedure is used to orthogonalise
the moments.
"""
nested_moments: List[List] = None
"""
A list of lists of modes, grouped by common relaxation times. This is usually used in
conjunction with `lbmpy.methods.default_moment_sets.mrt_orthogonal_modes_literature`.
If this argument is not provided, Gram-Schmidt orthogonalisation of the default modes is performed.
"""
force_model: Union[AbstractForceModel, ForceModel] = None
"""
Force model to determine how forcing terms enter the collision rule.
Possibilities are defined in :class: `lbmpy.enums.ForceModel`
"""
force: Union[Tuple, Field] = (0, 0, 0)
"""
Either constant force or a symbolic expression depending on field value
"""
continuous_equilibrium: bool = True
"""
Way to compute equilibrium moments/cumulants, if False the standard discretised LBM equilibrium is used,
otherwise the equilibrium moments are computed from the continuous Maxwellian. This makes only a
difference if sparse stencils are used e.g. D2Q9 and D3Q27 are not affected, D319 and DQ15 are affected.
"""
maxwellian_moments: bool = None
"""
Deprecated and due for removal by version 0.5; use `continuous_equilibrium` instead.
"""
initial_velocity: Tuple = None,
"""
Initial velocity in domain, can either be a tuple (x,y,z) velocity to set a constant
velocity everywhere, or a numpy array with the same size of the domain, with a last coordinate of shape dim to set
velocities on cell level
"""
galilean_correction: bool = False
"""
Special correction for D3Q27 cumulant LBMs. For Details see
:mod:`lbmpy.methods.cumulantbased.galilean_correction`
"""
fourth_order_correction: Union[float, bool] = False
"""
Special correction for rendering D3Q27 cumulant LBMs fourth-order accurate in diffusion. For Details see
:mod:`lbmpy.methods.cumulantbased.fourth_order_correction`. If set to `True`, the fourth-order correction is
employed without limiters (or more precisely with a very high limiter, practically disabling the limiters). If this
variable is set to a number, the latter is used for the limiters (uniformly for omega_3, omega_4 and omega_5).
"""
collision_space_info: CollisionSpaceInfo = None
"""
Information about the LB method's collision space (see :class:`lbmpy.methods.creationfunctions.CollisionSpaceInfo`)
including the classes defining how populations are transformed to these spaces.
If left at `None`, it will be inferred according to the value of `method`.
If an instance of the :class:`lbmpy.enums.CollisionSpace` enum is passed, a
:class:`lbmpy.method.CollisionSpaceInfo` instance with the space's default setup is created.
Otherwise, the selected collision space must be in accord with the chosen :class:`lbmpy.enum.Method`.
"""
entropic: bool = False
"""
In case there are two distinct relaxation rate in a method, one of them (usually the one, not
determining the viscosity) can be automatically chosen w.r.t an entropy condition. For details see
:mod:`lbmpy.methods.momentbased.entropic`
"""
entropic_newton_iterations: int = None
"""
For moment methods the entropy optimum can be calculated in closed form.
For cumulant methods this is not possible, in that case it is computed using Newton iterations.
This parameter can be used to force Newton iterations and specify how many should be done
"""
omega_output_field: Field = None
"""
A pystencils Field can be passed here, where the calculated free relaxation rate of
an entropic or subgrid-scale method is written to
"""
eddy_viscosity_field: Field = None
"""
A pystencils Field can be passed here, where the eddy-viscosity of a subgrid-scale model is written.
"""
subgrid_scale_model: Union[SubgridScaleModel, tuple[SubgridScaleModel, float], tuple[SubgridScaleModel, int]] = None
"""
Choose a subgrid-scale model (SGS) for large-eddy simulations. ``omega_output_field`` can be set to
write out adapted relaxation rates. Either provide just the SGS and use the default model constants or provide a
tuple of the SGS and its corresponding model constant.
"""
cassons: CassonsParameters = False
"""
Adds the Cassons model according to https://doi.org/10.1007/s10955-005-8415-x
The parameters are set with the ``CassonsParameters`` dataclass.
"""
fluctuating: dict = False
"""
Enables fluctuating lattice Boltzmann by randomizing collision process.
Pass dictionary with parameters to ``lbmpy.fluctuatinglb.add_fluctuations_to_collision_rule``.
Can only be used for weighed MRT collision operators.
"""
temperature: Any = None
"""
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
fields. In each timestep the corresponding quantities are written to the given fields. Possible input would be:
{'density': density_field, 'velocity': velocity_field}
"""
velocity_input: Field = None
"""
Symbolic field where the velocities are read from. If `None` is given the velocity is calculated inplace from
with first order moments.
"""
density_input: Field = None
"""
Symbolic field where the density is read from. If `None` is given the density is calculated inplace from
with zeroth order moment.
"""
conserved_moments: bool = True
"""
If lower order moments are conserved or not. If velocity or density input is set the lower order moments are not
conserved anymore.
"""
kernel_type: Union[str, Type[PdfFieldAccessor]] = 'default_stream_collide'
"""
Supported values: ``'default_stream_collide'`` (default), ``'collide_only'``, ``'stream_pull_only'``.
With ``'default_stream_collide'``, streaming pattern and even/odd time-step (for in-place patterns) can be specified
by the ``streaming_pattern`` and ``timestep`` arguments. For backwards compatibility, ``kernel_type`` also accepts
``'stream_pull_collide'``, ``'collide_stream_push'``, ``'esotwist_even'``, ``'esotwist_odd'``, ``'aa_even'``
and ``'aa_odd'`` for selection of the streaming pattern.
"""
streaming_pattern: str = 'pull'
"""
The streaming pattern to be used with a ``'default_stream_collide'`` kernel. Accepted values are
``'pull'``, ``'push'``, ``'aa'`` and ``'esotwist'``.
"""
timestep: Timestep = Timestep.BOTH
"""
Timestep modulus for the streaming pattern. For two-fields patterns, this argument is irrelevant and
by default set to ``Timestep.BOTH``. For in-place patterns, ``Timestep.EVEN`` or ``Timestep.ODD`` must be specified.
"""
field_name: str = 'src'
"""
Name of the PDF field.
"""
temporary_field_name: str = 'dst'
"""
Name of the temporary PDF field.
"""
lb_method: Type[AbstractLbMethod] = None
"""
Instance of `lbmpy.methods.abstractlbmethod.AbstractLbMethod`. If this parameter is `None`, the lb_method is derived
via `create_lb_method`.
"""
collision_rule: LbmCollisionRule = None
"""
Instance of :class:`lbmpy.methods.LbmCollisionRule`. If this parameter is `None`,
the collision rule is derived via *create_lb_collision_rule*.
"""
update_rule: LbmCollisionRule = None
"""
Instance of :class:`lbmpy.methods.LbmCollisionRule`. If this parameter is `None`,
the update rule is derived via *create_lb_update_rule*.
"""
ast: KernelFunction = None
"""
Instance of *pystencils.KernelFunction*. If this parameter is `None`,
the ast is derived via `create_lb_ast`.
"""
def __post_init__(self):
if isinstance(self.method, str):
new_method = Method[self.method.upper()]
warn(f'Method "{self.method}" as str is deprecated. Use {new_method} instead')
self.method = new_method
if self.maxwellian_moments is not None:
warn("Argument 'maxwellian_moments' is deprecated and will be removed by version 0.5."
"Use `continuous_equilibrium` instead.")
self.continuous_equilibrium = self.maxwellian_moments
if not isinstance(self.stencil, LBStencil):
self.stencil = LBStencil(self.stencil)
if self.relaxation_rates is None:
# Fall back to regularized method
if self.relaxation_rate is None:
self.relaxation_rate = sp.Symbol("omega")
# if only a single relaxation rate is defined,
# it is internally treated as a list with one element and just sets the relaxation_rates parameter
if self.relaxation_rate is not None:
if self.method in [Method.TRT, Method.TRT_KBC_N1, Method.TRT_KBC_N2, Method.TRT_KBC_N3, Method.TRT_KBC_N4]:
self.relaxation_rates = [self.relaxation_rate,
relaxation_rate_from_magic_number(self.relaxation_rate)]
else:
self.relaxation_rates = [self.relaxation_rate]
# Incompressible cumulant method is not available
if not self.compressible and self.method in (Method.MONOMIAL_CUMULANT, Method.CUMULANT):
raise ValueError("Incompressible cumulant-based methods are not supported (yet).")
if self.zero_centered and self.entropic:
raise ValueError("Entropic methods can only be created with `zero_centered=False`.")
# Check or infer delta-equilibrium
if self.delta_equilibrium is not None:
# Must be zero-centered
if self.delta_equilibrium:
if not self.zero_centered:
raise ValueError("`delta_equilibrium=True` requires `zero_centered=True`!")
# Must not be a cumulant-method
if self.method in (Method.MONOMIAL_CUMULANT, Method.CUMULANT):
raise ValueError("Cannot create a cumulant-based method from a delta-equilibrium!")
else:
if self.zero_centered:
if self.method in (Method.CENTRAL_MOMENT, Method.MONOMIAL_CUMULANT, Method.CUMULANT):
self.delta_equilibrium = False
else:
self.delta_equilibrium = True
else:
self.delta_equilibrium = False
# Check or infer collision space
if isinstance(self.collision_space_info, CollisionSpace):
self.collision_space_info = CollisionSpaceInfo(self.collision_space_info)
if self.collision_space_info is not None:
if (self.entropic or self.fluctuating) \
and self.collision_space_info.collision_space != CollisionSpace.POPULATIONS:
# Workaround until entropic method supports relaxation in subexpressions
# and the problem with RNGs in the assignment collection has been solved
raise ValueError("Entropic and Fluctuating methods are only available in population space.")
elif not self.collision_space_info.collision_space.compatible(self.method):
raise ValueError("Given method is not compatible with given collision space.")
else:
if self.method in {Method.SRT, Method.TRT,
Method.TRT_KBC_N1, Method.TRT_KBC_N2, Method.TRT_KBC_N3, Method.TRT_KBC_N4}:
self.collision_space_info = CollisionSpaceInfo(CollisionSpace.POPULATIONS)
elif self.entropic or self.fluctuating:
self.collision_space_info = CollisionSpaceInfo(CollisionSpace.POPULATIONS)
elif self.method in {Method.MRT_RAW, Method.MRT}:
self.collision_space_info = CollisionSpaceInfo(CollisionSpace.RAW_MOMENTS)
elif self.method in {Method.CENTRAL_MOMENT}:
self.collision_space_info = CollisionSpaceInfo(CollisionSpace.CENTRAL_MOMENTS)
elif self.method in {Method.MONOMIAL_CUMULANT, Method.CUMULANT}:
self.collision_space_info = CollisionSpaceInfo(CollisionSpace.CUMULANTS)
else:
raise Exception(f"No default collision space is given for method {self.method}."
"This is a bug; please report it to the developers.")
# for backwards compatibility
kernel_type_to_streaming_pattern = {
'stream_pull_collide': ('pull', Timestep.BOTH),
'collide_stream_push': ('push', Timestep.BOTH),
'aa_even': ('aa', Timestep.EVEN),
'aa_odd': ('aa', Timestep.ODD),
'esotwist_even': ('esotwist', Timestep.EVEN),
'esotwist_odd': ('esotwist', Timestep.ODD)
}
if self.kernel_type in kernel_type_to_streaming_pattern.keys():
self.streaming_pattern, self.timestep = kernel_type_to_streaming_pattern[self.kernel_type]
self.kernel_type = 'default_stream_collide'
if isinstance(self.force, Field):
self.force = tuple([self.force(i) for i in range(self.stencil.D)])
force_not_zero = False
for f_i in self.force:
if f_i != 0:
force_not_zero = True
if self.force_model is None and force_not_zero:
if self.method == Method.CUMULANT:
self.force_model = forcemodels.CentralMoment(self.force[:self.stencil.D])
else:
self.force_model = forcemodels.Guo(self.force[:self.stencil.D])
force_model_dict = {
'simple': forcemodels.Simple,
'luo': forcemodels.Luo,
'guo': forcemodels.Guo,
'schiller': forcemodels.Guo,
'buick': forcemodels.Buick,
'silva': forcemodels.Buick,
'edm': forcemodels.EDM,
'kupershtokh': forcemodels.EDM,
'he': forcemodels.He,
'shanchen': forcemodels.ShanChen,
'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_symbol) * 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 '
f'provide a class of type AbstractForceModel', category=DeprecationWarning)
force_model_class = force_model_dict[new_force_model.name.lower()]
self.force_model = force_model_class(force=self.force[:self.stencil.D])
elif isinstance(self.force_model, ForceModel):
force_model_class = force_model_dict[self.force_model.name.lower()]
self.force_model = force_model_class(force=self.force[:self.stencil.D])
if self.density_input or self.velocity_input:
self.conserved_moments = False
@dataclass
class LBMOptimisation:
"""
**Below all parameters for the LBMOptimisation are explained**
"""
cse_pdfs: bool = False
"""
Run common subexpression elimination for opposing stencil directions.
"""
cse_global: bool = False
"""
Run common subexpression elimination after all other simplifications have been executed.
"""
simplification: Union[str, bool, SimplificationStrategy] = 'auto'
"""
Simplifications applied during the derivation of the collision rule. If ``True`` or ``'auto'``,
a default simplification strategy is selected according to the type of the method;
see :func:`lbmpy.simplificationfactory.create_simplification_strategy`.
If ``False``, no simplification is applied.
Otherwise, the given simplification strategy will be applied.
"""
pre_simplification: bool = True
"""
Simplifications applied during the derivation of the collision rule for cumulant LBMs.
For details see :mod:`lbmpy.moment_transforms`.
"""
split: bool = False
"""
Split innermost loop, to handle only two directions per loop. This reduces the number of parallel
load/store streams and thus speeds up the kernel on most architectures.
"""
field_size: Any = None
"""
Create kernel for fixed field size.
"""
field_layout: str = 'fzyx'
"""
``'c'`` or ``'numpy'`` for standard numpy layout, ``'reverse_numpy'`` or ``'f'`` for fortran
layout, this does not apply when pdf_arr was given, then the same layout as pdf_arr is used.
"""
symbolic_field: pystencils.field.Field = None
"""
Pystencils field for source (pdf field that is read)
"""
symbolic_temporary_field: pystencils.field.Field = None
"""
Pystencils field for temporary (pdf field that is written in stream, or stream-collide)
"""
builtin_periodicity: Tuple[bool] = (False, False, False)
"""
Instead of handling periodicity by copying ghost layers, the periodicity
is built into the kernel. This parameters specifies if the domain is periodic in (x,y,z) direction. Even if the
periodicity is built into the kernel, the fields have one ghost layer to be consistent with other functions.
"""
def create_lb_function(ast=None, lbm_config=None, lbm_optimisation=None, config=None, optimization=None, **kwargs):
"""Creates a Python function for the LB method"""
lbm_config, lbm_optimisation, config = update_with_default_parameters(kwargs, optimization,
lbm_config, lbm_optimisation, config)
if lbm_config.ast is not None:
ast = lbm_config.ast
if ast is None:
ast = create_lb_ast(lbm_config.update_rule, lbm_config=lbm_config,
lbm_optimisation=lbm_optimisation, config=config)
res = ast.compile()
res.method = ast.method
res.update_rule = ast.update_rule
return res
def create_lb_ast(update_rule=None, lbm_config=None, lbm_optimisation=None, config=None, optimization=None, **kwargs):
"""Creates a pystencils AST for the LB method"""
lbm_config, lbm_optimisation, config = update_with_default_parameters(kwargs, optimization,
lbm_config, lbm_optimisation, config)
if lbm_config.update_rule is not None:
update_rule = lbm_config.update_rule
if update_rule is None:
update_rule = create_lb_update_rule(lbm_config.collision_rule, lbm_config=lbm_config,
lbm_optimisation=lbm_optimisation, config=config)
config = replace(config, ghost_layers=1)
ast = create_kernel(update_rule, config=config)
ast.method = update_rule.method
ast.update_rule = update_rule
lbm_config.ast = ast
return ast
@disk_cache_no_fallback
def create_lb_update_rule(collision_rule=None, lbm_config=None, lbm_optimisation=None, config=None,
optimization=None, **kwargs):
"""Creates an update rule (list of Assignments) for a LB method that describe a full sweep"""
lbm_config, lbm_optimisation, config = update_with_default_parameters(kwargs, optimization,
lbm_config, lbm_optimisation, config)
if lbm_config.collision_rule is not None:
collision_rule = lbm_config.collision_rule
if collision_rule is None:
collision_rule = create_lb_collision_rule(lbm_config.lb_method, lbm_config=lbm_config,
lbm_optimisation=lbm_optimisation,
config=config)
lb_method = collision_rule.method
if IS_PYSTENCILS_2:
fallback_field_data_type = config.get_option("default_dtype")
else:
fallback_field_data_type = config.data_type[lbm_config.field_name].numpy_dtype
q = collision_rule.method.stencil.Q
if lbm_optimisation.symbolic_field is not None:
src_field = lbm_optimisation.symbolic_field
elif lbm_optimisation.field_size:
field_size = tuple([s + 2 for s in lbm_optimisation.field_size] + [q])
src_field = Field.create_fixed_size(lbm_config.field_name, field_size, index_dimensions=1,
layout=lbm_optimisation.field_layout, dtype=fallback_field_data_type)
else:
src_field = Field.create_generic(lbm_config.field_name, spatial_dimensions=collision_rule.method.dim,
index_shape=(q,), layout=lbm_optimisation.field_layout,
dtype=fallback_field_data_type)
if lbm_optimisation.symbolic_temporary_field is not None:
dst_field = lbm_optimisation.symbolic_temporary_field
else:
dst_field = src_field.new_field_with_different_name(lbm_config.temporary_field_name)
kernel_type = lbm_config.kernel_type
if kernel_type == 'stream_pull_only':
update_rule = create_stream_pull_with_output_kernel(lb_method, src_field, dst_field, lbm_config.output)
else:
if kernel_type == 'default_stream_collide':
if lbm_config.streaming_pattern == 'pull' and any(lbm_optimisation.builtin_periodicity):
accessor = PeriodicTwoFieldsAccessor(lbm_optimisation.builtin_periodicity, ghost_layers=1)
else:
accessor = get_accessor(lbm_config.streaming_pattern, lbm_config.timestep)
elif kernel_type == 'collide_only':
accessor = CollideOnlyInplaceAccessor
elif isinstance(kernel_type, PdfFieldAccessor):
accessor = kernel_type
else:
raise ValueError("Invalid value of parameter 'kernel_type'", lbm_config.kernel_type)
update_rule = create_lbm_kernel(collision_rule, src_field, dst_field, accessor)
lbm_config.update_rule = update_rule
return update_rule
@disk_cache_no_fallback
def create_lb_collision_rule(lb_method=None, lbm_config=None, lbm_optimisation=None, config=None,
optimization=None, **kwargs):
"""Creates a collision rule (list of Assignments) for a LB method describing the collision operator (no stream)"""
lbm_config, lbm_optimisation, config = update_with_default_parameters(kwargs, optimization,
lbm_config, lbm_optimisation, config)
if lbm_config.lb_method is not None:
lb_method = lbm_config.lb_method
if lb_method is None:
lb_method = create_lb_method(lbm_config)
cqc = lb_method.conserved_quantity_computation
rho_in = lbm_config.density_input
u_in = lbm_config.velocity_input
if u_in is not None and isinstance(u_in, Field):
u_in = u_in.center_vector
if rho_in is not None and isinstance(rho_in, Field):
rho_in = rho_in.center
pre_simplification = lbm_optimisation.pre_simplification
if rho_in is not None or u_in is not None:
cqe = cqc.equilibrium_input_equations_from_pdfs(lb_method.pre_collision_pdf_symbols)
cqe_main_assignments = cqe.main_assignments_dict
if rho_in is not None:
if u_in is None:
raise ValueError("When setting 'density_input' parameter, "
"'velocity_input' has to be specified as well.")
cqe_main_assignments[cqc.density_symbol] = rho_in
cqe_main_assignments[cqc.density_deviation_symbol] = rho_in - cqc.background_density
if u_in is not None:
for u_sym, u in zip(cqc.velocity_symbols, u_in):
cqe_main_assignments[u_sym] = u
cqe.set_main_assignments_from_dict(cqe_main_assignments)
cqe = cqe.new_without_unused_subexpressions()
collision_rule = lb_method.get_collision_rule(conserved_quantity_equations=cqe,
pre_simplification=pre_simplification)
else:
collision_rule = lb_method.get_collision_rule(pre_simplification=pre_simplification)
if lbm_config.galilean_correction:
from lbmpy.methods.cumulantbased import add_galilean_correction
collision_rule = add_galilean_correction(collision_rule)
if lbm_config.fourth_order_correction:
from lbmpy.methods.cumulantbased import add_fourth_order_correction
# must provide a second relaxation rate in implementation; defaults to 1
if len(lbm_config.relaxation_rates) == 1:
lbm_config.relaxation_rates.append(1)
cumulant_limiter = 1e6 if lbm_config.fourth_order_correction is True else lbm_config.fourth_order_correction
collision_rule = add_fourth_order_correction(collision_rule=collision_rule,
shear_relaxation_rate=lbm_config.relaxation_rates[0],
bulk_relaxation_rate=lbm_config.relaxation_rates[1],
limiter=cumulant_limiter)
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 = replace_by_psm_collision_rule(collision_rule, lbm_config.psm_config)
if lbm_config.entropic:
if lbm_config.subgrid_scale_model or lbm_config.cassons:
raise ValueError("Choose either entropic, subgrid-scale or cassons")
if lbm_config.entropic_newton_iterations:
if isinstance(lbm_config.entropic_newton_iterations, bool):
iterations = 3
else:
iterations = lbm_config.entropic_newton_iterations
collision_rule = add_iterative_entropy_condition(collision_rule, newton_iterations=iterations,
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.subgrid_scale_model:
if lbm_config.cassons:
raise ValueError("Cassons model can not be combined with a subgrid-scale model")
model_constant = None
sgs_model = lbm_config.subgrid_scale_model
if isinstance(lbm_config.subgrid_scale_model, tuple):
sgs_model = lbm_config.subgrid_scale_model[0]
model_constant = lbm_config.subgrid_scale_model[1]
collision_rule = add_sgs_model(collision_rule=collision_rule, subgrid_scale_model=sgs_model,
model_constant=model_constant, omega_output_field=lbm_config.omega_output_field,
eddy_viscosity_field=lbm_config.eddy_viscosity_field)
if 'split_groups' in collision_rule.simplification_hints:
collision_rule.simplification_hints['split_groups'][0].append(sp.Symbol("sgs_omega"))
elif lbm_config.cassons:
collision_rule = add_cassons_model(collision_rule, parameter=lbm_config.cassons,
omega_output_field=lbm_config.omega_output_field)
if lbm_config.output:
output_eqs = cqc.output_equations_from_pdfs(lb_method.pre_collision_pdf_symbols, lbm_config.output)
collision_rule = collision_rule.new_merged(output_eqs)
if lbm_optimisation.simplification is True or lbm_optimisation.simplification == 'auto':
simplification = create_simplification_strategy(lb_method, split_inner_loop=lbm_optimisation.split)
elif callable(lbm_optimisation.simplification):
simplification = lbm_optimisation.simplification
else:
simplification = SimplificationStrategy()
collision_rule = simplification(collision_rule)
if isinstance(collision_rule.method, CumulantBasedLbMethod):
from lbmpy.methods.cumulantbased.cumulant_simplifications import check_for_logarithms
check_for_logarithms(collision_rule)
if lbm_config.fluctuating:
add_fluctuations_to_collision_rule(collision_rule, **lbm_config.fluctuating)
if lbm_optimisation.cse_pdfs:
from lbmpy.methods.momentbased.momentbasedsimplifications import cse_in_opposing_directions
collision_rule = cse_in_opposing_directions(collision_rule)
if lbm_optimisation.cse_global:
collision_rule = sympy_cse(collision_rule)
lbm_config.collision_rule = collision_rule
return collision_rule
def create_lb_method(lbm_config=None, **params):
"""Creates a LB method, defined by moments/cumulants for collision space, equilibrium and relaxation rates."""
lbm_config, _, _ = update_with_default_parameters(params, lbm_config=lbm_config)
relaxation_rates = lbm_config.relaxation_rates
dim = lbm_config.stencil.D
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_symbol
common_params = {
'compressible': lbm_config.compressible,
'zero_centered': lbm_config.zero_centered,
'delta_equilibrium': lbm_config.delta_equilibrium,
'equilibrium_order': lbm_config.equilibrium_order,
'force_model': lbm_config.force_model,
'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 = {
'zero_centered': lbm_config.zero_centered,
'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:
assert len(relaxation_rates) >= 1, "Not enough relaxation rates"
method = create_srt(lbm_config.stencil, relaxation_rates[0], **common_params)
elif lbm_config.method == Method.TRT:
assert len(relaxation_rates) >= 2, "Not enough relaxation rates"
method = create_trt(lbm_config.stencil, relaxation_rates[0], relaxation_rates[1], **common_params)
elif lbm_config.method == Method.MRT:
method = create_mrt_orthogonal(lbm_config.stencil, relaxation_rates, weighted=lbm_config.weighted,
nested_moments=lbm_config.nested_moments,
conserved_moments=lbm_config.conserved_moments, **common_params)
elif lbm_config.method == Method.CENTRAL_MOMENT:
method = create_central_moment(lbm_config.stencil, relaxation_rates,
nested_moments=lbm_config.nested_moments,
conserved_moments=lbm_config.conserved_moments, **common_params)
elif lbm_config.method == Method.MRT_RAW:
method = create_mrt_raw(lbm_config.stencil, relaxation_rates,
conserved_moments=lbm_config.conserved_moments, **common_params)
elif lbm_config.method in (Method.TRT_KBC_N1, Method.TRT_KBC_N2, Method.TRT_KBC_N3, Method.TRT_KBC_N4):
if lbm_config.stencil.D == 2 and lbm_config.stencil.Q == 9:
dim = 2
elif lbm_config.stencil.D == 3 and lbm_config.stencil.Q == 27:
dim = 3
else:
raise NotImplementedError("KBC type TRT methods can only be constructed for D2Q9 and D3Q27 stencils")
method_nr = lbm_config.method.name[-1]
method = create_trt_kbc(dim, relaxation_rates[0], relaxation_rates[1], 'KBC-N' + method_nr, **common_params)
elif lbm_config.method == Method.CUMULANT:
if lbm_config.fourth_order_correction:
if lbm_config.stencil.D != 3 and lbm_config.stencil.Q != 27:
raise ValueError("Fourth-order correction can only be applied to D3Q27 cumulant methods.")
assert len(relaxation_rates) <= 2, "Optimal parametrisation for fourth-order cumulants needs either one " \
"or two relaxation rates, associated with the shear (and bulk) " \
"viscosity. All other relaxation rates are automatically chosen " \
"optimally"
# define method in terms of symbolic relaxation rates and assign optimal values later
from lbmpy.methods.cumulantbased.fourth_order_correction import FOURTH_ORDER_RELAXATION_RATE_SYMBOLS
relaxation_rates = FOURTH_ORDER_RELAXATION_RATE_SYMBOLS
if lbm_config.nested_moments is not None:
method = create_cumulant(lbm_config.stencil, relaxation_rates, lbm_config.nested_moments,
conserved_moments=lbm_config.conserved_moments, **cumulant_params)
else:
method = create_with_default_polynomial_cumulants(lbm_config.stencil, relaxation_rates, **cumulant_params)
elif lbm_config.method == Method.MONOMIAL_CUMULANT:
method = create_with_monomial_cumulants(lbm_config.stencil, relaxation_rates,
conserved_moments=lbm_config.conserved_moments, **cumulant_params)
else:
raise ValueError("Failed to create LB method. Please use lbmpy.enums.Method for the creation")
# >>Entropic methods can only be created for methods with two relaxation rates One free relaxation rate
# determining the viscosity and one to be determined by the entropy condition<<
# Thus we fix the conserved quantities to one of the relaxation rates because zero would be recognised as
# a third relaxation rate here.
if lbm_config.entropic:
method.set_conserved_moments_relaxation_rate(relaxation_rates[0])
lbm_config.lb_method = method
return method
def create_psm_update_rule(lbm_config, lbm_optimisation):
if IS_PYSTENCILS_2:
raise NotImplementedError(
"`create_psm_update_rule` is not yet available when using pystencils 2.0. "
"To instead derive a (potentially less efficient) PSM kernel without branches, "
"use `create_lb_update_rule` with a `PsmConfig` object instead."
)
from pystencils.astnodes import Conditional, Block
from pystencils.node_collection import NodeCollection
if lbm_config.psm_config is None:
raise ValueError("Specify a PSM Config in the LBM Config, when creating a psm update rule")
config_without_particles = copy.deepcopy(lbm_config)
config_without_particles.psm_config.max_particles_per_cell = 0
lb_update_rule = create_lb_update_rule(
lbm_config=config_without_particles, lbm_optimisation=lbm_optimisation)
node_collection = lb_update_rule.all_assignments
if lbm_config.psm_config.individual_fraction_field is None:
assert lbm_config.psm_config.max_particles_per_cell == 1
fraction_field = lbm_config.psm_config.fraction_field
else:
fraction_field = lbm_config.psm_config.individual_fraction_field
for p in range(lbm_config.psm_config.max_particles_per_cell):
psm_solid_collision = add_psm_solid_collision_to_collision_rule(lb_update_rule, lbm_config, p)
psm_update_rule = create_lb_update_rule(
collision_rule=psm_solid_collision, lbm_config=lbm_config, lbm_optimisation=lbm_optimisation)
node_collection.append(
Conditional(
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
pystencils_config_params = ['target', 'backend', 'cpu_openmp', 'double_precision', 'gpu_indexing',
'gpu_indexing_params', 'cpu_vectorize_info']
if opt_params is not None:
config_params = {k: v for k, v in opt_params.items() if k in pystencils_config_params}
else:
config_params = {}
if 'double_precision' in config_params:
if config_params['double_precision']:
config_params['data_type'] = 'float64'
else:
config_params['data_type'] = 'float32'
del config_params['double_precision']
if not config:
config = CreateKernelConfig(**config_params)
else:
for k, v in config_params.items():
if not hasattr(config, k):
raise KeyError(f'{v} is not a valid kwarg. Please look in CreateKernelConfig for valid settings')
config = replace(config, **config_params)
lbm_opt_params = ['cse_pdfs', 'cse_global', 'simplification', 'pre_simplification', 'split', 'field_size',
'field_layout', 'symbolic_field', 'symbolic_temporary_field', 'builtin_periodicity']
if opt_params is not None:
opt_params_dict = {k: v for k, v in opt_params.items() if k in lbm_opt_params}
else:
opt_params_dict = {}
if not lbm_optimisation:
lbm_optimisation = LBMOptimisation(**opt_params_dict)
else:
for k, v in opt_params_dict.items():
if not hasattr(lbm_optimisation, k):
raise KeyError(f'{v} is not a valid kwarg. Please look in LBMOptimisation for valid settings')
lbm_optimisation = replace(lbm_optimisation, **opt_params_dict)
if params is None:
params = {}
if not lbm_config:
lbm_config = LBMConfig(**params)
else:
for k, v in params.items():
if not hasattr(lbm_config, k):
raise KeyError(f'{v} is not a valid kwarg. Please look in LBMConfig for valid settings')
lbm_config = replace(lbm_config, **params)
return lbm_config, lbm_optimisation, config
...@@ -102,7 +102,7 @@ def __cumulant_raw_moment_transform(index, dependent_var_dict, outer_function, d ...@@ -102,7 +102,7 @@ def __cumulant_raw_moment_transform(index, dependent_var_dict, outer_function, d
@memorycache(maxsize=16) @memorycache(maxsize=16)
def __get_discrete_cumulant_generating_function(func, stencil, wave_numbers): def __get_discrete_cumulant_generating_function(func, stencil, wave_numbers):
assert len(stencil) == len(func) assert stencil.Q == len(func)
laplace_transformation = sum([factor * sp.exp(scalar_product(wave_numbers, e)) for factor, e in zip(func, stencil)]) laplace_transformation = sum([factor * sp.exp(scalar_product(wave_numbers, e)) for factor, e in zip(func, stencil)])
return sp.ln(laplace_transformation) return sp.ln(laplace_transformation)
...@@ -121,10 +121,10 @@ def discrete_cumulant(func, cumulant, stencil): ...@@ -121,10 +121,10 @@ def discrete_cumulant(func, cumulant, stencil):
(similar to moment description) (similar to moment description)
stencil: sequence of directions stencil: sequence of directions
""" """
assert len(stencil) == len(func) assert stencil.Q == len(func)
dim = len(stencil[0]) dim = len(stencil[0])
wave_numbers = tuple([sp.Symbol("Xi_%d" % (i,)) for i in range(dim)]) wave_numbers = sp.symbols(f"Xi_:{dim}")
generating_function = __get_discrete_cumulant_generating_function(func, stencil, wave_numbers) generating_function = __get_discrete_cumulant_generating_function(func, stencil, wave_numbers)
if type(cumulant) is tuple: if type(cumulant) is tuple:
...@@ -157,9 +157,9 @@ def cumulants_from_pdfs(stencil, cumulant_indices=None, pdf_symbols=None): ...@@ -157,9 +157,9 @@ def cumulants_from_pdfs(stencil, cumulant_indices=None, pdf_symbols=None):
dim = len(stencil[0]) dim = len(stencil[0])
if cumulant_indices is None: if cumulant_indices is None:
cumulant_indices = moments_up_to_component_order(2, dim=dim) cumulant_indices = moments_up_to_component_order(2, dim=dim)
assert len(stencil) == len(cumulant_indices), "Stencil has to have same length as cumulant_indices sequence" assert stencil.Q == len(cumulant_indices), "Stencil has to have same length as cumulant_indices sequence"
if pdf_symbols is None: if pdf_symbols is None:
pdf_symbols = __get_indexed_symbols(pdf_symbols, "f", range(len(stencil))) pdf_symbols = __get_indexed_symbols(pdf_symbols, "f", range(stencil.Q))
return {idx: discrete_cumulant(tuple(pdf_symbols), idx, stencil) for idx in cumulant_indices} return {idx: discrete_cumulant(tuple(pdf_symbols), idx, stencil) for idx in cumulant_indices}
......
import numpy as np
import sympy as sp
from ._compat import IS_PYSTENCILS_2
if IS_PYSTENCILS_2:
raise ImportError("`lbmpy.custom_code_nodes` is only available when running with pystencils 1.x")
from pystencils.typing import TypedSymbol, create_type
from pystencils.backends.cbackend import CustomCodeNode
class NeighbourOffsetArrays(CustomCodeNode):
@staticmethod
def neighbour_offset(dir_idx, stencil):
if isinstance(sp.sympify(dir_idx), sp.Integer):
return stencil[dir_idx]
else:
return tuple([sp.IndexedBase(symbol, shape=(1,))[dir_idx]
for symbol in NeighbourOffsetArrays._offset_symbols(len(stencil[0]))])
@staticmethod
def _offset_symbols(dim):
return [TypedSymbol(f"neighbour_offset_{d}", create_type('int32')) for d in ['x', 'y', 'z'][:dim]]
def __init__(self, stencil, offsets_dtype=np.int32):
offsets_dtype = create_type(offsets_dtype)
dim = len(stencil[0])
array_symbols = NeighbourOffsetArrays._offset_symbols(dim)
code = "\n"
for i, arrsymb in enumerate(array_symbols):
code += _array_pattern(offsets_dtype, arrsymb.name, (d[i] for d in stencil))
offset_symbols = NeighbourOffsetArrays._offset_symbols(dim)
super(NeighbourOffsetArrays, self).__init__(code, symbols_read=set(),
symbols_defined=set(offset_symbols))
class MirroredStencilDirections(CustomCodeNode):
@staticmethod
def mirror_stencil(direction, mirror_axis):
assert mirror_axis <= len(direction), f"only {len(direction)} axis available for mirage"
direction = list(direction)
direction[mirror_axis] = -direction[mirror_axis]
return tuple(direction)
@staticmethod
def _mirrored_symbol(mirror_axis, _stencil):
axis = ['x', 'y', 'z']
return TypedSymbol(f"{axis[mirror_axis]}_axis_mirrored_stencil_dir", create_type('int32'))
def __init__(self, stencil, mirror_axis, dtype=np.int32):
offsets_dtype = create_type(dtype)
mirrored_stencil_symbol = MirroredStencilDirections._mirrored_symbol(mirror_axis, stencil)
mirrored_directions = [stencil.index(MirroredStencilDirections.mirror_stencil(direction, mirror_axis))
for direction in stencil]
code = "\n"
code += _array_pattern(offsets_dtype, mirrored_stencil_symbol.name, mirrored_directions)
super(MirroredStencilDirections, self).__init__(code, symbols_read=set(),
symbols_defined={mirrored_stencil_symbol})
class LbmWeightInfo(CustomCodeNode):
def __init__(self, lb_method, data_type='double'):
self.weights_symbol = TypedSymbol("weights", data_type)
weights = [f"(({self.weights_symbol.dtype.c_name})({str(w.evalf(17))}))" for w in lb_method.weights]
weights = ", ".join(weights)
w_sym = self.weights_symbol
code = f"const {self.weights_symbol.dtype.c_name} {w_sym.name} [] = {{{weights}}};\n"
super(LbmWeightInfo, self).__init__(code, symbols_read=set(), symbols_defined={w_sym})
def weight_of_direction(self, dir_idx, lb_method=None):
if isinstance(sp.sympify(dir_idx), sp.Integer):
return lb_method.weights[dir_idx].evalf(17)
else:
return sp.IndexedBase(self.weights_symbol, shape=(1,))[dir_idx]
class TranslationArraysNode(CustomCodeNode):
def __init__(self, array_content, symbols_defined):
code = ''
for content in array_content:
code += _array_pattern(*content)
super(TranslationArraysNode, self).__init__(code, symbols_read=set(), symbols_defined=symbols_defined)
def __str__(self):
return "Variable PDF Access Translation Arrays"
def __repr__(self):
return "Variable PDF Access Translation Arrays"
def _array_pattern(dtype, name, content):
return f"const {str(dtype)} {name} [] = {{ {','.join(str(c) for c in content)} }}; \n"
import json
import six
import inspect
from pystencils.runhelper.db import PystencilsJsonEncoder
from pystencils.simp import SimplificationStrategy
from lbmpy import LBStencil, Method, CollisionSpace, SubgridScaleModel
from lbmpy.creationfunctions import LBMConfig, LBMOptimisation
from lbmpy.methods import CollisionSpaceInfo
from lbmpy.forcemodels import AbstractForceModel
from lbmpy.non_newtonian_models import CassonsParameters
class LbmpyJsonEncoder(PystencilsJsonEncoder):
def default(self, obj):
if isinstance(obj, (LBMConfig, LBMOptimisation, CollisionSpaceInfo, CassonsParameters)):
return obj.__dict__
if isinstance(obj, (LBStencil, Method, CollisionSpace, SubgridScaleModel)):
return obj.name
if isinstance(obj, AbstractForceModel):
return obj.__class__.__name__
if isinstance(obj, SimplificationStrategy):
return obj.__str__()
if inspect.isclass(obj):
return obj.__name__
return PystencilsJsonEncoder.default(self, obj)
class LbmpyJsonSerializer(object):
@classmethod
def serialize(cls, data):
if six.PY3:
if isinstance(data, bytes):
return json.dumps(data.decode('utf-8'), cls=LbmpyJsonEncoder, ensure_ascii=False).encode('utf-8')
else:
return json.dumps(data, cls=LbmpyJsonEncoder, ensure_ascii=False).encode('utf-8')
else:
return json.dumps(data, cls=LbmpyJsonEncoder, ensure_ascii=False).encode('utf-8')
@classmethod
def deserialize(cls, data):
if six.PY3:
return json.loads(data.decode('utf-8'))
else:
return json.loads(data.decode('utf-8'))
from enum import Enum, auto
class Stencil(Enum):
"""
The Stencil enumeration represents all possible lattice Boltzmann stencils that are available in lbmpy.
It should be passed to :class:`lbmpy.stencils.LBStenil`. This class then creates a stencils representation
containing the concrete neighbour directions as a tuple of tuples.
The number of spatial dimensions *d* and the number of discrete velocities *q* are stated in the DdQq notation
"""
D2Q9 = auto()
"""
A two dimensional stencil using 9 discrete velocities.
"""
D2V17 = auto()
"""
A two dimensional stencil using 17 discrete velocities. (long range stencil).
"""
D2V37 = auto()
"""
A two dimensional stencil using 37 discrete velocities. (long range stencil).
"""
D3Q7 = auto()
"""
A three dimensional stencil using 7 discrete velocities.
"""
D3Q15 = auto()
"""
A three dimensional stencil using 15 discrete velocities.
"""
D3Q19 = auto()
"""
A three dimensional stencil using 19 discrete velocities.
"""
D3Q27 = auto()
"""
A three dimensional stencil using 27 discrete velocities.
"""
class Method(Enum):
"""
The Method enumeration represents all possible lattice Boltzmann collision operators that are available in lbmpy.
It should be passed to :class:`lbmpy.creationfunctions.LBMConfig`. The LBM configuration *dataclass* then derives
the respective collision equations when passed to the creations functions in the `lbmpy.creationfunctions`
module of lbmpy.
Note here, when using a specific enumeration to derive a particular LBM collision operator,
different parameters of the :class:`lbmpy.creationfunctions.LBMConfig` might become necessary.
For example, it does not make sense to define *relaxation_rates* for a single relaxation rate method, which
is essential for multiple relaxation rate methods. Important specific parameters are listed below to the enum value.
A specific creation function is stated for each case which explains these parameters in detail.
"""
SRT = auto()
"""
See :func:`lbmpy.methods.create_srt`,
Single relaxation time method
"""
TRT = auto()
"""
See :func:`lbmpy.methods.create_trt`,
Two relaxation time, the first relaxation rate is for even moments and determines the
viscosity (as in SRT). The second relaxation rate is used for relaxing odd moments and controls the
bulk viscosity. For details in the TRT collision operator see :cite:`TRT`
"""
MRT_RAW = auto()
"""
See :func:`lbmpy.methods.create_mrt_raw`,
Non-orthogonal MRT where all relaxation rates can be specified independently, i.e. there are as many relaxation
rates as stencil entries. Look at the generated method in Jupyter to see which moment<->relaxation rate mapping.
Originally defined in :cite:`raw_moments`
"""
MRT = auto()
"""
See :func:`lbmpy.methods.create_mrt_orthogonal`
Orthogonal multi relaxation time model, relaxation rates are used in this order for *shear modes*, *bulk modes*,
*third-order modes*, *fourth-order modes*, etc. Requires also a parameter *weighted* that should be `True` if the
moments should be orthogonal w.r.t. weighted scalar product using the lattice weights. If `False`, the normal
scalar product is used. For custom definition of the method, a *nested_moments* can be passed.
For example: [ [1, x, y], [x*y, x**2, y**2], ... ] that groups all moments together that should be relaxed
at the same rate. Literature values of this list can be obtained through
:func:`lbmpy.methods.creationfunctions.mrt_orthogonal_modes_literature`.
WMRT collision operators are reported to be numerically more stable and more accurate,
whilst also having a lower computational cos :cite:`FAKHARI201722`
"""
CENTRAL_MOMENT = auto()
"""
See :func:`lbmpy.methods.create_central_moment`
Creates moment based LB method where the collision takes place in the central moment space. By default,
a raw-moment set is used where the bulk and the shear viscosity are separated. An original derivation can be
found in :cite:`Geier2006`
"""
TRT_KBC_N1 = auto()
"""
See :func:`lbmpy.methods.create_trt_kbc`
Particular two-relaxation rate method. This is not the entropic method yet, only the relaxation pattern.
To get the entropic method also *entropic* needs to be set to `True`.
There are four KBC methods available in lbmpy. The naming is according to :cite:`karlin2015entropic`
"""
TRT_KBC_N2 = auto()
"""
See :func:`lbmpy.methods.create_trt_kbc`
Particular two-relaxation rate method. This is not the entropic method yet, only the relaxation pattern.
To get the entropic method also *entropic* needs to be set to `True`.
There are four KBC methods available in lbmpy. The naming is according to :cite:`karlin2015entropic`
"""
TRT_KBC_N3 = auto()
"""
See :func:`lbmpy.methods.create_trt_kbc`
Particular two-relaxation rate method. This is not the entropic method yet, only the relaxation pattern.
To get the entropic method also *entropic* needs to be set to `True`.
There are four KBC methods available in lbmpy. The naming is according to :cite:`karlin2015entropic`
"""
TRT_KBC_N4 = auto()
"""
See :func:`lbmpy.methods.create_trt_kbc`
Particular two-relaxation rate method. This is not the entropic method yet, only the relaxation pattern.
To get the entropic method also *entropic* needs to be set to `True`.
There are four KBC methods available in lbmpy. The naming is according to :cite:`karlin2015entropic`
"""
CUMULANT = auto()
"""
See :func:`lbmpy.methods.create_with_default_polynomial_cumulants`
Cumulant-based LB method which relaxes groups of polynomial cumulants chosen to optimize rotational invariance.
For details on the method see :cite:`geier2015`
"""
MONOMIAL_CUMULANT = auto()
"""
See :func:`lbmpy.methods.create_with_monomial_cumulants`
Cumulant-based LB method which relaxes monomial cumulants.
For details on the method see :cite:`geier2015` and :cite:`Coreixas2019`
"""
class CollisionSpace(Enum):
"""
The CollisionSpace enumeration lists all possible spaces for collision to occur in.
"""
POPULATIONS = auto()
"""
Population space, meaning post-collision populations are obtained directly by relaxation of linear combinations of
pre-collision populations. Default for `lbmpy.enums.Method.SRT` and `lbmpy.enums.Method.TRT`.
Results in the creation of an instance of :class:`lbmpy.methods.momentbased.MomentBasedLbMethod`.
"""
RAW_MOMENTS = auto()
"""
Raw moment space, meaning relaxation is applied to a set of linearly independent, polynomial raw moments of the
discrete population vector. Default for `lbmpy.enums.Method.MRT`.
Results in the creation of an instance of :class:`lbmpy.methods.momentbased.MomentBasedLbMethod`.
"""
CENTRAL_MOMENTS = auto()
"""
Central moment space, meaning relaxation is applied to a set of linearly independent, polynomial central moments
of the discrete population vector. Default for `lbmpy.enums.Method.CENTRAL_MOMENT`.
Results in the creation of an instance of :class:`lbmpy.methods.momentbased.CentralMomentBasedLbMethod`.
"""
CUMULANTS = auto()
"""
Cumulant space, meaning relaxation is applied to a set of linearly independent, polynomial cumulants of the
discrete population vector. Default for `lbmpy.enums.Method.CUMULANT` and `lbmpy.enums.Method.MONOMIAL_CUMULANT`.
Results in the creation of an instance of :class:`lbmpy.methods.cumulantbased.CumulantBasedLbMethod`.
"""
def compatible(self, method: Method):
"""Determines if the given `lbmpy.enums.Method` is compatible with this collision space."""
compat_dict = {
CollisionSpace.POPULATIONS: {Method.SRT, Method.TRT, Method.MRT_RAW, Method.MRT,
Method.TRT_KBC_N1, Method.TRT_KBC_N2, Method.TRT_KBC_N3, Method.TRT_KBC_N4},
CollisionSpace.RAW_MOMENTS: {Method.SRT, Method.TRT, Method.MRT_RAW, Method.MRT},
CollisionSpace.CENTRAL_MOMENTS: {Method.CENTRAL_MOMENT},
CollisionSpace.CUMULANTS: {Method.MONOMIAL_CUMULANT, Method.CUMULANT}
}
return method in compat_dict[self]
class ForceModel(Enum):
"""
The ForceModel enumeration defines which force model is used to introduce forcing terms in the collision operator
of the lattice Boltzmann method. A short summary of the theory behind is shown in `lbmpy.forcemodels`.
More precise definitions are given in Chapter 6 and 10 of :cite:`lbm_book`
"""
SIMPLE = auto()
"""
See :class:`lbmpy.forcemodels.Simple`
"""
LUO = auto()
"""
See :class:`lbmpy.forcemodels.Luo`
"""
GUO = auto()
"""
See :class:`lbmpy.forcemodels.Guo`
"""
BUICK = auto()
"""
See :class:`lbmpy.forcemodels.Buick`
"""
SILVA = auto()
"""
See :class:`lbmpy.forcemodels.Buick`
"""
EDM = auto()
"""
See :class:`lbmpy.forcemodels.EDM`
"""
KUPERSHTOKH = auto()
"""
See :class:`lbmpy.forcemodels.EDM`
"""
HE = auto()
"""
See :class:`lbmpy.forcemodels.He`
"""
SHANCHEN = auto()
"""
See :class:`lbmpy.forcemodels.ShanChen`
"""
CENTRALMOMENT = auto()
"""
See :class:`lbmpy.forcemodels.CentralMoment`
"""
class SubgridScaleModel(Enum):
"""
The SubgridScaleModel enumeration defines which subgrid-scale model (SGS) is used to perform
Large-Eddy-Simulations (LES).
"""
SMAGORINSKY = auto()
"""
See :func:`lbmpy.turbulence_models.add_smagorinsky_model`
"""
QR = auto()
"""
See :func:`lbmpy.turbulence_models.add_qr_model`
"""
r"""
This module contains various classes encapsulating equilibrium distributions used in the lattice Boltzmann
method. These include both the continuous and the discretized variants of the Maxwellian equilibrium of
hydrodynamics. Furthermore, a lightweight wrapper class for custom discrete equilibria is provided.
Custom equilibria may also be implemented by manually overriding the abstract base class
:class:`lbmpy.equilibrium.AbstractEquilibrium`.
"""
from .abstract_equilibrium import AbstractEquilibrium
from .continuous_hydro_maxwellian import ContinuousHydrodynamicMaxwellian, default_background_distribution
from .generic_discrete_equilibrium import GenericDiscreteEquilibrium, discrete_equilibrium_from_matching_moments
from .discrete_hydro_maxwellian import DiscreteHydrodynamicMaxwellian
__all__ = [
"AbstractEquilibrium",
"ContinuousHydrodynamicMaxwellian", "default_background_distribution",
"GenericDiscreteEquilibrium", "discrete_equilibrium_from_matching_moments",
"DiscreteHydrodynamicMaxwellian"
]
from abc import ABC, abstractmethod
import sympy as sp
from pystencils.cache import sharedmethodcache
from lbmpy.moments import polynomial_to_exponent_representation
class AbstractEquilibrium(ABC):
"""
Abstract Base Class for description of equilibrium distribution functions used in lattice
Boltzmann methods.
**Equilibrium Representation:**
This class provides the common interface for describing equilibrium distribution functions,
which is then used by the various method classes in the derivation of collision equations.
An equilibrium distribution is defined by either its continuous equation (see :attr:`continuous_equation`)
or a set of discrete populations
(see :attr:`discrete_populations` and :class:`lbmpy.equilibrium.GenericDiscreteEquilibrium`).
The distribution function may be given either in its regular, absolute form; or only as its
deviation from the rest state, represented by the background distribution (see :attr:`background_distribution`).
**Computation of Statistical Modes:**
The major computational task of an equilbrium class is the computation of the distribution's
statistical modes. For discrete distributions, the subclass :class:`lbmpy.equilibrium.GenericDiscreteEquilibrium`
provides a generic implementation for their computation. For continuous distributions, computation
of raw moments, central moments, and cumulants is more complicated, but may also be simplified using special
tricks.
As the computation of statistical modes is a time-consuming process, the abstract base class provides caching
functionality to avoid recomputing quantities that are already known.
**Instructions to Override:**
If you wish to model a simple custom discrete distribution, just using the class
:class:`lbmpy.equilibrium.GenericDiscreteEquilibrium` might already be sufficient.
If, however, you need to implement more specific functionality, custom properties,
a background distribution, etc., or if you wish to model a continuous distribution,
you will have to set up a custom subclass of :class:`AbstractEquilibrium`.
A subclass must implement all abstract properties according to their docstrings.
For computation of statistical modes, a large part of the infrastructure is already given in the abstract base
class. The public interface for computing e.g. raw moments reduces the computation of polynomial moments to their
contained monomials (for details on how moments are represented in *lbmpy*, see :mod:`lbmpy.moments`). The values
of both polynomial and monomial moments, once computed, will be cached per instance of the equilibrium class.
To take full advantage of the caching functionality, you will have to override only :func:`_monomial_raw_moment`
and its central moment and cumulant counterparts. These methods will be called only once for each monomial quantity
when it is required for the first time. Afterward, the cached value will be used.
"""
def __init__(self, dim=3):
self._dim = dim
@property
def dim(self):
"""This distribution's spatial dimensionality."""
return self._dim
# -------------- Abstract Properties, to be overridden in subclass ----------------------------------------------
@property
@abstractmethod
def deviation_only(self):
"""Whether or not this equilibrium distribution is represented only by its deviation
from the background distribution."""
raise NotImplementedError("'deviation_only' must be provided by subclass.")
@property
@abstractmethod
def continuous_equation(self):
"""Returns the continuous equation defining this equilibrium distribution,
or `None` if no such equation is available."""
raise NotImplementedError("'continuous_equation' must be provided by subclass.")
@property
@abstractmethod
def discrete_populations(self):
"""Returns the discrete populations of this equilibrium distribution as a tuple,
or `None` if none are available."""
raise NotImplementedError("'discrete_populations' must be provided by subclass.")
@property
@abstractmethod
def background_distribution(self):
"""Returns this equilibrium distribution's background distribution, which is
the distribution the discrete populations are centered around in the case of
zero-centered storage. If no background distribution is available, `None` must be
returned."""
raise NotImplementedError("'background_distribution' must be provided by subclass.")
@property
@abstractmethod
def zeroth_order_moment_symbol(self):
"""Returns a symbol referring to the zeroth-order moment of this distribution,
which is the area under it's curve."""
raise NotImplementedError("'zeroth_order_moment' must be provided by subclass.")
@property
@abstractmethod
def first_order_moment_symbols(self):
"""Returns a vector of symbols referring to the first-order moment of this distribution,
which is its mean value."""
raise NotImplementedError("'first_order_moments' must be provided by subclass.")
# -------------- Statistical Modes Interface --------------------------------------------------------------------
@sharedmethodcache("_moment_cache")
def moment(self, exponent_tuple_or_polynomial):
"""Returns this equilibrium distribution's moment specified by ``exponent_tuple_or_polynomial``.
Args:
exponent_tuple_or_polynomial: Moment specification, see :mod:`lbmpy.moments`.
"""
monomials = []
if isinstance(exponent_tuple_or_polynomial, tuple):
monomials = [(1, exponent_tuple_or_polynomial)]
else:
monomials = polynomial_to_exponent_representation(exponent_tuple_or_polynomial, dim=self._dim)
moment_value = sp.Integer(0)
for coeff, moment in monomials:
moment_value += coeff * self._cached_monomial_raw_moment(moment)
return moment_value.expand()
def moments(self, exponent_tuples_or_polynomials):
"""Returns a tuple of this equilibrium distribution's moments specified by 'exponent_tuple_or_polynomial'.
Args:
exponent_tuples_or_polynomials: Sequence of moment specifications, see :mod:`lbmpy.moments`.
"""
return tuple(self.moment(m) for m in exponent_tuples_or_polynomials)
@sharedmethodcache("_central_moment_cache")
def central_moment(self, exponent_tuple_or_polynomial, frame_of_reference):
"""Returns this equilibrium distribution's central moment specified by
``exponent_tuple_or_polynomial``, computed according to the given ``frame_of_reference``.
Args:
exponent_tuple_or_polynomial: Moment specification, see :mod:`lbmpy.moments`.
frame_of_reference: The frame of reference with respect to which the central moment should be computed.
"""
monomials = []
if isinstance(exponent_tuple_or_polynomial, tuple):
monomials = [(1, exponent_tuple_or_polynomial)]
else:
monomials = polynomial_to_exponent_representation(exponent_tuple_or_polynomial, dim=self._dim)
moment_value = sp.Integer(0)
for coeff, moment in monomials:
moment_value += coeff * self._cached_monomial_central_moment(moment, frame_of_reference)
return moment_value.expand()
def central_moments(self, exponent_tuples_or_polynomials, frame_of_reference):
"""Returns a list this equilibrium distribution's central moments specified by
``exponent_tuples_or_polynomials``, computed according to the given ``frame_of_reference``.
Args:
exponent_tuples_or_polynomials: Sequence of moment specifications, see :mod:`lbmpy.moments`.
frame_of_reference: The frame of reference with respect to which the central moment should be computed.
"""
return tuple(self.central_moment(m, frame_of_reference) for m in exponent_tuples_or_polynomials)
@sharedmethodcache("_cumulant_cache")
def cumulant(self, exponent_tuple_or_polynomial, rescale=True):
"""Returns this equilibrium distribution's cumulant specified by ``exponent_tuple_or_polynomial``.
Args:
exponent_tuple_or_polynomial: Moment specification, see :mod:`lbmpy.moments`.
rescale: If ``True``, the cumulant value should be multiplied by the zeroth-order moment.
"""
monomials = []
if isinstance(exponent_tuple_or_polynomial, tuple):
monomials = [(1, exponent_tuple_or_polynomial)]
else:
monomials = polynomial_to_exponent_representation(exponent_tuple_or_polynomial, dim=self._dim)
cumulant_value = sp.Integer(0)
for coeff, moment in monomials:
cumulant_value += coeff * self._cached_monomial_cumulant(moment, rescale=rescale)
return cumulant_value.expand()
def cumulants(self, exponent_tuples_or_polynomials, rescale=True):
"""Returns a list of this equilibrium distribution's cumulants specified by ``exponent_tuples_or_polynomial``.
Args:
exponent_tuples_or_polynomials: Sequence of moment specifications, see :mod:`lbmpy.moments`.
rescale: If ``True``, the cumulant value should be multiplied by the zeroth-order moment.
"""
return tuple(self.cumulant(m, rescale) for m in exponent_tuples_or_polynomials)
# -------------- Monomial moment computation, to be overridden in subclass --------------------------------------
@abstractmethod
def _monomial_raw_moment(self, exponents):
"""See :func:`lbmpy.equilibrium.AbstractEquilibrium.moment`."""
raise NotImplementedError("'_monomial_raw_moment' must be implemented by a subclass.")
@abstractmethod
def _monomial_central_moment(self, exponents, frame_of_reference):
"""See :func:`lbmpy.equilibrium.AbstractEquilibrium.central_moment`."""
raise NotImplementedError("'_monomial_central_moment' must be implemented by a subclass.")
@abstractmethod
def _monomial_cumulant(self, exponents, rescale):
"""See :func:`lbmpy.equilibrium.AbstractEquilibrium.cumulant`."""
raise NotImplementedError("'_monomial_cumulant' must be implemented by a subclass.")
# -------------- Cached monomial moment computation methods -----------------------------------------------------
@sharedmethodcache("_moment_cache")
def _cached_monomial_raw_moment(self, exponents):
return self._monomial_raw_moment(exponents)
@sharedmethodcache("_central_moment_cache")
def _cached_monomial_central_moment(self, exponents, frame_of_reference):
return self._monomial_central_moment(exponents, frame_of_reference)
@sharedmethodcache("_cumulant_cache")
def _cached_monomial_cumulant(self, exponents, rescale):
return self._monomial_cumulant(exponents, rescale)
# -------------- HTML Representation ----------------------------------------------------------------------------
def _repr_html_(self):
html = f"""
<table style="border:none; width: 100%">
<tr>
<th colspan="2" style="text-align: left">
Instance of {self.__class__.__name__}
</th>
</tr>
"""
cont_eq = self.continuous_equation
if cont_eq is not None:
html += f"""
<tr>
<td> Continuous Equation: </td>
<td style="text-align: center">
${sp.latex(self.continuous_equation)}$
</td>
</tr>
"""
else:
pdfs = self.discrete_populations
if pdfs is not None:
html += """
<tr>
<td colspan="2" style="text-align: right;"> Discrete Populations: </td>
</tr>
"""
for f, eq in zip(sp.symbols(f"f_:{len(pdfs)}"), pdfs):
html += f'<tr><td colspan="2" style="text-align: left;"> ${f} = {sp.latex(eq)}$ </td></tr>'
html += "</table>"
return html
# end class AbstractEquilibrium
import sympy as sp
from .abstract_equilibrium import AbstractEquilibrium
from lbmpy.moments import contained_moments
from lbmpy.maxwellian_equilibrium import continuous_maxwellian_equilibrium
from lbmpy.continuous_distribution_measures import continuous_moment, continuous_cumulant
from pystencils.sympyextensions import remove_higher_order_terms, simplify_by_equality
def default_background_distribution(dim):
return ContinuousHydrodynamicMaxwellian(dim=dim, compressible=True, deviation_only=False,
rho=sp.Integer(1), delta_rho=0, u=(0,) * dim,
c_s_sq=sp.Rational(1, 3))
class ContinuousHydrodynamicMaxwellian(AbstractEquilibrium):
r"""
The standard continuous Maxwellian equilibrium distribution for hydrodynamics.
This class represents the Maxwellian equilibrium distribution of hydrodynamics in its continuous form
in :math:`d` dimensions :cite:`lbm_book`:
.. math::
\Psi \left( \rho, \mathbf{u}, \mathbf{\xi} \right)
= \rho \left( \frac{1}{2 \pi c_s^2} \right)^{d/2}
\exp \left( \frac{- (\mathbf{\xi} - \mathbf{u})^2 }{2 c_s^2} \right)
Beyond this classic, 'compressible' form of the equilibrium, an alternative form known as the
incompressible equilibrium of the LBM can be obtained by setting the flag ``compressible=False``.
The continuous incompressible equilibrium can be expressed as
(:cite:`HeIncompressible,GruszczynskiCascadedPhaseFieldModel`):
.. math::
\Psi^{\mathrm{incomp}} \left( \rho, \mathbf{u}, \mathbf{\xi} \right)
= \Psi \left( \rho_0, \mathbf{u}, \mathbf{\xi} \right)
+ \Psi \left( \delta\rho, \mathbf{0}, \mathbf{\xi} \right)
Here, :math:`\rho_0` (typically :math:`\rho_0 = 1`) denotes the background density, and :math:`\delta\rho` is
the density deviation, such that the total fluid density amounts to :math:`\rho = \rho_0 + \delta\rho`.
To simplify computations when the zero-centered storage format is used for PDFs, both equilibrium variants can
also be expressed in a *deviation-only* or *delta-equilibrium* form, which is obtained by subtracting the
constant background distribution :math:`\Psi (\rho_0, \mathbf{0})`. The delta form expresses the equilibrium
distribution only by its deviation from the rest state:
.. math::
\delta\Psi \left( \rho, \mathbf{u}, \mathbf{\xi} \right)
&= \Psi \left( \rho, \mathbf{u}, \mathbf{\xi} \right)
- \Psi \left( \rho_0, \mathbf{0}, \mathbf{\xi} \right) \\
\delta\Psi^{\mathrm{incomp}} \left( \rho, \mathbf{u}, \mathbf{\xi} \right)
&= \Psi^{\mathrm{incomp}} \left( \rho, \mathbf{u}, \mathbf{\xi} \right)
- \Psi \left( \rho_0, \mathbf{0}, \mathbf{\xi} \right)
Parameters:
dim: Spatial dimensionality
compressible: If `False`, the incompressible equilibrium is created
deviation_only: If `True`, the delta-equilibrium is created
order: The discretization order in velocity to which computed statistical modes should be truncated
rho: Symbol or value for the density
rho_background: Symbol or value for the background density
delta_rho: Symbol or value for the density deviation
u: Sequence of symbols for the macroscopic velocity
v: Sequence of symbols for the particle velocity :math:`\xi`
c_s_sq: Symbol or value for the squared speed of sound
"""
def __init__(self, dim=3, compressible=True, deviation_only=False,
order=None,
rho=sp.Symbol("rho"),
rho_background=sp.Integer(1),
delta_rho=sp.Symbol("delta_rho"),
u=sp.symbols("u_:3"),
v=sp.symbols("v_:3"),
c_s_sq=sp.Symbol("c_s") ** 2):
super().__init__(dim=dim)
self._order = order
self._compressible = compressible
self._deviation_only = deviation_only
self._rho = rho
self._rho_background = rho_background
self._delta_rho = delta_rho
self._u = u[:dim]
self._v = v[:dim]
# trick to speed up sympy integration (otherwise it takes multiple minutes, or aborts):
# use a positive, real symbol to represent c_s_sq -> then replace this symbol afterwards with the real c_s_sq
# (see maxwellian_equilibrium.py)
self._c_s_sq = c_s_sq
self._c_s_sq_helper = sp.Symbol("csq_helper", positive=True, real=True)
def psi(rho, u):
return continuous_maxwellian_equilibrium(dim=self._dim,
rho=rho,
u=u,
v=self._v,
c_s_sq=self._c_s_sq_helper)
zeroth_moment_arg = self._rho if self._compressible else self._rho_background
self._base_equation = psi(zeroth_moment_arg, self._u)
self._corrections = []
if not self._compressible:
zeroth_order_correction = psi(self._delta_rho, (sp.Integer(0), ) * self._dim)
self._corrections.append((sp.Integer(1), zeroth_order_correction))
if self._deviation_only:
rest_state = psi(self._rho_background, (sp.Integer(0), ) * self._dim)
self._corrections.append((sp.Integer(-1), rest_state))
@property
def order(self):
return self._order
@property
def deviation_only(self):
return self._deviation_only
@property
def compressible(self):
return self._compressible
@property
def density(self):
return self._rho
@property
def background_density(self):
return self._rho_background
@property
def density_deviation(self):
return self._delta_rho
@property
def velocity(self):
return self._u
@property
def continuous_equation(self):
eq = self._base_equation + sum(f * e for f, e in self._corrections)
eq = eq.subs(self._c_s_sq_helper, self._c_s_sq)
return eq
@property
def zeroth_order_moment_symbol(self):
return self._delta_rho if self._deviation_only else self._rho
@property
def first_order_moment_symbols(self):
return self._u
@property
def background_distribution(self):
return ContinuousHydrodynamicMaxwellian(dim=self.dim, compressible=True, deviation_only=False,
order=self._order, rho=self._rho_background,
rho_background=self._rho_background,
delta_rho=0, u=(0,) * self.dim, v=self._v,
c_s_sq=self._c_s_sq)
@property
def discrete_populations(self):
return None
def central_moment(self, exponent_tuple_or_polynomial, velocity=None):
if velocity is None:
velocity = self._u
return super().central_moment(exponent_tuple_or_polynomial, velocity)
def central_moments(self, exponent_tuples_or_polynomials, velocity=None):
if velocity is None:
velocity = self._u
return super().central_moments(exponent_tuples_or_polynomials, velocity)
def cumulant(self, exponent_tuple_or_polynomial, rescale=True):
if not self._compressible or self._deviation_only:
raise Exception("Cumulants can only be computed for the compressible, "
"non-deviation maxwellian equilibrium!")
return super().cumulant(exponent_tuple_or_polynomial, rescale=rescale)
# ------------------ Overridden Moment Computation ------------------------------------------
def _monomial_raw_moment(self, exponents):
moment_value = continuous_moment(self._base_equation, exponents, self._v)
for coeff, corr in self._corrections:
moment_value += coeff * continuous_moment(corr, exponents, self._v)
moment_value = self._correct_order_and_cssq(moment_value)
moment_value = self._simplify_moment(moment_value)
return moment_value
def _monomial_central_moment(self, cm_exponents, velocity):
# Setting up the central moment-generating function using SymPy integration
# will take unfeasibly long at times
# So we compute the central moments by binomial expansion in raw moments
cm_order = sum(cm_exponents)
contained_raw_moments = contained_moments(cm_exponents, exclude_original=False)
moment_value = sp.Integer(0)
for rm_exponents in contained_raw_moments:
rm_order = sum(rm_exponents)
factor = (-1)**(cm_order - rm_order)
factor *= sp.Mul(*(u**(c - i) * sp.binomial(c, i)
for u, c, i in zip(velocity, cm_exponents, rm_exponents)))
rm_value = self._cached_monomial_raw_moment(rm_exponents)
moment_value += factor * rm_value
moment_value = self._correct_order_and_cssq(moment_value)
moment_value = self._simplify_moment(moment_value)
return moment_value
def _monomial_cumulant(self, c_exponents, rescale):
# this implementation works only for the compressible, non-deviation equilibrium
cumulant_value = continuous_cumulant(self._base_equation, c_exponents, self._v)
cumulant_value = self._correct_order_and_cssq(cumulant_value)
if rescale:
cumulant_value = self._rho * cumulant_value
return cumulant_value
def _correct_order_and_cssq(self, term):
term = term.subs(self._c_s_sq_helper, self._c_s_sq)
term = term.expand()
if self._order is not None:
return remove_higher_order_terms(term, order=self._order, symbols=self._u)
else:
return term
def _simplify_moment(self, moment_value):
if (self.deviation_only or not self.compressible) \
and isinstance(self.density, sp.Symbol) and isinstance(self.density_deviation, sp.Symbol):
moment_value = simplify_by_equality(moment_value, self.density,
self.density_deviation, self.background_density)
return moment_value
# ------------------ Utility ----------------------------------------------------------------
def __repr__(self):
return f"ContinuousHydrodynamicMaxwellian({self.dim}D, " \
f"compressible={self.compressible}, deviation_only:{self.deviation_only}" \
f"order={self.order})"
def _repr_html_(self):
def stylized_bool(b):
return "&#10003;" if b else "&#10007;"
html = f"""
<table style="border:none; width: 100%">
<tr>
<th colspan="3" style="text-align: left">
Continuous Hydrodynamic Maxwellian Equilibrium
</th>
<td rowspan="2" style="width: 50%; text-align: center">
$f ({sp.latex(self._rho)}, {sp.latex(self._u)}, {sp.latex(self._v)})
= {sp.latex(self.continuous_equation)}$
</td>
</tr>
<tr>
<td>Compressible: {stylized_bool(self._compressible)}</td>
<td>Deviation Only: {stylized_bool(self._deviation_only)}</td>
<td>Order: {"&#8734;" if self._order is None else self._order}</td>
</tr>
</table>
"""
return html
# end class ContinuousHydrodynamicMaxwellian
import sympy as sp
from pystencils.sympyextensions import simplify_by_equality
from lbmpy.maxwellian_equilibrium import discrete_maxwellian_equilibrium
from .generic_discrete_equilibrium import GenericDiscreteEquilibrium
class DiscreteHydrodynamicMaxwellian(GenericDiscreteEquilibrium):
r"""
The textbook discretization of the Maxwellian equilibrium distribution of hydrodynamics.
This class represents the default discretization of the Maxwellian in velocity space,
computed from the distribution's expansion in Hermite polynomials (cf. :cite:`lbm_book`).
In :math:`d` dimensions, its populations :math:`f_i` on a given stencil
:math:`(\mathbf{c}_i)_{i=0,\dots,q-1}` are given by
.. math::
f_i (\rho, \mathbf{u})
= w_i \rho \left(
1 + \frac{\mathbf{c}_i \cdot \mathbf{u}}{c_s^2}
+ \frac{(\mathbf{c}_i \cdot \mathbf{u})^2}{2 c_s^4}
- \frac{\mathbf{u} \cdot \mathbf{u}}{2 c_s^2}
\right).
Here :math:`w_i` denote the Hermite integration weights, also called lattice weights.
The incompressible variant of this distribution :cite:`HeIncompressible` can be written as
.. math::
f_i^{\mathrm{incomp}} (\rho, \mathbf{u})
= w_i \rho + w_i \rho_0 \left(
\frac{\mathbf{c}_i \cdot \mathbf{u}}{c_s^2}
+ \frac{(\mathbf{c}_i \cdot \mathbf{u})^2}{2 c_s^4}
- \frac{\mathbf{u} \cdot \mathbf{u}}{2 c_s^2}
\right).
Again, for usage with zero-centered PDF storage, both distributions may be expressed in a delta-form
by subtracting their values at the background rest state at :math:`\rho = \rho_0`,
:math:`\mathbf{u} = \mathbf{0}`, which are exactly the lattice weights:
.. math::
\delta f_i &= f_i - w_i \\
\delta f_i^{\mathrm{incomp}} &= f_i^{\mathrm{incomp}} - w_i \\
Parameters:
stencil: Discrete velocity set for the discretization, see :class:`lbmpy.stencils.LBStencil`
compressible: If `False`, the incompressible equilibrium is created
deviation_only: If `True`, the delta-equilibrium is created
order: The discretization order in velocity to which computed statistical modes should be truncated
rho: Symbol or value for the density
delta_rho: Symbol or value for the density deviation
u: Sequence of symbols for the macroscopic velocity
c_s_sq: Symbol or value for the squared speed of sound
"""
def __init__(self, stencil, compressible=True, deviation_only=False,
order=2,
rho=sp.Symbol("rho"),
delta_rho=sp.Symbol("delta_rho"),
u=sp.symbols("u_:3"),
c_s_sq=sp.Symbol("c_s") ** 2):
dim = stencil.D
if order is None:
order = 4
self._order = order
self._compressible = compressible
self._deviation_only = deviation_only
self._rho = rho
self._rho_background = sp.Integer(1)
self._delta_rho = delta_rho
self._u = u[:dim]
self._c_s_sq = c_s_sq
pdfs = discrete_maxwellian_equilibrium(stencil, rho=rho, u=u,
order=order, c_s_sq=c_s_sq,
compressible=compressible)
if deviation_only:
shift = discrete_maxwellian_equilibrium(stencil, rho=self._rho_background, u=(0,) * dim,
order=0, c_s_sq=c_s_sq, compressible=False)
pdfs = tuple(simplify_by_equality(f - s, rho, delta_rho, self._rho_background) for f, s in zip(pdfs, shift))
zeroth_order_moment = delta_rho if deviation_only else rho
super().__init__(stencil, pdfs, zeroth_order_moment, u, deviation_only)
@property
def order(self):
return self._order
@property
def deviation_only(self):
return self._deviation_only
@property
def compressible(self):
return self._compressible
@property
def density(self):
return self._rho
@property
def background_density(self):
return self._rho_background
@property
def density_deviation(self):
return self._delta_rho
@property
def velocity(self):
return self._u
@property
def background_distribution(self):
"""Returns the discrete Maxwellian background distribution, which amounts exactly to the
lattice weights."""
return DiscreteHydrodynamicMaxwellian(self._stencil, compressible=True, deviation_only=False,
order=self._order, rho=self._rho_background,
delta_rho=0, u=(0,) * self.dim, c_s_sq=self._c_s_sq)
def central_moment(self, exponent_tuple_or_polynomial, velocity=None):
if velocity is None:
velocity = self._u
return super().central_moment(exponent_tuple_or_polynomial, velocity)
def central_moments(self, exponent_tuples_or_polynomials, velocity=None):
if velocity is None:
velocity = self._u
return super().central_moments(exponent_tuples_or_polynomials, velocity)
def cumulant(self, exponent_tuple_or_polynomial, rescale=True):
if not self._compressible or self._deviation_only:
raise Exception("Cumulants can only be computed for the compressible, "
"non-deviation maxwellian equilibrium!")
return super().cumulant(exponent_tuple_or_polynomial, rescale=rescale)
# ------------------ Utility ----------------------------------------------------------------
def __repr__(self):
return f"DiscreteHydrodynamicMaxwellian({self.stencil}, " \
f"compressible={self.compressible}, deviation_only:{self.deviation_only}" \
f"order={self.order})"
def _repr_html_(self):
def stylized_bool(b):
return "&#10003;" if b else "&#10007;"
html = f"""
<div style="max-height: 150pt; overflow-y: auto;">
<table style="border:none; width: 100%">
<tr>
<th colspan="3" style="text-align: left">
Discrete Hydrodynamic Maxwellian Equilibrium
</th>
<td>Compressible: {stylized_bool(self._compressible)}</td>
<td>Deviation Only: {stylized_bool(self._deviation_only)}</td>
<td>Order: {"&#8734;" if self._order is None else self._order}</td>
</tr>
"""
pdfs = self.discrete_populations
for f, eq in zip(sp.symbols(f"f_:{len(pdfs)}"), pdfs):
html += f'<tr><td colspan="6" style="text-align: left;"> ${f} = {sp.latex(eq)}$ </td></tr>'
html += "</table></div>"
return html
# end class DiscreteHydrodynamicMaxwellian
import sympy as sp
from .abstract_equilibrium import AbstractEquilibrium
from lbmpy.moments import discrete_moment, moment_matrix
from lbmpy.cumulants import discrete_cumulant
def discrete_equilibrium_from_matching_moments(stencil, moment_constraints,
zeroth_order_moment_symbol,
first_order_moment_symbols,
deviation_only=False):
assert len(moment_constraints) == stencil.Q
moments = tuple(moment_constraints.keys())
mm = moment_matrix(moments, stencil)
try:
pdfs = mm.inv() * sp.Matrix(list(moment_constraints.values()))
pdfs = pdfs.expand()
return GenericDiscreteEquilibrium(stencil, pdfs, zeroth_order_moment_symbol,
first_order_moment_symbols, deviation_only=deviation_only)
except sp.matrices.inverse.NonInvertibleMatrixError as e:
raise ValueError("Could not construct equilibrium from given moment constraints.") from e
class GenericDiscreteEquilibrium(AbstractEquilibrium):
"""
Class for encapsulating arbitrary discrete equilibria, given by their equilibrium populations.
This class takes both a stencil and a sequence of populations modelling a discrete distribution function
and provides basic functionality for computing and caching that distribution's statistical modes.
Parameters:
stencil: Discrete velocity set, see :class:`lbmpy.stencils.LBStencil`.
equilibrium_pdfs: List of q populations, describing the particle distribution on the discrete velocity
set given by the stencil.
zeroth_order_moment_symbol: Symbol corresponding to the distribution's zeroth-order moment, the area under
it's curve (see :attr:`zeroth_order_moment_symbol`).
first_order_moment_symbols: Sequence of symbols corresponding to the distribution's first-order moment, the
vector of its mean values (see :attr:`first_order_moment_symbols`).
deviation_only: Set to `True` if the given populations model only the deviation from a rest state, to be
used in junction with the zero-centered storage format.
"""
def __init__(self, stencil, equilibrium_pdfs,
zeroth_order_moment_symbol,
first_order_moment_symbols,
deviation_only=False):
super().__init__(dim=stencil.D)
if len(equilibrium_pdfs) != stencil.Q:
raise ValueError(f"Wrong number of PDFs."
f"On the {stencil} stencil, exactly {stencil.Q} populations must be passed!")
self._stencil = stencil
self._pdfs = tuple(equilibrium_pdfs)
self._zeroth_order_moment_symbol = zeroth_order_moment_symbol
self._first_order_moment_symbols = first_order_moment_symbols
self._deviation_only = deviation_only
@property
def stencil(self):
return self._stencil
@property
def deviation_only(self):
return self._deviation_only
@property
def continuous_equation(self):
"""Always returns `None`."""
return None
@property
def discrete_populations(self):
return self._pdfs
@property
def background_distribution(self):
"""Always returns `None`. To specify a background distribution, override this class."""
return None
@property
def zeroth_order_moment_symbol(self):
return self._zeroth_order_moment_symbol
@property
def first_order_moment_symbols(self):
return self._first_order_moment_symbols
# Moment Computation
def _monomial_raw_moment(self, exponents):
return discrete_moment(self._pdfs, exponents, self._stencil)
def _monomial_central_moment(self, exponents, frame_of_reference):
return discrete_moment(self._pdfs, exponents, self._stencil, shift_velocity=frame_of_reference)
def _monomial_cumulant(self, exponents, rescale):
value = discrete_cumulant(self._pdfs, exponents, self._stencil)
if rescale:
value = self.zeroth_order_moment_symbol * value
return value