diff --git a/.gitignore b/.gitignore index 73c1ace7cf8cf1d79984375aa3f6db811804a3be..ab61b080cdcb95a93a50ce52fc9ed6dabbeb5610 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ __pycache__ _build /.idea .cache -_local_tmp \ No newline at end of file +_local_tmp +**/.vscode \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 86b33ed66c9c700f1012f24fe28668b6eaed1030..0facf9123769fa40308717ee35605ef960f45e7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -113,13 +113,9 @@ pycodegen-integration: - pip install -e pystencils/ - pip install -e lbmpy/ - ./install_walberla.sh - - export NUM_CORES=$(nproc --all) - - mkdir -p ~/.config/matplotlib - - echo "backend:template" > ~/.config/matplotlib/matplotlibrc - - cd ../lbmpy - - py.test -v -n $NUM_CORES . - - cd ../walberla/build/ - - make CodegenJacobiCPU CodegenJacobiGPU CodegenPoissonCPU CodegenPoissonGPU MicroBenchmarkGpuLbm LbCodeGenerationExample UniformGridBenchmarkGPU_trt UniformGridBenchmarkGPU_entropic_kbc_n4 + # build all integration tests + - cd walberla/build/ + - make -j $NUM_CORES MicroBenchmarkGpuLbm LbCodeGenerationExample - cd apps/benchmarks/UniformGridGPU - make -j $NUM_CORES - cd ../UniformGridGenerated diff --git a/lbmpy/advanced_streaming/__init__.py b/lbmpy/advanced_streaming/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c88e34637c31eae4cd6c43ec2d2e92c3ab133032 --- /dev/null +++ b/lbmpy/advanced_streaming/__init__.py @@ -0,0 +1,9 @@ +from .indexing import BetweenTimestepsIndexing, NeighbourOffsetArrays +from .communication import get_communication_slices, LBMPeriodicityHandling +from .utility import Timestep, get_accessor, is_inplace, get_timesteps, \ + numeric_index, numeric_offsets, inverse_dir_index, AccessPdfValues + +__all__ = ['BetweenTimestepsIndexing', 'NeighbourOffsetArrays', + 'get_communication_slices', 'LBMPeriodicityHandling', + 'Timestep', 'get_accessor', 'is_inplace', 'get_timesteps', + 'numeric_index', 'numeric_offsets', 'inverse_dir_index', 'AccessPdfValues'] diff --git a/lbmpy/advanced_streaming/communication.py b/lbmpy/advanced_streaming/communication.py new file mode 100644 index 0000000000000000000000000000000000000000..519ab9ef7faf0cca5ef8f357345c83b0a9239103 --- /dev/null +++ b/lbmpy/advanced_streaming/communication.py @@ -0,0 +1,239 @@ +from pystencils import Field, Assignment +from pystencils.slicing import shift_slice, get_slice_before_ghost_layer, normalize_slice +from lbmpy.advanced_streaming.utility import is_inplace, get_accessor, numeric_index, \ + numeric_offsets, Timestep, get_timesteps +from lbmpy.stencils import get_stencil +from pystencils.datahandling import SerialDataHandling +from itertools import chain + + +def _trim_slice_in_direction(slices, direction): + assert len(slices) == len(direction) + + result = [] + for s, d in zip(slices, direction): + if isinstance(s, int): + result.append(s) + continue + start = s.start + 1 if d == -1 else s.start + stop = s.stop - 1 if d == 1 else s.stop + result.append(slice(start, stop, s.step)) + + return tuple(result) + + +def _extend_dir(direction): + if len(direction) == 0: + yield tuple() + elif direction[0] == 0: + for d in [-1, 0, 1]: + for rest in _extend_dir(direction[1:]): + yield (d, ) + rest + else: + for rest in _extend_dir(direction[1:]): + yield (direction[0], ) + rest + + +def _get_neighbour_transform(direction, ghost_layers): + return tuple(d * (ghost_layers + 1) for d in direction) + + +def _fix_length_one_slices(slices): + """Slices of length one are replaced by their start value for correct periodic shifting""" + if isinstance(slices, int): + return slices + elif isinstance(slices, slice): + if slices.stop is not None and abs(slices.start - slices.stop) == 1: + return slices.start + elif slices.stop is None and slices.start == -1: + return -1 # [-1:] also has length one + else: + return slices + else: + return tuple(_fix_length_one_slices(s) for s in slices) + + +def get_communication_slices( + stencil, comm_stencil=None, streaming_pattern='pull', prev_timestep=Timestep.BOTH, ghost_layers=1): + """ + Return the source and destination slices for periodicity handling or communication between blocks. + + :param stencil: The stencil used by the LB method. + :param comm_stencil: The stencil defining the communication directions. If None, it will be set to stencil. + :param streaming_pattern: The streaming pattern. + :param prev_timestep: Timestep after which communication is run. + :param ghost_layers: Number of ghost layers in each direction. + + """ + if comm_stencil is None: + comm_stencil = stencil + + pdfs = Field.create_generic('pdfs', spatial_dimensions=len(stencil[0]), index_shape=(len(stencil),)) + write_accesses = get_accessor(streaming_pattern, prev_timestep).write(pdfs, stencil) + slices_per_comm_direction = dict() + + for comm_dir in comm_stencil: + if all(d == 0 for d in comm_dir): + continue + + slices_for_dir = [] + + for streaming_dir in set(_extend_dir(comm_dir)) & set(stencil): + d = stencil.index(streaming_dir) + write_offsets = numeric_offsets(write_accesses[d]) + write_index = numeric_index(write_accesses[d])[0] + + tangential_dir = tuple(s - c for s, c in zip(streaming_dir, comm_dir)) + origin_slice = get_slice_before_ghost_layer(comm_dir, ghost_layers=ghost_layers, thickness=1) + origin_slice = _fix_length_one_slices(origin_slice) + src_slice = shift_slice(_trim_slice_in_direction(origin_slice, tangential_dir), write_offsets) + + neighbour_transform = _get_neighbour_transform(comm_dir, ghost_layers) + dst_slice = shift_slice(src_slice, neighbour_transform) + + src_slice = src_slice + (write_index, ) + dst_slice = dst_slice + (write_index, ) + + slices_for_dir.append((src_slice, dst_slice)) + + slices_per_comm_direction[comm_dir] = slices_for_dir + return slices_per_comm_direction + + +def periodic_pdf_copy_kernel(pdf_field, src_slice, dst_slice, + domain_size=None, target='gpu', + opencl_queue=None, opencl_ctx=None): + """Copies a rectangular array slice onto another non-overlapping array slice""" + from pystencils.gpucuda.kernelcreation import create_cuda_kernel + + pdf_idx = src_slice[-1] + assert isinstance(pdf_idx, int), "PDF index needs to be an integer constant" + assert pdf_idx == dst_slice[-1], "Source and Destination PDF indices must be equal" + src_slice = src_slice[:-1] + dst_slice = dst_slice[:-1] + + if domain_size is None: + domain_size = pdf_field.spatial_shape + + normalized_from_slice = normalize_slice(src_slice, domain_size) + normalized_to_slice = normalize_slice(dst_slice, domain_size) + + def _start(s): + return s.start if isinstance(s, slice) else s + + def _stop(s): + return s.stop if isinstance(s, slice) else s + + offset = [_start(s1) - _start(s2) for s1, s2 in zip(normalized_from_slice, normalized_to_slice)] + assert offset == [_stop(s1) - _stop(s2) for s1, s2 in zip(normalized_from_slice, normalized_to_slice)], \ + "Slices have to have same size" + + copy_eq = Assignment(pdf_field(pdf_idx), pdf_field[tuple(offset)](pdf_idx)) + ast = create_cuda_kernel([copy_eq], iteration_slice=dst_slice, skip_independence_check=True) + if target == 'gpu': + from pystencils.gpucuda import make_python_function + return make_python_function(ast) + elif target == 'opencl': + from pystencils.opencl import make_python_function + return make_python_function(ast, opencl_queue, opencl_ctx) + else: + raise ValueError('Invalid target:', target) + + +class LBMPeriodicityHandling: + + def __init__(self, stencil, data_handling, pdf_field_name, + streaming_pattern='pull', ghost_layers=1, + opencl_queue=None, opencl_ctx=None, + pycuda_direct_copy=True): + """ + Periodicity Handling for Lattice Boltzmann Streaming. + + **On the usage with cuda/opencl:** + - pycuda allows the copying of sliced arrays within device memory using the numpy syntax, + e.g. `dst[:,0] = src[:,-1]`. In this implementation, this is the default for periodicity + handling. Alternatively, if you set `pycuda_direct_copy=False`, GPU kernels are generated and + compiled. The compiled kernels are almost twice as fast in execution as pycuda array copying, + but especially for large stencils like D3Q27, their compilation can take up to 20 seconds. + Choose your weapon depending on your use case. + + - pyopencl does not support copying of non-contiguous sliced arrays, so the usage of compiled + copy kernels is forced on us. On the positive side, compilation of the OpenCL kernels appears + to be about four times faster. + """ + if not isinstance(data_handling, SerialDataHandling): + raise ValueError('Only serial data handling is supported!') + + if isinstance(stencil, str): + stencil = get_stencil(stencil) + + self.stencil = stencil + self.dh = data_handling + + target = data_handling.default_target + assert target in ['cpu', 'gpu', 'opencl'] + + self.pdf_field_name = pdf_field_name + self.ghost_layers = ghost_layers + periodicity = data_handling.periodicity + self.inplace_pattern = is_inplace(streaming_pattern) + self.target = target + self.cpu = target == 'cpu' + self.opencl_queue = opencl_queue + self.opencl_ctx = opencl_ctx + self.pycuda_direct_copy = target == 'gpu' and pycuda_direct_copy + + def is_copy_direction(direction): + for d, p in zip(direction, periodicity): + if d != 0 and not p: + return False + + return True + + copy_directions = tuple(filter(is_copy_direction, stencil[1:])) + self.comm_slices = [] + timesteps = get_timesteps(streaming_pattern) + for timestep in timesteps: + slices_per_comm_dir = get_communication_slices(stencil=stencil, + comm_stencil=copy_directions, + streaming_pattern=streaming_pattern, + prev_timestep=timestep, + ghost_layers=ghost_layers) + self.comm_slices.append(list(chain.from_iterable(v for k, v in slices_per_comm_dir.items()))) + + if target == 'opencl' or (target == 'gpu' and not pycuda_direct_copy): + self.device_copy_kernels = [] + for timestep in timesteps: + self.device_copy_kernels.append(self._compile_copy_kernels(timestep)) + + def __call__(self, prev_timestep=Timestep.BOTH): + if self.cpu: + self._periodicity_handling_cpu(prev_timestep) + else: + self._periodicity_handling_gpu(prev_timestep) + + def _periodicity_handling_cpu(self, prev_timestep): + arr = self.dh.cpu_arrays[self.pdf_field_name] + comm_slices = self.comm_slices[prev_timestep.idx] + for src, dst in comm_slices: + arr[dst] = arr[src] + + def _compile_copy_kernels(self, timestep): + pdf_field = self.dh.fields[self.pdf_field_name] + kernels = [] + for src, dst in self.comm_slices[timestep.idx]: + kernels.append( + periodic_pdf_copy_kernel( + pdf_field, src, dst, target=self.target, + opencl_queue=self.opencl_queue, opencl_ctx=self.opencl_ctx)) + return kernels + + def _periodicity_handling_gpu(self, prev_timestep): + arr = self.dh.gpu_arrays[self.pdf_field_name] + if self.pycuda_direct_copy: + for src, dst in self.comm_slices[prev_timestep.idx]: + arr[dst] = arr[src] + else: + kernel_args = {self.pdf_field_name: arr} + for kernel in self.device_copy_kernels[prev_timestep.idx]: + kernel(**kernel_args) diff --git a/lbmpy/advanced_streaming/indexing.py b/lbmpy/advanced_streaming/indexing.py new file mode 100644 index 0000000000000000000000000000000000000000..6078cef7bb9c37f383915b301ee3c2ba7545b3a1 --- /dev/null +++ b/lbmpy/advanced_streaming/indexing.py @@ -0,0 +1,233 @@ +import numpy as np +import sympy as sp +import pystencils as ps + +from pystencils.data_types import TypedSymbol, create_type +from pystencils.backends.cbackend import CustomCodeNode + +from lbmpy.stencils import get_stencil +from lbmpy.advanced_streaming.utility import get_accessor, inverse_dir_index, is_inplace, Timestep + +from itertools import product + + +def _array_pattern(dtype, name, content): + return f"const {str(dtype)} {name} [] = {{ {','.join(str(c) for c in content)} }}; \n" + + +class BetweenTimestepsIndexing: + + # ============================================== + # Symbols for usage in kernel definitions + # ============================================== + + @property + def proxy_fields(self): + return ps.fields(f"f_out({self._q}), f_in({self._q}): [{self._dim}D]") + + @property + def dir_symbol(self): + return TypedSymbol('dir', create_type(self._index_dtype)) + + @property + def inverse_dir_symbol(self): + """Symbol denoting the inversion of a PDF field index. + Use only at top-level of index to f_out or f_in, otherwise it can't be correctly replaced.""" + return sp.IndexedBase('invdir') + + # ============================= + # Constructor and State + # ============================= + + def __init__(self, pdf_field, stencil, prev_timestep=Timestep.BOTH, streaming_pattern='pull', + index_dtype=np.int32, offsets_dtype=np.int32): + if prev_timestep == Timestep.BOTH and is_inplace(streaming_pattern): + raise ValueError('Cannot create index arrays for both kinds of timesteps for inplace streaming pattern ' + + streaming_pattern) + + if isinstance(stencil, str): + stencil = get_stencil(stencil) + + prev_accessor = get_accessor(streaming_pattern, prev_timestep) + next_accessor = get_accessor(streaming_pattern, prev_timestep.next()) + + outward_accesses = prev_accessor.write(pdf_field, stencil) + inward_accesses = next_accessor.read(pdf_field, stencil) + + self._accesses = {'out': outward_accesses, 'in': inward_accesses} + + self._pdf_field = pdf_field + self._stencil = stencil + self._dim = len(stencil[0]) + self._q = len(stencil) + self._coordinate_names = ['x', 'y', 'z'][:self._dim] + + self._index_dtype = create_type(index_dtype) + self._offsets_dtype = create_type(offsets_dtype) + + self._required_index_arrays = set() + self._required_offset_arrays = set() + self._trivial_index_translations, self._trivial_offset_translations = self._collect_trivial_translations() + + def _index_array_symbol(self, f_dir, inverse): + assert f_dir in ['in', 'out'] + inv = '_inv' if inverse else '' + name = f"f_{f_dir}{inv}_dir_idx" + return TypedSymbol(name, self._index_dtype) + + def _offset_array_symbols(self, f_dir, inverse): + assert f_dir in ['in', 'out'] + inv = '_inv' if inverse else '' + name_base = f"f_{f_dir}{inv}_offsets_" + symbols = [TypedSymbol(name_base + d, self._index_dtype) for d in self._coordinate_names] + return symbols + + def _array_symbols(self, f_dir, inverse, index): + if (f_dir, inverse) in self._trivial_index_translations: + translated_index = index + else: + index_array_symbol = self._index_array_symbol(f_dir, inverse) + translated_index = sp.IndexedBase(index_array_symbol, shape=(1,))[index] + self._required_index_arrays.add((f_dir, inverse)) + + if (f_dir, inverse) in self._trivial_offset_translations: + offsets = (0, ) * self._dim + else: + offset_array_symbols = self._offset_array_symbols(f_dir, inverse) + offsets = tuple(sp.IndexedBase(s, shape=(1,))[index] for s in offset_array_symbols) + self._required_offset_arrays.add((f_dir, inverse)) + + return {'index': translated_index, 'offsets': offsets} + + # ================================= + # Proxy fields substitution + # ================================= + + def substitute_proxies(self, assignments): + if isinstance(assignments, ps.Assignment): + assignments = [assignments] + + if not isinstance(assignments, ps.AssignmentCollection): + assignments = ps.AssignmentCollection(assignments) + + accesses = self._accesses + f_out, f_in = self.proxy_fields + inv_dir = self.inverse_dir_symbol + + accessor_subs = dict() + + for fa in assignments.atoms(ps.Field.Access): + if fa.field == f_out: + f_dir = 'out' + elif fa.field == f_in: + f_dir = 'in' + else: + continue + + inv = False + idx = fa.index[0] + if isinstance(idx, sp.Indexed) and idx.base == inv_dir: + idx = idx.indices[0] + if isinstance(sp.sympify(idx), sp.Integer): + idx = inverse_dir_index(self._stencil, idx) + inv = True + + if isinstance(sp.sympify(idx), sp.Integer): + accessor_subs[fa] = accesses[f_dir][idx].get_shifted(*(fa.offsets)) + else: + arr = self._array_symbols(f_dir, inv, idx) + accessor_subs[fa] = self._pdf_field[arr['offsets']](arr['index']).get_shifted(*(fa.offsets)) + + return assignments.new_with_substitutions(accessor_subs) + + # ================= + # Internals + # ================= + + def _get_translated_indices_and_offsets(self, f_dir, inv): + accesses = self._accesses[f_dir] + + if inv: + inverse_indices = [inverse_dir_index(self._stencil, i) + for i in range(len(self._stencil))] + accesses = [accesses[idx] for idx in inverse_indices] + + indices = [a.index[0] for a in accesses] + offsets = [] + for d in range(self._dim): + offsets.append([a.offsets[d] for a in accesses]) + return indices, offsets + + def _collect_trivial_translations(self): + trivial_index_translations = set() + trivial_offset_translations = set() + trivial_indices = list(range(self._q)) + trivial_offsets = [[0] * self._q] * self._dim + for f_dir, inv in product(['in', 'out'], [False, True]): + indices, offsets = self._get_translated_indices_and_offsets(f_dir, inv) + if indices == trivial_indices: + trivial_index_translations.add((f_dir, inv)) + if offsets == trivial_offsets: + trivial_offset_translations.add((f_dir, inv)) + return trivial_index_translations, trivial_offset_translations + + def create_code_node(self): + return BetweenTimestepsIndexing.TranslationArraysNode(self) + + class TranslationArraysNode(CustomCodeNode): + + def __init__(self, indexing): + code = '' + symbols_defined = set() + + for f_dir, inv in indexing._required_index_arrays: + indices, offsets = indexing._get_translated_indices_and_offsets(f_dir, inv) + index_array_symbol = indexing._index_array_symbol(f_dir, inv) + symbols_defined.add(index_array_symbol) + code += _array_pattern(indexing._index_dtype, index_array_symbol.name, indices) + + for f_dir, inv in indexing._required_offset_arrays: + indices, offsets = indexing._get_translated_indices_and_offsets(f_dir, inv) + offset_array_symbols = indexing._offset_array_symbols(f_dir, inv) + symbols_defined |= set(offset_array_symbols) + for d, arrsymb in enumerate(offset_array_symbols): + code += _array_pattern(indexing._offsets_dtype, arrsymb.name, offsets[d]) + + super(BetweenTimestepsIndexing.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" + +# end class AdvancedStreamingIndexing + + +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(np.int64)) 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)) diff --git a/lbmpy/advanced_streaming/utility.py b/lbmpy/advanced_streaming/utility.py new file mode 100644 index 0000000000000000000000000000000000000000..aa4b65c04d5268118d1029bc820612f83d69e011 --- /dev/null +++ b/lbmpy/advanced_streaming/utility.py @@ -0,0 +1,116 @@ +from lbmpy.fieldaccess import PdfFieldAccessor, \ + StreamPullTwoFieldsAccessor, \ + StreamPushTwoFieldsAccessor, \ + AAEvenTimeStepAccessor, \ + AAOddTimeStepAccessor, \ + EsoTwistEvenTimeStepAccessor, \ + EsoTwistOddTimeStepAccessor + +import numpy as np +import pystencils as ps +from enum import IntEnum + + +class Timestep(IntEnum): + EVEN = 0 + ODD = 1 + BOTH = 2 + + def next(self): + return self if self == Timestep.BOTH else Timestep((self + 1) % 2) + + @property + def idx(self): + """To use this timestep as an array index""" + return self % 2 + + +streaming_patterns = ['push', 'pull', 'aa', 'esotwist'] + +even_accessors = { + 'pull': StreamPullTwoFieldsAccessor, + 'push': StreamPushTwoFieldsAccessor, + 'aa': AAEvenTimeStepAccessor, + 'esotwist': EsoTwistEvenTimeStepAccessor +} + +odd_accessors = { + 'pull': StreamPullTwoFieldsAccessor, + 'push': StreamPushTwoFieldsAccessor, + 'aa': AAOddTimeStepAccessor, + 'esotwist': EsoTwistOddTimeStepAccessor +} + + +def get_accessor(streaming_pattern: str, timestep: Timestep) -> PdfFieldAccessor: + if streaming_pattern not in streaming_patterns: + raise ValueError( + "Invalid value of parameter 'streaming_pattern'.", streaming_pattern) + + if timestep == Timestep.EVEN: + return even_accessors[streaming_pattern] + else: + return odd_accessors[streaming_pattern] + + +def is_inplace(streaming_pattern): + if streaming_pattern not in streaming_patterns: + raise ValueError('Invalid streaming pattern', streaming_pattern) + + return streaming_pattern in ['aa', 'esotwist'] + + +def get_timesteps(streaming_pattern): + return (Timestep.EVEN, Timestep.ODD) if is_inplace(streaming_pattern) else (Timestep.BOTH, ) + + +def numeric_offsets(field_access: ps.Field.Access): + return tuple(int(o) for o in field_access.offsets) + + +def numeric_index(field_access: ps.Field.Access): + return tuple(int(i) for i in field_access.index) + + +def inverse_dir_index(stencil, direction): + return stencil.index(tuple(-d for d in stencil[direction])) + + +class AccessPdfValues: + """Allows to access values from a PDF array correctly depending on + the streaming pattern.""" + + def __init__(self, pdf_field, stencil, + streaming_pattern='pull', timestep=Timestep.BOTH, streaming_dir='out', + accessor=None): + if streaming_dir not in ['in', 'out']: + raise ValueError('Invalid streaming direction.', streaming_dir) + + if accessor is None: + accessor = get_accessor(streaming_pattern, timestep) + self.accs = accessor.read(pdf_field, stencil) \ + if streaming_dir == 'in' \ + else accessor.write(pdf_field, stencil) + + def write_pdf(self, pdf_arr, pos, d, value): + offsets = numeric_offsets(self.accs[d]) + pos = tuple(p + o for p, o in zip(pos, offsets)) + i = numeric_index(self.accs[d])[0] + pdf_arr[pos + (i,)] = value + + def read_pdf(self, pdf_arr, pos, d): + offsets = numeric_offsets(self.accs[d]) + pos = tuple(p + o for p, o in zip(pos, offsets)) + i = numeric_index(self.accs[d])[0] + return pdf_arr[pos + (i,)] + + def read_multiple(self, pdf_arr, indices): + """Returns PDF values for a list of index tuples (x, y, [z,] dir)""" + return np.array([self.read_pdf(pdf_arr, idx[:-1], idx[-1]) for idx in indices]) + + def collect_from_index_list(self, pdf_arr, index_list): + """To collect PDF values according to an pystencils boundary handling index list""" + def to_index_tuple(idx): + return tuple(idx[v] for v in ('x', 'y', 'z')[:len(idx) - 1] + ('dir',)) + + return self.read_multiple(pdf_arr, (to_index_tuple(idx) for idx in index_list)) diff --git a/lbmpy/boundaries/boundaries_in_kernel.py b/lbmpy/boundaries/boundaries_in_kernel.py index 9be2062643d456f1eddbe7919244dae62300ab97..63164fe023c117be9f865db2db14dcdcbae2106f 100644 --- a/lbmpy/boundaries/boundaries_in_kernel.py +++ b/lbmpy/boundaries/boundaries_in_kernel.py @@ -1,10 +1,12 @@ import sympy as sp -from lbmpy.boundaries.boundaryhandling import BoundaryOffsetInfo, LbmWeightInfo +from lbmpy.boundaries.boundaryhandling import LbmWeightInfo +from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing +from lbmpy.advanced_streaming.utility import Timestep, get_accessor +from pystencils.boundaries.boundaryhandling import BoundaryOffsetInfo from pystencils.assignment import Assignment from pystencils.astnodes import Block, Conditional, LoopOverCoordinate, SympyAssignment from pystencils.data_types import type_all_numbers -from pystencils.field import Field from pystencils.simp.assignment_collection import AssignmentCollection from pystencils.simp.simplifications import sympy_cse_on_assignment_list from pystencils.stencil import inverse_direction @@ -56,52 +58,24 @@ def border_conditions(direction, field, ghost_layers=1): return type_all_numbers(result, loop_ctr.dtype) -def transformed_boundary_rule(boundary, accessor_func, field, direction_symbol, lb_method, **kwargs): - tmp_field = field.new_field_with_different_name("t") - rule = boundary(tmp_field, direction_symbol, lb_method, **kwargs) - bsubs = boundary_substitutions(lb_method) - rule = [a.subs(bsubs) for a in rule] - accessor_writes = accessor_func(tmp_field, lb_method.stencil) - to_replace = set() - for assignment in rule: - to_replace.update({fa for fa in assignment.rhs.atoms(Field.Access) if fa.field == tmp_field}) - - def compute_replacement(fa): - f = fa.index[0] - shift = accessor_writes[f].offsets - new_index = tuple(a + b for a, b in zip(fa.offsets, shift)) - return field[new_index](accessor_writes[f].index[0]) - - substitutions = {fa: compute_replacement(fa) for fa in to_replace} - all_assignments = [assignment.subs(substitutions) for assignment in rule] - main_assignments = [a for a in all_assignments if isinstance(a.lhs, Field.Access)] - sub_expressions = [a for a in all_assignments if not isinstance(a.lhs, Field.Access)] - assert len(main_assignments) == 1 - ac = AssignmentCollection(main_assignments, sub_expressions).new_without_subexpressions() - return ac.main_assignments[0].rhs - - -def boundary_conditional(boundary, direction, read_of_next_accessor, lb_method, output_field, cse=False): +def boundary_conditional(boundary, direction, streaming_pattern, prev_timestep, lb_method, output_field, cse=False): stencil = lb_method.stencil - tmp_field = output_field.new_field_with_different_name("t") dir_indices = direction_indices_in_direction(direction, stencil) + indexing = BetweenTimestepsIndexing(output_field, lb_method.stencil, prev_timestep, streaming_pattern) + f_out, f_in = indexing.proxy_fields + inv_dir = indexing.inverse_dir_symbol assignments = [] for direction_idx in dir_indices: - rule = boundary(tmp_field, direction_idx, lb_method, index_field=None) - boundary_subs = boundary_substitutions(lb_method) - rule = [a.subs(boundary_subs) for a in rule] - - rhs_substitutions = {tmp_field(i): sym for i, sym in enumerate(lb_method.post_collision_pdf_symbols)} - offset = stencil[direction_idx] - inv_offset = inverse_direction(offset) - inv_idx = stencil.index(inv_offset) - - lhs_substitutions = { - tmp_field[offset](inv_idx): read_of_next_accessor(output_field, stencil)[inv_idx]} - rule = [Assignment(a.lhs.subs(lhs_substitutions), a.rhs.subs(rhs_substitutions)) for a in rule] - ac = AssignmentCollection([rule[-1]], rule[:-1]).new_without_subexpressions() + rule = boundary(f_out, f_in, direction_idx, inv_dir, lb_method, index_field=None) + + # 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)} + rule = AssignmentCollection([rule]).new_with_substitutions(rhs_substitutions) + rule = indexing.substitute_proxies(rule) + + ac = rule.new_without_subexpressions() assignments += ac.main_assignments border_cond = border_conditions(direction, output_field, ghost_layers=1) @@ -111,8 +85,10 @@ def boundary_conditional(boundary, direction, read_of_next_accessor, lb_method, return Conditional(border_cond, Block(assignments)) -def update_rule_with_push_boundaries(collision_rule, field, boundary_spec, accessor, read_of_next_accessor): +def update_rule_with_push_boundaries(collision_rule, field, boundary_spec, + streaming_pattern='pull', timestep=Timestep.BOTH): method = collision_rule.method + accessor = get_accessor(streaming_pattern, timestep) loads = [Assignment(a, b) for a, b in zip(method.pre_collision_pdf_symbols, accessor.read(field, method.stencil))] stores = [Assignment(a, b) for a, b in zip(accessor.write(field, method.stencil), method.post_collision_pdf_symbols)] @@ -121,7 +97,7 @@ def update_rule_with_push_boundaries(collision_rule, field, boundary_spec, acces result.subexpressions = loads + result.subexpressions result.main_assignments += stores for direction, boundary in boundary_spec.items(): - cond = boundary_conditional(boundary, direction, read_of_next_accessor, method, field) + cond = boundary_conditional(boundary, direction, streaming_pattern, timestep, method, field) result.main_assignments.append(cond) if 'split_groups' in result.simplification_hints: diff --git a/lbmpy/boundaries/boundaryconditions.py b/lbmpy/boundaries/boundaryconditions.py index e0c367f968492c74357bc0893d673f804fc8b2f8..6632228176e5b9d40c56004f00846a88edcde8e8 100644 --- a/lbmpy/boundaries/boundaryconditions.py +++ b/lbmpy/boundaries/boundaryconditions.py @@ -1,14 +1,14 @@ import sympy as sp - -from lbmpy.boundaries.boundaryhandling import BoundaryOffsetInfo, LbmWeightInfo -from lbmpy.simplificationfactory import create_simplification_strategy 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.advanced_streaming.indexing import NeighbourOffsetArrays -class Boundary: - """Base class all boundaries should derive from""" +class LbBoundary: + """Base class that all boundaries should derive from""" inner_or_boundary = True single_link = False @@ -16,23 +16,25 @@ class Boundary: def __init__(self, name=None): self._name = name - def __call__(self, pdf_field, direction_symbol, lb_method, index_field): + def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): """ This function defines the boundary behavior and must therefore be implemented by all boundaries. - Here the boundary is defined as a list of sympy equations, from which a boundary kernel is generated. - - Args: - pdf_field: pystencils field describing the pdf. The current cell is cell next to the boundary, - which is influenced by the boundary cell i.e. has a link from the boundary cell to - itself. - direction_symbol: a sympy symbol that can be used as index to the pdf_field. It describes - the direction pointing from the fluid to the boundary cell - lb_method: an instance of the LB method used. Use this to adapt the boundary to the method - (e.g. compressibility) - index_field: the boundary index field that can be used to retrieve and update boundary data - - Returns: - :return: list of sympy equations + The boundary is defined through a list of sympy equations from which a boundary kernel is generated. + + + :param f_out: a pystencils field acting as a proxy to access the populations streaming out of the current + cell, i.e. the post-collision PDFs of the previous LBM step + :param f_in: a pystencils field acting as a proxy to access the populations streaming into the current + cell, i.e. the pre-collision PDFs for the next LBM step + :param dir_symbol: a sympy symbol that can be used as an index to f_out and f_in. It describes the direction + pointing from the fluid to the boundary cell. + :param inv_dir: an indexed sympy symbol which describes the inversion of a direction index. It can be used in + the indices of f_out and f_in for retrieving the PDF of the inverse direction. + :param lb_method: an instance of the LB method used. Use this to adapt the boundary to the method + (e.g. compressibility) + :param index_field: the boundary index field that can be used to retrieve and update boundary data + + :return: list of pystencils assignments, or pystencils.AssignmentCollection """ raise NotImplementedError("Boundary class has to overwrite __call__") @@ -49,6 +51,10 @@ class Boundary: data-name to data for each element that should be initialized""" return None + 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 [] + @property def name(self): if self._name: @@ -60,21 +66,24 @@ class Boundary: def name(self, new_value): self._name = new_value +# end class Boundary + -class NoSlip(Boundary): +class NoSlip(LbBoundary): def __init__(self, name=None): """Set an optional name here, to mark boundaries, for example for force evaluations""" super(NoSlip, self).__init__(name) - """No-Slip, (half-way) simple bounce back boundary condition, enforcing zero velocity at obstacle""" - def __call__(self, pdf_field, direction_symbol, lb_method, **kwargs): - neighbor = BoundaryOffsetInfo.offset_from_dir(direction_symbol, lb_method.dim) - inverse_dir = BoundaryOffsetInfo.inv_dir(direction_symbol) - return [Assignment(pdf_field[neighbor](inverse_dir), pdf_field(direction_symbol))] + """ + No-Slip, (half-way) simple bounce back boundary condition, enforcing zero velocity at obstacle. + Extended for use with any streaming pattern. + """ + + def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): + return Assignment(f_in(inv_dir[dir_symbol]), f_out(dir_symbol)) def __hash__(self): - # All boundaries of these class behave equal -> should also be equal (as long as name is equal) return hash(self.name) def __eq__(self, other): @@ -82,8 +91,10 @@ class NoSlip(Boundary): return False return self.name == other.name +# end class NoSlip -class UBB(Boundary): + +class UBB(LbBoundary): """Velocity bounce back boundary condition, enforcing specified velocity at obstacle""" def __init__(self, velocity, adapt_velocity_to_force=False, dim=None, name=None): @@ -115,18 +126,22 @@ class UBB(Boundary): if callable(self._velocity): return self._velocity - def __call__(self, pdf_field, direction_symbol, lb_method, index_field, **kwargs): + def get_additional_code_nodes(self, lb_method): + return [LbmWeightInfo(lb_method), NeighbourOffsetArrays(lb_method.stencil)] + + def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): vel_from_idx_field = callable(self._velocity) - vel = [index_field('vel_%d' % (i,)) for i in range(self.dim)] if vel_from_idx_field else self._velocity - direction = direction_symbol + 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, "Dimension of UBB (%d) does not match dimension of method (%d)" \ - % (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})" - neighbor = BoundaryOffsetInfo.offset_from_dir(direction, lb_method.dim) - inverse_dir = BoundaryOffsetInfo.inv_dir(direction) + neighbor_offset = NeighbourOffsetArrays.neighbour_offset(direction, lb_method.stencil) - velocity = tuple(v_i.get_shifted(*neighbor) if isinstance(v_i, Field.Access) and not vel_from_idx_field else v_i + velocity = tuple(v_i.get_shifted(*neighbor_offset) + if isinstance(v_i, Field.Access) and not vel_from_idx_field + else v_i for v_i in vel) if self._adaptVelocityToForce: @@ -136,8 +151,9 @@ class UBB(Boundary): c_s_sq = sp.Rational(1, 3) weight_of_direction = LbmWeightInfo.weight_of_direction - vel_term = 2 / c_s_sq * sum([d_i * v_i - for d_i, v_i in zip(neighbor, velocity)]) * weight_of_direction(direction) + vel_term = 2 / c_s_sq \ + * sum([d_i * v_i for d_i, v_i in zip(neighbor_offset, velocity)]) \ + * weight_of_direction(direction, lb_method) # Better alternative: in conserved value computation # rename what is currently called density to "virtual_density" @@ -145,19 +161,20 @@ class UBB(Boundary): if not lb_method.conserved_quantity_computation.zero_centered_pdfs: cqc = lb_method.conserved_quantity_computation density_symbol = sp.Symbol("rho") - pdf_field_accesses = [pdf_field(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_symbol = lb_method.conserved_quantity_computation.defined_symbols()['density'] result = density_equations.all_assignments - result += [Assignment(pdf_field[neighbor](inverse_dir), - pdf_field(direction) - vel_term * density_symbol)] + result += [Assignment(f_in(inv_dir[direction]), + f_out(direction) - vel_term * density_symbol)] return result else: - return [Assignment(pdf_field[neighbor](inverse_dir), - pdf_field(direction) - vel_term)] + return [Assignment(f_in(inv_dir[direction]), + f_out(direction) - vel_term)] +# end class UBB -class FixedDensity(Boundary): +class FixedDensity(LbBoundary): def __init__(self, density, name=None): if name is None: @@ -165,7 +182,7 @@ class FixedDensity(Boundary): super(FixedDensity, self).__init__(name) self._density = density - def __call__(self, pdf_field, direction_symbol, lb_method, **kwargs): + def __call__(self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field): """Boundary condition that fixes the density/pressure at the obstacle""" def remove_asymmetric_part_of_main_assignments(assignment_collection, degrees_of_freedom): @@ -173,14 +190,11 @@ class FixedDensity(Boundary): for a in assignment_collection.main_assignments] return assignment_collection.copy(new_main_assignments) - neighbor = BoundaryOffsetInfo.offset_from_dir(direction_symbol, lb_method.dim) - inverse_dir = BoundaryOffsetInfo.inv_dir(direction_symbol) - cqc = lb_method.conserved_quantity_computation velocity = cqc.defined_symbols()['velocity'] symmetric_eq = remove_asymmetric_part_of_main_assignments(lb_method.get_equilibrium(), degrees_of_freedom=velocity) - substitutions = {sym: pdf_field(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)} symmetric_eq = symmetric_eq.new_with_substitutions(substitutions) simplification = create_simplification_strategy(lb_method) @@ -195,23 +209,27 @@ class FixedDensity(Boundary): assert density_eq.lhs == density_symbol transformed_density = density_eq.rhs - conditions = [(eq_i.rhs, sp.Equality(direction_symbol, i)) + conditions = [(eq_i.rhs, sp.Equality(dir_symbol, i)) for i, eq_i in enumerate(symmetric_eq.main_assignments)] + [(0, True)] eq_component = sp.Piecewise(*conditions) subexpressions = [Assignment(eq.lhs, transformed_density if eq.lhs == density_symbol else eq.rhs) for eq in symmetric_eq.subexpressions] - return subexpressions + [Assignment(pdf_field[neighbor](inverse_dir), - 2 * eq_component - pdf_field(direction_symbol))] + return subexpressions + [Assignment(f_in(inv_dir[dir_symbol]), + 2 * eq_component - f_out(dir_symbol))] +# end class FixedDensity -class NeumannByCopy(Boundary): - def __call__(self, pdf_field, direction_symbol, lb_method, **kwargs): - neighbor = BoundaryOffsetInfo.offset_from_dir(direction_symbol, lb_method.dim) - inverse_dir = BoundaryOffsetInfo.inv_dir(direction_symbol) - return [Assignment(pdf_field[neighbor](inverse_dir), pdf_field(inverse_dir)), - Assignment(pdf_field[neighbor](direction_symbol), pdf_field(direction_symbol))] +class NeumannByCopy(LbBoundary): + + def get_additional_code_nodes(self, lb_method): + return [NeighbourOffsetArrays(lb_method.stencil)] + + 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]), f_out(inv_dir[dir_symbol])), + 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 @@ -219,18 +237,21 @@ class NeumannByCopy(Boundary): def __eq__(self, other): return type(other) == NeumannByCopy +# end class NeumannByCopy -class StreamInConstant(Boundary): +class StreamInConstant(LbBoundary): def __init__(self, constant, name=None): super(StreamInConstant, self).__init__(name) self._constant = constant - def __call__(self, pdf_field, direction_symbol, lb_method, **kwargs): - neighbor = BoundaryOffsetInfo.offset_from_dir(direction_symbol, lb_method.dim) - inverse_dir = BoundaryOffsetInfo.inv_dir(direction_symbol) - return [Assignment(pdf_field[neighbor](inverse_dir), self._constant), - Assignment(pdf_field[neighbor](direction_symbol), self._constant)] + def get_additional_code_nodes(self, lb_method): + return [NeighbourOffsetArrays(lb_method.stencil)] + + 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)] def __hash__(self): # All boundaries of these class behave equal -> should also be equal @@ -238,3 +259,17 @@ class StreamInConstant(Boundary): def __eq__(self, other): return type(other) == StreamInConstant +# end class StreamInConstant + + +# ------------------------- Old, Deprecated Implementation ------------------------- + +class Boundary(LbBoundary): + + def __init__(self, name=None): + from lbmpy.boundaries.boundaryhandling import deprecation_message + deprecation_message() + self._name = name + + def __call__(self, pdf_field, direction_symbol, lb_method, index_field): + raise NotImplementedError("Boundary class has to overwrite __call__") diff --git a/lbmpy/boundaries/boundaryhandling.py b/lbmpy/boundaries/boundaryhandling.py index b9761abcb85f033e74dac0a93f464a3e7073c312..600d01bea159d4390565bcf4a1231c2b52707f9a 100644 --- a/lbmpy/boundaries/boundaryhandling.py +++ b/lbmpy/boundaries/boundaryhandling.py @@ -1,37 +1,105 @@ import numpy as np import sympy as sp - -from pystencils import Assignment, TypedSymbol, create_indexed_kernel -from pystencils.backends.cbackend import CustomCodeNode -from pystencils.boundaries import BoundaryHandling -from pystencils.boundaries.boundaryhandling import BoundaryOffsetInfo +from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing +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.createindexlist import numpy_data_type_for_boundary_object +from pystencils.backends.cbackend import CustomCodeNode class LatticeBoltzmannBoundaryHandling(BoundaryHandling): - - def __init__(self, lb_method, data_handling, pdf_field_name, name="boundary_handling", flag_interface=None, - target='cpu', openmp=True): - self.lb_method = lb_method + """ + 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 + object and the right one selected depending on the time step. + """ + + def __init__(self, lb_method, data_handling, pdf_field_name, streaming_pattern='pull', + name="boundary_handling", flag_interface=None, target='cpu', openmp=True): + self._lb_method = lb_method + self._streaming_pattern = streaming_pattern + self._inplace = is_inplace(streaming_pattern) + self._prev_timestep = None super(LatticeBoltzmannBoundaryHandling, self).__init__(data_handling, pdf_field_name, lb_method.stencil, name, flag_interface, target, openmp) - def force_on_boundary(self, boundary_obj): + # ------------------------- Overridden methods of pystencils.BoundaryHandling ------------------------- + + @property + def prev_timestep(self): + return self._prev_timestep + + def __call__(self, prev_timestep=Timestep.BOTH, **kwargs): + self._prev_timestep = prev_timestep + super(LatticeBoltzmannBoundaryHandling, self).__call__(**kwargs) + self._prev_timestep = None + + def add_fixed_steps(self, fixed_loop, **kwargs): + if self._inplace: # Fixed Loop can't do timestep selection + raise NotImplementedError("Adding to fixed loop is currently not supported for inplace kernels") + super(LatticeBoltzmannBoundaryHandling, self).add_fixed_steps(fixed_loop, **kwargs) + + def _add_boundary(self, boundary_obj, flag=None): + if self._inplace: + return self._add_inplace_boundary(boundary_obj, flag) + else: + return super(LatticeBoltzmannBoundaryHandling, self)._add_boundary(boundary_obj, flag) + + def _add_inplace_boundary(self, boundary_obj, flag=None): + if boundary_obj not in self._boundary_object_to_boundary_info: + sym_index_field = Field.create_generic('indexField', spatial_dimensions=1, + 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(), + self._create_boundary_kernel( + self._data_handling.fields[self._field_name], sym_index_field, boundary_obj, Timestep.ODD).compile()] + if flag is None: + flag = self.flag_interface.reserve_next_flag() + boundary_info = self.InplaceStreamingBoundaryInfo(self, boundary_obj, flag, kernels) + self._boundary_object_to_boundary_info[boundary_obj] = boundary_info + 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): + return create_lattice_boltzmann_boundary_kernel( + symbolic_field, symbolic_index_field, self._lb_method, boundary_obj, + prev_timestep=prev_timestep, streaming_pattern=self._streaming_pattern, + target=self._target, openmp=self._openmp) + + class InplaceStreamingBoundaryInfo(object): + + @property + def kernel(self): + prev_timestep = self._boundary_handling.prev_timestep + if prev_timestep is None: + raise Exception( + "The boundary kernel property was accessed while " + + "there was no boundary handling in progress.") + return self._kernels[prev_timestep] + + def __init__(self, boundary_handling, boundary_obj, flag, kernels): + self._boundary_handling = boundary_handling + self.boundary_object = boundary_obj + self.flag = flag + self._kernels = kernels + # end class InplaceStreamingBoundaryInfo + + # ------------------------------ Force On Boundary ------------------------------------------------------------ + + def force_on_boundary(self, boundary_obj, prev_timestep=Timestep.BOTH): from lbmpy.boundaries import NoSlip + self.__call__(prev_timestep=prev_timestep) if isinstance(boundary_obj, NoSlip): - return self._force_on_no_slip(boundary_obj) + return self._force_on_no_slip(boundary_obj, prev_timestep) else: - self.__call__() - return self._force_on_boundary(boundary_obj) - - # ------------------------------ Implementation Details ------------------------------------------------------------ + return self._force_on_boundary(boundary_obj, prev_timestep) - def _force_on_no_slip(self, boundary_obj): + def _force_on_no_slip(self, boundary_obj, prev_timestep): dh = self._data_handling ff_ghost_layers = dh.ghost_layers_of_field(self.flag_interface.flag_field_name) - method = self.lb_method + method = self._lb_method stencil = np.array(method.stencil) - result = np.zeros(self.dim) for b in dh.iterate(ghost_layers=ff_ghost_layers): @@ -39,21 +107,21 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): pdf_array = b[self._field_name] if boundary_obj in obj_to_ind_list: ind_arr = obj_to_ind_list[boundary_obj] - indices = [ind_arr[name] for name in ('x', 'y', 'z')[:method.dim]] - indices.append(ind_arr['dir']) - values = 2 * pdf_array[tuple(indices)] + acc = AccessPdfValues(dh.fields[self._field_name], self._lb_method.stencil, + streaming_pattern=self._streaming_pattern, timestep=prev_timestep, + streaming_dir='out') + values = 2 * acc.collect_from_index_list(pdf_array, ind_arr) forces = stencil[ind_arr['dir']] * values[:, np.newaxis] result += forces.sum(axis=0) return dh.reduce_float_sequence(list(result), 'sum') - def _force_on_boundary(self, boundary_obj): + def _force_on_boundary(self, boundary_obj, prev_timestep): dh = self._data_handling ff_ghost_layers = dh.ghost_layers_of_field(self.flag_interface.flag_field_name) - method = self.lb_method + method = self._lb_method stencil = np.array(method.stencil) inv_direction = np.array([method.stencil.index(inverse_direction(d)) - for d in method.stencil]) - + for d in method.stencil]) result = np.zeros(self.dim) for b in dh.iterate(ghost_layers=ff_ghost_layers): @@ -61,20 +129,25 @@ class LatticeBoltzmannBoundaryHandling(BoundaryHandling): pdf_array = b[self._field_name] if boundary_obj in obj_to_ind_list: ind_arr = obj_to_ind_list[boundary_obj] - indices = [ind_arr[name] for name in ('x', 'y', 'z')[:method.dim]] - offsets = stencil[ind_arr['dir']] - inv_dir = inv_direction[ind_arr['dir']] - fluid_values = pdf_array[tuple(indices) + (ind_arr['dir'],)] - boundary_values = pdf_array[tuple(ind + offsets[:, i] for i, ind in enumerate(indices)) + (inv_dir,)] + inverse_ind_arr = ind_arr.copy() + inverse_ind_arr['dir'] = inv_direction[inverse_ind_arr['dir']] + acc_out = AccessPdfValues(dh.fields[self._field_name], self._lb_method.stencil, + streaming_pattern=self._streaming_pattern, timestep=prev_timestep, + streaming_dir='out') + acc_in = AccessPdfValues(dh.fields[self._field_name], self._lb_method.stencil, + streaming_pattern=self._streaming_pattern, timestep=prev_timestep.next(), + streaming_dir='in') + acc_fluid = acc_out if boundary_obj.inner_or_boundary else acc_in + acc_boundary = acc_in if boundary_obj.inner_or_boundary else acc_out + fluid_values = acc_fluid.collect_from_index_list(pdf_array, ind_arr) + boundary_values = acc_boundary.collect_from_index_list(pdf_array, inverse_ind_arr) values = fluid_values + boundary_values forces = stencil[ind_arr['dir']] * values[:, np.newaxis] result += forces.sum(axis=0) return dh.reduce_float_sequence(list(result), 'sum') - def _create_boundary_kernel(self, symbolic_field, symbolic_index_field, boundary_obj): - return create_lattice_boltzmann_boundary_kernel(symbolic_field, symbolic_index_field, self.lb_method, - boundary_obj, target=self._target, openmp=self._openmp) +# end class LatticeBoltzmannBoundaryHandling class LbmWeightInfo(CustomCodeNode): @@ -82,8 +155,11 @@ class LbmWeightInfo(CustomCodeNode): # --------------------------- Functions to be used by boundaries -------------------------- @staticmethod - def weight_of_direction(dir_idx): - return sp.IndexedBase(LbmWeightInfo.WEIGHTS_SYMBOL, shape=(1,))[dir_idx] + def weight_of_direction(dir_idx, lb_method=None): + if isinstance(sp.sympify(dir_idx), sp.Integer): + return lb_method.weights[dir_idx].evalf() + else: + return sp.IndexedBase(LbmWeightInfo.WEIGHTS_SYMBOL, shape=(1,))[dir_idx] # ---------------------------------- Internal --------------------------------------------- @@ -94,10 +170,59 @@ class LbmWeightInfo(CustomCodeNode): 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 def create_lattice_boltzmann_boundary_kernel(pdf_field, index_field, lb_method, boundary_functor, + prev_timestep=Timestep.BOTH, streaming_pattern='pull', target='cpu', openmp=True, **kernel_creation_args): + from lbmpy.boundaries.boundaryconditions import Boundary as OldBoundary + if isinstance(boundary_functor, OldBoundary): + return create_lattice_boltzmann_boundary_kernel_old(pdf_field, index_field, lb_method, boundary_functor, + target=target, openmp=openmp, **kernel_creation_args) + + index_dtype = index_field.dtype.numpy_dtype.fields['dir'][0] + offsets_dtype = index_field.dtype.numpy_dtype.fields['x'][0] + indexing = BetweenTimestepsIndexing( + pdf_field, lb_method.stencil, prev_timestep, streaming_pattern, index_dtype, offsets_dtype) + + f_out, f_in = indexing.proxy_fields + dir_symbol = indexing.dir_symbol + inv_dir = indexing.inverse_dir_symbol + + boundary_assignments = boundary_functor(f_out, f_in, dir_symbol, inv_dir, lb_method, index_field) + boundary_assignments = indexing.substitute_proxies(boundary_assignments) + + # Code Elements inside the loop + elements = [Assignment(dir_symbol, index_field[0]('dir'))] + elements += boundary_assignments.all_assignments + + kernel = create_indexed_kernel(elements, [index_field], target=target, cpu_openmp=openmp, **kernel_creation_args) + + # Code Elements ahead of the loop + index_arrs_node = indexing.create_code_node() + for node in boundary_functor.get_additional_code_nodes(lb_method)[::-1]: + kernel.body.insert_front(node) + kernel.body.insert_front(index_arrs_node) + return kernel + + +# ----------------------------- Old, Deprecated Implementation ----------------------- + +def deprecation_message(): + import warnings + deprecation_message = "The old code generation scheme for LB boundaries has been deprecated. " \ + + "Please update your boundary implementation to derive from ``LbBoundary`` " \ + + "and use the new implementation scheme based on `BetweenTimestepsIndexing`." + warnings.simplefilter('always', DeprecationWarning) + warnings.warn(deprecation_message, DeprecationWarning, stacklevel=2) + warnings.simplefilter('default', DeprecationWarning) + + +def create_lattice_boltzmann_boundary_kernel_old(pdf_field, index_field, lb_method, boundary_functor, + target='cpu', openmp=True, **kernel_creation_args): + deprecation_message() + from pystencils.boundaries.boundaryhandling import BoundaryOffsetInfo elements = [BoundaryOffsetInfo(lb_method.stencil), LbmWeightInfo(lb_method)] index_arr_dtype = index_field.dtype.numpy_dtype dir_symbol = TypedSymbol("dir", index_arr_dtype.fields['dir'][0]) diff --git a/lbmpy/creationfunctions.py b/lbmpy/creationfunctions.py index 4b3cc76760f9474031016d5feee9f172d585c415..eef6d3a901941b2077c55c1906c97ba00110ab3e 100644 --- a/lbmpy/creationfunctions.py +++ b/lbmpy/creationfunctions.py @@ -57,8 +57,15 @@ General: - ``velocity_input``: symbolic field where the velocities are read from (for advection diffusion LBM) - ``density_input``: symbolic field or field access where to read density from. When passing this parameter, ``velocity_input`` has to be passed as well -- ``kernel_type``: supported values: 'stream_pull_collide' (default), 'collide_only', stream_pull_only, - collide_stream_push, esotwist_even, esotwist_odd, aa_even, aa_odd +- ``kernel_type``: 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``: The streaming pattern to be used with a 'default_stream_collide' kernel. Accepted values are + 'pull', 'push', 'aa' and 'esotwist'. +- ``timestep``: 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 speficied. Entropic methods: @@ -176,10 +183,7 @@ from copy import copy import sympy as sp import lbmpy.forcemodels as forcemodels -from lbmpy.fieldaccess import ( - AAEvenTimeStepAccessor, AAOddTimeStepAccessor, CollideOnlyInplaceAccessor, - EsoTwistEvenTimeStepAccessor, EsoTwistOddTimeStepAccessor, PdfFieldAccessor, - PeriodicTwoFieldsAccessor, StreamPullTwoFieldsAccessor, StreamPushTwoFieldsAccessor) +from lbmpy.fieldaccess import CollideOnlyInplaceAccessor, PdfFieldAccessor, PeriodicTwoFieldsAccessor from lbmpy.fluctuatinglb import add_fluctuations_to_collision_rule from lbmpy.methods import (create_mrt_orthogonal, create_mrt_raw, create_srt, create_trt, create_trt_kbc) from lbmpy.methods.creationfunctions import create_generic_mrt @@ -192,6 +196,7 @@ from lbmpy.simplificationfactory import create_simplification_strategy from lbmpy.stencils import get_stencil from lbmpy.turbulence_models import add_smagorinsky_model from lbmpy.updatekernels import create_lbm_kernel, create_stream_pull_with_output_kernel +from lbmpy.advanced_streaming.utility import Timestep, get_accessor from pystencils import Assignment, AssignmentCollection, create_kernel from pystencils.cache import disk_cache_no_fallback from pystencils.data_types import collate_types @@ -264,28 +269,19 @@ def create_lb_update_rule(collision_rule=None, optimization={}, **kwargs): dst_field = src_field.new_field_with_different_name(params['temporary_field_name']) kernel_type = params['kernel_type'] - if isinstance(kernel_type, PdfFieldAccessor): - accessor = kernel_type - return create_lbm_kernel(collision_rule, src_field, dst_field, accessor) - elif params['kernel_type'] == 'stream_pull_collide': - accessor = StreamPullTwoFieldsAccessor - if any(opt_params['builtin_periodicity']): - accessor = PeriodicTwoFieldsAccessor(opt_params['builtin_periodicity'], ghost_layers=1) - return create_lbm_kernel(collision_rule, src_field, dst_field, accessor) - elif params['kernel_type'] == 'stream_pull_only': + if kernel_type == 'stream_pull_only': return create_stream_pull_with_output_kernel(lb_method, src_field, dst_field, params['output']) else: - kernel_type_to_accessor = { - 'collide_only': CollideOnlyInplaceAccessor, - 'collide_stream_push': StreamPushTwoFieldsAccessor, - 'esotwist_even': EsoTwistEvenTimeStepAccessor, - 'esotwist_odd': EsoTwistOddTimeStepAccessor, - 'aa_even': AAEvenTimeStepAccessor, - 'aa_odd': AAOddTimeStepAccessor, - } - try: - accessor = kernel_type_to_accessor[kernel_type]() - except KeyError: + if kernel_type == 'default_stream_collide': + if params['streaming_pattern'] == 'pull' and any(opt_params['builtin_periodicity']): + accessor = PeriodicTwoFieldsAccessor(opt_params['builtin_periodicity'], ghost_layers=1) + else: + accessor = get_accessor(params['streaming_pattern'], params['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'", params['kernel_type']) return create_lbm_kernel(collision_rule, src_field, dst_field, accessor) @@ -341,7 +337,7 @@ def create_lb_collision_rule(lb_method=None, optimization={}, **kwargs): if 'split_groups' in collision_rule.simplification_hints: collision_rule.simplification_hints['split_groups'][0].append(sp.Symbol("smagorinsky_omega")) - if params['output'] and params['kernel_type'] == 'stream_pull_collide': + if params['output'] and params['kernel_type'] == 'default_stream_collide' and params['streaming_pattern'] == 'pull': cqc = lb_method.conserved_quantity_computation output_eqs = cqc.output_equations_from_pdfs(lb_method.pre_collision_pdf_symbols, params['output']) collision_rule = collision_rule.new_merged(output_eqs) @@ -540,7 +536,9 @@ def update_with_default_parameters(params, opt_params=None, fail_on_unknown_para 'velocity_input': None, 'density_input': None, - 'kernel_type': 'stream_pull_collide', + 'kernel_type': 'default_stream_collide', + 'streaming_pattern': 'pull', + 'timestep': Timestep.BOTH, 'field_name': 'src', 'temporary_field_name': 'dst', @@ -584,6 +582,22 @@ def update_with_default_parameters(params, opt_params=None, fail_on_unknown_para del params['relaxation_rate'] + # for backwards compatibility + if 'kernel_type' in params: + 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) + } + + kernel_type = params['kernel_type'] + if kernel_type in kernel_type_to_streaming_pattern.keys(): + params['kernel_type'] = 'default_stream_collide' + params['streaming_pattern'], params['timestep'] = kernel_type_to_streaming_pattern[kernel_type] + if 'pdf_arr' in opt_params: arr = opt_params['pdf_arr'] opt_params['field_size'] = tuple(e - 2 for e in arr.shape[:-1]) diff --git a/lbmpy/macroscopic_value_kernels.py b/lbmpy/macroscopic_value_kernels.py index fd4ac3d2708af24d65a1fe7b41cea3830c85f714..5d5a6f83bd5e19965f20197dc79f0af4b0e6c432 100644 --- a/lbmpy/macroscopic_value_kernels.py +++ b/lbmpy/macroscopic_value_kernels.py @@ -1,21 +1,41 @@ import functools from copy import deepcopy - from lbmpy.simplificationfactory import create_simplification_strategy from pystencils.field import Field, get_layout_of_array +from lbmpy.advanced_streaming.utility import get_accessor, Timestep + -def pdf_initialization_assignments(lb_method, density, velocity, pdfs): +def pdf_initialization_assignments(lb_method, density, velocity, pdfs, + streaming_pattern='pull', previous_timestep=Timestep.BOTH): """Assignments to initialize the pdf field with equilibrium""" + if isinstance(pdfs, Field): + previous_step_accessor = get_accessor(streaming_pattern, previous_timestep) + field_accesses = previous_step_accessor.write(pdfs, lb_method.stencil) + elif streaming_pattern == 'pull': + field_accesses = pdfs + else: + raise ValueError("Invalid value of pdfs: A PDF field reference is required to derive " + + f"initialization assignments for streaming pattern {streaming_pattern}.") + cqc = lb_method.conserved_quantity_computation inp_eqs = cqc.equilibrium_input_equations_from_init_values(density, velocity) setter_eqs = lb_method.get_equilibrium(conserved_quantity_equations=inp_eqs) - setter_eqs = setter_eqs.new_with_substitutions({sym: pdfs[i] + setter_eqs = setter_eqs.new_with_substitutions({sym: field_accesses[i] for i, sym in enumerate(lb_method.post_collision_pdf_symbols)}) return setter_eqs -def macroscopic_values_getter(lb_method, density, velocity, pdfs): +def macroscopic_values_getter(lb_method, density, velocity, pdfs, + streaming_pattern='pull', previous_timestep=Timestep.BOTH): + if isinstance(pdfs, Field): + previous_step_accessor = get_accessor(streaming_pattern, previous_timestep) + field_accesses = previous_step_accessor.write(pdfs, lb_method.stencil) + elif streaming_pattern == 'pull': + field_accesses = pdfs + else: + raise ValueError("Invalid value of pdfs: A PDF field reference is required to derive " + + f"getter assignments for streaming pattern {streaming_pattern}.") cqc = lb_method.conserved_quantity_computation assert not (velocity is None and density is None) output_spec = {} @@ -23,13 +43,16 @@ def macroscopic_values_getter(lb_method, density, velocity, pdfs): output_spec['velocity'] = velocity if density is not None: output_spec['density'] = density - return cqc.output_equations_from_pdfs(pdfs, output_spec) + return cqc.output_equations_from_pdfs(field_accesses, output_spec) macroscopic_values_setter = pdf_initialization_assignments -def compile_macroscopic_values_getter(lb_method, output_quantities, pdf_arr=None, field_layout='numpy', target='cpu'): +def compile_macroscopic_values_getter(lb_method, output_quantities, pdf_arr=None, + ghost_layers=1, iteration_slice=None, + field_layout='numpy', target='cpu', + streaming_pattern='pull', previous_timestep=Timestep.BOTH): """ Create kernel to compute macroscopic value(s) from a pdf field (e.g. density or velocity) @@ -37,8 +60,13 @@ def compile_macroscopic_values_getter(lb_method, output_quantities, pdf_arr=None lb_method: instance of :class:`lbmpy.methods.AbstractLbMethod` output_quantities: sequence of quantities to compute e.g. ['density', 'velocity'] pdf_arr: optional numpy array for pdf field - used to get optimal loop structure for kernel + ghost_layers: a sequence of pairs for each coordinate with lower and upper nr of ghost layers + that should be excluded from the iteration. If None, the number of ghost layers + is determined automatically and assumed to be equal for all dimensions. + iteration_slice: if not None, iteration is done only over this slice of the field field_layout: layout for output field, also used for pdf field if pdf_arr is not given target: 'cpu' or 'gpu' + previous_step_accessor: The accessor used by the streaming pattern of the previous timestep Returns: a function to compute macroscopic values: @@ -83,15 +111,19 @@ def compile_macroscopic_values_getter(lb_method, output_quantities, pdf_arr=None output_mapping[output_quantity] = output_mapping[output_quantity][0] stencil = lb_method.stencil - pdf_symbols = [pdf_field(i) for i in range(len(stencil))] + previous_step_accessor = get_accessor(streaming_pattern, previous_timestep) + pdf_symbols = previous_step_accessor.write(pdf_field, stencil) + eqs = cqc.output_equations_from_pdfs(pdf_symbols, output_mapping).all_assignments if target == 'cpu': import pystencils.cpu as cpu - kernel = cpu.make_python_function(cpu.create_kernel(eqs)) + kernel = cpu.make_python_function(cpu.create_kernel( + eqs, ghost_layers=ghost_layers, iteration_slice=iteration_slice)) elif target == 'gpu': import pystencils.gpucuda as gpu - kernel = gpu.make_python_function(gpu.create_cuda_kernel(eqs)) + kernel = gpu.make_python_function(gpu.create_cuda_kernel( + eqs, ghost_layers=ghost_layers, iteration_slice=iteration_slice)) else: raise ValueError("Unknown target '%s'. Possible targets are 'cpu' and 'gpu'" % (target,)) @@ -107,7 +139,10 @@ def compile_macroscopic_values_getter(lb_method, output_quantities, pdf_arr=None return getter -def compile_macroscopic_values_setter(lb_method, quantities_to_set, pdf_arr=None, field_layout='numpy', target='cpu'): +def compile_macroscopic_values_setter(lb_method, quantities_to_set, pdf_arr=None, + ghost_layers=1, iteration_slice=None, + field_layout='numpy', target='cpu', + streaming_pattern='pull', previous_timestep=Timestep.BOTH): """ Creates a function that sets a pdf field to specified macroscopic quantities The returned function can be called with the pdf field to set as single argument @@ -116,8 +151,13 @@ def compile_macroscopic_values_setter(lb_method, quantities_to_set, pdf_arr=None lb_method: instance of :class:`lbmpy.methods.AbstractLbMethod` quantities_to_set: map from conserved quantity name to fixed value or numpy array pdf_arr: optional numpy array for pdf field - used to get optimal loop structure for kernel + ghost_layers: a sequence of pairs for each coordinate with lower and upper nr of ghost layers + that should be excluded from the iteration. If None, the number of ghost layers + is determined automatically and assumed to be equal for all dimensions. + iteration_slice: if not None, iteration is done only over this slice of the field field_layout: layout of the pdf field if pdf_arr was not given target: 'cpu' or 'gpu' + previous_step_accessor: The accessor used by the streaming pattern of the previous timestep Returns: function taking pdf array as single argument and which sets the field to the given values @@ -155,7 +195,10 @@ def compile_macroscopic_values_setter(lb_method, quantities_to_set, pdf_arr=None else: eq = eq.new_without_subexpressions() - substitutions = {sym: pdf_field(i) for i, sym in enumerate(lb_method.post_collision_pdf_symbols)} + previous_step_accessor = get_accessor(streaming_pattern, previous_timestep) + write_accesses = previous_step_accessor.write(pdf_field, lb_method.stencil) + + substitutions = {sym: write_accesses[i] for i, sym in enumerate(lb_method.post_collision_pdf_symbols)} eq = eq.new_with_substitutions(substitutions).all_assignments if target == 'cpu': diff --git a/lbmpy/phasefield_allen_cahn/kernel_equations.py b/lbmpy/phasefield_allen_cahn/kernel_equations.py index 7316ed18914fe04ea4ede52f5209eb877646364b..0a57ea1b110a9a9f5c5f292897c118c87ab4aa55 100644 --- a/lbmpy/phasefield_allen_cahn/kernel_equations.py +++ b/lbmpy/phasefield_allen_cahn/kernel_equations.py @@ -1,9 +1,9 @@ -from lbmpy.creationfunctions import update_with_default_parameters -from lbmpy.fieldaccess import StreamPushTwoFieldsAccessor, CollideOnlyInplaceAccessor from pystencils.fd.derivation import FiniteDifferenceStencilDerivation -from lbmpy.maxwellian_equilibrium import get_weights from pystencils import Assignment, AssignmentCollection +from lbmpy.maxwellian_equilibrium import get_weights +from lbmpy.fieldaccess import StreamPushTwoFieldsAccessor, CollideOnlyInplaceAccessor + import sympy as sp import numpy as np @@ -320,29 +320,26 @@ def get_update_rules_velocity(src_field, u_in, lb_method, force, density, sub_it return update_u -def get_collision_assignments_hydro(density=1, optimization=None, sub_iterations=2, **kwargs): +def get_collision_assignments_hydro(lb_method, density, velocity_input, force, sub_iterations, symbolic_fields, + kernel_type): r""" Get collision assignments for the hydrodynamic lattice Boltzmann step. Here the force gets applied in the moment space. Afterwards the transformation back to the pdf space happens. Args: + lb_method: moment based lattice Boltzmann method density: the interpolated density of the simulation - optimization: for details see createfunctions.py + velocity_input: velocity field for the hydrodynamic and Allen-Chan LB step + force: force vector containing a summation of the surface tension-, pressure-, viscous- and bodyforce vector sub_iterations: number of updates of the velocity field + symbolic_fields: PDF fields for source and destination + kernel_type: collide_stream_push or collide_only """ - if optimization is None: - optimization = {} - params, opt_params = update_with_default_parameters(kwargs, optimization) - - lb_method = params['lb_method'] stencil = lb_method.stencil dimensions = len(stencil[0]) - u_in = params['velocity_input'] - force = params['force'] - - src_field = opt_params['symbolic_field'] - dst_field = opt_params['symbolic_temporary_field'] + src_field = symbolic_fields['symbolic_field'] + dst_field = symbolic_fields['symbolic_temporary_field'] moment_matrix = lb_method.moment_matrix rel = lb_method.relaxation_rates @@ -364,7 +361,8 @@ def get_collision_assignments_hydro(density=1, optimization=None, sub_iterations m = sp.symbols("m_:{}".format(len(stencil))) - update_m = get_update_rules_velocity(src_field, u_in, lb_method, force, density, sub_iterations=sub_iterations) + update_m = get_update_rules_velocity(src_field, velocity_input, lb_method, force, + density, sub_iterations=sub_iterations) u_symp = sp.symbols("u_:{}".format(dimensions)) for i in range(0, len(stencil)): @@ -372,7 +370,7 @@ def get_collision_assignments_hydro(density=1, optimization=None, sub_iterations update_g = list() var = np.dot(moment_matrix.inv().tolist(), m) - if params['kernel_type'] == 'collide_stream_push': + if kernel_type == 'collide_stream_push': push_accessor = StreamPushTwoFieldsAccessor() post_collision_accesses = push_accessor.write(dst_field, stencil) else: @@ -383,7 +381,7 @@ def get_collision_assignments_hydro(density=1, optimization=None, sub_iterations update_g.append(Assignment(post_collision_accesses[i], var[i])) for i in range(dimensions): - update_g.append(Assignment(u_in.center_vector[i], u_symp[i])) + update_g.append(Assignment(velocity_input.center_vector[i], u_symp[i])) hydro_lb_update_rule = AssignmentCollection(main_assignments=update_g, subexpressions=update_m) diff --git a/lbmpy/session.py b/lbmpy/session.py index baf9cdabbe80fe61813a48dd99ae3f3531309df1..4b0d051a3d7e30b7b8b85712913155e2ba42e260 100644 --- a/lbmpy/session.py +++ b/lbmpy/session.py @@ -3,6 +3,7 @@ import sympy as sp import lbmpy.plot as plt import pystencils as ps +from lbmpy.advanced_streaming import * from lbmpy.boundaries import * from lbmpy.creationfunctions import * from lbmpy.geometry import * diff --git a/lbmpy_tests/advanced_streaming/test_advanced_streaming_noslip.py b/lbmpy_tests/advanced_streaming/test_advanced_streaming_noslip.py new file mode 100644 index 0000000000000000000000000000000000000000..afbf31ea97ab145abfd497f1ca18c54260e53ea3 --- /dev/null +++ b/lbmpy_tests/advanced_streaming/test_advanced_streaming_noslip.py @@ -0,0 +1,62 @@ +import numpy as np +import sympy as sp + +from lbmpy.advanced_streaming import Timestep +from lbmpy.boundaries import NoSlip +from lbmpy.boundaries.boundaryhandling import create_lattice_boltzmann_boundary_kernel +from lbmpy.advanced_streaming.utility import even_accessors, odd_accessors, streaming_patterns, inverse_dir_index, AccessPdfValues +from lbmpy.creationfunctions import create_lb_method +from lbmpy.stencils import get_stencil + +import pystencils as ps + +from pystencils.boundaries.createindexlist import numpy_data_type_for_boundary_object +from pystencils.data_types import TypedSymbol, create_type +from pystencils.field import Field, FieldType + +from itertools import product + +import pytest + +@pytest.mark.parametrize("stencil", [ 'D2Q9', 'D3Q19', 'D3Q27']) +@pytest.mark.parametrize("streaming_pattern", streaming_patterns) +@pytest.mark.parametrize("prev_timestep", [Timestep.EVEN, Timestep.ODD]) +def test_advanced_streaming_noslip_single_cell(stencil, streaming_pattern, prev_timestep): + """ + Advanced Streaming NoSlip Test + """ + + stencil = get_stencil(stencil) + q = len(stencil) + dim = len(stencil[0]) + pdf_field = ps.fields(f'pdfs({q}): [{dim}D]') + + prev_pdf_access = AccessPdfValues(pdf_field, stencil, streaming_pattern, prev_timestep, 'out') + next_pdf_access = AccessPdfValues(pdf_field, stencil, streaming_pattern, prev_timestep.next(), 'in') + + pdfs = np.zeros((3,) * dim + (q,)) + pos = (1,) * dim + for d in range(q): + prev_pdf_access.write_pdf(pdfs, pos, d, d) + + lb_method = create_lb_method(stencil=stencil, method='srt') + noslip = NoSlip() + + index_struct_dtype = numpy_data_type_for_boundary_object(noslip, dim) + + index_field = Field('indexVector', FieldType.INDEXED, index_struct_dtype, layout=[0], + shape=(TypedSymbol("indexVectorSize", create_type(np.int64)), 1), strides=(1, 1)) + index_vector = np.array([ pos + (d,) for d in range(q) ], dtype=index_struct_dtype) + + ast = create_lattice_boltzmann_boundary_kernel(pdf_field, + index_field, lb_method, noslip, + prev_timestep=prev_timestep, + streaming_pattern=streaming_pattern) + + flex_kernel = ast.compile() + + flex_kernel(pdfs=pdfs, indexVector=index_vector, indexVectorSize=len(index_vector)) + + reflected_pdfs = [ next_pdf_access.read_pdf(pdfs, pos, d) for d in range(q)] + inverse_pdfs = [ inverse_dir_index(stencil, d) for d in range(q) ] + assert reflected_pdfs == inverse_pdfs \ No newline at end of file diff --git a/lbmpy_tests/advanced_streaming/test_communication.py b/lbmpy_tests/advanced_streaming/test_communication.py new file mode 100644 index 0000000000000000000000000000000000000000..bbdbd6ee7e175ded4d4dc72d80875ef76b04ba3a --- /dev/null +++ b/lbmpy_tests/advanced_streaming/test_communication.py @@ -0,0 +1,60 @@ +import numpy as np +from lbmpy.stencils import get_stencil +from pystencils.slicing import get_slice_before_ghost_layer, get_ghost_region_slice +from lbmpy.advanced_streaming.communication import get_communication_slices, _fix_length_one_slices +from lbmpy.advanced_streaming.utility import streaming_patterns, Timestep + +import pytest + +@pytest.mark.parametrize('stencil', ['D2Q9', 'D3Q19', 'D3Q27']) +@pytest.mark.parametrize('streaming_pattern', streaming_patterns) +@pytest.mark.parametrize('timestep', [Timestep.EVEN, Timestep.ODD]) +def test_slices_not_empty(stencil, streaming_pattern, timestep): + stencil = get_stencil(stencil) + dim = len(stencil[0]) + q = len(stencil) + arr = np.zeros( (4,) * dim + (q,) ) + slices = get_communication_slices(stencil, streaming_pattern=streaming_pattern, prev_timestep=timestep, ghost_layers=1) + for _, slices_list in slices.items(): + for src, dst in slices_list: + assert all(s != 0 for s in arr[src].shape) + assert all(s != 0 for s in arr[dst].shape) + + +@pytest.mark.parametrize('stencil', ['D2Q9', 'D3Q19', 'D3Q27']) +@pytest.mark.parametrize('streaming_pattern', streaming_patterns) +@pytest.mark.parametrize('timestep', [Timestep.EVEN, Timestep.ODD]) +def test_src_dst_same_shape(stencil, streaming_pattern, timestep): + stencil = get_stencil(stencil) + dim = len(stencil[0]) + q = len(stencil) + arr = np.zeros( (4,) * dim + (q,) ) + slices = get_communication_slices(stencil, streaming_pattern=streaming_pattern, prev_timestep=timestep, ghost_layers=1) + for _, slices_list in slices.items(): + for src, dst in slices_list: + src_shape = arr[src].shape + dst_shape = arr[dst].shape + assert src_shape == dst_shape + + +@pytest.mark.parametrize('stencil', ['D2Q9', 'D3Q19', 'D3Q27']) +def test_pull_communication_slices(stencil): + stencil = get_stencil(stencil) + + slices = get_communication_slices( + stencil, streaming_pattern='pull', prev_timestep=Timestep.BOTH, ghost_layers=1) + for i, d in enumerate(stencil): + if i == 0: + continue + + for s in slices[d]: + if s[0][-1] == i: + src = s[0][:-1] + dst = s[1][:-1] + break + + inner_slice = _fix_length_one_slices(get_slice_before_ghost_layer(d, ghost_layers=1)) + inv_dir = (-e for e in d) + gl_slice = _fix_length_one_slices(get_ghost_region_slice(inv_dir, ghost_layers=1)) + assert src == inner_slice + assert dst == gl_slice \ No newline at end of file diff --git a/lbmpy_tests/advanced_streaming/test_fully_periodic_flow.py b/lbmpy_tests/advanced_streaming/test_fully_periodic_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..e17b54707db8a035a4ba565590bba0b2b41c942a --- /dev/null +++ b/lbmpy_tests/advanced_streaming/test_fully_periodic_flow.py @@ -0,0 +1,168 @@ +import numpy as np +import sympy as sp + +from pystencils.datahandling import create_data_handling +from pystencils import create_kernel +from pystencils.plot import scalar_field, vector_field, vector_field_magnitude + +from lbmpy.creationfunctions import create_lb_collision_rule, create_lb_function +from lbmpy.macroscopic_value_kernels import macroscopic_values_getter, macroscopic_values_setter +from lbmpy.stencils import get_stencil + +from lbmpy.advanced_streaming import LBMPeriodicityHandling +from lbmpy.advanced_streaming.utility import is_inplace, streaming_patterns, get_timesteps + +import pytest +from numpy.testing import assert_allclose, assert_array_equal + +all_results = dict() + +targets = ['cpu'] + +try: + import pycuda.autoinit + targets += ['gpu'] +except Exception: + pass + +try: + import pystencils.opencl.autoinit + from pystencils.opencl.opencljit import get_global_cl_queue + if get_global_cl_queue() is not None: + targets += ['opencl'] +except Exception: + pass + + +@pytest.mark.parametrize('target', targets) +@pytest.mark.parametrize('stencil', ['D2Q9', 'D3Q19', 'D3Q27']) +@pytest.mark.parametrize('streaming_pattern', streaming_patterns) +def test_fully_periodic_flow(target, stencil, streaming_pattern): + + if target == 'opencl': + opencl_queue = get_global_cl_queue() + else: + opencl_queue = None + + gpu = target in ['gpu', 'opencl'] + + # Stencil + stencil = get_stencil(stencil) + q = len(stencil) + dim = len(stencil[0]) + + # Streaming + inplace = is_inplace(streaming_pattern) + timesteps = get_timesteps(streaming_pattern) + zeroth_timestep = timesteps[0] + + # Data Handling and PDF fields + domain_size = (30,) * dim + periodicity = (True,) * dim + + dh = create_data_handling(domain_size=domain_size, periodicity=periodicity, + default_target=target, opencl_queue=opencl_queue) + + pdfs = dh.add_array('pdfs', q) + if not inplace: + pdfs_tmp = dh.add_array_like('pdfs_tmp', pdfs.name) + + # LBM Streaming and Collision + method_params = { + 'stencil': stencil, + 'method': 'srt', + 'relaxation_rate': 1.0, + 'streaming_pattern': streaming_pattern + } + + optimization = { + 'symbolic_field': pdfs, + 'target': target + } + + if not inplace: + optimization['symbolic_temporary_field'] = pdfs_tmp + + lb_collision = create_lb_collision_rule(optimization=optimization, **method_params) + lb_method = lb_collision.method + + lb_kernels = [] + for t in timesteps: + lb_kernels.append(create_lb_function(collision_rule=lb_collision, + optimization=optimization, + timestep=t, + **method_params)) + + # Macroscopic Values + density = 1.0 + density_field = dh.add_array('rho', 1) + u_x = 0.01 + velocity = (u_x,) * dim + velocity_field = dh.add_array('u', dim) + + u_ref = np.full(domain_size + (dim,), u_x) + + setter = macroscopic_values_setter( + lb_method, density, velocity, pdfs, + streaming_pattern=streaming_pattern, previous_timestep=zeroth_timestep) + setter_kernel = create_kernel(setter, ghost_layers=1, target=target).compile() + + getter_kernels = [] + for t in timesteps: + getter = macroscopic_values_getter( + lb_method, density_field, velocity_field, pdfs, + streaming_pattern=streaming_pattern, previous_timestep=t) + getter_kernels.append(create_kernel(getter, ghost_layers=1, target=target).compile()) + + # Periodicity + periodicity_handler = LBMPeriodicityHandling(stencil, dh, pdfs.name, streaming_pattern=streaming_pattern) + + # Initialization and Timestep + current_timestep = zeroth_timestep + + def init(): + global current_timestep + current_timestep = zeroth_timestep + dh.run_kernel(setter_kernel) + + def one_step(): + global current_timestep + + # Periodicty + periodicity_handler(current_timestep) + + # Here, the next time step begins + current_timestep = current_timestep.next() + + # LBM Step + dh.run_kernel(lb_kernels[current_timestep.idx]) + + # Field Swaps + if not inplace: + dh.swap(pdfs.name, pdfs_tmp.name) + + # Macroscopic Values + dh.run_kernel(getter_kernels[current_timestep.idx]) + + # Run the simulation + init() + + for _ in range(100): + one_step() + + # Evaluation + if gpu: + dh.to_cpu(velocity_field.name) + u = dh.gather_array(velocity_field.name) + + # Equal to the steady-state velocity field up to numerical errors + assert_allclose(u, u_ref) + + # Flow must be equal up to numerical error for all streaming patterns + global all_results + for key, prev_u in all_results.items(): + if key[0] == stencil: + prev_pattern = key[1] + assert_allclose( + u, prev_u, err_msg=f'Velocity field for {streaming_pattern} differed from {prev_pattern}!') + all_results[(stencil, streaming_pattern)] = u diff --git a/lbmpy_tests/advanced_streaming/test_periodic_pipe_with_force.py b/lbmpy_tests/advanced_streaming/test_periodic_pipe_with_force.py new file mode 100644 index 0000000000000000000000000000000000000000..d5a69e9a09dfa0a5458fe310318c8fcf58160dbb --- /dev/null +++ b/lbmpy_tests/advanced_streaming/test_periodic_pipe_with_force.py @@ -0,0 +1,204 @@ +import numpy as np +import sympy as sp + +from pystencils.datahandling import create_data_handling +from pystencils import create_kernel +from pystencils.slicing import make_slice + +from lbmpy.creationfunctions import create_lb_collision_rule, create_lb_function +from lbmpy.macroscopic_value_kernels import macroscopic_values_getter, macroscopic_values_setter +from lbmpy.stencils import get_stencil + +from lbmpy.advanced_streaming import LBMPeriodicityHandling +from lbmpy.boundaries import NoSlip, LatticeBoltzmannBoundaryHandling +from lbmpy.advanced_streaming.utility import is_inplace, streaming_patterns, Timestep, get_timesteps + +import pytest +from numpy.testing import assert_allclose + +all_results = dict() + +targets = ['cpu'] + +try: + import pycuda.autoinit + targets += ['gpu'] +except Exception: + pass + +try: + import pystencils.opencl.autoinit + from pystencils.opencl.opencljit import get_global_cl_queue + if get_global_cl_queue() is not None: + targets += ['opencl'] +except Exception: + pass + + +class PeriodicPipeFlow: + def __init__(self, stencil, streaming_pattern, wall_boundary=None, target='cpu'): + + if wall_boundary is None: + wall_boundary = NoSlip() + + self.target = target + self.gpu = target in ['gpu', 'opencl'] + + # Stencil + self.stencil = stencil + self.q = len(self.stencil) + self.dim = len(self.stencil[0]) + + # Streaming + self.streaming_pattern = streaming_pattern + self.inplace = is_inplace(self.streaming_pattern) + self.timesteps = get_timesteps(streaming_pattern) + self.zeroth_timestep = self.timesteps[0] + + # Domain, Data Handling and PDF fields + self.pipe_length = 60 + self.pipe_radius = 15 + self.domain_size = (self.pipe_length, ) + (2 * self.pipe_radius,) * (self.dim - 1) + self.periodicity = (True, ) + (False, ) * (self.dim - 1) + self.force = (0.0001, ) + (0.0,) * (self.dim - 1) + + self.dh = create_data_handling(domain_size=self.domain_size, + periodicity=self.periodicity, default_target=self.target) + + self.pdfs = self.dh.add_array('pdfs', self.q) + if not self.inplace: + self.pdfs_tmp = self.dh.add_array_like('pdfs_tmp', self.pdfs.name) + + # LBM Streaming and Collision + method_params = { + 'stencil': stencil, + 'method': 'srt', + 'relaxation_rate': 1.0, + 'force_model': 'guo', + 'force': self.force, + 'streaming_pattern': streaming_pattern + } + + optimization = { + 'symbolic_field': self.pdfs, + 'target': self.target + } + + if not self.inplace: + optimization['symbolic_temporary_field'] = self.pdfs_tmp + + self.lb_collision = create_lb_collision_rule(optimization=optimization, **method_params) + self.lb_method = self.lb_collision.method + + self.lb_kernels = [] + for t in self.timesteps: + self.lb_kernels.append(create_lb_function(collision_rule=self.lb_collision, + optimization=optimization, + timestep=t, + **method_params)) + + # Macroscopic Values + self.density = 1.0 + self.density_field = self.dh.add_array('rho', 1) + u_x = 0.0 + self.velocity = (u_x,) * self.dim + self.velocity_field = self.dh.add_array('u', self.dim) + + setter = macroscopic_values_setter( + self.lb_method, self.density, self.velocity, self.pdfs, + streaming_pattern=self.streaming_pattern, previous_timestep=self.zeroth_timestep) + self.init_kernel = create_kernel(setter, ghost_layers=1, target=self.target).compile() + + self.getter_kernels = [] + for t in self.timesteps: + getter = macroscopic_values_getter( + self.lb_method, self.density_field, self.velocity_field, self.pdfs, + streaming_pattern=self.streaming_pattern, previous_timestep=t) + self.getter_kernels.append(create_kernel(getter, ghost_layers=1, target=self.target).compile()) + + # Periodicity + self.periodicity_handler = LBMPeriodicityHandling( + self.stencil, self.dh, self.pdfs.name, streaming_pattern=self.streaming_pattern) + + # Boundary Handling + self.wall = wall_boundary + self.bh = LatticeBoltzmannBoundaryHandling( + self.lb_method, self.dh, self.pdfs.name, + streaming_pattern=self.streaming_pattern, target=self.target) + + self.bh.set_boundary(boundary_obj=self.wall, mask_callback=self.mask_callback) + + self.current_timestep = self.zeroth_timestep + + def mask_callback(self, x, y, z=None): + y = y - self.pipe_radius + z = z - self.pipe_radius if z is not None else 0 + return np.sqrt(y**2 + z**2) >= self.pipe_radius + + def init(self): + self.current_timestep = self.zeroth_timestep + self.dh.run_kernel(self.init_kernel) + + def step(self): + # Order matters! First communicate, then boundaries, otherwise + # periodicity handling overwrites reflected populations + # Periodicty + self.periodicity_handler(self.current_timestep) + + # Boundaries + self.bh(prev_timestep=self.current_timestep) + + # Here, the next time step begins + self.current_timestep = self.current_timestep.next() + + # LBM Step + self.dh.run_kernel(self.lb_kernels[self.current_timestep.idx]) + + # Field Swaps + if not self.inplace: + self.dh.swap(self.pdfs.name, self.pdfs_tmp.name) + + # Macroscopic Values + self.dh.run_kernel(self.getter_kernels[self.current_timestep.idx]) + + def run(self, iterations): + for _ in range(iterations): + self.step() + + @property + def velocity_array(self): + if self.gpu: + self.dh.to_cpu(self.velocity_field.name) + return self.dh.gather_array(self.velocity_field.name) + + def get_trimmed_velocity_array(self): + if self.gpu: + self.dh.to_cpu(self.velocity_field.name) + u = np.copy(self.dh.gather_array(self.velocity_field.name)) + mask = self.bh.get_mask(None, self.wall) + for idx in np.ndindex(u.shape[:-1]): + if mask[idx] != 0: + u[idx] = np.full((self.dim, ), np.nan) + return u + + +@pytest.mark.parametrize('stencil', ['D2Q9', 'D3Q19', 'D3Q27']) +@pytest.mark.parametrize('streaming_pattern', streaming_patterns) +@pytest.mark.parametrize('target', targets) +def test_periodic_pipe(stencil, streaming_pattern, target): + stencil = get_stencil(stencil) + pipeflow = PeriodicPipeFlow(stencil, streaming_pattern, target=target) + pipeflow.init() + pipeflow.run(100) + u = pipeflow.get_trimmed_velocity_array() + + # Flow must be equal up to numerical error for all streaming patterns + global all_results + for key, prev_u in all_results.items(): + if key[0] == stencil: + prev_pattern = key[1] + assert_allclose( + u, prev_u, + rtol=1, atol=1e-16, + err_msg=f'Velocity field for {streaming_pattern} differed from {prev_pattern}!') + all_results[(stencil, streaming_pattern)] = u diff --git a/lbmpy_tests/phasefield_allen_cahn/test_codegen_3d.py b/lbmpy_tests/phasefield_allen_cahn/test_codegen_3d.py index f588ff3f9766b7bfdbe6ec01320aa100de0a4dda..6ca2a1dd35d0a1d3aa483dad67aaf2e9983bfc02 100644 --- a/lbmpy_tests/phasefield_allen_cahn/test_codegen_3d.py +++ b/lbmpy_tests/phasefield_allen_cahn/test_codegen_3d.py @@ -118,14 +118,16 @@ def test_codegen_3d(): density=density, velocity_input=u, force=force_g, - optimization={"symbolic_field": g, - "symbolic_temporary_field": g_tmp}, + sub_iterations=2, + symbolic_fields={"symbolic_field": g, + "symbolic_temporary_field": g_tmp}, kernel_type='collide_only') hydro_lb_update_rule_push = get_collision_assignments_hydro(lb_method=method_hydro, density=density, velocity_input=u, force=force_g, - optimization={"symbolic_field": g, - "symbolic_temporary_field": g_tmp}, + sub_iterations=2, + symbolic_fields={"symbolic_field": g, + "symbolic_temporary_field": g_tmp}, kernel_type='collide_stream_push') diff --git a/lbmpy_tests/phasefield_allen_cahn/test_phase_field_allen_cahn_2d.ipynb b/lbmpy_tests/phasefield_allen_cahn/test_phase_field_allen_cahn_2d.ipynb index 1acdde983d84a17a83e18ac6c3da2591ef71be70..cdf0127472986eb98f54cc26afb02050fb7e91e1 100644 --- a/lbmpy_tests/phasefield_allen_cahn/test_phase_field_allen_cahn_2d.ipynb +++ b/lbmpy_tests/phasefield_allen_cahn/test_phase_field_allen_cahn_2d.ipynb @@ -250,8 +250,8 @@ " velocity_input=u,\n", " force = force_g,\n", " sub_iterations=2,\n", - " optimization={\"symbolic_field\": g,\n", - " \"symbolic_temporary_field\": g_tmp},\n", + " symbolic_fields={\"symbolic_field\": g,\n", + " \"symbolic_temporary_field\": g_tmp},\n", " kernel_type='collide_only')\n", "\n", "hydro_lb_update_rule = sympy_cse(hydro_lb_update_rule)\n", @@ -391,7 +391,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.4" + "version": "3.8.2" } }, "nbformat": 4, diff --git a/lbmpy_tests/test_boundary_handling.py b/lbmpy_tests/test_boundary_handling.py index 7142f97fdeb690e8f6f3511bd2f88559d79e2705..946101ae27908f12bdb1a08a2d9ff8d1e5266a31 100644 --- a/lbmpy_tests/test_boundary_handling.py +++ b/lbmpy_tests/test_boundary_handling.py @@ -19,8 +19,14 @@ def test_simple(target): pytest.importorskip('pyopencl') import pystencils.opencl.autoinit - dh = create_data_handling((10, 5), parallel=False, default_target=target) - dh.add_array('pdfs', values_per_cell=9, cpu=True, gpu=target!='cpu') + dh = create_data_handling((4, 4), parallel=False, default_target=target) + dh.add_array('pdfs', values_per_cell=9, cpu=True, gpu=target != 'cpu') + for i in range(9): + dh.fill("pdfs", i, value_idx=i, ghost_layers=True) + + if target == 'gpu' or target == 'opencl': + dh.all_to_gpu() + lb_func = create_lb_function(stencil='D2Q9', compressible=False, relaxation_rate=1.8, @@ -29,7 +35,7 @@ def test_simple(target): bh = LatticeBoltzmannBoundaryHandling(lb_func.method, dh, 'pdfs', target=target) wall = NoSlip() - moving_wall = UBB((0.001, 0)) + moving_wall = UBB((1, 0)) bh.set_boundary(wall, make_slice[0, :]) bh.set_boundary(wall, make_slice[-1, :]) bh.set_boundary(wall, make_slice[:, 0]) @@ -38,6 +44,64 @@ def test_simple(target): bh.prepare() bh() + if target == 'gpu' or target == 'opencl': + dh.all_to_cpu() + # left lower corner + assert (dh.cpu_arrays['pdfs'][0, 0, 6] == 7) + + assert (dh.cpu_arrays['pdfs'][0, 1, 4] == 3) + assert (dh.cpu_arrays['pdfs'][0, 1, 6] == 7) + + assert (dh.cpu_arrays['pdfs'][1, 0, 1] == 2) + assert (dh.cpu_arrays['pdfs'][1, 0, 6] == 7) + + # left side + assert (all(dh.cpu_arrays['pdfs'][0, 2:4, 4] == 3)) + assert (all(dh.cpu_arrays['pdfs'][0, 2:4, 6] == 7)) + assert (all(dh.cpu_arrays['pdfs'][0, 2:4, 5] == 5)) + + # left upper corner + assert (dh.cpu_arrays['pdfs'][0, 4, 4] == 3) + assert (dh.cpu_arrays['pdfs'][0, 4, 8] == 5) + + assert (dh.cpu_arrays['pdfs'][0, 5, 8] == 5 + 6 / 36) + + assert (dh.cpu_arrays['pdfs'][1, 5, 8] == 5 + 6 / 36) + assert (dh.cpu_arrays['pdfs'][1, 5, 2] == 1) + + # top side + assert (all(dh.cpu_arrays['pdfs'][2:4, 5, 2] == 1)) + assert (all(dh.cpu_arrays['pdfs'][2:4, 5, 7] == 6 - 6 / 36)) + assert (all(dh.cpu_arrays['pdfs'][2:4, 5, 8] == 5 + 6 / 36)) + + # right upper corner + assert (dh.cpu_arrays['pdfs'][4, 5, 2] == 1) + assert (dh.cpu_arrays['pdfs'][4, 5, 7] == 6 - 6 / 36) + + assert (dh.cpu_arrays['pdfs'][5, 5, 7] == 6 - 6 / 36) + + assert (dh.cpu_arrays['pdfs'][5, 4, 3] == 4) + assert (dh.cpu_arrays['pdfs'][5, 4, 7] == 6) + + # right side + assert (all(dh.cpu_arrays['pdfs'][5, 2:4, 3] == 4)) + assert (all(dh.cpu_arrays['pdfs'][5, 2:4, 5] == 8)) + assert (all(dh.cpu_arrays['pdfs'][5, 2:4, 7] == 6)) + + # right lower corner + assert (dh.cpu_arrays['pdfs'][5, 1, 3] == 4) + assert (dh.cpu_arrays['pdfs'][5, 1, 5] == 8) + + assert (dh.cpu_arrays['pdfs'][5, 0, 5] == 8) + + assert (dh.cpu_arrays['pdfs'][4, 0, 1] == 2) + assert (dh.cpu_arrays['pdfs'][4, 0, 5] == 8) + + # lower side + assert (all(dh.cpu_arrays['pdfs'][0, 2:4, 4] == 3)) + assert (all(dh.cpu_arrays['pdfs'][0, 2:4, 6] == 7)) + assert (all(dh.cpu_arrays['pdfs'][0, 2:4, 8] == 5)) + def test_exotic_boundaries(): step = LatticeBoltzmannStep((50, 50), relaxation_rate=1.8, compressible=False, periodicity=False) diff --git a/lbmpy_tests/test_compiled_in_boundaries.ipynb b/lbmpy_tests/test_compiled_in_boundaries.ipynb index 070c7c2ba5236b2238798c0d719d86e26b2e14b8..ead57ea44120c5dfa20a5c34d4fcdb3837bc4b1f 100644 --- a/lbmpy_tests/test_compiled_in_boundaries.ipynb +++ b/lbmpy_tests/test_compiled_in_boundaries.ipynb @@ -40,7 +40,8 @@ "source": [ "dh = create_data_handling(domain_size, default_target='cpu')\n", "pdfs = dh.add_array('pdfs', values_per_cell=19)\n", - "u = dh.add_array('u', values_per_cell=len(domain_size))" + "u = dh.add_array('u', values_per_cell=len(domain_size))\n", + "streaming_pattern = 'aa'" ] }, { @@ -60,11 +61,13 @@ "opt = {'symbolic_field': pdfs, 'cse_global': False, 'cse_pdfs': True}\n", "cr_even = create_lb_collision_rule(stencil=\"D3Q19\", relaxation_rate=relaxation_rate, compressible=False, optimization=opt)\n", "cr_odd = create_lb_collision_rule(stencil=\"D3Q19\", relaxation_rate=relaxation_rate, compressible=False, optimization=opt)\n", - "update_rule_aa_even = update_rule_with_push_boundaries(cr_even, pdfs, boundaries, AAEvenTimeStepAccessor, AAOddTimeStepAccessor.read)\n", - "update_rule_aa_odd = update_rule_with_push_boundaries(cr_odd, pdfs, boundaries, AAOddTimeStepAccessor, AAEvenTimeStepAccessor.read)\n", + "update_rule_aa_even = update_rule_with_push_boundaries(cr_even, pdfs, boundaries, streaming_pattern, Timestep.EVEN)\n", + "update_rule_aa_odd = update_rule_with_push_boundaries(cr_odd, pdfs, boundaries, streaming_pattern, Timestep.ODD)\n", "\n", "getter_assignments = macroscopic_values_getter(update_rule_aa_even.method, velocity=u.center_vector,\n", - " pdfs=pdfs.center_vector, density=None)\n", + " pdfs=pdfs, density=None,\n", + " streaming_pattern=streaming_pattern, \n", + " previous_timestep=Timestep.EVEN)\n", "\n", "getter_kernel = ps.create_kernel(getter_assignments, target=dh.default_target).compile()\n", "even_kernel = ps.create_kernel(update_rule_aa_even, target=dh.default_target, ghost_layers=1).compile()\n", @@ -96,16 +99,25 @@ "metadata": {}, "outputs": [ { + "output_type": "execute_result", "data": { - "image/png": "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\n", "text/plain": [ - "<Figure size 1152x432 with 2 Axes>" + "<matplotlib.colorbar.Colorbar at 0x7f9d260364c0>" ] }, + "metadata": {}, + "execution_count": 6 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "<Figure size 1152x432 with 2 Axes>", + "image/svg+xml": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Created with matplotlib (https://matplotlib.org/) -->\n<svg height=\"357.238125pt\" version=\"1.1\" viewBox=\"0 0 844.941125 357.238125\" width=\"844.941125pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <metadata>\n <rdf:RDF xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n <cc:Work>\n <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\"/>\n <dc:date>2020-10-27T11:39:57.760623</dc:date>\n <dc:format>image/svg+xml</dc:format>\n <dc:creator>\n <cc:Agent>\n <dc:title>Matplotlib v3.3.1, https://matplotlib.org/</dc:title>\n </cc:Agent>\n </dc:creator>\n </cc:Work>\n </rdf:RDF>\n </metadata>\n <defs>\n <style type=\"text/css\">*{stroke-linecap:butt;stroke-linejoin:round;}</style>\n </defs>\n <g id=\"figure_1\">\n <g id=\"patch_1\">\n <path d=\"M 0 357.238125 \nL 844.941125 357.238125 \nL 844.941125 0 \nL 0 0 \nz\n\" style=\"fill:none;\"/>\n </g>\n <g id=\"axes_1\">\n <g id=\"patch_2\">\n <path d=\"M 26.925 333.36 \nL 741.165 333.36 \nL 741.165 7.2 \nL 26.925 7.2 \nz\n\" style=\"fill:#ffffff;\"/>\n </g>\n <g clip-path=\"url(#p581c60997f)\">\n <image height=\"327\" id=\"image73022fa66c\" transform=\"scale(1 -1)translate(0 -327)\" width=\"327\" x=\"220.965\" xlink:href=\"data:image/png;base64,\n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\" y=\"-6.36\"/>\n </g>\n <g id=\"matplotlib.axis_1\">\n <g id=\"xtick_1\">\n <g id=\"line2d_1\">\n <defs>\n <path d=\"M 0 0 \nL 0 3.5 \n\" id=\"m1c59ad8331\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"124.13625\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_1\">\n <!-- −10 -->\n <g transform=\"translate(113.583906 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 10.59375 35.5 \nL 73.1875 35.5 \nL 73.1875 27.203125 \nL 10.59375 27.203125 \nz\n\" id=\"DejaVuSans-8722\"/>\n <path d=\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id=\"DejaVuSans-49\"/>\n <path d=\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id=\"DejaVuSans-48\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-8722\"/>\n <use x=\"83.789062\" xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"147.412109\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_2\">\n <g id=\"line2d_2\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"226.06125\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_2\">\n <!-- 0 -->\n <g transform=\"translate(222.88 347.958438)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_3\">\n <g id=\"line2d_3\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"327.98625\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_3\">\n <!-- 10 -->\n <g transform=\"translate(321.62375 347.958438)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_4\">\n <g id=\"line2d_4\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"429.91125\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_4\">\n <!-- 20 -->\n <g transform=\"translate(423.54875 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id=\"DejaVuSans-50\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_5\">\n <g id=\"line2d_5\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"531.83625\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_5\">\n <!-- 30 -->\n <g transform=\"translate(525.47375 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id=\"DejaVuSans-51\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_6\">\n <g id=\"line2d_6\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"633.76125\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_6\">\n <!-- 40 -->\n <g transform=\"translate(627.39875 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 37.796875 64.3125 \nL 12.890625 25.390625 \nL 37.796875 25.390625 \nz\nM 35.203125 72.90625 \nL 47.609375 72.90625 \nL 47.609375 25.390625 \nL 58.015625 25.390625 \nL 58.015625 17.1875 \nL 47.609375 17.1875 \nL 47.609375 0 \nL 37.796875 0 \nL 37.796875 17.1875 \nL 4.890625 17.1875 \nL 4.890625 26.703125 \nz\n\" id=\"DejaVuSans-52\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-52\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_7\">\n <g id=\"line2d_7\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"735.68625\" xlink:href=\"#m1c59ad8331\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_7\">\n <!-- 50 -->\n <g transform=\"translate(729.32375 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id=\"DejaVuSans-53\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n </g>\n <g id=\"matplotlib.axis_2\">\n <g id=\"ytick_1\">\n <g id=\"line2d_8\">\n <defs>\n <path d=\"M 0 0 \nL -3.5 0 \n\" id=\"mb41b1d6a38\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"328.26375\"/>\n </g>\n </g>\n <g id=\"text_8\">\n <!-- 0 -->\n <g transform=\"translate(13.5625 332.062969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_2\">\n <g id=\"line2d_9\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"277.30125\"/>\n </g>\n </g>\n <g id=\"text_9\">\n <!-- 5 -->\n <g transform=\"translate(13.5625 281.100469)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_3\">\n <g id=\"line2d_10\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"226.33875\"/>\n </g>\n </g>\n <g id=\"text_10\">\n <!-- 10 -->\n <g transform=\"translate(7.2 230.137969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_4\">\n <g id=\"line2d_11\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"175.37625\"/>\n </g>\n </g>\n <g id=\"text_11\">\n <!-- 15 -->\n <g transform=\"translate(7.2 179.175469)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_5\">\n <g id=\"line2d_12\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"124.41375\"/>\n </g>\n </g>\n <g id=\"text_12\">\n <!-- 20 -->\n <g transform=\"translate(7.2 128.212969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_6\">\n <g id=\"line2d_13\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"73.45125\"/>\n </g>\n </g>\n <g id=\"text_13\">\n <!-- 25 -->\n <g transform=\"translate(7.2 77.250469)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_7\">\n <g id=\"line2d_14\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#mb41b1d6a38\" y=\"22.48875\"/>\n </g>\n </g>\n <g id=\"text_14\">\n <!-- 30 -->\n <g transform=\"translate(7.2 26.287969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n </g>\n <g id=\"patch_3\">\n <path d=\"M 26.925 333.36 \nL 26.925 7.2 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_4\">\n <path d=\"M 741.165 333.36 \nL 741.165 7.2 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_5\">\n <path d=\"M 26.925 333.36 \nL 741.165 333.36 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_6\">\n <path d=\"M 26.925 7.2 \nL 741.165 7.2 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n </g>\n <g id=\"axes_2\">\n <g id=\"patch_7\">\n <path clip-path=\"url(#p551391f2ca)\" d=\"M 785.805 333.36 \nL 785.805 332.085938 \nL 785.805 8.474063 \nL 785.805 7.2 \nL 802.113 7.2 \nL 802.113 8.474063 \nL 802.113 332.085938 \nL 802.113 333.36 \nz\n\" style=\"fill:#ffffff;stroke:#ffffff;stroke-linejoin:miter;stroke-width:0.01;\"/>\n </g>\n <image height=\"326\" id=\"image380bdcc536\" transform=\"scale(1 -1)translate(0 -326)\" width=\"16\" x=\"786\" xlink:href=\"data:image/png;base64,\niVBORw0KGgoAAAANSUhEUgAAABAAAAFGCAYAAABjUx8/AAABrklEQVR4nO2cwW3EMAwEKdmlpYT0X0qcIobArATe37r1aJYWDvCtn/X7Ffi8tTa5vt61F1tAT8C+vjpuoThEm8HalAG8hbfsBA0iUQ/WmDgitTDgEC8Qaco0IjVA/DgDmEDfxgAT9V0IYDAQIxiw6xPa+OkT6YMQA0TCHlzQRr9MVKSENvJboAvoP8L4DEakgVgZEPWxfr4HEQcMuECAiQMRJ5hDVgTE8xlMG0ekpgTs+gSI+i4EMPBF8tt4PoMxMUKkYTBtrGljS4IOBuyM4jMIgHg+A+yBD9GfBz4DXmfdRH8bAyAmdMFmAH+Lu4IBX6Dssd7ggb0LNEEERJzAZoA9wAx0ExMg2gkSGFCRNk8wEC+AuOHT2YcYIJLuQQDEBpHsXQiAaN8C3sYrGNgLBLSxgcGfm2AgdjB47IFyg0jP8QywBx1t1BnABAHPxgeLRFWmCTogQgZ+nWmCCIgXjHXKYNrIxzo+oTSYaENsMBEf83QG/kQ6v4380WbPxAQG+kRKOB9gBvYC8D3ZCBNxggAG8IWoBgY4gf2eawNEuEADRIah46SKGcB/qdIZ/AMyVI2OEN2MIAAAAABJRU5ErkJggg==\" y=\"-7\"/>\n <g id=\"matplotlib.axis_3\"/>\n <g id=\"matplotlib.axis_4\">\n <g id=\"ytick_8\">\n <g id=\"line2d_15\">\n <defs>\n <path d=\"M 0 0 \nL 3.5 0 \n\" id=\"m7d18a55188\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"291.918742\"/>\n </g>\n </g>\n <g id=\"text_15\">\n <!-- 0.005 -->\n <g transform=\"translate(809.113 295.71796)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id=\"DejaVuSans-46\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_9\">\n <g id=\"line2d_16\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"250.466707\"/>\n </g>\n </g>\n <g id=\"text_16\">\n <!-- 0.010 -->\n <g transform=\"translate(809.113 254.265926)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_10\">\n <g id=\"line2d_17\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"209.014673\"/>\n </g>\n </g>\n <g id=\"text_17\">\n <!-- 0.015 -->\n <g transform=\"translate(809.113 212.813892)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_11\">\n <g id=\"line2d_18\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"167.562639\"/>\n </g>\n </g>\n <g id=\"text_18\">\n <!-- 0.020 -->\n <g transform=\"translate(809.113 171.361858)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_12\">\n <g id=\"line2d_19\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"126.110605\"/>\n </g>\n </g>\n <g id=\"text_19\">\n <!-- 0.025 -->\n <g transform=\"translate(809.113 129.909824)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_13\">\n <g id=\"line2d_20\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"84.658571\"/>\n </g>\n </g>\n <g id=\"text_20\">\n <!-- 0.030 -->\n <g transform=\"translate(809.113 88.457789)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_14\">\n <g id=\"line2d_21\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#m7d18a55188\" y=\"43.206536\"/>\n </g>\n </g>\n <g id=\"text_21\">\n <!-- 0.035 -->\n <g transform=\"translate(809.113 47.005755)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n </g>\n <g id=\"patch_8\">\n <path d=\"M 785.805 333.36 \nL 785.805 332.085938 \nL 785.805 8.474063 \nL 785.805 7.2 \nL 802.113 7.2 \nL 802.113 8.474063 \nL 802.113 332.085938 \nL 802.113 333.36 \nz\n\" style=\"fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n </g>\n </g>\n <defs>\n <clipPath id=\"p581c60997f\">\n <rect height=\"326.16\" width=\"714.24\" x=\"26.925\" y=\"7.2\"/>\n </clipPath>\n <clipPath id=\"p551391f2ca\">\n <rect height=\"326.16\" width=\"16.308\" x=\"785.805\" y=\"7.2\"/>\n </clipPath>\n </defs>\n</svg>\n", + "image/png": "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\n" + }, "metadata": { "needs_background": "light" - }, - "output_type": "display_data" + } } ], "source": [ @@ -113,7 +125,7 @@ "aa_time_loop(time_steps)\n", "vel_version1 = dh.gather_array(u.name, ghost_layers=False).copy()\n", "plt.vector_field_magnitude(vel_version1[:, :, domain_size[2]//2, :])\n", - "plt.colorbar();" + "plt.colorbar()" ] }, { @@ -129,16 +141,25 @@ "metadata": {}, "outputs": [ { + "output_type": "execute_result", "data": { - "image/png": "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\n", "text/plain": [ - "<Figure size 1152x432 with 2 Axes>" + "<matplotlib.colorbar.Colorbar at 0x7f9d2158a520>" ] }, + "metadata": {}, + "execution_count": 7 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "<Figure size 1152x432 with 2 Axes>", + "image/svg+xml": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Created with matplotlib (https://matplotlib.org/) -->\n<svg height=\"357.238125pt\" version=\"1.1\" viewBox=\"0 0 844.941125 357.238125\" width=\"844.941125pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <metadata>\n <rdf:RDF xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n <cc:Work>\n <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\"/>\n <dc:date>2020-10-27T11:40:00.139854</dc:date>\n <dc:format>image/svg+xml</dc:format>\n <dc:creator>\n <cc:Agent>\n <dc:title>Matplotlib v3.3.1, https://matplotlib.org/</dc:title>\n </cc:Agent>\n </dc:creator>\n </cc:Work>\n </rdf:RDF>\n </metadata>\n <defs>\n <style type=\"text/css\">*{stroke-linecap:butt;stroke-linejoin:round;}</style>\n </defs>\n <g id=\"figure_1\">\n <g id=\"patch_1\">\n <path d=\"M 0 357.238125 \nL 844.941125 357.238125 \nL 844.941125 0 \nL 0 0 \nz\n\" style=\"fill:none;\"/>\n </g>\n <g id=\"axes_1\">\n <g id=\"patch_2\">\n <path d=\"M 26.925 333.36 \nL 741.165 333.36 \nL 741.165 7.2 \nL 26.925 7.2 \nz\n\" style=\"fill:#ffffff;\"/>\n </g>\n <g clip-path=\"url(#pc9b48b4476)\">\n <image height=\"327\" id=\"image9e0a1973bf\" transform=\"scale(1 -1)translate(0 -327)\" width=\"327\" x=\"220.965\" xlink:href=\"data:image/png;base64,\n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\" y=\"-6.36\"/>\n </g>\n <g id=\"matplotlib.axis_1\">\n <g id=\"xtick_1\">\n <g id=\"line2d_1\">\n <defs>\n <path d=\"M 0 0 \nL 0 3.5 \n\" id=\"m9545a64655\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"124.13625\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_1\">\n <!-- −10 -->\n <g transform=\"translate(113.583906 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 10.59375 35.5 \nL 73.1875 35.5 \nL 73.1875 27.203125 \nL 10.59375 27.203125 \nz\n\" id=\"DejaVuSans-8722\"/>\n <path d=\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id=\"DejaVuSans-49\"/>\n <path d=\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id=\"DejaVuSans-48\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-8722\"/>\n <use x=\"83.789062\" xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"147.412109\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_2\">\n <g id=\"line2d_2\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"226.06125\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_2\">\n <!-- 0 -->\n <g transform=\"translate(222.88 347.958438)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_3\">\n <g id=\"line2d_3\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"327.98625\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_3\">\n <!-- 10 -->\n <g transform=\"translate(321.62375 347.958438)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_4\">\n <g id=\"line2d_4\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"429.91125\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_4\">\n <!-- 20 -->\n <g transform=\"translate(423.54875 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id=\"DejaVuSans-50\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_5\">\n <g id=\"line2d_5\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"531.83625\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_5\">\n <!-- 30 -->\n <g transform=\"translate(525.47375 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id=\"DejaVuSans-51\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_6\">\n <g id=\"line2d_6\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"633.76125\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_6\">\n <!-- 40 -->\n <g transform=\"translate(627.39875 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 37.796875 64.3125 \nL 12.890625 25.390625 \nL 37.796875 25.390625 \nz\nM 35.203125 72.90625 \nL 47.609375 72.90625 \nL 47.609375 25.390625 \nL 58.015625 25.390625 \nL 58.015625 17.1875 \nL 47.609375 17.1875 \nL 47.609375 0 \nL 37.796875 0 \nL 37.796875 17.1875 \nL 4.890625 17.1875 \nL 4.890625 26.703125 \nz\n\" id=\"DejaVuSans-52\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-52\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_7\">\n <g id=\"line2d_7\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"735.68625\" xlink:href=\"#m9545a64655\" y=\"333.36\"/>\n </g>\n </g>\n <g id=\"text_7\">\n <!-- 50 -->\n <g transform=\"translate(729.32375 347.958438)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id=\"DejaVuSans-53\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n </g>\n <g id=\"matplotlib.axis_2\">\n <g id=\"ytick_1\">\n <g id=\"line2d_8\">\n <defs>\n <path d=\"M 0 0 \nL -3.5 0 \n\" id=\"m0adae693b2\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"328.26375\"/>\n </g>\n </g>\n <g id=\"text_8\">\n <!-- 0 -->\n <g transform=\"translate(13.5625 332.062969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_2\">\n <g id=\"line2d_9\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"277.30125\"/>\n </g>\n </g>\n <g id=\"text_9\">\n <!-- 5 -->\n <g transform=\"translate(13.5625 281.100469)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_3\">\n <g id=\"line2d_10\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"226.33875\"/>\n </g>\n </g>\n <g id=\"text_10\">\n <!-- 10 -->\n <g transform=\"translate(7.2 230.137969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_4\">\n <g id=\"line2d_11\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"175.37625\"/>\n </g>\n </g>\n <g id=\"text_11\">\n <!-- 15 -->\n <g transform=\"translate(7.2 179.175469)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_5\">\n <g id=\"line2d_12\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"124.41375\"/>\n </g>\n </g>\n <g id=\"text_12\">\n <!-- 20 -->\n <g transform=\"translate(7.2 128.212969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_6\">\n <g id=\"line2d_13\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"73.45125\"/>\n </g>\n </g>\n <g id=\"text_13\">\n <!-- 25 -->\n <g transform=\"translate(7.2 77.250469)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_7\">\n <g id=\"line2d_14\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"26.925\" xlink:href=\"#m0adae693b2\" y=\"22.48875\"/>\n </g>\n </g>\n <g id=\"text_14\">\n <!-- 30 -->\n <g transform=\"translate(7.2 26.287969)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n </g>\n <g id=\"patch_3\">\n <path d=\"M 26.925 333.36 \nL 26.925 7.2 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_4\">\n <path d=\"M 741.165 333.36 \nL 741.165 7.2 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_5\">\n <path d=\"M 26.925 333.36 \nL 741.165 333.36 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_6\">\n <path d=\"M 26.925 7.2 \nL 741.165 7.2 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n </g>\n <g id=\"axes_2\">\n <g id=\"patch_7\">\n <path clip-path=\"url(#pad89d0221e)\" d=\"M 785.805 333.36 \nL 785.805 332.085938 \nL 785.805 8.474063 \nL 785.805 7.2 \nL 802.113 7.2 \nL 802.113 8.474063 \nL 802.113 332.085938 \nL 802.113 333.36 \nz\n\" style=\"fill:#ffffff;stroke:#ffffff;stroke-linejoin:miter;stroke-width:0.01;\"/>\n </g>\n <image height=\"326\" id=\"imageb33b54f2e9\" transform=\"scale(1 -1)translate(0 -326)\" width=\"16\" x=\"786\" xlink:href=\"data:image/png;base64,\niVBORw0KGgoAAAANSUhEUgAAABAAAAFGCAYAAABjUx8/AAABrklEQVR4nO2cwW3EMAwEKdmlpYT0X0qcIobArATe37r1aJYWDvCtn/X7Ffi8tTa5vt61F1tAT8C+vjpuoThEm8HalAG8hbfsBA0iUQ/WmDgitTDgEC8Qaco0IjVA/DgDmEDfxgAT9V0IYDAQIxiw6xPa+OkT6YMQA0TCHlzQRr9MVKSENvJboAvoP8L4DEakgVgZEPWxfr4HEQcMuECAiQMRJ5hDVgTE8xlMG0ekpgTs+gSI+i4EMPBF8tt4PoMxMUKkYTBtrGljS4IOBuyM4jMIgHg+A+yBD9GfBz4DXmfdRH8bAyAmdMFmAH+Lu4IBX6Dssd7ggb0LNEEERJzAZoA9wAx0ExMg2gkSGFCRNk8wEC+AuOHT2YcYIJLuQQDEBpHsXQiAaN8C3sYrGNgLBLSxgcGfm2AgdjB47IFyg0jP8QywBx1t1BnABAHPxgeLRFWmCTogQgZ+nWmCCIgXjHXKYNrIxzo+oTSYaENsMBEf83QG/kQ6v4380WbPxAQG+kRKOB9gBvYC8D3ZCBNxggAG8IWoBgY4gf2eawNEuEADRIah46SKGcB/qdIZ/AMyVI2OEN2MIAAAAABJRU5ErkJggg==\" y=\"-7\"/>\n <g id=\"matplotlib.axis_3\"/>\n <g id=\"matplotlib.axis_4\">\n <g id=\"ytick_8\">\n <g id=\"line2d_15\">\n <defs>\n <path d=\"M 0 0 \nL 3.5 0 \n\" id=\"mb5e694fbcd\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"291.992282\"/>\n </g>\n </g>\n <g id=\"text_15\">\n <!-- 0.005 -->\n <g transform=\"translate(809.113 295.791501)scale(0.1 -0.1)\">\n <defs>\n <path d=\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id=\"DejaVuSans-46\"/>\n </defs>\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_9\">\n <g id=\"line2d_16\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"250.574889\"/>\n </g>\n </g>\n <g id=\"text_16\">\n <!-- 0.010 -->\n <g transform=\"translate(809.113 254.374108)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_10\">\n <g id=\"line2d_17\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"209.157495\"/>\n </g>\n </g>\n <g id=\"text_17\">\n <!-- 0.015 -->\n <g transform=\"translate(809.113 212.956714)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_11\">\n <g id=\"line2d_18\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"167.740101\"/>\n </g>\n </g>\n <g id=\"text_18\">\n <!-- 0.020 -->\n <g transform=\"translate(809.113 171.53932)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_12\">\n <g id=\"line2d_19\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"126.322708\"/>\n </g>\n </g>\n <g id=\"text_19\">\n <!-- 0.025 -->\n <g transform=\"translate(809.113 130.121927)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_13\">\n <g id=\"line2d_20\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"84.905314\"/>\n </g>\n </g>\n <g id=\"text_20\">\n <!-- 0.030 -->\n <g transform=\"translate(809.113 88.704533)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_14\">\n <g id=\"line2d_21\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"802.113\" xlink:href=\"#mb5e694fbcd\" y=\"43.487921\"/>\n </g>\n </g>\n <g id=\"text_21\">\n <!-- 0.035 -->\n <g transform=\"translate(809.113 47.287139)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-51\"/>\n <use x=\"222.65625\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n </g>\n <g id=\"patch_8\">\n <path d=\"M 785.805 333.36 \nL 785.805 332.085938 \nL 785.805 8.474063 \nL 785.805 7.2 \nL 802.113 7.2 \nL 802.113 8.474063 \nL 802.113 332.085938 \nL 802.113 333.36 \nz\n\" style=\"fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n </g>\n </g>\n <defs>\n <clipPath id=\"pc9b48b4476\">\n <rect height=\"326.16\" width=\"714.24\" x=\"26.925\" y=\"7.2\"/>\n </clipPath>\n <clipPath id=\"pad89d0221e\">\n <rect height=\"326.16\" width=\"16.308\" x=\"785.805\" y=\"7.2\"/>\n </clipPath>\n </defs>\n</svg>\n", + "image/png": "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\n" + }, "metadata": { "needs_background": "light" - }, - "output_type": "display_data" + } } ], "source": [ @@ -147,7 +168,7 @@ "vel_version2 = ldc.velocity[:, :, :, :]\n", "\n", "plt.vector_field_magnitude(vel_version2[:, :, domain_size[2]//2, :])\n", - "plt.colorbar();" + "plt.colorbar()" ] } ], @@ -167,9 +188,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.8.2-final" } }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/lbmpy_tests/test_force_on_boundary.py b/lbmpy_tests/test_force_on_boundary.py index 11bb17fffad9fc3bc547071a6f35a327221720db..023bd8014ba379285b446029867446db3b3478f3 100644 --- a/lbmpy_tests/test_force_on_boundary.py +++ b/lbmpy_tests/test_force_on_boundary.py @@ -4,10 +4,10 @@ from lbmpy.boundaries import UBB, NoSlip from lbmpy.scenarios import create_channel from pystencils import make_slice -try: - import waLBerla as wLB -except ImportError: - wLB = None +# try: +# import waLBerla as wLB +# except ImportError: +wLB = None def calculate_force(step, obstacle): diff --git a/lbmpy_tests/test_macroscopic_value_kernels.py b/lbmpy_tests/test_macroscopic_value_kernels.py index 09a0d66d2df28837a771841e3c3b194f2465e1ae..dcef102e9ec9cd2603214f40bf2a406ee18e168c 100644 --- a/lbmpy_tests/test_macroscopic_value_kernels.py +++ b/lbmpy_tests/test_macroscopic_value_kernels.py @@ -3,30 +3,45 @@ import numpy as np from lbmpy.creationfunctions import create_lb_method from lbmpy.macroscopic_value_kernels import ( compile_macroscopic_values_getter, compile_macroscopic_values_setter) +from lbmpy.advanced_streaming.utility import streaming_patterns, Timestep +import pytest -def test_set_get_density_velocity_with_fields(): - for stencil in ['D2Q9', 'D3Q19']: - for force_model in ['guo', 'luo', 'none']: - for compressible in [True, False]: - force = (0.1, 0.12, -0.17) - method = create_lb_method(stencil=stencil, force_model=force_model, method='trt', - compressible=compressible, force=force) - size = (3, 7, 4)[:method.dim] - pdf_arr = np.zeros(size + (len(method.stencil),)) - density_input_field = 1 + 0.2 * (np.random.random_sample(size) - 0.5) - velocity_input_field = 0.1 * (np.random.random_sample(size + (method.dim, )) - 0.5) - setter = compile_macroscopic_values_setter(method, pdf_arr=pdf_arr, - quantities_to_set={'density': density_input_field, - 'velocity': velocity_input_field}, ) - setter(pdf_arr) +@pytest.mark.parametrize('stencil', ['D2Q9', 'D3Q19']) +@pytest.mark.parametrize('force_model', ['guo', 'luo', None]) +@pytest.mark.parametrize('compressible', ['compressible', False]) +@pytest.mark.parametrize('streaming_pattern', streaming_patterns) +@pytest.mark.parametrize('prev_timestep', [Timestep.EVEN, Timestep.ODD]) +def test_set_get_density_velocity_with_fields(stencil, force_model, compressible, streaming_pattern, prev_timestep): + force = (0.1, 0.12, -0.17) + method = create_lb_method(stencil=stencil, force_model=force_model, method='trt', + compressible=compressible, force=force) + ghost_layers = 1 + inner_size = (3, 7, 4)[:method.dim] + total_size = tuple(s + 2 * ghost_layers for s in inner_size) + pdf_arr = np.zeros(total_size + (len(method.stencil),)) + + inner_slice = (slice(ghost_layers, -ghost_layers, 1), ) * method.dim + density_input_field = np.zeros(total_size) + velocity_input_field = np.zeros(total_size + (method.dim, )) + density_input_field[inner_slice] = 1 + 0.2 * (np.random.random_sample(inner_size) - 0.5) + velocity_input_field[inner_slice] = 0.1 * \ + (np.random.random_sample(inner_size + (method.dim, )) - 0.5) + + quantities_to_set = {'density': density_input_field, 'velocity': velocity_input_field} + + setter = compile_macroscopic_values_setter(method, pdf_arr=pdf_arr, quantities_to_set=quantities_to_set, + ghost_layers=ghost_layers, streaming_pattern=streaming_pattern, previous_timestep=prev_timestep) + setter(pdf_arr) + + getter = compile_macroscopic_values_getter(method, ['density', 'velocity'], pdf_arr=pdf_arr, + ghost_layers=ghost_layers, streaming_pattern=streaming_pattern, previous_timestep=prev_timestep) - getter = compile_macroscopic_values_getter(method, ['density', 'velocity'], pdf_arr=pdf_arr) - density_output_field = np.empty_like(density_input_field) - velocity_output_field = np.empty_like(velocity_input_field) - getter(pdfs=pdf_arr, density=density_output_field, velocity=velocity_output_field) - np.testing.assert_almost_equal(density_input_field, density_output_field) - np.testing.assert_almost_equal(velocity_input_field, velocity_output_field) + density_output_field = np.zeros_like(density_input_field) + velocity_output_field = np.zeros_like(velocity_input_field) + getter(pdfs=pdf_arr, density=density_output_field, velocity=velocity_output_field) + np.testing.assert_almost_equal(density_input_field, density_output_field) + np.testing.assert_almost_equal(velocity_input_field, velocity_output_field) def test_set_get_constant_velocity(): @@ -40,11 +55,11 @@ def test_set_get_constant_velocity(): compressible=compressible, force=force) size = (1, 1, 1)[:method.dim] pdf_arr = np.zeros(size + (len(method.stencil),)) - setter = compile_macroscopic_values_setter(method, pdf_arr=pdf_arr, + setter = compile_macroscopic_values_setter(method, pdf_arr=pdf_arr, ghost_layers=0, quantities_to_set={'velocity': ref_velocity[:method.dim]}, ) setter(pdf_arr) - getter = compile_macroscopic_values_getter(method, ['velocity'], pdf_arr=pdf_arr) + getter = compile_macroscopic_values_getter(method, ['velocity'], pdf_arr=pdf_arr, ghost_layers=0) velocity_output_field = np.zeros(size + (method.dim, )) getter(pdfs=pdf_arr, velocity=velocity_output_field) if method.dim == 2: