From 8a63f3928f2d0accf8ac1f48e15ac54ee4a6eb13 Mon Sep 17 00:00:00 2001 From: Daniel Bauer <daniel.j.bauer@fau.de> Date: Wed, 9 Jun 2021 15:42:42 +0000 Subject: [PATCH] add DiffusionDirichlet boundary condition --- lbmpy/boundaries/__init__.py | 6 +- lbmpy/boundaries/boundaryconditions.py | 115 ++++++++++++++++--- lbmpy/maxwellian_equilibrium.py | 2 +- lbmpy_tests/test_boundary_handling.py | 45 +++++++- lbmpy_tests/test_diffusion.py | 148 +++++++++++++++++++++++++ lbmpy_tests/test_vectorization.py | 21 ++-- 6 files changed, 306 insertions(+), 31 deletions(-) create mode 100644 lbmpy_tests/test_diffusion.py diff --git a/lbmpy/boundaries/__init__.py b/lbmpy/boundaries/__init__.py index 8fb94c54..1d196589 100644 --- a/lbmpy/boundaries/__init__.py +++ b/lbmpy/boundaries/__init__.py @@ -1,6 +1,8 @@ from lbmpy.boundaries.boundaryconditions import ( - UBB, FixedDensity, SimpleExtrapolationOutflow, ExtrapolationOutflow, NeumannByCopy, NoSlip, StreamInConstant) + UBB, FixedDensity, DiffusionDirichlet, SimpleExtrapolationOutflow, + ExtrapolationOutflow, NeumannByCopy, NoSlip, StreamInConstant) from lbmpy.boundaries.boundaryhandling import LatticeBoltzmannBoundaryHandling -__all__ = ['NoSlip', 'UBB', 'SimpleExtrapolationOutflow', 'ExtrapolationOutflow', 'FixedDensity', 'NeumannByCopy', +__all__ = ['NoSlip', 'UBB', 'SimpleExtrapolationOutflow', 'ExtrapolationOutflow', + 'FixedDensity', 'DiffusionDirichlet', 'NeumannByCopy', 'LatticeBoltzmannBoundaryHandling', 'StreamInConstant'] diff --git a/lbmpy/boundaries/boundaryconditions.py b/lbmpy/boundaries/boundaryconditions.py index 5c95ef66..f6dd1bd5 100644 --- a/lbmpy/boundaries/boundaryconditions.py +++ b/lbmpy/boundaries/boundaryconditions.py @@ -105,7 +105,7 @@ class NoSlip(LbBoundary): def __eq__(self, other): if not isinstance(other, NoSlip): return False - return self.name == other.name + return self.__dict__ == other.__dict__ # end class NoSlip @@ -126,7 +126,6 @@ class UBB(LbBoundary): """ def __init__(self, velocity, adapt_velocity_to_force=False, dim=None, name=None, data_type='double'): - super(UBB, self).__init__(name) self._velocity = velocity self._adaptVelocityToForce = adapt_velocity_to_force if callable(self._velocity) and not dim: @@ -136,12 +135,14 @@ class UBB(LbBoundary): self.dim = dim self.data_type = data_type + super(UBB, self).__init__(name) + @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.velocity_is_callable: - return [('vel_%d' % (i,), create_type(self.data_type)) for i in range(self.dim)] + return [(f'vel_{i}', create_type(self.data_type)) for i in range(self.dim)] else: return [] @@ -212,6 +213,14 @@ class UBB(LbBoundary): return [Assignment(f_in(inv_dir[direction]), f_out(direction) - vel_term)] + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + if not isinstance(other, UBB): + return False + return self.__dict__ == other.__dict__ + # end class UBB @@ -259,6 +268,16 @@ class SimpleExtrapolationOutflow(LbBoundary): 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])) + + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + if not isinstance(other, SimpleExtrapolationOutflow): + return False + return self.__dict__ == other.__dict__ + + # end class SimpleExtrapolationOutflow @@ -294,11 +313,10 @@ class ExtrapolationOutflow(LbBoundary): streaming_pattern='pull', zeroth_timestep=Timestep.BOTH, initial_density=None, initial_velocity=None, data_type='double'): - self.data_type = data_type - self.lb_method = lb_method self.stencil = lb_method.stencil self.dim = len(self.stencil[0]) + if isinstance(normal_direction, str): normal_direction = direction_string_to_offset(normal_direction, dim=self.dim) @@ -316,6 +334,8 @@ class ExtrapolationOutflow(LbBoundary): self.initial_velocity = initial_velocity self.equilibrium_calculation = None + self.data_type = data_type + if initial_density and initial_velocity: equilibrium = lb_method.get_equilibrium(conserved_quantity_equations=AssignmentCollection([])) rho = lb_method.zeroth_order_equilibrium_moment_symbol @@ -405,6 +425,14 @@ class ExtrapolationOutflow(LbBoundary): return AssignmentCollection(boundary_assignments, subexpressions=subexpressions) + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + if not isinstance(other, ExtrapolationOutflow): + return False + return self.__dict__ == other.__dict__ + # end class ExtrapolationOutflow @@ -420,8 +448,9 @@ class FixedDensity(LbBoundary): def __init__(self, density, name=None): if name is None: name = "Fixed Density " + str(density) + self.density = density + super(FixedDensity, self).__init__(name) - self._density = density def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): def remove_asymmetric_part_of_main_assignments(assignment_collection, degrees_of_freedom): @@ -441,7 +470,7 @@ class FixedDensity(LbBoundary): 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 = equilibrium_input.new_without_subexpressions() density_eq = equilibrium_input.main_assignments[0] @@ -458,10 +487,61 @@ class FixedDensity(LbBoundary): return subexpressions + [Assignment(f_in(inv_dir[dir_symbol]), 2 * eq_component - f_out(dir_symbol))] + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + if not isinstance(other, FixedDensity): + return False + return self.__dict__ == other.__dict__ + # end class FixedDensity +class DiffusionDirichlet(LbBoundary): + """Boundary condition for advection-diffusion problems that fixes the concentration at the obstacle. + + Args: + concentration: value of the concentration which should be set. + name: optional name of the boundary. + """ + + def __init__(self, concentration, name=None): + if name is None: + name = "Diffusion Dirichlet " + str(concentration) + self.concentration = concentration + + super(DiffusionDirichlet, self).__init__(name) + + 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 + """ + return [LbmWeightInfo(lb_method)] + + 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))] + + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + if not isinstance(other, DiffusionDirichlet): + return False + return self.__dict__ == other.__dict__ + + +# end class DiffusionDirichlet + + class NeumannByCopy(LbBoundary): """Neumann boundary condition which is implemented by coping the PDF values to achieve similar values at the fluid and the boundary node""" @@ -483,11 +563,12 @@ class NeumannByCopy(LbBoundary): Assignment(f_out[neighbour_offset](dir_symbol), f_out(dir_symbol))] def __hash__(self): - # All boundaries of these class behave equal -> should also be equal - return hash("NeumannByCopy") + return hash(self.name) def __eq__(self, other): - return type(other) == NeumannByCopy + if not isinstance(other, NeumannByCopy): + return False + return self.__dict__ == other.__dict__ # end class NeumannByCopy @@ -504,7 +585,7 @@ class StreamInConstant(LbBoundary): def __init__(self, constant, name=None): super(StreamInConstant, self).__init__(name) - self._constant = constant + self.constant = constant 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. @@ -519,13 +600,15 @@ class StreamInConstant(LbBoundary): def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): neighbour_offset = NeighbourOffsetArrays.neighbour_offset(dir_symbol, lb_method.stencil) - return [Assignment(f_in(inv_dir[dir_symbol]), self._constant), - Assignment(f_out[neighbour_offset](dir_symbol), self._constant)] + return [Assignment(f_in(inv_dir[dir_symbol]), self.constant), + Assignment(f_out[neighbour_offset](dir_symbol), self.constant)] def __hash__(self): - # All boundaries of these class behave equal -> should also be equal - return hash("StreamInConstant") + return hash(self.name) def __eq__(self, other): - return type(other) == StreamInConstant + if not isinstance(other, StreamInConstant): + return False + return self.__dict__ == other.__dict__ + # end class StreamInConstant diff --git a/lbmpy/maxwellian_equilibrium.py b/lbmpy/maxwellian_equilibrium.py index 4cf85697..a210ceeb 100644 --- a/lbmpy/maxwellian_equilibrium.py +++ b/lbmpy/maxwellian_equilibrium.py @@ -12,7 +12,7 @@ from sympy import Rational as R from pystencils.cache import disk_cache -def get_weights(stencil, c_s_sq): +def get_weights(stencil, c_s_sq=sp.Rational(1, 3)): q = len(stencil) if c_s_sq != sp.Rational(1, 3) and c_s_sq != sp.Symbol("c_s") ** 2: diff --git a/lbmpy_tests/test_boundary_handling.py b/lbmpy_tests/test_boundary_handling.py index 946101ae..9be3e4e8 100644 --- a/lbmpy_tests/test_boundary_handling.py +++ b/lbmpy_tests/test_boundary_handling.py @@ -1,11 +1,13 @@ import numpy as np import pytest -from lbmpy.boundaries import UBB, NeumannByCopy, NoSlip, StreamInConstant +from lbmpy.boundaries import NoSlip, UBB, SimpleExtrapolationOutflow, ExtrapolationOutflow,\ + FixedDensity, DiffusionDirichlet, NeumannByCopy, StreamInConstant from lbmpy.boundaries.boundaryhandling import LatticeBoltzmannBoundaryHandling -from lbmpy.creationfunctions import create_lb_function +from lbmpy.creationfunctions import create_lb_function, create_lb_method from lbmpy.geometry import add_box_boundary from lbmpy.lbstep import LatticeBoltzmannStep +from lbmpy.stencils import get_stencil from pystencils import create_data_handling, make_slice @@ -109,3 +111,42 @@ def test_exotic_boundaries(): step.boundary_handling.set_boundary(StreamInConstant(0), make_slice[0, :]) step.run(100) assert np.max(step.velocity[:, :, :]) < 1e-13 + + +def test_boundary_utility_functions(): + stencil = get_stencil("D2Q9") + method = create_lb_method(stencil=stencil) + + noslip = NoSlip("noslip") + assert noslip == NoSlip("noslip") + assert not noslip == NoSlip("test") + + ubb = UBB((0, 0), name="ubb") + assert ubb == UBB((0, 0), name="ubb") + assert not noslip == UBB((0, 0), name="test") + + simple_extrapolation = SimpleExtrapolationOutflow(normal_direction=stencil[4], stencil=stencil, name="simple") + assert simple_extrapolation == SimpleExtrapolationOutflow(normal_direction=stencil[4], + stencil=stencil, name="simple") + assert not simple_extrapolation == SimpleExtrapolationOutflow(normal_direction=stencil[4], + stencil=stencil, name="test") + + outflow = ExtrapolationOutflow(normal_direction=stencil[4], lb_method=method, name="outflow") + assert outflow == ExtrapolationOutflow(normal_direction=stencil[4], lb_method=method, name="outflow") + assert not outflow == ExtrapolationOutflow(normal_direction=stencil[4], lb_method=method, name="test") + + density = FixedDensity(density=1.0, name="fixedDensity") + assert density == FixedDensity(density=1.0, name="fixedDensity") + assert not density == FixedDensity(density=1.0, name="test") + + diffusion = DiffusionDirichlet(concentration=1.0, name="diffusion") + assert diffusion == DiffusionDirichlet(concentration=1.0, name="diffusion") + assert not diffusion == DiffusionDirichlet(concentration=1.0, name="test") + + neumann = NeumannByCopy(name="Neumann") + assert neumann == NeumannByCopy(name="Neumann") + assert not neumann == NeumannByCopy(name="test") + + stream = StreamInConstant(constant=1.0, name="stream") + assert stream == StreamInConstant(constant=1.0, name="stream") + assert not stream == StreamInConstant(constant=1.0, name="test") diff --git a/lbmpy_tests/test_diffusion.py b/lbmpy_tests/test_diffusion.py new file mode 100644 index 00000000..d656edd0 --- /dev/null +++ b/lbmpy_tests/test_diffusion.py @@ -0,0 +1,148 @@ +import math +import pytest + +import pystencils as ps +from pystencils.slicing import slice_from_direction + +from lbmpy import pdf_initialization_assignments + +from lbmpy.creationfunctions import create_lb_method, create_lb_update_rule +from lbmpy.boundaries.boundaryhandling import LatticeBoltzmannBoundaryHandling +from lbmpy.boundaries.boundaryconditions import DiffusionDirichlet, NeumannByCopy +from lbmpy.geometry import add_box_boundary +from lbmpy.stencils import get_stencil +from lbmpy.maxwellian_equilibrium import get_weights + +import sympy as sp +import numpy as np + + +def test_diffusion_boundary(): + domain_size = (10, 10) + stencil = get_stencil('D2Q9') + weights = get_weights(stencil, c_s_sq=sp.Rational(1, 3)) + concentration = 1.0 + + # Data Handling + dh = ps.create_data_handling(domain_size=domain_size) + + dh.add_array('pdfs', values_per_cell=len(stencil)) + dh.fill("pdfs", 0.0, ghost_layers=True) + + method = create_lb_method(stencil=stencil, method="srt", relaxation_rate=1.8, compressible=True) + + # Boundary Handling + bh = LatticeBoltzmannBoundaryHandling(method, dh, 'pdfs', name="bh") + add_box_boundary(bh, boundary=DiffusionDirichlet(concentration)) + + bh() + + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[0, 1:-2, 4] - 2 * weights[4]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[0, 1:-2, 6] - 2 * weights[6]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[0, 2:-1, 8] - 2 * weights[8]) < 1e-14) + + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[-1, 1:-2, 3] - 2 * weights[3]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[-1, 1:-2, 5] - 2 * weights[5]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[-1, 2:-1, 7] - 2 * weights[7]) < 1e-14) + + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[1:-2, 0, 1] - 2 * weights[1]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[2:, 0, 5] - 2 * weights[5]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[:-2, 0, 6] - 2 * weights[6]) < 1e-14) + + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[1:-2, -1, 2] - 2 * weights[2]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[2:, -1, 7] - 2 * weights[7]) < 1e-14) + assert all(np.abs(dh.gather_array("pdfs", ghost_layers=True)[:-2, -1, 8] - 2 * weights[8]) < 1e-14) + + +@pytest.mark.longrun +def test_diffusion(): + """ + Runs the "Diffusion from Plate in Uniform Flow" benchmark as it is described + in [ch. 8.6.3, The Lattice Boltzmann Method, Krüger et al.]. + + dC/dy = 0 + ┌───────────────┠+ │ → → → │ + C = 0 │ → u → │ dC/dx = 0 + │ → → → │ + └───────────────┘ + C = 1 + + The analytical solution is given by: + C(x,y) = 1 * erfc(y / sqrt(4Dx/u)) + + The hydrodynamic field is not simulated, instead a constant velocity is assumed. + """ + + # Parameters + domain_size = (1600, 160) + omega = 1.38 + diffusion = (1 / omega - 0.5) / 3 + velocity = 0.05 + time_steps = 50000 + stencil = get_stencil('D2Q9') + + # Data Handling + dh = ps.create_data_handling(domain_size=domain_size) + + vel_field = dh.add_array('vel_field', values_per_cell=dh.dim) + dh.fill('vel_field', velocity, 0, ghost_layers=True) + dh.fill('vel_field', 0.0, 1, ghost_layers=True) + + con_field = dh.add_array('con_field', values_per_cell=1) + dh.fill('con_field', 0.0, ghost_layers=True) + + pdfs = dh.add_array('pdfs', values_per_cell=len(stencil)) + pdfs_tmp = dh.add_array('pdfs_tmp', values_per_cell=len(stencil)) + + # Lattice Boltzmann method + params = {'stencil': stencil, + 'method': 'mrt', + 'relaxation_rates': [1, 1.5, 1, 1.5, 1], + 'velocity_input': vel_field, + 'output': {'density': con_field}, + 'compressible': True, + 'weighted': True, + 'optimization': {'symbolic_field': pdfs, + 'symbolic_temporary_field': pdfs_tmp}, + 'kernel_type': 'stream_pull_collide' + } + + method = create_lb_method(**params) + method.set_conserved_moments_relaxation_rate(omega) + + update_rule = create_lb_update_rule(lb_method=method, **params) + kernel = ps.create_kernel(update_rule).compile() + + # PDF initalization + init = pdf_initialization_assignments(method, con_field.center, + vel_field.center_vector, pdfs.center_vector) + dh.run_kernel(ps.create_kernel(init).compile()) + + # Boundary Handling + bh = LatticeBoltzmannBoundaryHandling(update_rule.method, dh, 'pdfs', name="bh") + add_box_boundary(bh, boundary=NeumannByCopy()) + bh.set_boundary(DiffusionDirichlet(0), slice_from_direction('W', dh.dim)) + bh.set_boundary(DiffusionDirichlet(1), slice_from_direction('S', dh.dim)) + + # Timeloop + for i in range(time_steps): + bh() + dh.run_kernel(kernel) + dh.swap("pdfs", "pdfs_tmp") + + # Verification + x = np.arange(1, domain_size[0], 1) + y = np.arange(0, domain_size[1], 1) + X, Y = np.meshgrid(x, y) + analytical = np.zeros(domain_size) + analytical[1:, :] = np.vectorize(math.erfc)(Y / np.vectorize(math.sqrt)(4 * diffusion * X / velocity)).transpose() + simulated = dh.gather_array('con_field', ghost_layers=False) + + residual = 0 + for i in x: + for j in y: + residual += (simulated[i, j] - analytical[i, j]) ** 2 + residual = math.sqrt(residual / (domain_size[0] * domain_size[1])) + + assert residual < 1e-2 diff --git a/lbmpy_tests/test_vectorization.py b/lbmpy_tests/test_vectorization.py index 03d09fe3..924d71f5 100644 --- a/lbmpy_tests/test_vectorization.py +++ b/lbmpy_tests/test_vectorization.py @@ -15,12 +15,13 @@ def test_lbm_vectorization_short(): ldc1_ref.run(10) ldc1 = create_lid_driven_cavity(size1, relaxation_rate=relaxation_rate, - optimization={'vectorization': {'instruction_set': get_supported_instruction_sets()[-1], - 'assume_aligned': True, - 'nontemporal': True, - 'assume_inner_stride_one': True, - 'assume_sufficient_line_padding': False, - }}, + optimization={ + 'vectorization': {'instruction_set': get_supported_instruction_sets()[-1], + 'assume_aligned': True, + 'nontemporal': True, + 'assume_inner_stride_one': True, + 'assume_sufficient_line_padding': False, + }}, fixed_loop_sizes=False) ldc1.run(10) @@ -33,10 +34,10 @@ def test_lbm_vectorization_short(): @pytest.mark.longrun def test_lbm_vectorization(instruction_set, aligned_and_padding, nontemporal, double_precision, fixed_loop_sizes): vectorization_options = {'instruction_set': instruction_set, - 'assume_aligned': aligned_and_padding[0], - 'nontemporal': nontemporal, - 'assume_inner_stride_one': True, - 'assume_sufficient_line_padding': aligned_and_padding[1]} + 'assume_aligned': aligned_and_padding[0], + 'nontemporal': nontemporal, + 'assume_inner_stride_one': True, + 'assume_sufficient_line_padding': aligned_and_padding[1]} time_steps = 100 size1 = (64, 32) size2 = (666, 34) -- GitLab