diff --git a/datahandling/datahandling_interface.py b/datahandling/datahandling_interface.py index f2e3e1999229db97cbbbc0c8ecd9a27a6b30720d..3d1ad21beada5a8ffd384eb3bc97ab3f3867c439 100644 --- a/datahandling/datahandling_interface.py +++ b/datahandling/datahandling_interface.py @@ -91,10 +91,24 @@ class DataHandling(ABC): def fields(self): """Dictionary mapping data name to symbolic pystencils field - use this to create pystencils kernels""" + @property + @abstractmethod + def arrayNames(self): + """Tuple of all array names""" + + @property + @abstractmethod + def customDataNames(self): + """Tuple of all custom data names""" + @abstractmethod def ghostLayersOfField(self, name): """Returns the number of ghost layers for a specific field/array""" + @abstractmethod + def fSize(self, name): + """Returns fSize of array""" + @abstractmethod def iterate(self, sliceObj=None, gpu=False, ghostLayers=None, innerGhostLayers=True): """ @@ -191,3 +205,53 @@ class DataHandling(ABC): def reduceIntSequence(self, sequence, operation, allReduce=False): """See function reduceFloatSequence - this is the same for integers""" + + def fill(self, arrayName, val, fValue=None, sliceObj=None, ghostLayers=False, innerGhostLayers=False): + if ghostLayers is True: + ghostLayers = self.ghostLayersOfField(arrayName) + if innerGhostLayers is True: + ghostLayers = self.ghostLayersOfField(arrayName) + if fValue is not None and self.fSize(arrayName) < 2: + raise ValueError("fValue parameter only valid for fields with fSize > 1") + for b in self.iterate(sliceObj, ghostLayers=ghostLayers, innerGhostLayers=innerGhostLayers): + if fValue is not None: + b[arrayName][..., fValue].fill(val) + else: + b[arrayName].fill(val) + + def min(self, arrayName, sliceObj=None, ghostLayers=False, innerGhostLayers=False, reduce=True): + result = None + if ghostLayers is True: + ghostLayers = self.ghostLayersOfField(arrayName) + if innerGhostLayers is True: + ghostLayers = self.ghostLayersOfField(arrayName) + for b in self.iterate(sliceObj, ghostLayers=ghostLayers, innerGhostLayers=innerGhostLayers): + m = np.min(b[arrayName]) + result = m if result is None else np.min(result, m) + return self.reduceFloatSequence([result], 'min')[0] if reduce else result + + def max(self, arrayName, sliceObj=None, ghostLayers=False, innerGhostLayers=False, reduce=True): + result = None + if ghostLayers is True: + ghostLayers = self.ghostLayersOfField(arrayName) + if innerGhostLayers is True: + ghostLayers = self.ghostLayersOfField(arrayName) + for b in self.iterate(sliceObj, ghostLayers=ghostLayers, innerGhostLayers=innerGhostLayers): + m = np.max(b[arrayName]) + result = m if result is None else np.max(result, m) + return self.reduceFloatSequence([result], 'max')[0] if reduce else result + + def __str__(self): + result = "" + + rowFormat = "{:>35}|{:>21}|{:>21}\n" + separatorLine = "" * (25 + 21 + 21 + 2) + "\n" + result += rowFormat.format("Name", "Inner (min/max)", "WithGl (min/max)") + result += separatorLine + for arrName in sorted(self.arrayNames): + innerMinMax = (self.min(arrName, ghostLayers=False), self.max(arrName, ghostLayers=False)) + withGlMinMax = (self.min(arrName, ghostLayers=True), self.max(arrName, ghostLayers=True)) + innerMinMax = "({0[0]:3.3g},{0[1]:3.3g})".format(innerMinMax) + withGlMinMax = "({0[0]:3.3g},{0[1]:3.3g})".format(withGlMinMax) + result += rowFormat.format(arrName, innerMinMax, withGlMinMax) + return result diff --git a/datahandling/parallel_datahandling.py b/datahandling/parallel_datahandling.py index acbdea1b0053cd4015b7dc40c5e734282236bbcb..d35dbd3b0a6cbdd96b53a9c66f3374c72d23a162 100644 --- a/datahandling/parallel_datahandling.py +++ b/datahandling/parallel_datahandling.py @@ -33,7 +33,7 @@ class ParallelDataHandling(DataHandling): self._fieldInformation = {} self._cpuGpuPairs = [] self._customDataTransferFunctions = {} - + self._customDataNames = [] self._reduceMap = { 'sum': wlb.mpi.SUM, 'min': wlb.mpi.MIN, @@ -62,6 +62,9 @@ class ParallelDataHandling(DataHandling): def ghostLayersOfField(self, name): return self._fieldInformation[name]['ghostLayers'] + def fSize(self, name): + return self._fieldInformation[name]['fSize'] + def addCustomData(self, name, cpuCreationFunction, gpuCreationFunction=None, cpuToGpuTransferFunc=None, gpuToCpuTransferFunc=None): if cpuCreationFunction and gpuCreationFunction: @@ -73,6 +76,7 @@ class ParallelDataHandling(DataHandling): self.blocks.addBlockData(name, cpuCreationFunction) if gpuCreationFunction: self.blocks.addBlockData(self.GPU_DATA_PREFIX + name, gpuCreationFunction) + self._customDataNames.append(name) def addArray(self, name, fSize=1, dtype=np.float64, latexName=None, ghostLayers=None, layout=None, cpu=True, gpu=False): @@ -126,6 +130,14 @@ class ParallelDataHandling(DataHandling): def hasData(self, name): return name in self._fields + @property + def arrayNames(self): + return tuple(self.fields.keys()) + + @property + def customDataNames(self): + return tuple(self._customDataNames) + def addArrayLike(self, name, nameOfTemplateField, latexName=None, cpu=True, gpu=False): self.addArray(name, latexName=latexName, cpu=cpu, gpu=gpu, **self._fieldInformation[nameOfTemplateField]) @@ -141,10 +153,15 @@ class ParallelDataHandling(DataHandling): ghostLayers = self.defaultGhostLayers elif ghostLayers is False: ghostLayers = 0 + elif isinstance(ghostLayers, str): + ghostLayers = self.ghostLayersOfField(ghostLayers) + if innerGhostLayers is True: innerGhostLayers = self.defaultGhostLayers elif innerGhostLayers is False: innerGhostLayers = 0 + elif isinstance(ghostLayers, str): + ghostLayers = self.ghostLayersOfField(ghostLayers) prefix = self.GPU_DATA_PREFIX if gpu else "" if sliceObj is not None: diff --git a/datahandling/serial_datahandling.py b/datahandling/serial_datahandling.py index 0752668b90a9fffdc6403f1b634a47e9d29d3630..caf446719357df72e9f072bf37b60e1a05e4ec9e 100644 --- a/datahandling/serial_datahandling.py +++ b/datahandling/serial_datahandling.py @@ -63,6 +63,9 @@ class SerialDataHandling(DataHandling): def ghostLayersOfField(self, name): return self._fieldInformation[name]['ghostLayers'] + def fSize(self, name): + return self._fieldInformation[name]['fSize'] + def addArray(self, name, fSize=1, dtype=np.float64, latexName=None, ghostLayers=None, layout=None, cpu=True, gpu=False): if ghostLayers is None: @@ -136,6 +139,8 @@ class SerialDataHandling(DataHandling): ghostLayers = self.defaultGhostLayers elif ghostLayers is False: ghostLayers = 0 + elif isinstance(ghostLayers, str): + ghostLayers = self.ghostLayersOfField(ghostLayers) if sliceObj is None: sliceObj = (slice(None, None, None),) * self.dim @@ -270,6 +275,14 @@ class SerialDataHandling(DataHandling): return resultFunctor + @property + def arrayNames(self): + return tuple(self.fields.keys()) + + @property + def customDataNames(self): + return tuple(self.customDataCpu.keys()) + @staticmethod def reduceFloatSequence(sequence, operation, allReduce=False): return np.array(sequence) diff --git a/finitedifferences.py b/finitedifferences.py index a60bb8b9cb2b81a61023613f9ac2d51c191888e1..835934bb07e278093346e40bc2ad006c8a064083 100644 --- a/finitedifferences.py +++ b/finitedifferences.py @@ -136,9 +136,7 @@ def __upDownOffsets(d, dim): # --------------------------------------- Advection Diffusion ---------------------------------------------------------- - class Advection(sp.Function): - """Advection term, create with advection(scalarField, vectorField)""" @property def scalar(self): @@ -170,11 +168,27 @@ class Advection(sp.Function): for i in range(self.dim)] return " + ".join(args) + # --- Interface for discretization strategy -def advection(scalar, vector, idx=None): - assert isinstance(scalar, Field), "Advected scalar has to be a pystencils.Field" + def velocityFieldAtOffset(self, offsetDim, offsetValue, index): + v = self.vector + if isinstance(v, Field): + assert v.indexDimensions == 1 + return v.neighbor(offsetDim, offsetValue)(index) + else: + return v[index] + + def advectedScalarAtOffset(self, offsetDim, offsetValue): + idx = 0 if self.scalarIndex is None else int(self.scalarIndex) + return self.scalar.neighbor(offsetDim, offsetValue)(idx) + + +def advection(advectedScalar, velocityField, idx=None): + """Advection term: divergence( velocityField * advectedScalar )""" + + assert isinstance(advectedScalar, Field), "Advected scalar has to be a pystencils.Field" - args = [scalar.center, vector if not isinstance(vector, Field) else vector.center] + args = [advectedScalar.center, velocityField if not isinstance(velocityField, Field) else velocityField.center] if idx is not None: args.append(idx) return Advection(*args) @@ -207,6 +221,19 @@ class Diffusion(sp.Function): return r"div(%s \nabla %s)" % (printer.doprint(diffCoeff), printer.doprint(sp.Symbol(self.scalar.name+nameSuffix))) + # --- Interface for discretization strategy + + def diffusionScalarAtOffset(self, offsetDim, offsetValue): + idx = 0 if self.scalarIndex is None else self.scalarIndex + return self.scalar.neighbor(offsetDim, offsetValue)(idx) + + def diffusionCoefficientAtOffset(self, offsetDim, offsetValue): + d = self.diffusionCoeff + if isinstance(d, Field): + return d.neighbor(offsetDim, offsetValue) + else: + return d + def diffusion(scalar, diffusionCoeff, idx=None): assert isinstance(scalar, Field), "Advected scalar has to be a pystencils.Field" @@ -219,7 +246,10 @@ def diffusion(scalar, diffusionCoeff, idx=None): class Transient(sp.Function): @property def scalar(self): - return self.args[0].field + if self.scalarIndex is None: + return self.args[0].field + else: + return self.args[0].field(self.scalarIndex) @property def scalarIndex(self): @@ -244,34 +274,20 @@ class Discretization2ndOrder: def _discretize_diffusion(self, expr): result = 0 - scalar = expr.scalar for c in range(expr.dim): - if isinstance(expr.diffusionCoeff, Field): - firstDiffs = [offset * - (scalar.neighbor(c, offset) * expr.diffusionCoeff.neighbor(c, offset) - - scalar.center * expr.diffusionCoeff.center()) - for offset in [-1, 1]] - else: - firstDiffs = [offset * - (scalar.neighbor(c, offset) * expr.diffusionCoeff - - scalar.center * expr.diffusionCoeff) - for offset in [-1, 1]] + firstDiffs = [offset * + (expr.diffusionScalarAtOffset(c, offset) * expr.diffusionCoefficientAtOffset(c, offset) - + expr.diffusionScalarAtOffset(0, 0) * expr.diffusionCoefficientAtOffset(0, 0)) + for offset in [-1, 1]] result += firstDiffs[1] - firstDiffs[0] return result / (self.dx**2) def _discretize_advection(self, expr): - idx = 0 if expr.scalarIndex is None else int(expr.scalarIndex) result = 0 for c in range(expr.dim): - if isinstance(expr.vector, Field): - assert expr.vector.indexDimensions == 1 - interpolated = [(expr.scalar.neighbor(c, offset)(idx) * expr.vector.neighbor(c, offset)(c) + - expr.scalar.neighbor(c, 0)(idx) * expr.vector.neighbor(c, 0)(c)) / 2 - for offset in [-1, 1]] - else: - interpolated = [(expr.scalar.neighbor(c, offset)(idx) * expr.vector(c) - - expr.scalar.neighbor(c, 0)(idx) * expr.vector(c)) / 2 - for offset in [-1, 1]] + interpolated = [(expr.advectedScalarAtOffset(c, offset) * expr.velocityFieldAtOffset(c, offset, c) + + expr.advectedScalarAtOffset(c, 0) * expr.velocityFieldAtOffset(c, 0, c)) / 2 + for offset in [-1, 1]] result += interpolated[1] - interpolated[0] return result / self.dx @@ -300,4 +316,3 @@ class Discretization2ndOrder: else: raise NotImplementedError("Cannot discretize expression with more than one transient term") - diff --git a/kernelstep.py b/kernelstep.py new file mode 100644 index 0000000000000000000000000000000000000000..75c1bf5b6b1fdb02d1989d9192108a3d669f6528 --- /dev/null +++ b/kernelstep.py @@ -0,0 +1,8 @@ + + +class KernelStep: + + def __init__(self, dataHandling, updateRule): + # fields to sync before + # GPU sync fields + pass diff --git a/timeloop.py b/timeloop.py new file mode 100644 index 0000000000000000000000000000000000000000..b6f3cfd518d696a18e28edcba16c618a6a872a16 --- /dev/null +++ b/timeloop.py @@ -0,0 +1,87 @@ +import time + + +class TimeLoop: + def __init__(self): + self._preRunFunctions = [] + self._postRunFunctions = [] + self._timeStepFunctions = [] + self.timeStepsRun = 0 + + def addStep(self, stepObj): + if hasattr(stepObj, 'preRun'): + self.addPreRunFunction(stepObj.preRun) + if hasattr(stepObj, 'postRun'): + self.addPostRunFunction(stepObj.postRun) + self.add(stepObj.timeStep) + + def add(self, timeStepFunction): + self._timeStepFunctions.append(timeStepFunction) + + def addKernel(self, dataHandling, kernelFunc): + self._timeStepFunctions.append(lambda: dataHandling.runKernel(kernelFunc)) + + def addPreRunFunction(self, f): + self._preRunFunctions.append(f) + + def addPostRunFunction(self, f): + self._postRunFunctions.append(f) + + def run(self, timeSteps=0): + self.preRun() + + try: + for i in range(timeSteps): + self.timeStep() + except KeyboardInterrupt: + pass + + self.postRun() + + def benchmarkRun(self, timeSteps=0, initTimeSteps=0): + self.preRun() + for i in range(initTimeSteps): + self.timeStep() + + start = time.perf_counter() + for i in range(timeSteps): + self.timeStep() + end = time.perf_counter() + self.postRun() + + timeForOneIteration = (end - start) / timeSteps + return timeForOneIteration + + def benchmark(self, timeForBenchmark=5, initTimeSteps=10, numberOfTimeStepsForEstimation=20): + """ + Returns the time in seconds for one time step + + :param timeForBenchmark: number of seconds benchmark should take + :param initTimeSteps: number of time steps run initially for warm up, to get arrays into cache etc + :param numberOfTimeStepsForEstimation: time steps run before real benchmarks, to determine number of time steps + that approximately take 'timeForBenchmark' + """ + # Run a few time step to get first estimate + durationOfTimeStep = self.benchmarkRun(numberOfTimeStepsForEstimation, initTimeSteps) + + # Run for approximately 'timeForBenchmark' seconds + timeSteps = int(timeForBenchmark / durationOfTimeStep) + timeSteps = max(timeSteps, 20) + return self.benchmarkRun(timeSteps, initTimeSteps) + + def preRun(self): + for f in self._preRunFunctions: + f() + + def postRun(self): + for f in self._postRunFunctions: + f() + + def timeStep(self): + for f in self._timeStepFunctions: + f() + self.timeStepsRun += 1 + + + +