From 4651c191bc366300c025d35e98427f57019fa3e8 Mon Sep 17 00:00:00 2001 From: Martin Bauer <martin.bauer@fau.de> Date: Thu, 26 Apr 2018 19:17:42 +0200 Subject: [PATCH] Documentation overhaul Part2 --- __init__.py | 5 +- cpu/cpujit.py | 6 ++ display_utils.py | 2 +- field.py | 147 +++++++++++++++++++++++++++++++------------- gpucuda/__init__.py | 5 +- gpucuda/indexing.py | 2 +- kernelcreation.py | 76 ++++++++++++++++++----- plot2d.py | 30 +++++++++ transformations.py | 1 + 9 files changed, 207 insertions(+), 67 deletions(-) diff --git a/__init__.py b/__init__.py index 20db8d6b9..5dd9cd572 100644 --- a/__init__.py +++ b/__init__.py @@ -3,7 +3,7 @@ from . import sympy_gmpy_bug_workaround # NOQA from .field import Field, FieldType, fields from .data_types import TypedSymbol from .slicing import make_slice -from .kernelcreation import create_kernel, create_indexed_kernel +from .kernelcreation import create_kernel, create_indexed_kernel, create_staggered_kernel from .display_utils import show_code, to_dot from .simp import AssignmentCollection from .assignment import Assignment @@ -15,7 +15,7 @@ from . import fd __all__ = ['Field', 'FieldType', 'fields', 'TypedSymbol', 'make_slice', - 'create_kernel', 'create_indexed_kernel', + 'create_kernel', 'create_indexed_kernel', 'create_staggered_kernel', 'show_code', 'to_dot', 'AssignmentCollection', 'Assignment', @@ -23,4 +23,3 @@ __all__ = ['Field', 'FieldType', 'fields', 'create_data_handling', 'kernel', 'fd'] - diff --git a/cpu/cpujit.py b/cpu/cpujit.py index b793302e2..ca915d38b 100644 --- a/cpu/cpujit.py +++ b/cpu/cpujit.py @@ -1,5 +1,11 @@ r""" +*pystencils* automatically searches for a compiler, so in most cases no explicit configuration is required. +On Linux make sure that 'gcc' and 'g++' are installed and in your path. +On Windows a recent Visual Studio installation is required. +In case anything does not work as expected or a special compiler should be used, changes can be specified +in a configuration file. + *pystencils* looks for a configuration file in JSON format at the following locations in the listed order. 1. at the path specified in the environment variable ``PYSTENCILS_CONFIG`` diff --git a/display_utils.py b/display_utils.py index 612d69aa3..2d0fa87f7 100644 --- a/display_utils.py +++ b/display_utils.py @@ -33,7 +33,7 @@ def highlight_cpp(code: str): def show_code(ast: KernelFunction): - """Returns an object to display C code. + """Returns an object to display generated code (C/C++ or CUDA) Can either be displayed as HTML in Jupyter notebooks or printed as normal string. """ diff --git a/field.py b/field.py index 87b4785a2..69513a3ab 100644 --- a/field.py +++ b/field.py @@ -9,38 +9,40 @@ from pystencils.alignedarray import aligned_empty from pystencils.data_types import TypedSymbol, create_type, create_composite_type_from_string, StructType from pystencils.sympyextensions import is_integer_sequence +__all__ = ['Field', 'fields', 'FieldType'] + def fields(description=None, index_dimensions=0, layout=None, **kwargs): """Creates pystencils fields from a string description. Examples: - Create a 2D scalar and vector field - >>> s, v = fields("s, v(2): double[2D]") - >>> assert s.spatial_dimensions == 2 and s.index_dimensions == 0 - >>> assert v.spatial_dimensions == 2 and v.index_dimensions == 1 and v.index_shape == (2,) - - Create an integer field of shape (10, 20) - >>> f = fields("f : int32[10, 20]") - >>> f.has_fixed_shape, f.shape - (True, (10, 20)) + Create a 2D scalar and vector field: + >>> s, v = fields("s, v(2): double[2D]") + >>> assert s.spatial_dimensions == 2 and s.index_dimensions == 0 + >>> assert (v.spatial_dimensions, v.index_dimensions, v.index_shape) == (2, 1, (2,)) - Numpy arrays can be used as template for shape and data type of field - >>> arr_s, arr_v = np.zeros([20, 20]), np.zeros([20, 20, 2]) - >>> s, v = fields("s, v(2)", s=arr_s, v=arr_v) - >>> assert s.index_dimensions == 0 and v.index_shape == (2,) and s.dtype.numpy_dtype == arr_s.dtype + Create an integer field of shape (10, 20): + >>> f = fields("f : int32[10, 20]") + >>> f.has_fixed_shape, f.shape + (True, (10, 20)) - Format string can be left out, field names are taken from keyword arguments. - >>> fields(f1=arr_s, f2=arr_s) - [f1, f2] + Numpy arrays can be used as template for shape and data type of field: + >>> arr_s, arr_v = np.zeros([20, 20]), np.zeros([20, 20, 2]) + >>> s, v = fields("s, v(2)", s=arr_s, v=arr_v) + >>> assert s.index_dimensions == 0 and s.dtype.numpy_dtype == arr_s.dtype + >>> assert v.index_shape == (2,) - The keyword names 'index_dimension' and 'layout' have special meaning and thus can not be used to pass - numpy arrays: - >>> f = fields(f=arr_v, index_dimensions=1) - >>> assert f.index_dimensions == 1 - >>> f = fields("pdfs(19) : float32[3D]", layout='fzyx') - >>> f.layout - (2, 1, 0) + Format string can be left out, field names are taken from keyword arguments. + >>> fields(f1=arr_s, f2=arr_s) + [f1, f2] + + The keyword names ``index_dimension`` and ``layout`` have special meaning, don't use them for field names + >>> f = fields(f=arr_v, index_dimensions=1) + >>> assert f.index_dimensions == 1 + >>> f = fields("pdfs(19) : float32[3D]", layout='fzyx') + >>> f.layout + (2, 1, 0) """ result = [] if description: @@ -108,36 +110,41 @@ class Field: This Field class knows about the dimension, memory layout (strides) and optionally about the size of an array. Creating Fields: - - To create a field use one of the static create* members. There are two options: - - 1. create a kernel with fixed loop sizes i.e. the shape of the array is already known. This is usually the - case if just-in-time compilation directly from Python is done. (see :func:`Field.create_from_numpy_array`) + The preferred method to create fields is the `fields` function. + Alternatively one can use one of the static functions `Field.create_generic`, `Field.create_from_numpy_array` + and `Field.create_fixed_size`. Don't instantiate the Field directly! + Fields can be created with known or unknown shapes: + + 1. If you want to create a kernel with fixed loop sizes i.e. the shape of the array is already known. + This is usually the case if just-in-time compilation directly from Python is done. + (see `Field.create_from_numpy_array` 2. create a more general kernel that works for variable array sizes. This can be used to create kernels - beforehand for a library. (see :func:`Field.create_generic`) + beforehand for a library. (see `Field.create_generic`) - Dimensions: + Dimensions and Indexing: A field has spatial and index dimensions, where the spatial dimensions come first. The interpretation is that the field has multiple cells in (usually) two or three dimensional space which are - looped over. Additionally N values are stored per cell. In this case spatial_dimensions is two or three, + looped over. Additionally N values are stored per cell. In this case spatial_dimensions is two or three, and index_dimensions equals N. If you want to store a matrix on each point in a two dimensional grid, there are four dimensions, two spatial and two index dimensions: ``len(arr.shape) == spatial_dims + index_dims`` - Indexing: - When accessing (indexing) a field the result is a FieldAccess which is derived from sympy Symbol. + The shape of the index dimension does not have to be specified. Just use the 'index_dimensions' parameter. + However, it is good practice to define the shape, since out of bounds accesses can be directly detected in this + case. The shape can be passed with the 'index_shape' parameter of the field creation functions. + + When accessing (indexing) a field the result is a `Field.Access` which is derived from sympy Symbol. First specify the spatial offsets in [], then in case index_dimension>0 the indices in () e.g. ``f[-1,0,0](7)`` - Example without index dimensions: + Example using no index dimensions: >>> a = np.zeros([10, 10]) >>> f = Field.create_from_numpy_array("f", a, index_dimensions=0) - >>> jacobi = ( f[-1,0] + f[1,0] + f[0,-1] + f[0,1] ) / 4 + >>> jacobi = (f[-1,0] + f[1,0] + f[0,-1] + f[0,1]) / 4 - Example with index dimensions: LBM D2Q9 stream pull + Examples for index dimensions to create LB field and implement stream pull: >>> from pystencils import Assignment >>> stencil = np.array([[0,0], [0,1], [0,-1]]) - >>> src = Field.create_generic("src", spatial_dimensions=2, index_dimensions=1) - >>> dst = Field.create_generic("dst", spatial_dimensions=2, index_dimensions=1) + >>> src, dst = fields("src(3), dst(3) : double[2D]") >>> for i, offset in enumerate(stencil): ... Assignment(dst[0,0](i), src[-offset](i)) Assignment(dst_C^0, src_C^0) @@ -380,6 +387,24 @@ class Field: # noinspection PyAttributeOutsideInit,PyUnresolvedReferences class Access(sp.Symbol): + """Class representing a relative access into a `Field`. + + This class behaves like a normal sympy Symbol, it is actually derived from it. One can built up + sympy expressions using field accesses, solve for them, etc. + + Examples: + >>> vector_field_2d = fields("v(2): double[2D]") # create a 2D vector field + >>> northern_neighbor_y_component = vector_field_2d[0, 1](1) + >>> northern_neighbor_y_component + v_N^1 + >>> central_y_component = vector_field_2d(1) + >>> central_y_component + v_C^1 + >>> central_y_component.get_shifted(1, 0) # move the existing access + v_E^1 + >>> central_y_component.at_index(0) # change component + v_C^0 + """ def __new__(cls, name, *args, **kwargs): obj = Field.Access.__xnew_cached_(cls, name, *args, **kwargs) return obj @@ -458,15 +483,18 @@ class Field: raise TypeError("Field access is not iterable") @property - def field(self): + def field(self) -> 'Field': + """Field that the Access points to""" return self._field @property - def offsets(self): + def offsets(self) -> Tuple: + """Spatial offset as tuple""" return tuple(self._offsets) @property - def required_ghost_layers(self): + def required_ghost_layers(self) -> int: + """Largest spatial distance that is accessed.""" return int(np.max(np.abs(self._offsets))) @property @@ -475,21 +503,54 @@ class Field: @property def offset_name(self) -> str: + """Spatial offset as string, East-West for x, North-South for y and Top-Bottom for z coordinate. + + Example: + >>> f = fields("f: double[2D]") + >>> f[1, 1].offset_name # north-east + 'NE' + """ return self._offsetName @property def index(self): + """Value of index coordinates as tuple.""" return self._index def neighbor(self, coord_id: int, offset: Sequence[int]) -> 'Field.Access': + """Returns a new Access with changed spatial coordinates. + + Args: + coord_id: index of the coordinate to change (0 for x, 1 for y,...) + offset: incremental change of this coordinate + + Example: + >>> f = fields('f: [2D]') + >>> f[0,0].neighbor(coord_id=1, offset=-1) + f_S + """ offset_list = list(self.offsets) offset_list[coord_id] += offset return Field.Access(self.field, tuple(offset_list), self.index) def get_shifted(self, *shift)-> 'Field.Access': + """Returns a new Access with changed spatial coordinates + + Example: + >>> f = fields("f: [2D]") + >>> f[0,0].get_shifted(1, 1) + f_NE + """ return Field.Access(self.field, tuple(a + b for a, b in zip(shift, self.offsets)), self.index) - def at_index(self, *idx_tuple): + def at_index(self, *idx_tuple) -> 'Field.Access': + """Returns new Access with changed index. + + Example: + >>> f = fields("f(9): [2D]") + >>> f(0).at_index(8) + f_C^8 + """ return Field.Access(self.field, self.offsets, idx_tuple) def _hashable_content(self): @@ -729,12 +790,12 @@ def direction_string_to_offset(direction: str, dim: int = 3): direction = direction[1:] return offset[:dim] + def _parse_type_description(type_description): if not type_description: return np.float64, None elif '[' in type_description: assert type_description[-1] == ']' - splitted = type_description[:-1].split("[", ) type_part, size_part = type_description[:-1].split("[", ) if not type_part: type_part = "float64" diff --git a/gpucuda/__init__.py b/gpucuda/__init__.py index f360794ee..9b178c470 100644 --- a/gpucuda/__init__.py +++ b/gpucuda/__init__.py @@ -1,4 +1,5 @@ from pystencils.gpucuda.kernelcreation import create_cuda_kernel, created_indexed_cuda_kernel from pystencils.gpucuda.cudajit import make_python_function - -__all__ = ['create_cuda_kernel', 'created_indexed_cuda_kernel', 'make_python_function'] +from .indexing import AbstractIndexing, BlockIndexing, LineIndexing +__all__ = ['create_cuda_kernel', 'created_indexed_cuda_kernel', 'make_python_function', + 'AbstractIndexing', 'BlockIndexing', 'LineIndexing'] diff --git a/gpucuda/indexing.py b/gpucuda/indexing.py index 862143abb..d39a54688 100644 --- a/gpucuda/indexing.py +++ b/gpucuda/indexing.py @@ -1,5 +1,5 @@ import abc -from typing import Tuple +from typing import Tuple # noqa import sympy as sp from pystencils.astnodes import Conditional, Block from pystencils.slicing import normalize_slice diff --git a/kernelcreation.py b/kernelcreation.py index 17d804b2d..87bcf5d49 100644 --- a/kernelcreation.py +++ b/kernelcreation.py @@ -1,10 +1,10 @@ from types import MappingProxyType import sympy as sp -from .assignment import Assignment -from .astnodes import LoopOverCoordinate, Conditional, Block, SympyAssignment -from .simp.assignment_collection import AssignmentCollection -from .gpucuda.indexing import indexing_creator_from_params -from .transformations import remove_conditionals_in_staggered_kernel +from pystencils.assignment import Assignment +from pystencils.astnodes import LoopOverCoordinate, Conditional, Block, SympyAssignment +from pystencils.simp.assignment_collection import AssignmentCollection +from pystencils.gpucuda.indexing import indexing_creator_from_params +from pystencils.transformations import remove_conditionals_in_staggered_kernel def create_kernel(assignments, target='cpu', data_type="double", iteration_slice=None, ghost_layers=None, @@ -14,7 +14,7 @@ def create_kernel(assignments, target='cpu', data_type="double", iteration_slice Creates abstract syntax tree (AST) of kernel, using a list of update equations. Args: - assignments: either be a plain list of equations or a AssignmentCollection object + assignments: can be a single assignment, sequence of assignments or an `AssignmentCollection` target: 'cpu', 'llvm' or 'gpu' data_type: data type used for all untyped symbols (i.e. non-fields), can also be a dict from symbol name to type @@ -22,18 +22,34 @@ def create_kernel(assignments, target='cpu', data_type="double", iteration_slice part of the field is iterated over ghost_layers: if left to default, the number of necessary ghost layers is determined automatically a single integer specifies the ghost layer count at all borders, can also be a sequence of - pairs [(x_lower_gl, x_upper_gl), .... ] + pairs ``[(x_lower_gl, x_upper_gl), .... ]`` cpu_openmp: True or number of threads for OpenMP parallelization, False for no OpenMP - cpu_vectorize_info: pair of instruction set name ('sse, 'avx', 'avx512') and data type ('float', 'double') - - gpu_indexing: either 'block' or 'line' , or custom indexing class (see gpucuda/indexing.py) + cpu_vectorize_info: pair of instruction set name, i.e. one of 'sse, 'avx' or 'avx512' + and data type 'float' or 'double'. For example ``('avx', 'double')`` + gpu_indexing: either 'block' or 'line' , or custom indexing class, see `pystencils.gpucuda.AbstractIndexing` gpu_indexing_params: dict with indexing parameters (constructor parameters of indexing class) - e.g. for 'block' one can specify {'block_size': (20, 20, 10) } + e.g. for 'block' one can specify '{'block_size': (20, 20, 10) }' Returns: - abstract syntax tree object, that can either be printed as source code with `show_code` or can be compiled with - through its `compile()` member + abstract syntax tree (AST) object, that can either be printed as source code with `show_code` or + can be compiled with through its `compile()` member + + Example: + >>> import pystencils as ps + >>> import numpy as np + >>> s, d = ps.fields('s, d: [2D]') + >>> assignment = ps.Assignment(d[0,0], s[0, 1] + s[0, -1] + s[1, 0] + s[-1, 0]) + >>> ast = ps.create_kernel(assignment, target='cpu', cpu_openmp=True) + >>> kernel = ast.compile() + >>> d_arr = np.zeros([5, 5]) + >>> kernel(d=d_arr, s=np.ones([5, 5])) + >>> d_arr + array([[0., 0., 0., 0., 0.], + [0., 4., 4., 4., 0.], + [0., 4., 4., 4., 0.], + [0., 4., 4., 4., 0.], + [0., 0., 0., 0., 0.]]) """ # ---- Normalizing parameters @@ -89,9 +105,33 @@ def create_indexed_kernel(assignments, index_fields, target='cpu', data_type="do index_fields: list of index fields, i.e. 1D fields with struct data type coordinate_names: name of the coordinate fields in the struct data type - """ - if isinstance(assignments, AssignmentCollection): + Example: + >>> import pystencils as ps + >>> import numpy as np + >>> + >>> # Index field stores the indices of the cell to visit together with optional values + >>> index_arr_dtype = np.dtype([('x', np.int32), ('y', np.int32), ('val', np.double)]) + >>> index_arr = np.array([(1, 1, 0.1), (2, 2, 0.2), (3, 3, 0.3)], dtype=index_arr_dtype) + >>> idx_field = ps.fields(idx=index_arr) + >>> + >>> # Additional values stored in index field can be accessed in the kernel as well + >>> s, d = ps.fields('s, d: [2D]') + >>> assignment = ps.Assignment(d[0,0], 2 * s[0, 1] + 2 * s[1, 0] + idx_field('val')) + >>> ast = create_indexed_kernel(assignment, [idx_field], coordinate_names=('x', 'y')) + >>> kernel = ast.compile() + >>> d_arr = np.zeros([5, 5]) + >>> kernel(s=np.ones([5, 5]), d=d_arr, idx=index_arr) + >>> d_arr + array([[0. , 0. , 0. , 0. , 0. ], + [0. , 4.1, 0. , 0. , 0. ], + [0. , 0. , 4.2, 0. , 0. ], + [0. , 0. , 0. , 4.3, 0. ], + [0. , 0. , 0. , 0. , 0. ]]) + """ + if isinstance(assignments, Assignment): + assignments = [assignments] + elif isinstance(assignments, AssignmentCollection): assignments = assignments.all_assignments if target == 'cpu': from pystencils.cpu import create_indexed_kernel @@ -116,10 +156,12 @@ def create_indexed_kernel(assignments, index_fields, target='cpu', data_type="do def create_staggered_kernel(staggered_field, expressions, subexpressions=(), target='cpu', **kwargs): """Kernel that updates a staggered field. + .. image:: /img/staggered_grid.svg + Args: staggered_field: field that has one index coordinate and - where e.g. f[0,0](0) is interpreted as value at the left cell boundary, f[1,0](0) the right cell - boundary and f[0,0](1) the southern cell boundary etc. + where e.g. ``f[0,0](0)`` is interpreted as value at the left cell boundary, ``f[1,0](0)`` the right cell + boundary and ``f[0,0](1)`` the southern cell boundary etc. expressions: sequence of expressions of length dim, defining how the east, southern, (bottom) cell boundary should be update subexpressions: optional sequence of Assignments, that define subexpressions used in the main expressions diff --git a/plot2d.py b/plot2d.py index 6157f9314..68cc8d729 100644 --- a/plot2d.py +++ b/plot2d.py @@ -253,6 +253,36 @@ def vector_field_magnitude_animation(run_function, plot_setup_function=lambda *_ return animation.FuncAnimation(fig, update_figure, interval=interval, frames=frames) +def scalar_field_animation(run_function, plot_setup_function=lambda *_: None, rescale=True, + plot_update_function=lambda *_: None, interval=30, frames=180, **kwargs): + import matplotlib.animation as animation + + fig = gcf() + im = None + field = run_function() + if rescale: + f_min, f_max = np.min(field), np.max(field) + field = (field - f_min) / (f_max - f_min) + im = scalar_field(field, vmin=0.0, vmax=1.0, **kwargs) + else: + im = scalar_field(field, **kwargs) + plot_setup_function(im) + + def update_figure(*_): + f = run_function() + if rescale: + f_min, f_max = np.min(f), np.max(f) + f = (f - f_min) / (f_max - f_min) + if hasattr(f, 'mask'): + f = np.ma.masked_array(f, mask=f.mask[:, :, 0]) + f = np.swapaxes(f, 0, 1) + im.set_array(f) + plot_update_function(im) + return im, + + return animation.FuncAnimation(fig, update_figure, interval=interval, frames=frames) + + def surface_plot_animation(run_function, frames=90, interval=30): from mpl_toolkits.mplot3d import Axes3D import matplotlib.animation as animation diff --git a/transformations.py b/transformations.py index 62b78258d..727aae027 100644 --- a/transformations.py +++ b/transformations.py @@ -687,6 +687,7 @@ class KernelConstraintsCheck: def process_expression(self, rhs): self._update_accesses_rhs(rhs) if isinstance(rhs, Field.Access): + self.fields_read.add(rhs.field) return rhs elif isinstance(rhs, TypedSymbol): return rhs -- GitLab