From ffcf6991cbbb1c7862229e9000990cf3b7be1bc9 Mon Sep 17 00:00:00 2001 From: Martin Bauer <martin.bauer@fau.de> Date: Thu, 15 Feb 2018 18:55:43 +0100 Subject: [PATCH] Boundary handling for Finite Differences - splitted existing LBM boundary handling into two parts: -> generic part , that is used for FD as well and moved it to pystencils -> LBM specific part - remained in lbmpy - bugfixes --- boundaries/__init__.py | 1 + boundaries/boundaryconditions.py | 68 ++++++ boundaries/boundaryhandling.py | 328 +++++++++++++++++++++++++++ boundaries/createindexlist.py | 79 +++++++ boundaries/createindexlistcython.pyx | 63 +++++ kernelcreation.py | 2 +- sympyextensions.py | 47 ++-- 7 files changed, 573 insertions(+), 15 deletions(-) create mode 100644 boundaries/__init__.py create mode 100644 boundaries/boundaryconditions.py create mode 100644 boundaries/boundaryhandling.py create mode 100644 boundaries/createindexlist.py create mode 100644 boundaries/createindexlistcython.pyx diff --git a/boundaries/__init__.py b/boundaries/__init__.py new file mode 100644 index 000000000..a7ab44b75 --- /dev/null +++ b/boundaries/__init__.py @@ -0,0 +1 @@ +from pystencils.boundaries.boundaryhandling import BoundaryHandling diff --git a/boundaries/boundaryconditions.py b/boundaries/boundaryconditions.py new file mode 100644 index 000000000..80906feb7 --- /dev/null +++ b/boundaries/boundaryconditions.py @@ -0,0 +1,68 @@ +import sympy as sp +from pystencils.boundaries.boundaryhandling import BoundaryOffsetInfo + + +class Boundary(object): + """Base class all boundaries should derive from""" + + def __init__(self, name=None): + self._name = name + + def __call__(self, field, directionSymbol, indexField): + """ + 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. + :param field: pystencils field where boundary condition should be applied. + 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. + :param directionSymbol: a sympy symbol that can be used as index to the pdfField. It describes + the direction pointing from the fluid to the boundary cell + :param indexField: the boundary index field that can be used to retrieve and update boundary data + :return: list of sympy equations + """ + raise NotImplementedError("Boundary class has to overwrite __call__") + + @property + def additionalData(self): + """Return a list of (name, type) tuples for additional data items required in this boundary + These data items can either be initialized in separate kernel see additionalDataKernelInit or by + Python callbacks - see additionalDataCallback """ + return [] + + @property + def additionalDataInitCallback(self): + """Return a callback function called with a boundary data setter object and returning a dict of + data-name to data for each element that should be initialized""" + return None + + @property + def name(self): + if self._name: + return self._name + else: + return type(self).__name__ + + @name.setter + def name(self, newValue): + self._name = newValue + + +class Neumann(Boundary): + def __call__(self, field, directionSymbol, **kwargs): + + neighbor = BoundaryOffsetInfo.offsetFromDir(directionSymbol, field.spatialDimensions) + if field.indexDimensions == 0: + return [sp.Eq(field[neighbor], field.center)] + else: + from itertools import product + if not field.hasFixedIndexShape: + raise NotImplementedError("Neumann boundary works only for fields with fixed index shape") + indexIter = product(*(range(i) for i in field.indexShape)) + return [sp.Eq(field[neighbor](idx), field(idx)) for idx in indexIter] + + def __hash__(self): + # All boundaries of these class behave equal -> should also be equal + return hash("Neumann") + + def __eq__(self, other): + return type(other) == Neumann diff --git a/boundaries/boundaryhandling.py b/boundaries/boundaryhandling.py new file mode 100644 index 000000000..9591acad7 --- /dev/null +++ b/boundaries/boundaryhandling.py @@ -0,0 +1,328 @@ +import numpy as np +import sympy as sp +from pystencils import Field, TypedSymbol, createIndexedKernel +from pystencils.backends.cbackend import CustomCppCode +from pystencils.boundaries.createindexlist import numpyDataTypeForBoundaryObject, createBoundaryIndexArray +from pystencils.cache import memorycache +from pystencils.data_types import createType + + +class BoundaryHandling: + + def __init__(self, dataHandling, fieldName, stencil, name="boundaryHandling", target='cpu', openMP=True): + assert dataHandling.hasData(fieldName) + + self._dataHandling = dataHandling + self._fieldName = fieldName + self._flagFieldName = name + "Flags" + self._indexArrayName = name + "IndexArrays" + self._target = target + self._openMP = openMP + self._boundaryObjectToBoundaryInfo = {} + self._fluidFlag = 1 << 0 + self._nextFreeFlag = 1 + self.stencil = stencil + self._dirty = True + + # Add flag field to data handling if it does not yet exist + if dataHandling.hasData(self._flagFieldName) or dataHandling.hasData(self._indexArrayName): + raise ValueError("There is already a boundary handling registered at the data handling." + "If you want to add multiple handlings, choose a different name.") + + gpu = self._target == 'gpu' + dataHandling.addArray(self._flagFieldName, dtype=np.uint32, cpu=True, gpu=False) + dataHandling.addCustomClass(self._indexArrayName, self.IndexFieldBlockData, cpu=True, gpu=gpu) + + ffGhostLayers = self._dataHandling.ghostLayersOfField(self._flagFieldName) + for b in self._dataHandling.iterate(ghostLayers=ffGhostLayers): + b[self._flagFieldName].fill(self._fluidFlag) + + @property + def dataHandling(self): + return self._dataHandling + + @property + def shape(self): + return self._dataHandling.shape + + @property + def dim(self): + return self._dataHandling.dim + + @property + def boundaryObjects(self): + return tuple(self._boundaryObjectToName.keys()) + + @property + def flagArrayName(self): + return self._flagFieldName + + def getBoundaryNameToFlagDict(self): + result = {bObj.name: bInfo.flag for bObj, bInfo in self._boundaryObjectToBoundaryInfo.items()} + result['fluid'] = self._fluidFlag + return result + + def getMask(self, sliceObj, boundaryObj, inverse=False): + if isinstance(boundaryObj, str) and boundaryObj.lower() == 'fluid': + flag = self._fluidFlag + else: + flag = self._boundaryObjectToBoundaryInfo[boundaryObj].flag + + arr = self.dataHandling.gatherArray(self.flagArrayName, sliceObj) + if arr is None: + return None + else: + result = np.bitwise_and(arr, flag) + if inverse: + result = np.logical_not(result) + return result + + def setBoundary(self, boundaryObject, sliceObj=None, maskCallback=None, ghostLayers=True, innerGhostLayers=True): + """ + Sets boundary using either a rectangular slice, a boolean mask or a combination of both + + :param boundaryObject: instance of a boundary object that should be set + :param sliceObj: a slice object (can be created with makeSlice[]) that selects a part of the domain where + the boundary should be set. If none, the complete domain is selected which makes only sense + if a maskCallback is passed. The slice can have ':' placeholders, which are interpreted + depending on the 'includeGhostLayers' parameter i.e. if it is True, the slice extends + into the ghost layers + :param maskCallback: callback function getting x,y (z) parameters of the cell midpoints and returning a + boolean mask with True entries where boundary cells should be set. + The x, y, z arrays have 2D/3D shape such that they can be used directly + to create the boolean return array. i.e return x < 10 sets boundaries in cells with + midpoint x coordinate smaller than 10. + :param ghostLayers see DataHandling.iterate() + """ + if isinstance(boundaryObject, str) and boundaryObject.lower() == 'fluid': + flag = self._fluidFlag + else: + flag = self._getFlagForBoundary(boundaryObject) + + for b in self._dataHandling.iterate(sliceObj, ghostLayers=ghostLayers, innerGhostLayers=innerGhostLayers): + flagArr = b[self._flagFieldName] + if maskCallback is not None: + mask = maskCallback(*b.midpointArrays) + flagArr[mask] = flag + else: + flagArr.fill(flag) + + self._dirty = True + + def prepare(self): + if not self._dirty: + return + self._createIndexFields() + self._dirty = False + + def triggerReinitializationOfBoundaryData(self, **kwargs): + if self._dirty: + self.prepare() + else: + ffGhostLayers = self._dataHandling.ghostLayersOfField(self._flagFieldName) + for b in self._dataHandling.iterate(ghostLayers=ffGhostLayers): + for bObj, setter in b[self._indexArrayName].boundaryObjectToDataSetter.items(): + self._boundaryDataInitialization(bObj, setter, **kwargs) + + def __call__(self, **kwargs): + if self._dirty: + self.prepare() + + for b in self._dataHandling.iterate(gpu=self._target == 'gpu'): + for bObj, idxArr in b[self._indexArrayName].boundaryObjectToIndexList.items(): + kwargs[self._fieldName] = b[self._fieldName] + self._boundaryObjectToBoundaryInfo[bObj].kernel(indexField=idxArr, **kwargs) + + def geometryToVTK(self, fileName='geometry', boundaries='all', ghostLayers=False): + """ + Writes a VTK field where each cell with the given boundary is marked with 1, other cells are 0 + This can be used to display the simulation geometry in Paraview + :param fileName: vtk filename + :param boundaries: boundary object, or special string 'fluid' for fluid cells or special string 'all' for all + boundary conditions. + can also be a sequence, to write multiple boundaries to VTK file + :param ghostLayers: number of ghost layers to write, or True for all, False for none + """ + if boundaries == 'all': + boundaries = list(self._boundaryObjectToBoundaryInfo.keys()) + ['fluid'] + elif not hasattr(boundaries, "__len__"): + boundaries = [boundaries] + + masksToName = {} + for b in boundaries: + if b == 'fluid': + masksToName[self._fluidFlag] = 'fluid' + else: + masksToName[self._boundaryObjectToBoundaryInfo[b].flag] = b.name + + writer = self.dataHandling.vtkWriterFlags(fileName, self._flagFieldName, masksToName, ghostLayers=ghostLayers) + writer(1) + + # ------------------------------ Implementation Details ------------------------------------------------------------ + + def _getFlagForBoundary(self, boundaryObject): + if boundaryObject not in self._boundaryObjectToBoundaryInfo: + symbolicIndexField = Field.createGeneric('indexField', spatialDimensions=1, + dtype=numpyDataTypeForBoundaryObject(boundaryObject, self.dim)) + ast = self._createBoundaryKernel(self._dataHandling.fields[self._fieldName], + symbolicIndexField, boundaryObject) + boundaryInfo = self.BoundaryInfo(boundaryObject, flag=1 << self._nextFreeFlag, kernel=ast.compile()) + + self._nextFreeFlag += 1 + self._boundaryObjectToBoundaryInfo[boundaryObject] = boundaryInfo + return self._boundaryObjectToBoundaryInfo[boundaryObject].flag + + def _createBoundaryKernel(self, symbolicField, symbolicIndexField, boundaryObject): + return createBoundaryKernel(symbolicField, symbolicIndexField, self.stencil, boundaryObject, + target=self._target, openMP=self._openMP) + + def _createIndexFields(self): + dh = self._dataHandling + ffGhostLayers = dh.ghostLayersOfField(self._flagFieldName) + for b in dh.iterate(ghostLayers=ffGhostLayers): + flagArr = b[self._flagFieldName] + pdfArr = b[self._fieldName] + indexArrayBD = b[self._indexArrayName] + indexArrayBD.clear() + for bInfo in self._boundaryObjectToBoundaryInfo.values(): + idxArr = createBoundaryIndexArray(flagArr, self.stencil, bInfo.flag, self._fluidFlag, + bInfo.boundaryObject, dh.ghostLayersOfField(self._flagFieldName)) + if idxArr.size == 0: + continue + + boundaryDataSetter = BoundaryDataSetter(idxArr, b.offset, self.stencil, ffGhostLayers, pdfArr) + indexArrayBD.boundaryObjectToIndexList[bInfo.boundaryObject] = idxArr + indexArrayBD.boundaryObjectToDataSetter[bInfo.boundaryObject] = boundaryDataSetter + self._boundaryDataInitialization(bInfo.boundaryObject, boundaryDataSetter) + + def _boundaryDataInitialization(self, boundaryObject, boundaryDataSetter, **kwargs): + if boundaryObject.additionalDataInitCallback: + boundaryObject.additionalDataInitCallback(boundaryDataSetter, **kwargs) + if self._target == 'gpu': + self._dataHandling.toGpu(self._indexArrayName) + + class BoundaryInfo(object): + def __init__(self, boundaryObject, flag, kernel): + self.boundaryObject = boundaryObject + self.flag = flag + self.kernel = kernel + + class IndexFieldBlockData: + def __init__(self, *args, **kwargs): + self.boundaryObjectToIndexList = {} + self.boundaryObjectToDataSetter = {} + + def clear(self): + self.boundaryObjectToIndexList.clear() + self.boundaryObjectToDataSetter.clear() + + @staticmethod + def toCpu(gpuVersion, cpuVersion): + gpuVersion = gpuVersion.boundaryObjectToIndexList + cpuVersion = cpuVersion.boundaryObjectToIndexList + for obj, cpuArr in cpuVersion.values(): + gpuVersion[obj].get(cpuArr) + + @staticmethod + def toGpu(gpuVersion, cpuVersion): + from pycuda import gpuarray + gpuVersion = gpuVersion.boundaryObjectToIndexList + cpuVersion = cpuVersion.boundaryObjectToIndexList + for obj, cpuArr in cpuVersion.items(): + if obj not in gpuVersion: + gpuVersion[obj] = gpuarray.to_gpu(cpuArr) + else: + gpuVersion[obj].set(cpuArr) + + +class BoundaryDataSetter: + + def __init__(self, indexArray, offset, stencil, ghostLayers, pdfArray): + self.indexArray = indexArray + self.offset = offset + self.stencil = np.array(stencil) + self.pdfArray = pdfArray.view() + self.pdfArray.flags.writeable = False + + arrFieldNames = indexArray.dtype.names + self.dim = 3 if 'z' in arrFieldNames else 2 + assert 'x' in arrFieldNames and 'y' in arrFieldNames and 'dir' in arrFieldNames, str(arrFieldNames) + self.boundaryDataNames = set(self.indexArray.dtype.names) - set(['x', 'y', 'z', 'dir']) + self.coordMap = {0: 'x', 1: 'y', 2: 'z'} + self.ghostLayers = ghostLayers + + def fluidCellPositions(self, coord): + assert coord < self.dim + return self.indexArray[self.coordMap[coord]] + self.offset[coord] - self.ghostLayers + 0.5 + + @memorycache() + def linkOffsets(self): + return self.stencil[self.indexArray['dir']] + + @memorycache() + def linkPositions(self, coord): + return self.fluidCellPositions(coord) + 0.5 * self.linkOffsets()[:, coord] + + @memorycache() + def boundaryCellPositions(self, coord): + return self.fluidCellPositions(coord) + self.linkOffsets()[:, coord] + + def __setitem__(self, key, value): + if key not in self.boundaryDataNames: + raise KeyError("Invalid boundary data name %s. Allowed are %s" % (key, self.boundaryDataNames)) + self.indexArray[key] = value + + def __getitem__(self, item): + if item not in self.boundaryDataNames: + raise KeyError("Invalid boundary data name %s. Allowed are %s" % (item, self.boundaryDataNames)) + return self.indexArray[item] + + +class BoundaryOffsetInfo(CustomCppCode): + + # --------------------------- Functions to be used by boundaries -------------------------- + + @staticmethod + def offsetFromDir(dirIdx, dim): + return tuple([sp.IndexedBase(symbol, shape=(1,))[dirIdx] + for symbol in BoundaryOffsetInfo._offsetSymbols(dim)]) + + @staticmethod + def invDir(dirIdx): + return sp.IndexedBase(BoundaryOffsetInfo.INV_DIR_SYMBOL, shape=(1,))[dirIdx] + + # ---------------------------------- Internal --------------------------------------------- + + def __init__(self, stencil): + dim = len(stencil[0]) + + offsetSym = BoundaryOffsetInfo._offsetSymbols(dim) + code = "\n" + for i in range(dim): + offsetStr = ", ".join([str(d[i]) for d in stencil]) + code += "const int64_t %s [] = { %s };\n" % (offsetSym[i].name, offsetStr) + + invDirs = [] + for direction in stencil: + inverseDir = tuple([-i for i in direction]) + invDirs.append(str(stencil.index(inverseDir))) + + code += "const int %s [] = { %s };\n" % (self.INV_DIR_SYMBOL.name, ", ".join(invDirs)) + offsetSymbols = BoundaryOffsetInfo._offsetSymbols(dim) + super(BoundaryOffsetInfo, self).__init__(code, symbolsRead=set(), + symbolsDefined=set(offsetSymbols + [self.INV_DIR_SYMBOL])) + + @staticmethod + def _offsetSymbols(dim): + return [TypedSymbol("c_%d" % (d,), createType(np.int64)) for d in range(dim)] + + INV_DIR_SYMBOL = TypedSymbol("invDir", "int") + + +def createBoundaryKernel(field, indexField, stencil, boundaryFunctor, target='cpu', openMP=True): + elements = [BoundaryOffsetInfo(stencil)] + indexArrDtype = indexField.dtype.numpyDtype + dirSymbol = TypedSymbol("dir", indexArrDtype.fields['dir'][0]) + elements += [sp.Eq(dirSymbol, indexField[0]('dir'))] + elements += boundaryFunctor(field, directionSymbol=dirSymbol, indexField=indexField) + return createIndexedKernel(elements, [indexField], target=target, cpuOpenMP=openMP) diff --git a/boundaries/createindexlist.py b/boundaries/createindexlist.py new file mode 100644 index 000000000..ee46b0b42 --- /dev/null +++ b/boundaries/createindexlist.py @@ -0,0 +1,79 @@ +import numpy as np +import itertools +import warnings + +try: + import pyximport; + + pyximport.install() + from pystencils.boundaries.createindexlistcython import createBoundaryIndexList2D, createBoundaryIndexList3D + + cythonFuncsAvailable = True +except Exception: + cythonFuncsAvailable = False + createBoundaryIndexList2D = None + createBoundaryIndexList3D = None + +boundaryIndexArrayCoordinateNames = ["x", "y", "z"] +directionMemberName = "dir" + + +def numpyDataTypeForBoundaryObject(boundaryObject, dim): + coordinateNames = boundaryIndexArrayCoordinateNames[:dim] + return np.dtype([(name, np.int32) for name in coordinateNames] + + [(directionMemberName, np.int32)] + + [(i[0], i[1].numpyDtype) for i in boundaryObject.additionalData], align=True) + + +def _createBoundaryIndexListPython(flagFieldArr, nrOfGhostLayers, boundaryMask, fluidMask, stencil): + coordinateNames = boundaryIndexArrayCoordinateNames[:len(flagFieldArr.shape)] + indexArrDtype = np.dtype([(name, np.int32) for name in coordinateNames] + [(directionMemberName, np.int32)]) + + result = [] + gl = nrOfGhostLayers + for cell in itertools.product(*reversed([range(gl, i-gl) for i in flagFieldArr.shape])): + cell = cell[::-1] + if not flagFieldArr[cell] & fluidMask: + continue + for dirIdx, direction in enumerate(stencil): + neighborCell = tuple([cell_i + dir_i for cell_i, dir_i in zip(cell, direction)]) + if flagFieldArr[neighborCell] & boundaryMask: + result.append(cell + (dirIdx,)) + + return np.array(result, dtype=indexArrDtype) + + +def createBoundaryIndexList(flagField, stencil, boundaryMask, fluidMask, nrOfGhostLayers=1): + dim = len(flagField.shape) + coordinateNames = boundaryIndexArrayCoordinateNames[:dim] + indexArrDtype = np.dtype([(name, np.int32) for name in coordinateNames] + [(directionMemberName, np.int32)]) + + if cythonFuncsAvailable: + stencil = np.array(stencil, dtype=np.int32) + if dim == 2: + idxList = createBoundaryIndexList2D(flagField, nrOfGhostLayers, boundaryMask, fluidMask, stencil) + elif dim == 3: + idxList = createBoundaryIndexList3D(flagField, nrOfGhostLayers, boundaryMask, fluidMask, stencil) + else: + raise ValueError("Flag field has to be a 2 or 3 dimensional numpy array") + return np.array(idxList, dtype=indexArrDtype) + else: + if flagField.size > 1e6: + warnings.warn("Boundary setup may take very long! Consider installing cython to speed it up") + return _createBoundaryIndexListPython(flagField, nrOfGhostLayers, boundaryMask, fluidMask, stencil) + + +def createBoundaryIndexArray(flagField, stencil, boundaryMask, fluidMask, boundaryObject, nrOfGhostLayers=1): + idxArray = createBoundaryIndexList(flagField, stencil, boundaryMask, fluidMask, nrOfGhostLayers) + dim = len(flagField.shape) + + if boundaryObject.additionalData: + coordinateNames = boundaryIndexArrayCoordinateNames[:dim] + indexArrDtype = numpyDataTypeForBoundaryObject(boundaryObject, dim) + extendedIdxField = np.empty(len(idxArray), dtype=indexArrDtype) + for prop in coordinateNames + ['dir']: + extendedIdxField[prop] = idxArray[prop] + + idxArray = extendedIdxField + + return idxArray diff --git a/boundaries/createindexlistcython.pyx b/boundaries/createindexlistcython.pyx new file mode 100644 index 000000000..dfa84069d --- /dev/null +++ b/boundaries/createindexlistcython.pyx @@ -0,0 +1,63 @@ +# Workaround for cython bug +# see https://stackoverflow.com/questions/8024805/cython-compiled-c-extension-importerror-dynamic-module-does-not-define-init-fu +WORKAROUND = "Something" + +import cython + +ctypedef fused IntegerType: + short + int + long + long long + unsigned short + unsigned int + unsigned long + +@cython.boundscheck(False) # turn off bounds-checking for entire function +@cython.wraparound(False) # turn off negative index wrapping for entire function +def createBoundaryIndexList2D(object[IntegerType, ndim=2] flagField, + int nrOfGhostLayers, IntegerType boundaryMask, IntegerType fluidMask, + object[int, ndim=2] stencil): + cdef int xs, ys, x, y + cdef int dirIdx, numDirections, dx, dy + + xs, ys = flagField.shape + boundaryIndexList = [] + numDirections = stencil.shape[0] + + for y in range(nrOfGhostLayers,ys-nrOfGhostLayers): + for x in range(nrOfGhostLayers,xs-nrOfGhostLayers): + if flagField[x,y] & fluidMask: + for dirIdx in range(1, numDirections): + dx = stencil[dirIdx,0] + dy = stencil[dirIdx,1] + if flagField[x+dx, y+dy] & boundaryMask: + boundaryIndexList.append((x,y, dirIdx)) + return boundaryIndexList + + +@cython.boundscheck(False) # turn off bounds-checking for entire function +@cython.wraparound(False) # turn off negative index wrapping for entire function +def createBoundaryIndexList3D(object[IntegerType, ndim=3] flagField, + int nrOfGhostLayers, IntegerType boundaryMask, IntegerType fluidMask, + object[int, ndim=2] stencil): + cdef int xs, ys, zs, x, y, z + cdef int dirIdx, numDirections, dx, dy, dz + + xs, ys, zs = flagField.shape + boundaryIndexList = [] + numDirections = stencil.shape[0] + + for z in range(nrOfGhostLayers, zs-nrOfGhostLayers): + for y in range(nrOfGhostLayers,ys-nrOfGhostLayers): + for x in range(nrOfGhostLayers,xs-nrOfGhostLayers): + if flagField[x, y, z] & fluidMask: + for dirIdx in range(1, numDirections): + dx = stencil[dirIdx,0] + dy = stencil[dirIdx,1] + dz = stencil[dirIdx,2] + if flagField[x + dx, y + dy, z + dz] & boundaryMask: + boundaryIndexList.append((x,y,z, dirIdx)) + return boundaryIndexList + + diff --git a/kernelcreation.py b/kernelcreation.py index 65760903e..7d27ed6c9 100644 --- a/kernelcreation.py +++ b/kernelcreation.py @@ -88,7 +88,7 @@ def createIndexedKernel(equations, indexFields, target='cpu', dataType="double", if target == 'cpu': from pystencils.cpu import createIndexedKernel from pystencils.cpu import addOpenMP - ast = createIndexedKernel(equations, indexField=indexFields, typeForSymbol=dataType, + ast = createIndexedKernel(equations, indexFields=indexFields, typeForSymbol=dataType, coordinateNames=coordinateNames) if cpuOpenMP: addOpenMP(ast, numThreads=cpuOpenMP) diff --git a/sympyextensions.py b/sympyextensions.py index f56781e67..566cc7441 100644 --- a/sympyextensions.py +++ b/sympyextensions.py @@ -5,6 +5,8 @@ import itertools import warnings import sympy as sp +from pystencils.data_types import getTypeOfExpression, getBaseType + def prod(seq): """Takes a sequence and returns the product of all elements""" @@ -414,7 +416,7 @@ def mostCommonTermFactorization(term): return sp.Mul(commonFactor, term, evaluate=False) -def countNumberOfOperations(term): +def countNumberOfOperations(term, onlyType='real'): """ Counts the number of additions, multiplications and division :param term: a sympy term, equation or sequence of terms/equations @@ -433,15 +435,31 @@ def countNumberOfOperations(term): term = term.evalf() + def checkType(e): + if onlyType is None: + return True + try: + type = getBaseType(getTypeOfExpression(e)) + except ValueError: + return False + if onlyType == 'int' and (type.is_int() or type.is_uint()): + return True + if onlyType == 'real' and (type.is_float()): + return True + else: + return type == onlyType + def visit(t): visitChildren = True if t.func is sp.Add: - result['adds'] += len(t.args) - 1 + if checkType(t): + result['adds'] += len(t.args) - 1 elif t.func is sp.Mul: - result['muls'] += len(t.args) - 1 - for a in t.args: - if a == 1 or a == -1: - result['muls'] -= 1 + if checkType(t): + result['muls'] += len(t.args) - 1 + for a in t.args: + if a == 1 or a == -1: + result['muls'] -= 1 elif t.func is sp.Float: pass elif isinstance(t, sp.Symbol): @@ -451,14 +469,15 @@ def countNumberOfOperations(term): elif t.is_integer: pass elif t.func is sp.Pow: - visitChildren = False - if t.exp.is_integer and t.exp.is_number: - if t.exp >= 0: - result['muls'] += int(t.exp) - 1 - else: - result['muls'] -= 1 - result['divs'] += 1 - result['muls'] += (-int(t.exp)) - 1 + if checkType(t): + visitChildren = False + if t.exp.is_integer and t.exp.is_number: + if t.exp >= 0: + result['muls'] += int(t.exp) - 1 + else: + result['muls'] -= 1 + result['divs'] += 1 + result['muls'] += (-int(t.exp)) - 1 else: warnings.warn("Counting operations: only integer exponents are supported in Pow, " "counting will be inaccurate") -- GitLab