From 0b493f17388a9c53dc3466d41ec4ce7d11d9cb7d Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Fri, 21 Feb 2025 17:21:29 +0100
Subject: [PATCH] update documentation; start writing user guide on field api
 reflection

---
 docs/source/api/lang.rst                     |   6 +
 docs/source/usage/api_modelling.md           | 115 ++++++++++++++++++-
 src/pystencilssfg/composer/basic_composer.py |   4 +-
 src/pystencilssfg/lang/extractions.py        |  33 +++++-
 4 files changed, 152 insertions(+), 6 deletions(-)

diff --git a/docs/source/api/lang.rst b/docs/source/api/lang.rst
index c83317e..bd5e4fa 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 cfb0e10..88dbe5c 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 0f9a339..05ebc71 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 40c6922..e920fcb 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):
-- 
GitLab