From 7a48b9d6635738187cfe5b36adfc58d137bf896d Mon Sep 17 00:00:00 2001 From: Martin Bauer <martin.bauer@fau.de> Date: Thu, 6 Apr 2017 15:29:09 +0200 Subject: [PATCH] Documentation & boundary helpers for lbmpy - documentation for lbmpy creationfunctions - new tutorial - boundary handling: various helpers to setup geometry --- boundaries/boundaryhandling.py | 158 ++++++++----- creationfunctions.py | 84 ++++--- plot2d.py | 109 ++++++++- scenarios.py | 418 +++++++++++++++++++-------------- 4 files changed, 498 insertions(+), 271 deletions(-) diff --git a/boundaries/boundaryhandling.py b/boundaries/boundaryhandling.py index 41e77228..86293d09 100644 --- a/boundaries/boundaryhandling.py +++ b/boundaries/boundaryhandling.py @@ -5,13 +5,20 @@ from lbmpy.stencils import getStencil from pystencils import TypedSymbol, Field from pystencils.backends.cbackend import CustomCppCode from lbmpy.boundaries.createindexlist import createBoundaryIndexList -from pystencils.slicing import normalizeSlice +from pystencils.slicing import normalizeSlice, makeSlice INV_DIR_SYMBOL = TypedSymbol("invDir", "int") WEIGHTS_SYMBOL = TypedSymbol("weights", "double") class BoundaryHandling(object): + class BoundaryInfo(object): + def __init__(self, name, flag, function, kernel): + self.name = name + self.flag = flag + self.function = function + self.kernel = kernel + def __init__(self, pdfField, domainShape, lbMethod, ghostLayers=1, target='cpu'): """ Class for managing boundary kernels @@ -32,13 +39,16 @@ class BoundaryHandling(object): self._symbolicPdfField = symbolicPdfField self._shapeWithGhostLayers = [d + 2 * ghostLayers for d in domainShape] - self._fluidFlag = 2 ** 30 + self._fluidFlag = 2 ** 0 self.flagField = np.full(self._shapeWithGhostLayers, self._fluidFlag, dtype=np.int32) self._ghostLayers = ghostLayers self._lbMethod = lbMethod - self._boundaryFunctions = [] - self._nameToIndex = {} - self._boundarySweeps = [] + + self._boundaryInfos = [] + self._nameToBoundary = {} + self._periodicityKernels = [] + + self._dirty = False self._periodicity = [False, False, False] self._target = target if target not in ('cpu', 'gpu'): @@ -46,34 +56,59 @@ class BoundaryHandling(object): @property def periodicity(self): + """List that indicates for x,y (z) coordinate if domain is periodic in that direction""" return self._periodicity + @property + def fluidFlag(self): + """Flag that is set where the lattice Boltzmann update should happen""" + return self._fluidFlag + + def getFlag(self, name): + """Flag that represents the boundary with given name. Raises KeyError if no such boundary exists.""" + return self._nameToBoundary[name].flag + + def getBoundaryNames(self): + """List of names of all registered boundary conditions""" + return [b.name for b in self._boundaryInfos] + def setPeriodicity(self, x=False, y=False, z=False): + """Enable periodic boundary conditions at the border of the domain""" self._periodicity = [x, y, z] - self.invalidateIndexCache() + self._compilePeriodicityKernels() + + def hasBoundary(self, name): + """Returns boolean indicating if a boundary with that name exists""" + return name in self._nameToBoundary - def setBoundary(self, function, indexExpr, maskArr=None, name=None): + def setBoundary(self, function, indexExpr=None, maskArr=None, name=None): """ Sets boundary in a rectangular region (slice) - :param function: boundary + :param function: boundary function or the string 'fluid' to remove boundary conditions :param indexExpr: slice expression, where boundary should be set, see :mod:`pystencils.slicing` :param maskArr: optional boolean (masked) array specifying where the boundary should be set :param name: name of the boundary """ - if name is None: - if hasattr(function, '__name__'): - name = function.__name__ - elif hasattr(function, 'name'): - name = function.name - else: - raise ValueError("Boundary function has to have a '__name__' or 'name' attribute " - "if name is not specified") + if indexExpr is None: + indexExpr = [slice(None, None, None)] * len(self.flagField.shape) + if function == 'fluid': + flag = self._fluidFlag + else: + if name is None: + if hasattr(function, '__name__'): + name = function.__name__ + elif hasattr(function, 'name'): + name = function.name + else: + raise ValueError("Boundary function has to have a '__name__' or 'name' attribute " + "if name is not specified") - if function not in self._boundaryFunctions: - self.addBoundary(function, name) + if not self.hasBoundary(name): + self.addBoundary(function, name) - flag = self.getFlag(name) + flag = self.getFlag(name) + assert flag != self._fluidFlag indexExpr = normalizeSlice(indexExpr, self._shapeWithGhostLayers) if maskArr is None: @@ -81,8 +116,7 @@ class BoundaryHandling(object): else: flagFieldView = self.flagField[indexExpr] flagFieldView[maskArr] = flag - - self.invalidateIndexCache() + self._dirty = True def addBoundary(self, boundaryFunction, name=None): """ @@ -97,45 +131,69 @@ class BoundaryHandling(object): if name is None: name = boundaryFunction.__name__ - if name in self._nameToIndex: - return 2 ** self._nameToIndex[name] + if self.hasBoundary(name): + return self._boundaryInfos[name].flag - newIdx = len(self._boundaryFunctions) - self._nameToIndex[name] = newIdx - self._boundaryFunctions.append(boundaryFunction) - return 2 ** newIdx + newIdx = len(self._boundaryInfos) + 1 # +1 because 2**0 is reserved for fluid flag + boundaryInfo = self.BoundaryInfo(name, 2 ** newIdx, boundaryFunction, None) + self._boundaryInfos.append(boundaryInfo) + self._nameToBoundary[name] = boundaryInfo + self._dirty = True + return boundaryInfo.flag - def invalidateIndexCache(self): - self._boundarySweeps = [] + def indices(self, dx=1.0, includeGhostLayers=False): + if not includeGhostLayers: + params = [np.arange(start=-1, stop=s-1) * dx for s in self.flagField.shape] + else: + params = [np.arange(s) * dx for s in self.flagField.shape] + return np.meshgrid(*params, indexing='ij') + + def __call__(self, **kwargs): + """Run the boundary handling, all keyword args are passed through to the boundary sweeps""" + if self._dirty: + self.prepare() + for boundary in self._boundaryInfos: + boundary.kernel(**kwargs) + for k in self._periodicityKernels: + k(**kwargs) def clear(self): + """Removes all boundaries and fills the domain with fluid""" np.fill(self._fluidFlag) - self.invalidateIndexCache() - - def getFlag(self, name): - return 2 ** self._nameToIndex[name] + self._dirty = False + self._boundaryInfos = [] + self._nameToBoundary = {} def prepare(self): - self.invalidateIndexCache() - for boundaryIdx, boundaryFunc in enumerate(self._boundaryFunctions): + """Fills data structures to speed up the boundary handling and compiles all boundary kernels. + This is automatically done when first called. With this function this can be triggered before running.""" + for boundary in self._boundaryInfos: + assert boundary.flag != self._fluidFlag idxField = createBoundaryIndexList(self.flagField, self._lbMethod.stencil, - 2 ** boundaryIdx, self._fluidFlag, self._ghostLayers) - ast = generateBoundaryHandling(self._symbolicPdfField, idxField, self._lbMethod, boundaryFunc, + boundary.flag, self._fluidFlag, self._ghostLayers) + ast = generateBoundaryHandling(self._symbolicPdfField, idxField, self._lbMethod, boundary.function, target=self._target) if self._target == 'cpu': from pystencils.cpu import makePythonFunction as makePythonCpuFunction - self._boundarySweeps.append(makePythonCpuFunction(ast, {'indexField': idxField})) + boundary.kernel = makePythonCpuFunction(ast, {'indexField': idxField}) elif self._target == 'gpu': from pystencils.gpucuda import makePythonFunction as makePythonGpuFunction import pycuda.gpuarray as gpuarray idxGpuField = gpuarray.to_gpu(idxField) - self._boundarySweeps.append(makePythonGpuFunction(ast, {'indexField': idxGpuField})) + boundary.kernel = makePythonGpuFunction(ast, {'indexField': idxGpuField}) else: assert False - self._addPeriodicityHandlers() + self._dirty = False + + def invalidateIndexCache(self): + """Invalidates the cache for optimization data structures. When setting boundaries the cache is automatically + invalidated, so there is no need to call this function manually, as long as the flag field is not manually + modified.""" + self._dirty = True - def _addPeriodicityHandlers(self): + def _compilePeriodicityKernels(self): + self._periodicityKernels = [] dim = len(self.flagField.shape) if dim == 2: stencil = getStencil("D2Q9") @@ -147,7 +205,7 @@ class BoundaryHandling(object): filteredStencil = [] for direction in stencil: useDirection = True - if direction == (0,0) or direction == (0,0,0): + if direction == (0, 0) or direction == (0, 0, 0): useDirection = False for component, periodicity in zip(direction, self._periodicity): if not periodicity and component != 0: @@ -158,22 +216,16 @@ class BoundaryHandling(object): if len(filteredStencil) > 0: if self._target == 'cpu': from pystencils.slicing import getPeriodicBoundaryFunctor - self._boundarySweeps.append(getPeriodicBoundaryFunctor(filteredStencil, ghostLayers=1)) + self._periodicityKernels.append(getPeriodicBoundaryFunctor(filteredStencil, ghostLayers=1)) elif self._target == 'gpu': from pystencils.gpucuda.periodicity import getPeriodicBoundaryFunctor - self._boundarySweeps.append(getPeriodicBoundaryFunctor(filteredStencil, self.flagField.shape, - indexDimensions=1, - indexDimShape=len(self._lbMethod.stencil), - ghostLayers=1)) + self._periodicityKernels.append(getPeriodicBoundaryFunctor(filteredStencil, self.flagField.shape, + indexDimensions=1, + indexDimShape=len(self._lbMethod.stencil), + ghostLayers=1)) else: assert False - def __call__(self, **kwargs): - if len(self._boundarySweeps) == 0: - self.prepare() - for boundarySweep in self._boundarySweeps: - boundarySweep(**kwargs) - # -------------------------------------- Helper Functions -------------------------------------------------------------- diff --git a/creationfunctions.py b/creationfunctions.py index e7a8191d..3041e356 100644 --- a/creationfunctions.py +++ b/creationfunctions.py @@ -2,42 +2,6 @@ r""" Creating LBM kernels ==================== - -Terminology and creation pipeline ---------------------------------- - -Kernel functions are created in three steps: - -1. *Method*: - the method defines the collision process. Currently there are two big categories: - moment and cumulant based methods. A method defines how each moment or cumulant is relaxed by - storing the equilibrium value and the relaxation rate for each moment/cumulant. -2. *Collision/Update Rule*: - Methods can generate a "collision rule" which is an equation collection that define the - post collision values as a function of the pre-collision values. On these equation collection - simplifications are applied to reduce the number of floating point operations. - At this stage an entropic optimization step can also be added to determine one relaxation rate by an - entropy condition. - Then a streaming rule is added which transforms the collision rule into an update rule. - The streaming step depends on the pdf storage (source/destination, AABB pattern, EsoTwist). - Currently only the simple source/destination pattern is supported. -3. *AST*: - The abstract syntax tree describes the structure of the kernel, including loops and conditionals. - The ast can be modified e.g. to add OpenMP pragmas, reorder loops or apply other optimizations. -4. *Function*: - This step compiles the AST into an executable function, either for CPU or GPUs. This function - behaves like a normal Python function and runs one LBM time step. - -The function :func:`createLatticeBoltzmannFunction` runs the whole pipeline, the other functions in this module -execute this pipeline only up to a certain step. Each function optionally also takes the result of the previous step. - -For example, to modify the AST one can run:: - - ast = createLatticeBoltzmannAst(...) - # modify ast here - func = createLatticeBoltzmannFunction(ast=ast, ...) - - Parameters ---------- @@ -69,7 +33,8 @@ General: yet, only the relaxation pattern. To get the entropic method, see parameters below! (:func:`lbmpy.methods.createKBCTypeTRT`) - ``relaxationRates``: sequence of relaxation rates, number depends on selected method. If you specify more rates than - method needs, the additional rates are ignored. + method needs, the additional rates are ignored. For SRT and TRT models it is possible ot define a single + ``relaxationRate`` instead of a list, the second rate for TRT is then determined via magic number. - ``compressible=False``: affects the selection of equilibrium moments. Both options approximate the *incompressible* Navier Stokes Equations. However when chosen as False, the approximation is better, the standard LBM derivation is compressible. @@ -130,6 +95,44 @@ Other: specifying the number of threads. If True is specified OpenMP chooses the number of threads - ``doublePrecision=True``: by default simulations run with double precision floating point numbers, by setting this parameter to False, single precision is used, which is much faster, especially on GPUs + + + + +Terminology and creation pipeline +--------------------------------- + +Kernel functions are created in three steps: + +1. *Method*: + the method defines the collision process. Currently there are two big categories: + moment and cumulant based methods. A method defines how each moment or cumulant is relaxed by + storing the equilibrium value and the relaxation rate for each moment/cumulant. +2. *Collision/Update Rule*: + Methods can generate a "collision rule" which is an equation collection that define the + post collision values as a function of the pre-collision values. On these equation collection + simplifications are applied to reduce the number of floating point operations. + At this stage an entropic optimization step can also be added to determine one relaxation rate by an + entropy condition. + Then a streaming rule is added which transforms the collision rule into an update rule. + The streaming step depends on the pdf storage (source/destination, AABB pattern, EsoTwist). + Currently only the simple source/destination pattern is supported. +3. *AST*: + The abstract syntax tree describes the structure of the kernel, including loops and conditionals. + The ast can be modified e.g. to add OpenMP pragmas, reorder loops or apply other optimizations. +4. *Function*: + This step compiles the AST into an executable function, either for CPU or GPUs. This function + behaves like a normal Python function and runs one LBM time step. + +The function :func:`createLatticeBoltzmannFunction` runs the whole pipeline, the other functions in this module +execute this pipeline only up to a certain step. Each function optionally also takes the result of the previous step. + +For example, to modify the AST one can run:: + + ast = createLatticeBoltzmannAst(...) + # modify ast here + func = createLatticeBoltzmannFunction(ast=ast, ...) + """ import sympy as sp from copy import copy @@ -138,6 +141,7 @@ from functools import partial from lbmpy.methods import createSRT, createTRT, createOrthogonalMRT, createKBCTypeTRT, \ createRawMRT, createThreeRelaxationRateMRT from lbmpy.methods.entropic import addIterativeEntropyCondition, addEntropyCondition +from lbmpy.methods.relaxationrates import relaxationRateFromMagicNumber from lbmpy.stencils import getStencil import lbmpy.forcemodels as forceModels from lbmpy.simplificationfactory import createSimplificationStrategy @@ -179,6 +183,12 @@ def updateWithDefaultParameters(params, optParams): 'gpuIndexing': 'block', 'gpuIndexingParams': {}, } + if 'relaxationRate' in params: + if 'relaxationRates' not in params: + params['relaxationRates'] = [params['relaxationRate'], + relaxationRateFromMagicNumber(params['relaxationRate'])] + del params['relaxationRate'] + unknownParams = [k for k in params.keys() if k not in defaultMethodDescription] unknownOptParams = [k for k in optParams.keys() if k not in defaultOptimizationDescription] if unknownParams: diff --git a/plot2d.py b/plot2d.py index c303162f..f49f7fe4 100644 --- a/plot2d.py +++ b/plot2d.py @@ -2,28 +2,127 @@ from matplotlib.pyplot import * def vectorField(field, step=2, **kwargs): + """ + Plot given vector field as quiver (arrow) plot. + + :param field: numpy array with 3 dimensions, first two are spatial x,y coordinate, the last + coordinate should have shape 2 and stores the 2 velocity components + :param step: plots only every steps's cell + :param kwargs: keyword arguments passed to :func:`matplotlib.pyplot.quiver` + """ veln = field.swapaxes(0, 1) - quiver(veln[::step, ::step, 0], veln[::step, ::step, 1], **kwargs) + res = quiver(veln[::step, ::step, 0], veln[::step, ::step, 1], **kwargs) + axis('equal') + return res def vectorFieldMagnitude(field, **kwargs): + """ + Plots the magnitude of a vector field as colormap + :param field: numpy array with 3 dimensions, first two are spatial x,y coordinate, the last + coordinate should have shape 2 and stores the 2 velocity components + :param kwargs: keyword arguments passed to :func:`matplotlib.pyplot.imshow` + """ from numpy.linalg import norm norm = norm(field, axis=2, ord=2) if hasattr(field, 'mask'): - norm = np.ma.masked_array(norm, mask=field.mask[:,:,0]) + norm = np.ma.masked_array(norm, mask=field.mask[:, :, 0]) return scalarField(norm, **kwargs) def scalarField(field, **kwargs): + """ + Plots field values as colormap + + :param field: two dimensional numpy array + :param kwargs: keyword arguments passed to :func:`matplotlib.pyplot.imshow` + """ import numpy as np field = np.swapaxes(field, 0, 1) - return imshow(field, origin='lower', **kwargs) + res = imshow(field, origin='lower', **kwargs) + axis('equal') + return res + + +def plotBoundaryHandling(boundaryHandling, boundaryNameToColor=None): + """ + Shows boundary cells + + :param boundaryHandling: instance of :class:`lbmpy.boundaries.BoundaryHandling` + :param boundaryNameToColor: optional dictionary mapping boundary names to colors + """ + import matplotlib + import matplotlib.pyplot as plt + + if len(boundaryHandling.flagField.shape) != 2: + raise NotImplementedError("Only implemented for 2D boundary handlings") + + if boundaryNameToColor: + fixedColors = boundaryNameToColor + else: + fixedColors = { + 'fluid': '#1f77ff11', + 'noSlip': '#000000' + } + + boundaryNames = ['fluid'] + boundaryHandling.getBoundaryNames() + flagValues = [boundaryHandling.fluidFlag] + [boundaryHandling.getFlag(n) + for n in boundaryHandling.getBoundaryNames()] + + defaultCycler = matplotlib.rcParams['axes.prop_cycle'] + colorValues = [fixedColors[name] if name in fixedColors else cycle['color'] + for cycle, name in zip(defaultCycler, boundaryNames)] + + cmap = matplotlib.colors.ListedColormap(colorValues) + bounds = np.array(flagValues, dtype=float) - 0.5 + bounds = list(bounds) + [bounds[-1] + 1] + norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N) + + flagField = boundaryHandling.flagField.swapaxes(0, 1) + plt.imshow(flagField, interpolation='none', origin='lower', + cmap=cmap, norm=norm) + + patches = [matplotlib.patches.Patch(color=color, label=name) for color, name in zip(colorValues, boundaryNames)] + plt.axis('equal') + plt.legend(handles=patches, bbox_to_anchor=(1.02, 0.5), loc=2, borderaxespad=0.) + +# ------------------------------------------- Animations --------------------------------------------------------------- + + +def vectorFieldAnimation(runFunction, step=2, rescale=True, plotSetupFunction=lambda: None, + plotUpdateFunction=lambda: None, interval=30, frames=180, **kwargs): + import matplotlib.animation as animation + from numpy.linalg import norm + + fig = gcf() + im = None + field = runFunction() + if rescale: + maxNorm = np.max(norm(field, axis=2, ord=2)) + field /= maxNorm + if 'scale' not in kwargs: + kwargs['scale'] = 1.0 + + quiverPlot = vectorField(field, step=step, **kwargs) + plotSetupFunction() + + def updatefig(*args): + f = runFunction() + f = np.swapaxes(f, 0, 1) + if rescale: + maxNorm = np.max(norm(f, axis=2, ord=2)) + f /= maxNorm + u, v = f[::step, ::step, 0], f[::step, ::step, 1] + quiverPlot.set_UVC(u, v) + plotUpdateFunction() + return im, + + return animation.FuncAnimation(fig, updatefig, interval=interval, frames=frames) def vectorFieldMagnitudeAnimation(runFunction, plotSetupFunction=lambda: None, plotUpdateFunction=lambda: None, interval=30, frames=180, **kwargs): import matplotlib.animation as animation - import numpy as np from numpy.linalg import norm fig = gcf() @@ -42,4 +141,4 @@ def vectorFieldMagnitudeAnimation(runFunction, plotSetupFunction=lambda: None, plotUpdateFunction() return im, - return animation.FuncAnimation(fig, updatefig, interval=interval, frames=frames) + return animation.FuncAnimation(fig, updatefig, interval=interval, frames=frames) \ No newline at end of file diff --git a/scenarios.py b/scenarios.py index 1f07e470..f2e96ab6 100644 --- a/scenarios.py +++ b/scenarios.py @@ -1,8 +1,33 @@ +""" +Scenario setup +============== + +This module contains functions to set up pre-defined scenarios like a lid-driven cavity or channel flows. +It is a good starting point if you are new to lbmpy. + +>>> scenario = createForceDrivenChannel(dim=2, radius=10, length=20, force=1e-5, +... method='srt', relaxationRate=1.9) +>>> scenario.run(100) +>>> scenario.plotVelocity() + +All scenarios can be modified, for example you can create a simple channel first, then place an object in it: + +>>> from lbmpy.boundaries import noSlip +>>> from pystencils.slicing import makeSlice +>>> scenario.boundaryHandling.setBoundary(noSlip, makeSlice[0.3:0.4, 0.0:0.3]) + +Functions for scenario setup: +----------------------------- + +All of the following scenario creation functions take keyword arguments specifying which LBM method should be used +and a ``optimizationParams`` dictionary, defining performance related options. These parameters are documented +at :mod:`lbmpy.creationfunctions`. The only mandatory keyword parameter is ``relaxationRate``, +that defines the viscosity of the fluid (valid values being between 0 and 2). +""" import numpy as np from functools import partial from pystencils.field import getLayoutOfArray, createNumpyArrayWithLayout -from pystencils.slicing import sliceFromDirection, addGhostLayers, getPeriodicBoundaryFunctor, removeGhostLayers, \ - normalizeSlice, makeSlice +from pystencils.slicing import sliceFromDirection, addGhostLayers, removeGhostLayers, normalizeSlice, makeSlice from lbmpy.creationfunctions import createLatticeBoltzmannFunction, updateWithDefaultParameters from lbmpy.macroscopic_value_kernels import compileMacroscopicValuesGetter, compileMacroscopicValuesSetter from lbmpy.boundaries import BoundaryHandling, noSlip, ubb, fixedDensity @@ -10,27 +35,210 @@ from lbmpy.stencils import getStencil from lbmpy.updatekernels import createPdfArray +# ---------------------------------------- Example Scenarios ----------------------------------------------------------- + + +def createFullyPeriodicFlow(initialVelocity, optimizationParams={}, lbmKernel=None, **kwargs): + """ + Creates a fully periodic setup with prescribed velocity field + + :param initialVelocity: numpy array that defines an initial velocity for each cell. The shape of this + array determines the domain size. + :param optimizationParams: see :mod:`lbmpy.creationfunctions` + :param lbmKernel: a LBM function, which would otherwise automatically created + :param kwargs: other parameters are passed on to the method, see :mod:`lbmpy.creationfunctions` + :return: instance of :class:`Scenario` + """ + domainSize = initialVelocity.shape[:-1] + scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel, initialVelocity) + scenario.boundaryHandling.setPeriodicity(True, True, True) + return scenario + + +def createLidDrivenCavity(domainSize, lidVelocity=0.005, optimizationParams={}, lbmKernel=None, **kwargs): + """ + Creates a lid driven cavity scenario + + :param domainSize: tuple specifying the number of cells in each dimension + :param lidVelocity: x velocity of lid in lattice coordinates. + :param optimizationParams: see :mod:`lbmpy.creationfunctions` + :param lbmKernel: a LBM function, which would otherwise automatically created + :param kwargs: other parameters are passed on to the method, see :mod:`lbmpy.creationfunctions` + :return: instance of :class:`Scenario` + """ + scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel) + + myUbb = partial(ubb, velocity=[lidVelocity, 0, 0][:scenario.method.dim]) + myUbb.name = 'ubb' + dim = scenario.method.dim + scenario.boundaryHandling.setBoundary(myUbb, sliceFromDirection('N', dim)) + for direction in ('W', 'E', 'S') if scenario.method.dim == 2 else ('W', 'E', 'S', 'T', 'B'): + scenario.boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) + + return scenario + + +def createForceDrivenChannel(force=1e-6, domainSize=None, dim=2, radius=None, length=None, initialVelocity=None, + optimizationParams={}, lbmKernel=None, **kwargs): + """ + Creates a channel flow in x direction, which is driven by a constant force along the x axis + + :param force: force in x direction (lattice units) that drives the channel + :param domainSize: tuple with size of channel in x, y, (z) direction. If not specified, pass dim, radius and length. + In 3D, this creates a channel with rectangular cross section + :param dim: dimension of the channel (only required if domainSize is not passed) + :param radius: radius in 3D, or half of the height in 2D (only required if domainSize is not passed). + In 3D, this creates a channel with circular cross section + :param length: extend in x direction (only required if domainSize is not passed) + :param initialVelocity: initial velocity, either array to specify velocity for each cell or tuple for constant + :param optimizationParams: see :mod:`lbmpy.creationfunctions` + :param lbmKernel: a LBM function, which would otherwise automatically created + :param kwargs: other parameters are passed on to the method, see :mod:`lbmpy.creationfunctions` + :return: instance of :class:`Scenario` + """ + if domainSize is not None: + dim = len(domainSize) + else: + if dim is None or radius is None or length is None: + raise ValueError("Pass either 'domainSize' or 'dim', 'radius' and 'length'") + + assert dim in (2, 3) + kwargs['force'] = tuple([force, 0, 0][:dim]) + + if radius is not None: + assert length is not None + if dim == 3: + domainSize = (length, 2 * radius + 1, 2 * radius + 1) + roundChannel = True + else: + if domainSize is None: + domainSize = (length, 2 * radius) + else: + roundChannel = False + + if 'forceModel' not in kwargs: + kwargs['forceModel'] = 'guo' + + scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, + initialVelocity=initialVelocity) + + boundaryHandling = scenario.boundaryHandling + boundaryHandling.setPeriodicity(True, False, False) + if dim == 2: + for direction in ('N', 'S'): + boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) + elif dim == 3: + if roundChannel: + noSlipIdx = boundaryHandling.addBoundary(noSlip) + ff = boundaryHandling.flagField + yMid = ff.shape[1] // 2 + zMid = ff.shape[2] // 2 + y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2])) + ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx + else: + for direction in ('N', 'S', 'T', 'B'): + boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) + + assert domainSize is not None + if 'forceModel' not in kwargs: + kwargs['forceModel'] = 'guo' + + return scenario + + +def createPressureGradientDrivenChannel(pressureDifference, domainSize=None, dim=2, radius=None, length=None, + initialVelocity=None, optimizationParams={}, lbmKernel=None, **kwargs): + """ + Creates a channel flow in x direction, which is driven by two pressure boundaries. + Consider using :func:`createForceDrivenChannel` which does not have artifacts an inflow and outflow. + + :param pressureDifference: pressure drop in channel in lattice units + :param domainSize: tuple with size of channel in x, y, (z) direction. If not specified, pass dim, radius and length. + In 3D, this creates a channel with rectangular cross section + :param dim: dimension of the channel (only required if domainSize is not passed) + :param radius: radius in 3D, or half of the height in 2D (only required if domainSize is not passed). + In 3D, this creates a channel with circular cross section + :param length: extend in x direction (only required if domainSize is not passed) + :param initialVelocity: initial velocity, either array to specify velocity for each cell or tuple for constant + :param optimizationParams: see :mod:`lbmpy.creationfunctions` + :param lbmKernel: a LBM function, which would otherwise automatically created + :param kwargs: other parameters are passed on to the method, see :mod:`lbmpy.creationfunctions` + :return: instance of :class:`Scenario` + """ + if domainSize is not None: + dim = len(domainSize) + else: + if dim is None or radius is None or length is None: + raise ValueError("Pass either 'domainSize' or 'dim', 'radius' and 'length'") + + assert dim in (2, 3) + + if radius is not None: + assert length is not None + if dim == 3: + domainSize = (length, 2 * radius + 1, 2 * radius + 1) + roundChannel = True + else: + if domainSize is None: + domainSize = (length, 2 * radius) + else: + roundChannel = False + + assert dim in (2, 3) + + scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, initialVelocity=initialVelocity) + boundaryHandling = scenario.boundaryHandling + pressureBoundaryInflow = partial(fixedDensity, density=1.0 + pressureDifference) + pressureBoundaryInflow.__name__ = "Inflow" + + pressureBoundaryOutflow = partial(fixedDensity, density=1.0) + pressureBoundaryOutflow.__name__ = "Outflow" + boundaryHandling.setBoundary(pressureBoundaryInflow, sliceFromDirection('W', dim)) + boundaryHandling.setBoundary(pressureBoundaryOutflow, sliceFromDirection('E', dim)) + + if dim == 2: + for direction in ('N', 'S'): + boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) + elif dim == 3: + if roundChannel: + noSlipIdx = boundaryHandling.addBoundary(noSlip) + ff = boundaryHandling.flagField + yMid = ff.shape[1] // 2 + zMid = ff.shape[2] // 2 + y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2])) + ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx + else: + for direction in ('N', 'S', 'T', 'B'): + boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) + + return scenario + + +# ------------------------------------------ Scenario Class ------------------------------------------------------------ + + class Scenario(object): - def __init__(self, domainSize, methodParameters, optimizationParams, lbmKernel=None, - initialVelocity=None, preUpdateFunctions=[], kernelParams={}): - """ - Constructor for generic scenarios. You probably want to use one of the simpler scenario factory functions - of this file. - - :param domainSize: tuple, defining the domain size without ghost layers - :param methodParameters: dict with method parameters, as documented in :mod:`lbmpy.creationfunctions`, - passed to :func:`lbmpy.creationfunction.createLatticeBoltzmannFunction` - :param optimizationParams: dict with optimization parameters, as documented in :mod:`lbmpy.creationfunctions`, - passed to :func:`lbmpy.creationfunction.createLatticeBoltzmannFunction` - :param lbmKernel: a lattice boltzmann function can be passed here, if None it is created with the parameters - specified above - :param initialVelocity: tuple with initial velocity of the domain, can either be a constant or a numpy array - with first axes shaped like the domain, and the last dimension of size #dimensions - :param preUpdateFunctions: list of functions that are called before the LBM kernel. They get the pdf array as - only argument. Can be used for custom boundary conditions, periodicity, etc. - :param kernelParams: dict which is passed to the kernel as additional parameters - """ + """Scenario containing boundary handling and LBM update function + + You probably want to use one of the simpler scenario factory functions of this module instead of using + this constructor. + + :param domainSize: tuple, defining the domain size without ghost layers + :param methodParameters: dict with method parameters, as documented in :mod:`lbmpy.creationfunctions`, + passed to :func:`lbmpy.creationfunctions.createLatticeBoltzmannFunction` + :param optimizationParams: dict with optimization parameters, as documented in :mod:`lbmpy.creationfunctions`, + passed to :func:`lbmpy.creationfunctions.createLatticeBoltzmannFunction` + :param lbmKernel: a lattice boltzmann function can be passed here, if None it is created with the parameters + specified above + :param initialVelocity: tuple with initial velocity of the domain, can either be a constant or a numpy array + with first axes shaped like the domain, and the last dimension of size #dimensions + :param preUpdateFunctions: list of functions that are called before the LBM kernel. They get the pdf array as + only argument. Can be used for custom boundary conditions, periodicity, etc. + """ + + def __init__(self, domainSize, methodParameters, optimizationParams, lbmKernel=None, + initialVelocity=None, preUpdateFunctions=[]): ghostLayers = 1 domainSizeWithGhostLayer = tuple([s + 2 * ghostLayers for s in domainSize]) D = len(domainSize) @@ -67,8 +275,10 @@ class Scenario(object): target=optimizationParams['target']) self._preUpdateFunctions = preUpdateFunctions - self.kernelParams = kernelParams + self.kernelParams = {} self._pdfGpuArrays = [] + self.timeStepsRun = 0 + self.domainSize = domainSize if initialVelocity is None: initialVelocity = [0] * D @@ -89,6 +299,7 @@ class Scenario(object): self._gpuTimeloop(timeSteps) else: self._cpuTimeloop(timeSteps) + self.timeStepsRun += timeSteps @property def velocity(self): @@ -97,6 +308,15 @@ class Scenario(object): mask = np.repeat(mask[..., np.newaxis], self.dim, axis=2) return removeGhostLayers(np.ma.masked_array(self._velocity, mask), indexDimensions=1) + @property + def vorticity(self): + if self.dim != 2: + raise NotImplementedError("Vorticity only implemented for 2D scenarios") + vel = self.velocity + grad_y_of_x = np.gradient(vel[:, :, 0], axis=1) + grad_x_of_y = np.gradient(vel[:, :, 1], axis=0) + return grad_x_of_y - grad_y_of_x + @property def density(self): """Density as numpy array""" @@ -199,157 +419,3 @@ class Scenario(object): gpuArr.get(cpuArr) self._getMacroscopic(pdfs=self._pdfArrays[0], density=self._density, velocity=self._velocity) - - -# ---------------------------------------- Example Scenarios ----------------------------------------------------------- - - -def createFullyPeriodicFlow(initialVelocity, optimizationParams={}, lbmKernel=None, kernelParams={}, **kwargs): - """ - Creates a fully periodic setup with prescribed velocity field - """ - domainSize = initialVelocity.shape[:-1] - scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel, initialVelocity, kernelParams) - scenario.boundaryHandling.setPeriodicity(True, True, True) - return scenario - - -def createLidDrivenCavity(domainSize, lidVelocity=0.005, optimizationParams={}, lbmKernel=None, - kernelParams={}, **kwargs): - """ - Creates a lid driven cavity scenario with prescribed lid velocity in lattice coordinates - """ - scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, kernelParams=kernelParams) - - myUbb = partial(ubb, velocity=[lidVelocity, 0, 0][:scenario.method.dim]) - myUbb.name = 'ubb' - dim = scenario.method.dim - scenario.boundaryHandling.setBoundary(myUbb, sliceFromDirection('N', dim)) - for direction in ('W', 'E', 'S') if scenario.method.dim == 2 else ('W', 'E', 'S', 'T', 'B'): - scenario.boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) - - return scenario - - -def createPressureGradientDrivenChannel(dim, pressureDifference, domainSize=None, radius=None, length=None, - lbmKernel=None, optimizationParams={}, initialVelocity=None, - boundarySetupFunctions=[], kernelParams={}, **kwargs): - """ - Creates a channel flow in x direction, which is driven by two pressure boundaries. - """ - assert dim in (2, 3) - - if radius is not None: - assert length is not None - if dim == 3: - domainSize = (length, 2 * radius + 1, 2 * radius + 1) - roundChannel = True - else: - if domainSize is None: - domainSize = (length, 2 * radius) - else: - roundChannel = False - - if domainSize is None: - raise ValueError("Missing domainSize or radius and length parameters!") - - scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, kernelParams=kernelParams, - initialVelocity=initialVelocity) - boundaryHandling = scenario.boundaryHandling - pressureBoundaryInflow = partial(fixedDensity, density=1.0 + pressureDifference) - pressureBoundaryInflow.__name__ = "Inflow" - - pressureBoundaryOutflow = partial(fixedDensity, density=1.0) - pressureBoundaryOutflow.__name__ = "Outflow" - boundaryHandling.setBoundary(pressureBoundaryInflow, sliceFromDirection('W', dim)) - boundaryHandling.setBoundary(pressureBoundaryOutflow, sliceFromDirection('E', dim)) - - if dim == 2: - for direction in ('N', 'S'): - boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) - elif dim == 3: - if roundChannel: - noSlipIdx = boundaryHandling.addBoundary(noSlip) - ff = boundaryHandling.flagField - yMid = ff.shape[1] // 2 - zMid = ff.shape[2] // 2 - y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2])) - ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx - else: - for direction in ('N', 'S', 'T', 'B'): - boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) - - for userFunction in boundarySetupFunctions: - userFunction(boundaryHandling, scenario.method, domainSize) - - return scenario - - -def createForceDrivenChannel(dim, force, domainSize=None, radius=None, length=None, lbmKernel=None, - optimizationParams={}, initialVelocity=None, boundarySetupFunctions=[], - kernelParams={}, **kwargs): - """ - Creates a channel flow in x direction, which is driven by a constant force along the x axis - """ - assert dim in (2, 3) - kwargs['force'] = tuple([force, 0, 0][:dim]) - - if radius is not None: - assert length is not None - if dim == 3: - domainSize = (length, 2 * radius + 1, 2 * radius + 1) - roundChannel = True - else: - if domainSize is None: - domainSize = (length, 2 * radius) - else: - roundChannel = False - - if 'forceModel' not in kwargs: - kwargs['forceModel'] = 'guo' - - scenario = Scenario(domainSize, kwargs, optimizationParams, lbmKernel=lbmKernel, - initialVelocity=initialVelocity, kernelParams=kernelParams) - - boundaryHandling = scenario.boundaryHandling - boundaryHandling.setPeriodicity(True, False, False) - if dim == 2: - for direction in ('N', 'S'): - boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) - elif dim == 3: - if roundChannel: - noSlipIdx = boundaryHandling.addBoundary(noSlip) - ff = boundaryHandling.flagField - yMid = ff.shape[1] // 2 - zMid = ff.shape[2] // 2 - y, z = np.meshgrid(range(ff.shape[1]), range(ff.shape[2])) - ff[(y - yMid) ** 2 + (z - zMid) ** 2 > radius ** 2] = noSlipIdx - else: - for direction in ('N', 'S', 'T', 'B'): - boundaryHandling.setBoundary(noSlip, sliceFromDirection(direction, dim)) - for userFunction in boundarySetupFunctions: - userFunction(boundaryHandling, scenario.method, domainSize) - - assert domainSize is not None - if 'forceModel' not in kwargs: - kwargs['forceModel'] = 'guo' - - return scenario - -if __name__ == '__main__': - from lbmpy.scenarios import createForceDrivenChannel - from lbmpy.boundaries.geometry import BlackAndWhiteImageBoundary - from pystencils.slicing import makeSlice - from lbmpy.boundaries import noSlip - import numpy as np - domainSize = (10, 10) - scenario = createForceDrivenChannel(dim=2, method='srt', force=0.000002, - domainSize=domainSize, - relaxationRates=[1.92], forceModel='guo', - compressible=True, - optimizationParams={'target': 'gpu'}) - imageSetup = BlackAndWhiteImageBoundary("/home/staff/bauer/opensource.png", - noSlip, targetSlice=makeSlice[2:-2, 1:-2]) - imageSetup(scenario.boundaryHandling, scenario.method, domainSize) - scenario.boundaryHandling.prepare() - scenario.run(1) -- GitLab