diff --git a/docs/source/api/lang.rst b/docs/source/api/lang.rst index c83317e506549eb9ca9d86bc71765d196306ef09..bd5e4fa7993f30d4c8f6e5c532eb3d234c64061d 100644 --- a/docs/source/api/lang.rst +++ b/docs/source/api/lang.rst @@ -21,6 +21,12 @@ Data Types .. automodule:: pystencilssfg.lang.types :members: +Extraction Protocols +-------------------- + +.. automodule:: pystencilssfg.lang.extractions + :members: + C++ Standard Library (``pystencilssfg.lang.cpp``) ------------------------------------------------- diff --git a/docs/source/usage/api_modelling.md b/docs/source/usage/api_modelling.md index cfb0e10776ae11072c13423c1ef5b78a72fd8459..88dbe5ce8f6af06a6b8e3ee1b795712bbc205b75 100644 --- a/docs/source/usage/api_modelling.md +++ b/docs/source/usage/api_modelling.md @@ -233,7 +233,120 @@ expr, lang.depends(expr), lang.includes(expr) (field_data_structure_reflection)= ## Reflecting Field Data Structures +One key feature of pystencils-sfg is its ability to map symbolic fields +onto arbitrary array data structures +using the composer's {any}`map_field <SfgBasicComposer.map_field>` method. +The APIs of a custom field data structure can naturally be injected into pystencils-sfg +using the modelling framework described above. +However, for them to be recognized by `map_field`, +the reflection class also needs to implement the {any}`SupportsFieldExtraction` protocol. +This requires that the following three methods are implemented: + +```{code-block} python +def _extract_ptr(self) -> AugExpr: ... + +def _extract_size(self, coordinate: int) -> AugExpr | None: ... + +def _extract_stride(self, coordinate: int) -> AugExpr | None: ... +``` + +The first, `_extract_ptr`, must return an expression that evaluates +to the base pointer of the field's memory buffer. +This pointer has to point at the field entry which pystencils accesses +at all-zero index and offsets. + +### Sample Field API Reflection + +Consider the following class template for a field, which takes its element type +and dimensionality as template parameters +and exposes its data pointer, shape, and strides through public methods: + +```{code-block} C++ +template< std::floating_point ElemType, size_t DIM > +class MyField { +public: + size_t shape(size_t coord); + size_t stride(size_t coord); + ElemType * data(); +} +``` + +It could be reflected by the following class. +Note that in this case we define a custom `__init__` method in order to +intercept the template arguments `elem_type` and `dim` +and store them as instance members. +Our `__init__` then forwards all its arguments up to `CppClass.__init__`. +We then define reflection methods for `shape`, `stride` and `data` - +the implementation of the field extraction protocol then simply calls these methods. + +```{code-cell} ipython3 +from pystencilssfg.lang import SupportsFieldExtraction +from pystencils.types import UserTypeSpec + +class MyField(lang.CppClass, SupportsFieldExtraction): + template = lang.cpptype( + "MyField< {ElemType}, {DIM} >", + "MyField.hpp" + ) + + def __init__( + self, + elem_type: UserTypeSpec, + dim: int, + **kwargs, + ) -> None: + self._elem_type = elem_type + self._dim = dim + super().__init__(ElemType=dtype, DIM=dim, **kwargs) + + # Reflection of Public Methods + def shape(self, coord: int | lang.AugExpr) -> lang.AugExpr: + return lang.AugExpr.format("{}.shape({})", self, coord) + + def stride(self, coord: int | lang.AugExpr) -> lang.AugExpr: + return lang.AugExpr.format("{}.stride({})", self, coord) + + def data(self) -> lang.AugExpr: + return lang.AugExpr.format("{}.data()", self, coord) + + # Field Extraction Protocol that uses the above interface + def _extract_ptr(self) -> lang.AugExpr: + return self.data() + + def _extract_size(self, coordinate: int) -> lang.AugExpr | None: + if coordinate > self._dim: + return None + else: + return self.shape(coordinate) + + def _extract_stride(self, coordinate: int) -> lang.AugExpr | None: + if coordinate > self._dim: + return None + else: + return self.stride(coordinate) +``` + :::{admonition} To Do -Write guide on field data structure reflection +Demonstrate in a generator script ::: + +### A Note on Ghost Layers + +Some care has to be taken when reflecting data structures that model the notion +of ghost layers. +Consider an array with the index space $[0, N_x) \times [0, N_y)$, +its base pointer identifying the entry $(0, 0)$. +When a pystencils kernel is generated with a shell of $k$ ghost layers +(see {any}`CreateKernelConfig.ghost_layers <pystencils.codegen.config.CreateKernelConfig.ghost_layers>`), +it will process only the subspace $[k, N_x - k) \times [k, N_x - k)$. + +If your data structure is implemented such that ghost layer nodes have coordinates +$< 0$ and $\ge N_{x, y}$, +you must hence take care that + - either, `_extract_ptr` returns a pointer identifying the array entry at `(-k, -k)`; + - or, ensure that kernels operating on your data structure are always generated + with `ghost_layers = 0`. + +In either case, you must make sure that the number of ghost layers in your data structure +matches the expected number of ghost layers of the kernel. diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py index 0f9a339137b201f24405a6d06a190d20326986f9..05ebc71ffdcc1879e1ccaa4e5636b064776a44b2 100644 --- a/src/pystencilssfg/composer/basic_composer.py +++ b/src/pystencilssfg/composer/basic_composer.py @@ -518,7 +518,7 @@ class SfgBasicComposer(SfgIComposer): Args: field: The pystencils field to be mapped - index_provider: An expression representing a field, or a field extraction provider instance + index_provider: An object that provides the field indexing information cast_indexing_symbols: Whether to always introduce explicit casts for indexing symbols """ return SfgDeferredFieldMapping( @@ -540,7 +540,7 @@ class SfgBasicComposer(SfgIComposer): Args: lhs_components: Vector components as a list of symbols. - rhs: A `SrcVector` object representing a vector data structure. + rhs: An object providing access to vector components """ components: list[SfgVar | sp.Symbol] = [ (asvar(c) if isinstance(c, _VarLike) else c) for c in lhs_components diff --git a/src/pystencilssfg/lang/extractions.py b/src/pystencilssfg/lang/extractions.py index 40c69220fd40f2ddc6358d21feea647f8aa350ba..e920fcbfc453d53c22f0486ab7c051ed6c5a7c7f 100644 --- a/src/pystencilssfg/lang/extractions.py +++ b/src/pystencilssfg/lang/extractions.py @@ -13,14 +13,41 @@ class SupportsFieldExtraction(Protocol): They can therefore be passed to `sfg.map_field <SfgBasicComposer.map_field>`. """ +# how-to-guide begin @abstractmethod - def _extract_ptr(self) -> AugExpr: ... + def _extract_ptr(self) -> AugExpr: + """Extract the field base pointer. + + Return an expression which represents the base pointer + of this field data structure. + + :meta public: + """ @abstractmethod - def _extract_size(self, coordinate: int) -> AugExpr | None: ... + def _extract_size(self, coordinate: int) -> AugExpr | None: + """Extract field size in a given coordinate. + + If ``coordinate`` is valid for this field (i.e. smaller than its dimensionality), + return an expression representing the logical size of this field + in the given dimension. + Otherwise, return `None`. + + :meta public: + """ @abstractmethod - def _extract_stride(self, coordinate: int) -> AugExpr | None: ... + def _extract_stride(self, coordinate: int) -> AugExpr | None: + """Extract field stride in a given coordinate. + + If ``coordinate`` is valid for this field (i.e. smaller than its dimensionality), + return an expression representing the memory linearization stride of this field + in the given dimension. + Otherwise, return `None`. + + :meta public: + """ +# how-to-guide end class SupportsVectorExtraction(Protocol):