Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (37)
Showing
with 1729 additions and 347 deletions
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
# dev environment # dev environment
**/.venv **/.venv
**/venv **/venv
**/.nox
# build artifacts # build artifacts
dist dist
......
...@@ -4,44 +4,28 @@ stages: ...@@ -4,44 +4,28 @@ stages:
- "Documentation" - "Documentation"
- deploy - deploy
.nox-base:
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/nox:alpine
tags:
- docker
linter: linter:
extends: .nox-base
stage: "Code Quality" stage: "Code Quality"
needs: [] needs: []
except:
variables:
- $ENABLE_NIGHTLY_BUILDS
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full
script: script:
- flake8 src/pystencilssfg - nox --session lint
tags:
- docker
typechecker: typechecker:
extends: .nox-base
stage: "Code Quality" stage: "Code Quality"
needs: [] needs: []
except:
variables:
- $ENABLE_NIGHTLY_BUILDS
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full
script: script:
- pip install mypy - nox --session typecheck
- mypy src/pystencilssfg
tags:
- docker
testsuite: .testsuite-base:
stage: "Tests" stage: "Tests"
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full
needs: [] needs: []
tags:
- docker
before_script:
- pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev"
- pip install -e .[tests]
script:
- pytest -v --cov=src/pystencilssfg --cov-report=term --cov-config=pyproject.toml
- coverage html
- coverage xml
coverage: '/TOTAL.*\s+(\d+%)$/' coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts: artifacts:
when: always when: always
...@@ -53,24 +37,34 @@ testsuite: ...@@ -53,24 +37,34 @@ testsuite:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
"testsuite-py3.10+cuda":
extends: .testsuite-base
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/nox:ubuntu24.04-cuda12.6
script:
- nox --session testsuite-3.10
tags:
- docker
- cuda11
"testsuite-py3.13":
extends: .testsuite-base
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/nox:alpine
script:
- nox --session testsuite-3.13
build-documentation: build-documentation:
extends: .nox-base
stage: "Documentation" stage: "Documentation"
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full
needs: [] needs: []
before_script:
- pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev"
- pip install -e .[docs]
script: script:
- cd docs - nox -s docs -- --fail-on-warnings
- make html
tags:
- docker
artifacts: artifacts:
paths: paths:
- docs/build/html - docs/build/html
when: always
pages: pages:
image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full image: alpine:latest
stage: deploy stage: deploy
script: script:
- ls -l - ls -l
...@@ -82,3 +76,12 @@ pages: ...@@ -82,3 +76,12 @@ pages:
- docker - docker
only: only:
- master@pycodegen/pystencils-sfg - master@pycodegen/pystencils-sfg
cmake-standalone:
stage: deploy
needs: []
script:
- echo "Publishing Cmake standalone"
artifacts:
paths:
- standalone
...@@ -13,6 +13,19 @@ As such, any submission of contributions via merge requests is considered as agr ...@@ -13,6 +13,19 @@ As such, any submission of contributions via merge requests is considered as agr
## Developing `pystencils-sfg` ## Developing `pystencils-sfg`
### Prequesites
To develop pystencils-sfg, you will need at least these packages:
- Python 3.10
- Git
- A C++ compiler supporting at least C++20 (gcc >= 10, or clang >= 10)
- GNU Make
- CMake
- Nox
Before continuing, make sure that the above packages are installed on your machine.
### Fork and Clone ### Fork and Clone
To work within the `pystencils-sfg` source tree, first create a *fork* of this repository To work within the `pystencils-sfg` source tree, first create a *fork* of this repository
...@@ -29,28 +42,29 @@ source .venv/bin/activate ...@@ -29,28 +42,29 @@ source .venv/bin/activate
pip install -e . pip install -e .
``` ```
If you have [nox](https://nox.thea.codes/en/stable/) installed, you can also set up your virtual environment
by running `nox --session dev_env`.
### Code Style and Type Checking ### Code Style and Type Checking
To contribute, please adhere to the Python code style set by [PEP 8](https://peps.python.org/pep-0008/). To contribute, please adhere to the Python code style set by [PEP 8](https://peps.python.org/pep-0008/).
For consistency, format all your source files using the [black](https://pypi.org/project/black/) formatter. For consistency, format all your source files using the [black](https://pypi.org/project/black/) formatter,
Use flake8 to check your code style: and check them regularily using the `flake8` linter through Nox:
```shell ```shell
flake8 src/pystencilssfg nox --session lint
``` ```
Further, `pystencils-sfg` is being fully type-checked using [MyPy](https://www.mypy-lang.org/). Further, `pystencils-sfg` is being fully type-checked using [MyPy](https://www.mypy-lang.org/).
All submitted code should contain type annotations ([PEP 484](https://peps.python.org/pep-0484/)) and must be All submitted code should contain type annotations ([PEP 484](https://peps.python.org/pep-0484/)) and must be
correctly statically typed. correctly statically typed.
Before each commit, check your types by calling Regularily check your code for type errors using
```shell ```shell
mypy src/pystencilssfg nox --session typecheck
``` ```
Both `flake8` and `mypy` are also run in the integration pipeline. Both `flake8` and `mypy` are also run in the integration pipeline.
You can automate the code quality checks by running them via a git pre-commit hook.
Such a hook can be installed using the [`install_git_hooks.sh`](install_git_hooks.sh) script located at the project root.
### Test Your Code ### Test Your Code
...@@ -65,3 +79,11 @@ In [tests/generator_scripts](tests/generator_scripts), a framework is provided t ...@@ -65,3 +79,11 @@ In [tests/generator_scripts](tests/generator_scripts), a framework is provided t
for successful execution, correctness, and compilability of their output. for successful execution, correctness, and compilability of their output.
Read the documentation within [test_generator_scripts.py](tests/generator_scripts/test_generator_scripts.py) Read the documentation within [test_generator_scripts.py](tests/generator_scripts/test_generator_scripts.py)
for more information. for more information.
Run the test suite by calling it through Nox:
```shell
nox --session testsuite
```
This will also collect coverage information and produce a coverage report as a HTML site placed in the `htmlcov` folder.
...@@ -2,19 +2,30 @@ import pytest ...@@ -2,19 +2,30 @@ import pytest
from os import path from os import path
@pytest.fixture(autouse=True)
def prepare_composer(doctest_namespace):
from pystencilssfg import SfgContext, SfgComposer
# Place a composer object in the environment for doctests
sfg = SfgComposer(SfgContext())
doctest_namespace["sfg"] = sfg
DATA_DIR = path.join(path.split(__file__)[0], "tests/data") DATA_DIR = path.join(path.split(__file__)[0], "tests/data")
@pytest.fixture @pytest.fixture
def sample_config_module(): def sample_config_module():
return path.join(DATA_DIR, "project_config.py") return path.join(DATA_DIR, "project_config.py")
@pytest.fixture
def sfg():
from pystencilssfg import SfgContext, SfgComposer
from pystencilssfg.ir import SfgSourceFile, SfgSourceFileType
return SfgComposer(
SfgContext(
header_file=SfgSourceFile("", SfgSourceFileType.HEADER),
impl_file=SfgSourceFile("", SfgSourceFileType.TRANSLATION_UNIT),
)
)
@pytest.fixture(autouse=True)
def prepare_doctest_namespace(doctest_namespace, sfg):
from pystencilssfg import lang
doctest_namespace["sfg"] = sfg
doctest_namespace["lang"] = lang
**/_sfg_out
\ No newline at end of file
import pystencilssfg
from pystencilssfg.config import SfgConfig
from os.path import splitext
class DocsPatchedGenerator(pystencilssfg.SourceFileGenerator):
"""Mockup wrapper around SourceFileGenerator for use in documentation
notebooks to print the generated code directly to the HTML
instead of writing it to file."""
scriptname: str = "demo"
glue: bool = False
display: bool = True
@classmethod
def setup(cls, scriptname: str, glue: bool = False, display: bool = True):
cls.scriptname = scriptname
cls.glue = glue
cls.display = display
def _scriptname(self) -> str:
return f"{DocsPatchedGenerator.scriptname}.py"
def __init__(
self, sfg_config: SfgConfig | None = None, keep_unknown_argv: bool = False
):
super().__init__(sfg_config, keep_unknown_argv=True)
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self._finish_files()
emitter = self._get_emitter()
header_code = emitter.dumps(self._header_file)
header_ext = splitext(self._header_file.name)[1]
mdcode = ":::::{tab-set}\n"
mdcode += f"::::{{tab-item}} Generated Header ({header_ext})\n"
mdcode += ":::{code-block} C++\n\n"
mdcode += header_code
mdcode += "\n:::\n::::\n"
if self._impl_file is not None:
impl_code = emitter.dumps(self._impl_file)
impl_ext = splitext(self._impl_file.name)[1]
mdcode += f"::::{{tab-item}} Generated Implementation ({impl_ext})\n"
mdcode += ":::{code-block} C++\n\n"
mdcode += impl_code
mdcode += "\n:::\n::::\n"
mdcode += ":::::"
from IPython.display import Markdown
mdobj = Markdown(mdcode)
if self.glue:
from myst_nb import glue
glue(f"sfg_out_{self.scriptname}", mdobj, display=False)
if self.display:
from IPython.display import display
display(mdobj)
pystencilssfg.SourceFileGenerator = DocsPatchedGenerator
...@@ -2,35 +2,68 @@ ...@@ -2,35 +2,68 @@
Composer API (``pystencilssfg.composer``) Composer API (``pystencilssfg.composer``)
***************************************** *****************************************
.. autoclass:: pystencilssfg.composer.SfgComposer .. module:: pystencilssfg.composer
.. autoclass:: SfgComposer
:members:
.. autoclass:: SfgIComposer
:members: :members:
.. autoclass:: pystencilssfg.composer.SfgIComposer .. autoclass:: SfgBasicComposer
:members: :members:
.. autoclass:: pystencilssfg.composer.SfgBasicComposer .. autoclass:: SfgClassComposer
:members: :members:
.. autoclass:: pystencilssfg.composer.SfgClassComposer .. autoclass:: SfgGpuComposer
:members: :members:
Custom Generators Custom Generators
================= =================
.. autoclass:: pystencilssfg.composer.custom.CustomGenerator .. module:: pystencilssfg.composer.custom
.. autoclass:: CustomGenerator
:members: :members:
Helper Methods and Builders Helper Methods and Builders
=========================== ===========================
.. autofunction:: pystencilssfg.composer.make_sequence .. module:: pystencilssfg.composer.basic_composer
.. autofunction:: make_sequence
.. autoclass:: KernelsAdder
:members:
.. autoclass:: SfgFunctionSequencer
:members:
:inherited-members:
.. autoclass:: SfgNodeBuilder
:members:
.. autoclass:: pystencilssfg.composer.basic_composer.SfgNodeBuilder .. autoclass:: SfgBranchBuilder
:members: :members:
.. autoclass:: pystencilssfg.composer.basic_composer.SfgBranchBuilder .. autoclass:: SfgSwitchBuilder
:members:
.. module:: pystencilssfg.composer.class_composer
.. autoclass:: SfgMethodSequencer
:members:
:inherited-members:
Context and Cursor
==================
.. module:: pystencilssfg.context
.. autoclass:: SfgContext
:members: :members:
.. autoclass:: pystencilssfg.composer.basic_composer.SfgSwitchBuilder .. autoclass:: SfgCursor
:members: :members:
...@@ -21,7 +21,7 @@ Categories, Parameter Types, and Special Values ...@@ -21,7 +21,7 @@ Categories, Parameter Types, and Special Values
.. autoclass:: _GlobalNamespace .. autoclass:: _GlobalNamespace
.. autodata:: GLOBAL_NAMESPACE .. autodata:: GLOBAL_NAMESPACE
.. autoclass:: OutputMode .. autoclass:: FileExtensions
:members: :members:
.. autoclass:: CodeStyle .. autoclass:: CodeStyle
...@@ -30,7 +30,3 @@ Categories, Parameter Types, and Special Values ...@@ -30,7 +30,3 @@ Categories, Parameter Types, and Special Values
.. autoclass:: ClangFormatOptions .. autoclass:: ClangFormatOptions
:members: :members:
Option Descriptors
------------------
.. autoclass:: Option
Internal Code Representation (`pystencilssfg.ir`) Internal Code Representation (`pystencilssfg.ir`)
================================================= =================================================
.. autoclass:: pystencilssfg.SfgContext .. automodule:: pystencilssfg.ir
:members: :members:
.. automodule:: pystencilssfg.ir Postprocessing
--------------
.. automodule:: pystencilssfg.ir.postprocessing
:members: :members:
...@@ -21,6 +21,12 @@ Data Types ...@@ -21,6 +21,12 @@ Data Types
.. automodule:: pystencilssfg.lang.types .. automodule:: pystencilssfg.lang.types
:members: :members:
Extraction Protocols
--------------------
.. automodule:: pystencilssfg.lang.extractions
:members:
C++ Standard Library (``pystencilssfg.lang.cpp``) C++ Standard Library (``pystencilssfg.lang.cpp``)
------------------------------------------------- -------------------------------------------------
...@@ -35,3 +41,9 @@ Implementation ...@@ -35,3 +41,9 @@ Implementation
.. automodule:: pystencilssfg.lang.cpp .. automodule:: pystencilssfg.lang.cpp
:members: :members:
GPU Runtime APIs
----------------
.. automodule:: pystencilssfg.lang.gpu
:members:
...@@ -24,7 +24,7 @@ html_title = f"pystencils-sfg v{version} Documentation" ...@@ -24,7 +24,7 @@ html_title = f"pystencils-sfg v{version} Documentation"
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ extensions = [
"myst_parser", "myst_nb",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.napoleon", "sphinx.ext.napoleon",
"sphinx.ext.autosummary", "sphinx.ext.autosummary",
...@@ -37,16 +37,7 @@ extensions = [ ...@@ -37,16 +37,7 @@ extensions = [
templates_path = ["_templates"] templates_path = ["_templates"]
exclude_patterns = [] exclude_patterns = []
source_suffix = {
".rst": "restructuredtext",
".md": "markdown",
}
master_doc = "index" master_doc = "index"
nitpicky = True
myst_enable_extensions = [
"colon_fence",
"dollarmath"
]
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
...@@ -67,7 +58,7 @@ intersphinx_mapping = { ...@@ -67,7 +58,7 @@ intersphinx_mapping = {
"python": ("https://docs.python.org/3.8", None), "python": ("https://docs.python.org/3.8", None),
"numpy": ("https://numpy.org/doc/stable/", None), "numpy": ("https://numpy.org/doc/stable/", None),
"sympy": ("https://docs.sympy.org/latest/", None), "sympy": ("https://docs.sympy.org/latest/", None),
"pystencils": ("https://da15siwa.pages.i10git.cs.fau.de/dev-docs/pystencils-nbackend/", None), "pystencils": ("https://pycodegen.pages.i10git.cs.fau.de/docs/pystencils/2.0dev/", None),
} }
# References # References
...@@ -79,7 +70,10 @@ default_role = "any" ...@@ -79,7 +70,10 @@ default_role = "any"
autodoc_member_order = "bysource" autodoc_member_order = "bysource"
autodoc_typehints = "description" autodoc_typehints = "description"
# autodoc_class_signature = "separated" # autodoc_type_aliases = {
# "VarLike": "pystencilssfg.lang.expressions.VarLike",
# "ExprLike": "pystencilssfg.lang.expressions.ExprLike"
# }
# Doctest Setup # Doctest Setup
...@@ -89,16 +83,12 @@ sfg = SfgComposer(SfgContext()) ...@@ -89,16 +83,12 @@ sfg = SfgComposer(SfgContext())
''' '''
# Prepare code generation examples # -- Options for MyST / MyST-NB ----------------------------------------------
def build_examples():
import subprocess
import os
examples_dir = os.path.join("usage", "examples",) nb_execution_mode = "cache" # do not execute notebooks by default
subprocess.run(["python", "build.py"], cwd=examples_dir).check_returncode() myst_enable_extensions = [
"dollarmath",
"colon_fence",
print("Generating output of example scripts...") ]
build_examples() nb_render_markdown_format = "myst"
---
file_format: mystnb
kernelspec:
name: python3
---
(getting_started_guide)=
# Getting Started
```{code-cell} ipython3
:tags: [remove-cell]
import sys
from pathlib import Path
mockup_path = Path("_util").resolve()
sys.path.append(str(mockup_path))
from sfg_monkeypatch import DocsPatchedGenerator # monkeypatch SFG for docs
```
This guide will explain the basics of using pystencils-sfg through generator scripts.
Generator scripts are the primary way to run code generation with pystencils-sfg.
A generator script is a Python script that, when executed, produces one or more
C++ source files with the same base name, but different file extensions.
## Writing a Basic Generator Script
To start using pystencils-sfg, create a new empty Python file and populate it with the
following minimal skeleton:
```{code-block} python
from pystencilssfg import SourceFileGenerator
with SourceFileGenerator() as sfg:
...
```
The above snippet defines the basic structure of a *generator script*.
When executed, the above will produce two (nearly) empty C++ files
in the current folder, both with the same name as your Python script
but with `.hpp` and `.cpp` file extensions instead.
In the generator script, code generation is orchestrated by the `SourceFileGenerator` context manager.
When entering into the region controlled by the `SourceFileGenerator`,
it supplies us with a *composer object*, customarily called `sfg`.
Through the composer, we can declaratively populate the generated files with code.
## Adding a pystencils Kernel
One of the core applications of pystencils-sfg is to generate and wrap pystencils-kernels
for usage within C++ applications.
To register a kernel, pass its assignments to `sfg.kernels.create`, which returns a *kernel handle* object:
```{code-block} python
src, dst = ps.fields("src, dst: double[1D]")
c = sp.Symbol("c")
@ps.kernel
def scale():
dst.center @= c * src.center()
# Register the kernel for code generation
scale_kernel = sfg.kernels.create(scale, "scale_kernel")
```
In order to call the kernel, and expose it to the outside world,
we have to create a wrapper function for it, using `sfg.function`.
In its body, we use `sfg.call` to invoke the kernel:
```{code-block} python
sfg.function("scale")(
sfg.call(scale_kernel)
)
```
The `function` composer has a special syntax that mimics the generated C++ code.
We call it twice in sequence,
first providing the name of the function, and then populating its body.
Here's our full first generator script:
```{code-cell} ipython3
:tags: [remove-cell]
DocsPatchedGenerator.scriptname = "add_kernel_demo"
DocsPatchedGenerator.glue = True
DocsPatchedGenerator.display = False
```
```{code-cell} ipython3
from pystencilssfg import SourceFileGenerator
import pystencils as ps
import sympy as sp
with SourceFileGenerator() as sfg:
# Define a copy kernel
src, dst = ps.fields("src, dst: double[1D]")
c = sp.Symbol("c")
@ps.kernel
def scale():
dst.center @= c * src.center()
# Register the kernel for code generation
scale_kernel = sfg.kernels.create(scale, "scale_kernel")
# Wrap it in a function
sfg.function("scale")(
sfg.call(scale_kernel)
)
```
When executing the above script, two files will be generated: a C++ header and implementation file containing
the `scale_kernel` and its wrapper function:
:::{glue:md} sfg_out_add_kernel_demo
:format: myst
:::
As you can see, the header file contains a declaration `void scale(...)` of a function
which is defined in the associated implementation file,
and there calls our generated numerical kernel.
As of now, it forwards the entire set of low-level kernel arguments -- array pointers and indexing information --
to the outside.
In numerical applications, this information is most of the time hidden from the user by encapsulating
it in high-level C++ data structures.
Pystencils-sfg offers means of representing such data structures in the code generator, and supports the
automatic extraction of the low-level indexing information from them.
## Mapping Fields to Data Structures
Since C++23 there exists the archetypical [std::mdspan][mdspan], which represents a non-owning n-dimensional view
on a contiguous data array.
Pystencils-sfg offers native support for mapping pystencils fields onto `mdspan` instances in order to
hide their memory layout details.
Import `std` from `pystencilssfg.lang.cpp` and use `std.mdspan.from_field` to create representations
of your pystencils fields as `std::mdspan` objects:
```{code-block} python
from pystencilssfg.lang.cpp import std
...
src_mdspan = std.mdspan.from_field(src)
dst_mdspan = std.mdspan.from_field(dst)
```
Then, inside the wrapper function, instruct the SFG to map the fields onto their corresponding mdspans:
```{code-block} python
sfg.function("scale")(
sfg.map_field(src, src_mdspan),
sfg.map_field(dst, dst_mdspan),
sfg.call(scale_kernel)
)
```
Here's the full script and its output:
```{code-cell} ipython3
:tags: [remove-cell]
DocsPatchedGenerator.setup("mdspan_demo", False, True)
```
```{code-cell} ipython3
from pystencilssfg import SourceFileGenerator
import pystencils as ps
import sympy as sp
from pystencilssfg.lang.cpp import std
with SourceFileGenerator() as sfg:
# Define a copy kernel
src, dst = ps.fields("src, dst: double[1D]")
c = sp.Symbol("c")
@ps.kernel
def scale():
dst.center @= c * src.center()
# Register the kernel for code generation
scale_kernel = sfg.kernels.create(scale, "scale_kernel")
# Create mdspan objects
src_mdspan = std.mdspan.from_field(src)
dst_mdspan = std.mdspan.from_field(dst)
# Wrap it in a function
sfg.function("scale")(
sfg.map_field(src, src_mdspan),
sfg.map_field(dst, dst_mdspan),
sfg.call(scale_kernel)
)
```
:::{note}
As of early 2025, `std::mdspan` is still not fully adopted by standard library implementors
(see [cppreference.com][cppreference_compiler_support]);
most importantly, the GNU libstdc++ does not yet ship an implementation of it.
However, a reference implementation is available at https://github.com/kokkos/mdspan.
If you are using the reference implementation, refer to the documentation of {any}`StdMdspan`
for advice on how to configure the header file and namespace where the class is defined.
:::
[mdspan]: https://en.cppreference.com/w/cpp/container/mdspan
[cppreference_compiler_support]: https://en.cppreference.com/w/cpp/compiler_support
# The pystencils Source File Generator # The pystencils Source File Generator
```{toctree}
:maxdepth: 1
:hidden:
:caption: User Guide
usage/generator_scripts
usage/project_integration
usage/tips_n_tricks
```
```{toctree}
:maxdepth: 1
:hidden:
:caption: API Reference
api/generation
api/composer
api/lang
api/ir
api/errors
```
[![pipeline](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/badges/master/pipeline.svg)](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master) [![pipeline](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/badges/master/pipeline.svg)](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master)
[![coverage](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/badges/master/coverage.svg)](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master) [![coverage](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/badges/master/coverage.svg)](https://i10git.cs.fau.de/pycodegen-/pystencils-sfg/commits/master)
[![licence](https://img.shields.io/gitlab/license/pycodegen%2Fpystencils-sfg?gitlab_url=https%3A%2F%2Fi10git.cs.fau.de)](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/-/blob/master/LICENSE) [![licence](https://img.shields.io/gitlab/license/pycodegen%2Fpystencils-sfg?gitlab_url=https%3A%2F%2Fi10git.cs.fau.de)](https://i10git.cs.fau.de/pycodegen/pystencils-sfg/-/blob/master/LICENSE)
A bridge over the semantic gap between code emitted by [pystencils](https://pypi.org/project/pystencils/) *A bridge over the semantic gap between [pystencils](https://pypi.org/project/pystencils/) and C++ HPC frameworks.*
and your C/C++/Cuda/HIP framework.
## Installation
### From Git
Install the package into your current Python environment from the git repository using pip
(usage of virtual environments is strongly encouraged!):
```bash
pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git"
```
````{caution}
*pystencils-sfg* requires *pystencils 2.0* and is not compatible with *pystencils 1.3.x*.
However, *pystencils 2.0* is still under development and only available as a pre-release version.
To use *pystencils-sfg*, explicitly install *pystencils* from the v2.0 development branch:
```bash
pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev"
```
````
### From PyPI
Not yet available.
## Primer
With *pystencils-sfg*, including your *pystencils*-generated kernels with handwritten code becomes straightforward
and intuitive. To illustrate, generating a Jacobi smoother for the two-dimensional Poisson equation
and mapping it onto C++23 `std::mdspan`s takes just a few lines of code:
```python
import sympy as sp
from pystencils import fields, kernel
from pystencilssfg import SourceFileGenerator
from pystencilssfg.lang.cpp import std
with SourceFileGenerator() as sfg:
u_src, u_dst, f = fields("u_src, u_dst, f(1) : double[2D]", layout="fzyx")
h = sp.Symbol("h")
@kernel
def poisson_jacobi():
u_dst[0,0] @= (h**2 * f[0, 0] + u_src[1, 0] + u_src[-1, 0] + u_src[0, 1] + u_src[0, -1]) / 4
poisson_kernel = sfg.kernels.create(poisson_jacobi)
sfg.function("jacobi_smooth")(
sfg.map_field(u_src, std.mdspan.from_field(u_src)),
sfg.map_field(u_dst, std.mdspan.from_field(u_dst)),
sfg.map_field(f, std.mdspan.from_field(f)),
sfg.call(poisson_kernel)
)
```
The script above, and the code within the region controlled by the `SourceFileGenerator`,
constructs a C++ header/implementation file pair by describing its contents.
We first describe our Jacobi smoother symbolically using *pystencils*
and then pass it to the `sfg` to add it to the output file.
Then, a wrapper function `jacobi_smooth` is defined which maps the symbolic fields onto `std::mdspan`
objects and then executes the kernel.
Take this code, store it into a file `poisson_smoother.py`, and execute the script from a terminal:
```shell The pystencils Source File Generator is a code generation tool that allows you to
python poisson_smoother.py declaratively describe and automatically generate C++ code using its Python API.
``` It is part of the wider [pycodegen][pycodegen] family of packages for scientific code generation.
During execution, *pystencils-sfg* assembles the above constructs into an internal representation of the C++ files.
It then takes the name of your Python script, replaces `.py` with `.cpp` and `.hpp`,
and exports the constructed code to the files
`poisson_smoother.cpp` and `poisson_smoother.hpp` into the current directory, ready to be `#include`d.
````{dropdown} poisson_smoother.hpp
```C++
#pragma once
#include <cstdint>
#include <experimental/mdspan>
#define RESTRICT __restrict__
void jacobi_smooth(
std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent, 1>> &f,
const double h,
std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_dst,
std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_src
);
```
````
````{dropdown} poisson_smoother.cpp
```C++
#include "poisson_smoother.hpp"
#include <math.h> The primary purpose of pystencils-sfg is to embed the [pystencils][pystencils] code generator for
high-performance stencil computations into C++ HPC applications and frameworks of all scales.
Its features include:
#define FUNC_PREFIX inline - Exporting pystencils kernels to C++ source files for use in larger projects
- Mapping of symbolic pystencils fields onto a wide variety of n-dimensional array data structures
- Orchestration of code generation as part of a Makefile or CMake project
- Declarative description of C++ code structure including functions and classes using the versatile composer API
- Reflection of C++ APIs in the code generator, including automatic tracking of variables and `#include`s
/*************************************************************************************
* Kernels
*************************************************************************************/
namespace kernels { ## Table of Contents
FUNC_PREFIX void kernel(const int64_t _size_f_0, const int64_t _size_f_1, ```{toctree}
const int64_t _stride_f_0, const int64_t _stride_f_1, :maxdepth: 1
const int64_t _stride_u_dst_0,
const int64_t _stride_u_dst_1,
const int64_t _stride_u_src_0,
const int64_t _stride_u_src_1, double *const f_data,
const double h, double *const u_dst_data,
double *const u_src_data) {
const double __c_1_0o4_0 = 1.0 / 4.0;
for (int64_t ctr_1 = 1LL; ctr_1 < _size_f_1 - 1LL; ctr_1 += 1LL) {
for (int64_t ctr_0 = 1LL; ctr_0 < _size_f_0 - 1LL; ctr_0 += 1LL) {
u_dst_data[ctr_0 * _stride_u_dst_0 + ctr_1 * _stride_u_dst_1] =
__c_1_0o4_0 * u_src_data[(ctr_0 + 1LL) * _stride_u_src_0 +
ctr_1 * _stride_u_src_1] +
__c_1_0o4_0 * u_src_data[ctr_0 * _stride_u_src_0 +
(ctr_1 + 1LL) * _stride_u_src_1] +
__c_1_0o4_0 * u_src_data[ctr_0 * _stride_u_src_0 +
(ctr_1 + -1LL) * _stride_u_src_1] +
__c_1_0o4_0 * u_src_data[(ctr_0 + -1LL) * _stride_u_src_0 +
ctr_1 * _stride_u_src_1] +
__c_1_0o4_0 * (h * h) *
f_data[ctr_0 * _stride_f_0 + ctr_1 * _stride_f_1];
}
}
}
} // namespace kernels
/*************************************************************************************
* Functions
*************************************************************************************/
void jacobi_smooth(
std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent, 1>> &f,
const double h,
std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_dst,
std::mdspan<double, std::extents<uint64_t, std::dynamic_extent, std::dynamic_extent>> &u_src)
{
double *const u_src_data{u_src.data_handle()};
const int64_t _stride_u_src_0{u_src.stride(0)};
const int64_t _stride_u_src_1{u_src.stride(1)};
double *const u_dst_data{u_dst.data_handle()};
const int64_t _stride_u_dst_0{u_dst.stride(0)};
const int64_t _stride_u_dst_1{u_dst.stride(1)};
double *const f_data{f.data_handle()};
const int64_t _size_f_0{f.extents().extent(0)};
const int64_t _size_f_1{f.extents().extent(1)};
/* f.extents().extent(2) == 1 */
const int64_t _stride_f_0{f.stride(0)};
const int64_t _stride_f_1{f.stride(1)};
kernels::kernel(_size_f_0, _size_f_1, _stride_f_0, _stride_f_1,
_stride_u_dst_0, _stride_u_dst_1, _stride_u_src_0,
_stride_u_src_1, f_data, h, u_dst_data, u_src_data);
}
```
````
The above is what we call a *generator script*; a Python script that, when executed, produces a pair
of source files of the same name, but with different extensions.
Generator scripts are the primary front-end pattern of *pystencils-sfg*; to learn more about them,
read the [Usage Guide](usage/generator_scripts.md).
## CMake Integration
*Pystencils-sfg* comes with a CMake module to register generator scripts for on-the-fly code generation.
With the module loaded, use the function `pystencilssfg_generate_target_sources` inside your `CMakeLists.txt`
to register one or multiple generator scripts; their outputs will automatically be added to the specified target.
```CMake installation
pystencilssfg_generate_target_sources( <target name> getting_started
SCRIPTS kernels.py ...
FILE_EXTENSIONS .h .cpp
)
``` ```
*Pystencils-sfg* makes sure that all generated files are on the project's include path. ```{toctree}
To `#include` them, add the prefix `gen/<target name>`: :maxdepth: 1
:caption: User Guide
```C++ usage/how_to_composer
#include "gen/<target name>/kernels.h" usage/api_modelling
usage/config_and_cli
usage/project_integration
usage/tips_n_tricks
``` ```
For details on how to add *pystencils-sfg* to your CMake project, refer to
[the project integration guide](#guide_project_integration).
## Learn To Use pystencils-sfg
Here is an overview of user guides for pystencils-sfg available on this site.
A basic understanding of [pystencils](https://pycodegen.pages.i10git.cs.fau.de/pystencils/index.html)
is required.
```{card} Writing Generator Scripts
:link: guide:generator_scripts
:link-type: ref
Learn about *generator scripts*, the primary usage idiom of *pystencils-sfg*:
Embedd *pystencils*-generated kernels into C++ source files and augment them with
arbitrary C++ glue code.
```
```{card} CLI and Build System Integration ```{toctree}
:link: guide_project_integration :maxdepth: 1
:link-type: ref :caption: API Reference
Learn how to control code generation from the command line api/generation
and how to embedd *pystencils-sfg* into your build system. api/composer
api/lang
api/ir
api/errors
``` ```
```{card} Tips and Tricks [pycodegen]: https://pycodegen.pages.i10git.cs.fau.de
:link: guide:tips_n_tricks [pystencils]: https://pycodegen.pages.i10git.cs.fau.de/docs/pystencils/2.0dev
:link-type: ref
A collection of various tricks that might come in handy when working with *pystencils-sfg*.
```
# Installation and Setup
## Prequesites
To use pystencils-sfg, you will need at least Python 3.10.
You will also need the appropriate compilers for building the generated code,
such as
- a modern C++ compiler (e.g. GCC, clang)
- `nvcc` for CUDA or `hipcc` for HIP
- Intel OneAPI or AdaptiveCpp for SYCL
Furthermore, an installation of clang-format for automatic code formatting is strongly recommended.
## Install the Latest Development Revision
As pystencils-sfg is still unreleased, it can at this time only be obtained directly
from its Git repository.
Create a fresh [virtual environment](https://docs.python.org/3/library/venv.html) or activate
an existing one. Install both the pystencils 2.0 and pystencils-sfg development revisions from Git:
```{code-block} bash
pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev"
pip install "git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git"
```
````{caution}
*pystencils-sfg* is not compatible with the *pystencils 1.3.x* releases available from PyPI;
at the moment, you will still have to manually install the latest version of pystencils 2.0.
````
## Check your Installation
To verify that the SFG was successfully installed, execute the following command:
```{code-block} bash
sfg-cli version
```
You should see an output like `0.1a4+...`.
## Next Steps
Move on to [](#getting_started_guide) for a guide on how to author simple generator scripts.
---
file_format: mystnb
kernelspec:
name: python3
---
(how_to_cpp_api_modelling)=
# How To Reflect C++ APIs
```{code-cell} ipython3
:tags: [remove-cell]
from __future__ import annotations
import sys
from pathlib import Path
mockup_path = Path("../_util").resolve()
sys.path.append(str(mockup_path))
from sfg_monkeypatch import DocsPatchedGenerator # monkeypatch SFG for docs
from pystencilssfg import SourceFileGenerator
```
Pystencils-SFG is designed to help you generate C++ code that interfaces with pystencils on the one side,
and with your handwritten code on the other side.
This requires that the C++ classes and APIs of your framework or application be represented within the SFG system.
This guide shows how you can use the facilities of the {any}`pystencilssfg.lang` module to model your C++ interfaces
for use with the code generator.
To begin, import the `lang` module:
```{code-cell} ipython3
from pystencilssfg import lang
```
## Defining C++ Types and Type Templates
The first C++ entities that need to be mirrored for the SFGs are the types and type templates a library
or application uses or exposes.
### Non-Templated Types
To define a C++ type, we use {any}`pystencilssfg.lang.cpptype <pystencilssfg.lang.types.cpptype>`:
```{code-cell} ipython3
MyClassTypeFactory = lang.cpptype("my_namespace::MyClass", "MyClass.hpp")
MyClassTypeFactory
```
This defines two properties of the type: its fully qualified name, and the set of headers
that need to be included when working with the type.
Now, whenever this type occurs as the type of a variable given to pystencils-sfg,
the code generator will make sure that `MyClass.hpp` is included into the respective
generated code file.
The object returned by `cpptype` is not the type itself, but a factory for instances of the type.
Even as `MyClass` does not have any template parameters, we can create different instances of it:
`const` and non-`const`, as well as references and non-references.
We do this by calling the factory:
```{code-cell} ipython3
MyClass = MyClassTypeFactory()
str(MyClass)
```
To produce a `const`-qualified version of the type:
```{code-cell} ipython3
MyClassConst = MyClassTypeFactory(const=True)
str(MyClassConst)
```
And finally, to produce a reference instead:
```{code-cell} ipython3
MyClassRef = MyClassTypeFactory(ref=True)
str(MyClassRef)
```
Of course, `const` and `ref` can also be combined to create a reference-to-const.
### Types with Template Parameters
We can add template parameters to our type by the use of
[Python format strings](https://docs.python.org/3/library/string.html#formatstrings):
```{code-cell} ipython3
MyClassTemplate = lang.cpptype("my_namespace::MyClass< {T1}, {T2} >", "MyClass.hpp")
MyClassTemplate
```
Here, the type parameters `T1` and `T2` are specified in braces.
For them, values must be provided when calling the factory to instantiate the type:
```{code-cell} ipython3
MyClassIntDouble = MyClassTemplate(T1="int", T2="double")
str(MyClassIntDouble)
```
The way type parameters are passed to the factory is identical to the behavior of {any}`str.format`,
except that it does not support attribute or element accesses.
In particular, this means that we can also use unnamed, implicit positional parameters:
```{code-cell} ipython3
MyClassTemplate = lang.cpptype("my_namespace::MyClass< {}, {} >", "MyClass.hpp")
MyClassIntDouble = MyClassTemplate("int", "double")
str(MyClassIntDouble)
```
## Creating Variables and Expressions
Type templates and types will not get us far on their own.
To use them in APIs, as function or constructor parameters,
or as class members and local objects,
we need to create *variables* with certain types.
To do so, we need to inject our defined types into the expression framework of pystencils-sfg.
We wrap the type in an interface that allows us to create variables and, later, more complex expressions,
using {any}`lang.CppClass <pystencilssfg.lang.expressions.CppClass>`:
```{code-cell} ipython3
class MyClass(lang.CppClass):
template = lang.cpptype("my_namespace::MyClass< {T1}, {T2} >", "MyClass.hpp")
```
Instances of `MyClass` can now be created via constructor call, in the same way as above.
This gives us an unbound `MyClass` object, which we can bind to a variable name by calling `var` on it:
```{code-cell} ipython3
my_obj = MyClass(T1="int", T2="void").var("my_obj")
my_obj, str(my_obj.dtype)
```
## Reflecting C++ Class APIs
In the previous section, we showed how to reflect a C++ class in pystencils-sfg in order to create
a variable representing an object of that class.
We can now extend this to reflect the public API of the class, in order to create complex expressions
involving objects of `MyClass` during code generation.
### Public Methods
Assume `MyClass` has the following public interface:
```C++
template< typename T1, typename T2 >
class MyClass {
public:
T1 & getA();
std::tuple< T1, T2 > getBoth();
void replace(T1 a_new, T2 b_new);
}
```
We mirror this in our Python reflection of `CppClass` using methods that create `AugExpr` objects,
which represent C++ expressions annotated with variables they depend on.
A possible implementation might look like this:
```{code-cell} ipython3
---
tags: [remove-cell]
---
class MyClass(lang.CppClass):
template = lang.cpptype("my_namespace::MyClass< {T1}, {T2} >", "MyClass.hpp")
def ctor(self, a: lang.AugExpr, b: lang.AugExpr) -> MyClass:
return self.ctor_bind(a, b)
def getA(self) -> lang.AugExpr:
return lang.AugExpr.format("{}.getA()", self)
def getBoth(self) -> lang.AugExpr:
return lang.AugExpr.format("{}.getBoth()", self)
def replace(self, a_new: lang.AugExpr, b_new: lang.AugExpr) -> lang.AugExpr:
return lang.AugExpr.format("{}.replace({}, {})", self, a_new, b_new)
```
```{code-block} python
class MyClass(lang.CppClass):
template = lang.cpptype("my_namespace::MyClass< {T1}, {T2} >", "MyClass.hpp")
def getA(self) -> lang.AugExpr:
return lang.AugExpr.format("{}.getA()", self)
def getBoth(self) -> lang.AugExpr:
return lang.AugExpr.format("{}.getBoth()", self)
def replace(self, a_new: lang.AugExpr, b_new: lang.AugExpr) -> lang.AugExpr:
return lang.AugExpr.format("{}.replace({}, {})", self, a_new, b_new)
```
Each method of `MyClass` reflects a method of the same name in its public C++ API.
These methods do not return values, but *expressions*;
here, we use the generic `AugExpr` class to model expressions that we don't know anything
about except how they should be constructed.
We create these expressions using `AugExpr.format`, which takes a format string
and interpolation arguments in the same way as `cpptype`.
Internally, it will analyze the format arguments (e.g. `self`, `a_new` and `b_new` in `replace`),
and combine information from any `AugExpr`s found among them.
These are:
- **Variables**: If any of the input expression depend on variables, the resulting expression will
depend on the union of all these variable sets
- **Headers**: If any of the input expression requires certain header files to be evaluated,
the resulting expression will require the same header files.
We can see this in action by calling one of the methods on a variable of type `MyClass`:
```{code-cell} ipython3
my_obj = MyClass(T1="int", T2="void").var("my_obj")
expr = my_obj.getBoth()
expr, lang.depends(expr), lang.includes(expr)
```
We can see: the newly created expression `my_obj.getBoth()` depends on the variable `my_obj` and
requires the header `MyClass.hpp` to be included; this header it has inherited from `my_obj`.
### Constructors
Using the `AugExpr` system, we can also model constructors of `MyClass`.
Assume `MyClass` has the constructor `MyClass(T1 a, T2 b)`.
We implement this by adding a `ctor` method to our Python interface:
```{code-block} python
class MyClass(lang.CppClass):
...
def ctor(self, a: lang.AugExpr, b: lang.AugExpr) -> MyClass:
return self.ctor_bind(a, b)
```
Here, we don't use `AugExpr.format`; instead, we use `ctor_bind`, which is exposed by `CppClass`.
This will generate the correct constructor invocation from the type of our `MyClass` object
and also ensure the headers required by `MyClass` are correctly attached to the resulting
expression:
```{code-cell} ipython3
a = lang.AugExpr("int").var("a")
b = lang.AugExpr("double").var("b")
expr = MyClass(T1="int", T2="double").ctor(a, b)
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 (see [](#note-on-ghost-layers)).
The other two, when called with a coordinate $c \ge 0$, shall return
the size and linearization stride of the field in that direction.
If the coordinate is equal or larger than the field's dimensionality,
return `None` instead.
### 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 get_shape(size_t coord);
size_t get_stride(size_t coord);
ElemType * data_ptr();
}
```
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=elem_type, DIM=dim, **kwargs)
# Reflection of Public Methods
def get_shape(self, coord: int | lang.AugExpr) -> lang.AugExpr:
return lang.AugExpr.format("{}.get_shape({})", self, coord)
def get_stride(self, coord: int | lang.AugExpr) -> lang.AugExpr:
return lang.AugExpr.format("{}.get_stride({})", self, coord)
def data_ptr(self) -> lang.AugExpr:
return lang.AugExpr.format("{}.data_ptr()", self)
# Field Extraction Protocol that uses the above interface
def _extract_ptr(self) -> lang.AugExpr:
return self.data_ptr()
def _extract_size(self, coordinate: int) -> lang.AugExpr | None:
if coordinate > self._dim:
return None
else:
return self.get_shape(coordinate)
def _extract_stride(self, coordinate: int) -> lang.AugExpr | None:
if coordinate > self._dim:
return None
else:
return self.get_stride(coordinate)
```
Our custom field reflection is now ready to be used.
The following generator script demonstrates what code is generated when an instance of `MyField`
is passed to `sfg.map_field`:
```{code-cell} ipython3
import pystencils as ps
from pystencilssfg.lang.cpp import std
with SourceFileGenerator() as sfg:
# Create symbolic fields
f = ps.fields("f: double[3D]")
f_myfield = MyField(f.dtype, f.ndim, ref=True).var(f.name)
# Create the kernel
asm = ps.Assignment(f(0), 2 * f(0))
khandle = sfg.kernels.create(asm)
# Create the wrapper function
sfg.function("invoke")(
sfg.map_field(f, f_myfield),
sfg.call(khandle)
)
```
### Add a Factory Function
In the above example, an instance of `MyField` representing the field `f` is created by the
slightly verbose expression `MyField(f.dtype, f.ndim, ref=True).var(f.name)`.
Having to write this sequence every time, for every field, introduces unnecessary
cognitive load and lots of potential sources of error.
Whenever it is possible to create a field reflection using just information contained in a
pystencils {any}`Field <pystencils.field.Field>` object,
the API reflection should therefore implement a factory method `from_field`:
```{code-cell} ipython3
class MyField(lang.CppClass, SupportsFieldExtraction):
...
@classmethod
def from_field(cls, field: ps.Field, const: bool = False, ref: bool = False) -> MyField:
return cls(f.dtype, f.ndim, const=const, ref=ref).var(f.name)
```
The above signature is idiomatic for `from_field`, and you should stick to it as far as possible.
We can now use it inside the generator script:
```{code-block} python
f = ps.fields("f: double[3D]")
f_myfield = MyField.from_field(f)
```
(note-on-ghost-layers)=
### 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.
(guide:generator_scripts)=
# Generator Scripts
Writing generator scripts is the primary usage idiom of *pystencils-sfg*. (how_to_generator_scripts_config)=
A generator script is a Python script, say `kernels.py`, which contains *pystencils-sfg* # Generator Script Configuration and Command-Line Interface
code at the top level that, when executed, emits source code to a pair of files `kernels.hpp`
and `kernels.cpp`. This guide describes how to write such a generator script, its structure, and how
it can be used to generate code.
## Anatomy
The code generation process in a generator script is controlled by the `SourceFileGenerator` context manager.
It configures the code generator by combining configuration options from the
environment (e.g. a CMake build system) with options specified in the script,
and infers the names of the output files from the script's name.
It then returns a {py:class}`composer <pystencilssfg.composer.SfgComposer>` to the user,
which provides a convenient interface for constructing the source files.
To start, place the following code in a Python script, e.g. `kernels.py`:
```{literalinclude} examples/guide_generator_scripts/01/kernels.py
```
The source file is constructed within the context manager's managed region.
During execution of the script, when the region ends, a header/source file pair
`kernels.hpp` and `kernels.cpp` will be written to disk next to your script.
Execute the script as-is and inspect the generated files, which will of course still be empty:
``````{dropdown} Generated Files
`````{tab-set}
````{tab-item} kernels.hpp
```{literalinclude} examples/guide_generator_scripts/01/kernels.hpp
```
````
````{tab-item} kernels.cpp
```{literalinclude} examples/guide_generator_scripts/01/kernels.cpp
```
````
`````
``````
## Using the Composer
The object `sfg` constructed in above snippet is an instance of [SfgComposer](#pystencilssfg.composer.SfgComposer).
The composer is the central part of the user front-end of *pystencils-sfg*.
It provides an interface for constructing source files that closely mimics
C++ syntactic structures within Python.
::::{dropdown} Composer API Overview
```{eval-rst}
.. currentmodule:: pystencilssfg.composer
```
Structure and Verbatim Code:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.prelude
SfgBasicComposer.include
SfgBasicComposer.namespace
SfgBasicComposer.code
SfgBasicComposer.define_once
```
Kernels and Kernel Namespaces:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.kernels
SfgBasicComposer.kernel_namespace
SfgBasicComposer.kernel_function
```
Function definition, parameters, and header inclusion:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.function
SfgBasicComposer.params
SfgBasicComposer.require
```
Variables, expressions, and variable initialization:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.var
SfgBasicComposer.vars
SfgBasicComposer.expr
SfgBasicComposer.init
SfgBasicComposer.map_field
SfgBasicComposer.set_param
```
Parameter mappings:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.set_param
SfgBasicComposer.map_field
SfgBasicComposer.map_vector
```
Control Flow:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.branch
SfgBasicComposer.switch
```
Kernel Invocation:
```{eval-rst}
.. autosummary::
:nosignatures:
SfgBasicComposer.call
SfgBasicComposer.cuda_invoke
```
::::
### Includes and Definitions
With {any}`include <SfgBasicComposer.include>`, the code generator can be instructed to include header files.
As in C++, you can use the `<>` delimiters for system headers, and omit them for project headers.
`````{tab-set}
````{tab-item} kernels.py
```{literalinclude} examples/guide_generator_scripts/02/kernels.py
```
````
````{tab-item} kernels.hpp
```{literalinclude} examples/guide_generator_scripts/02/kernels.hpp
```
````
````{tab-item} kernels.cpp
```{literalinclude} examples/guide_generator_scripts/02/kernels.cpp
```
````
`````
### Adding Kernels
[pystencils](https://pycodegen.pages.i10git.cs.fau.de/pystencils/)-generated kernels are managed in *kernel namespaces*.
The default kernel namespace is called `kernels` and is available via
[`sfg.kernels`](#pystencilssfg.composer.SfgBasicComposer.kernels).
Adding an existing *pystencils* AST, or creating one from a list of assignments, is possible through
[`kernels.add`](#pystencilssfg.ir.SfgKernelNamespace.add)
and
[`kernels.create`](#pystencilssfg.ir.SfgKernelNamespace.create).
The latter is a wrapper around
[`pystencils.create_kernel`](
https://pycodegen.pages.i10git.cs.fau.de/pystencils/sphinx/kernel_compile_and_call.html#pystencils.create_kernel
).
Both functions return a [kernel handle](#pystencilssfg.ir.SfgKernelHandle)
through which the kernel can be accessed, e.g. for calling it in a function.
To access other kernel namespaces than the default one,
the [`sfg.kernel_namespace`](#pystencilssfg.composer.SfgBasicComposer.kernel_namespace) method can be used.
`````{tab-set}
````{tab-item} kernels.py
```{literalinclude} examples/guide_generator_scripts/03/kernels.py
```
````
````{tab-item} kernels.hpp
```{literalinclude} examples/guide_generator_scripts/03/kernels.hpp
```
````
````{tab-item} kernels.cpp
```{literalinclude} examples/guide_generator_scripts/03/kernels.cpp
```
````
`````
### Building Functions
Through the composer, you can define free functions in your generated C++ file.
These may contain arbitrary code;
their primary intended task however is to wrap kernel calls with the necessary boilerplate code
to integrate them into a framework.
The composer provides an interface for constructing functions that tries to mimic the look of the generated C++ code.
Use `sfg.function` to create a function, and `sfg.call` to call a kernel:
`````{tab-set}
````{tab-item} kernels.py
```{literalinclude} examples/guide_generator_scripts/04/kernels.py
:start-after: start
:end-before: end
```
````
````{tab-item} kernels.hpp
```{literalinclude} examples/guide_generator_scripts/04/kernels.hpp
```
````
````{tab-item} kernels.cpp
```{literalinclude} examples/guide_generator_scripts/04/kernels.cpp
```
````
`````
Note the special syntax: To mimic the look of a C++ function, the composer uses a sequence of two calls
to construct the function.
The function body can furthermore be populated with code to embedd the generated kernel into
the target C++ application.
If you examine the generated files of the previous example, you will notice that your
function `scale_kernel` has lots of raw pointers and integer indices in its interface.
We can wrap those up into proper C++ data structures,
such as, for example, `std::span` or `std::vector`, like this:
`````{tab-set}
````{tab-item} kernels.py
```{literalinclude} examples/guide_generator_scripts/05/kernels.py
:start-after: start
:end-before: end
```
````
````{tab-item} kernels.hpp
```{literalinclude} examples/guide_generator_scripts/05/kernels.hpp
```
````
````{tab-item} kernels.cpp
```{literalinclude} examples/guide_generator_scripts/05/kernels.cpp
```
````
`````
If you now inspect the generated code, you will see that the interface of your function is
considerably simplified.
Also, all the necessary code was added to its body to extract the low-level information required
by the actual kernel from the data structures.
The `sfg.map_field` API can be used to map pystencils fields to a variety of different data structures.
The pystencils-sfg provides modelling support for a number of C++ standard library classes
(see {any}`pystencilssfg.lang.cpp.std`).
It also provides the necessary infrastructure for modelling the data structures of any C++ framework
in a similar manner.
## Configuration and Invocation
There are several ways to affect the behavior and output of a generator script. There are several ways to affect the behavior and output of a generator script.
For one, the `SourceFileGenerator` itself may be configured from the combination of three For one, the `SourceFileGenerator` itself may be configured from the combination of three
...@@ -285,11 +12,11 @@ different configuration sources: ...@@ -285,11 +12,11 @@ different configuration sources:
the generator script to set some of its configuration options; see [Command-Line Options](#cmdline_options) the generator script to set some of its configuration options; see [Command-Line Options](#cmdline_options)
- **Project Configuration:** When embedded into a larger project, using a build system such as CMake, generator scripts - **Project Configuration:** When embedded into a larger project, using a build system such as CMake, generator scripts
may be configured globally within that project by the use of a *configuration module*. may be configured globally within that project by the use of a *configuration module*.
Settings specified inside that configuration module are always overridden by the former to configuration sources. Settings specified inside that configuration module are always overridden by the two other configuration sources listed above.
For details on configuration modules, refer to the guide on [Project and Build System Integration](#guide_project_integration). For details on configuration modules, refer to the guide on [Project and Build System Integration](#guide_project_integration).
(inline_config)= (inline_config)=
### Inline Configuration ## Inline Configuration
To configure the source file generator within your generator script, import the {any}`SfgConfig` from `pystencilssfg`. To configure the source file generator within your generator script, import the {any}`SfgConfig` from `pystencilssfg`.
You may then set up the configuration object before passing it to the `SourceFileGenerator` constructor. You may then set up the configuration object before passing it to the `SourceFileGenerator` constructor.
...@@ -299,8 +26,63 @@ of the generator script to `gen_src`: ...@@ -299,8 +26,63 @@ of the generator script to `gen_src`:
```{literalinclude} examples/guide_generator_scripts/inline_config/kernels.py ```{literalinclude} examples/guide_generator_scripts/inline_config/kernels.py
``` ```
For a selection of common configuration options, see [below](#config_options).
The inline configuration will override any values set by the [project configuration](#config_module)
and must not conflict with any [command line arguments](#custom_cli_args).
(config_options)=
## Configuration Options
Here is a selection of common configuration options to be set in the [inline configuration](#inline_config) or
[project configuration](#config_module).
### Output Options
The file extensions of the generated files can be modified through
{any}`cfg.extensions.header <FileExtensions.header>`
and {any}`cfg.extensions.impl <FileExtensions.impl>`;
and the output directory of the code generator can be set through {any}`cfg.output_directory <SfgConfig.output_directory>`.
The [header-only mode](#header_only_mode) can be enabled using {any}`cfg.header_only <SfgConfig.header_only>`.
:::{danger}
When running generator scripts through [CMake](#cmake_integration), the file extensions,
output directory, and header-only mode settings will be managed fully by the pystencils-sfg
CMake module and the (optional) project configuration module.
They should therefore not be set in the inline configuration,
as this will likely lead to errors being raised during code generation.
:::
### Outer Namespace
To specify the outer namespace to which all generated code should be emitted,
set {any}`cfg.outer_namespace <SfgConfig.outer_namespace>`.
### Code Style and Formatting
Pystencils-sfg gives you some options to affect its output code style.
These are controlled by the options in the {any}`cfg.code_style <CodeStyle>` category.
Furthermore, pystencils-sfg uses `clang-format` to beautify generated code.
The behaviour of the clang-format integration is managed by the
the {any}`cfg.clang_format <ClangFormatOptions>` category,
where you can set options to skip or enforce formatting,
or change the formatter binary.
To set the code style used by `clang-format` either create a `.clang-format` file
in any of the parent folders of your generator script,
or modify the {any}`cfg.clang_format.code_style <ClangFormatOptions.code_style>` option.
:::{seealso}
[Clang-Format Style Options](https://clang.llvm.org/docs/ClangFormatStyleOptions.html)
:::
Clang-format will, by default, sort `#include` statements alphabetically and separate
local and system header includes.
To override this, you can set a custom sorting key for `#include` sorting via
{any}`cfg.code_style.includes_sorting_key <CodeStyle.includes_sorting_key>`.
(cmdline_options)= (cmdline_options)=
### Command-Line Options ## Command-Line Options
The `SourceFileGenerator` consumes a number of command-line parameters that may be passed to the script The `SourceFileGenerator` consumes a number of command-line parameters that may be passed to the script
on invocation. These include: on invocation. These include:
...@@ -308,7 +90,7 @@ on invocation. These include: ...@@ -308,7 +90,7 @@ on invocation. These include:
- `--sfg-output-dir <path>`: Set the output directory of the generator script. This corresponds to {any}`SfgConfig.output_directory`. - `--sfg-output-dir <path>`: Set the output directory of the generator script. This corresponds to {any}`SfgConfig.output_directory`.
- `--sfg-file-extensions <exts>`: Set the file extensions used for the generated files; - `--sfg-file-extensions <exts>`: Set the file extensions used for the generated files;
`exts` must be a comma-separated list not containing any spaces. Corresponds to {any}`SfgConfig.extensions`. `exts` must be a comma-separated list not containing any spaces. Corresponds to {any}`SfgConfig.extensions`.
- `--sfg-output-mode <mode>`: Set the output mode of the generator script. Corresponds to {any}`SfgConfig.output_mode`. - `[--no]--sfg-header-only`: Enable or disable header-only code generation. Corresponds to {any}`SfgConfig.header_only`.
If any configuration option is set to conflicting values on the command line and in the inline configuration, If any configuration option is set to conflicting values on the command line and in the inline configuration,
the generator script will terminate with an error. the generator script will terminate with an error.
...@@ -320,6 +102,18 @@ with the `--help` flag: ...@@ -320,6 +102,18 @@ with the `--help` flag:
$ python kernels.py --help $ python kernels.py --help
``` ```
(header_only_mode)=
## Header-Only Mode
When the header-only output mode is enabled,
the code generator will emit only a header file and no separate implementation file.
In this case, the composer will automatically place all function, method,
and kernel definitions in the header file.
Header-only code generation can be enabled by setting the `--header-only` command-line flag
or the {any}`SfgConfig.header_only` configuration option.
(custom_cli_args)=
## Adding Custom Command-Line Options ## Adding Custom Command-Line Options
Sometimes, you might want to add your own command-line options to a generator script Sometimes, you might want to add your own command-line options to a generator script
......
...@@ -5,7 +5,7 @@ import sympy as sp ...@@ -5,7 +5,7 @@ import sympy as sp
with SourceFileGenerator() as sfg: with SourceFileGenerator() as sfg:
# Define a copy kernel # Define a copy kernel
src, dst = ps.fields("src, dst: [1D]") src, dst = ps.fields("src, dst: double[1D]")
c = sp.Symbol("c") c = sp.Symbol("c")
@ps.kernel @ps.kernel
...@@ -19,7 +19,7 @@ with SourceFileGenerator() as sfg: ...@@ -19,7 +19,7 @@ with SourceFileGenerator() as sfg:
import pystencilssfg.lang.cpp.std as std import pystencilssfg.lang.cpp.std as std
sfg.include("<span>") sfg.include("<span>")
sfg.function("scale_kernel")( sfg.function("scale_kernel")(
sfg.map_field(src, std.vector.from_field(src)), sfg.map_field(src, std.vector.from_field(src)),
sfg.map_field(dst, std.span.from_field(dst)), sfg.map_field(dst, std.span.from_field(dst)),
......
This diff is collapsed.
...@@ -57,25 +57,59 @@ If you are using pystencils-sfg with CMake through the provided CMake module, ...@@ -57,25 +57,59 @@ If you are using pystencils-sfg with CMake through the provided CMake module,
### Add the module ### Add the module
To include the module in your CMake source tree, a separate find module is provided. To include the module in your CMake source tree, you must first add the pystencils-sfg *Find-module*
You can use the global CLI to obtain the find module; simply run to your CMake module path.
To create the Find-module, navigate to the directory it should be placed in and run the following command:
```shell ```shell
sfg-cli cmake make-find-module sfg-cli cmake make-find-module
``` ```
to create the file `FindPystencilsSfg.cmake` in the current directory. This will create the `FindPystencilsSfg.cmake` file.
Add it to the CMake module path, and load the *pystencils-sfg* module via *find_package*: Make sure that its containing directory is added to the CMake module path.
To load pystencils-sfg into CMake, we first need to set the Python interpreter
of the environment SFG is installed in.
There are several ways of doing this:
#### Set Python via a Find-Module Hint
Set the `PystencilsSfg_PYTHON_PATH` hint variable inside your `CMakeLists.txt` to point at the
Python executable which should be used to invoke pystencils-sfg, e.g.:
```CMake ```CMake
find_package( PystencilsSfg ) set(PystencilsSfg_PYTHON_PATH ${CMAKE_SOURCE_DIR}/.venv/bin/python)
```
This is the recommended way, especially when other parts of your project also use Python.
#### Set Python via a Cache Variable
On the command line or in a [CMake configure preset](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html),
set the `PystencilsSfg_PYTHON_INTERPRETER` cache variable to point at the Python executable to be used to invoke pystencils-sfg;
e.g.:
```bash
cmake -S . -B build -DPystencilsSfg_PYTHON_INTERPRETER=`pwd`/.venv/bin/python
``` ```
Make sure to set the `Python_ROOT_DIR` cache variable to point to the correct Python interpreter If both the cache variable and the `PystencilsSfg_PYTHON_PATH` hint are set, the cache variable takes precedence,
(i.e. the virtual environment you have installed *pystencils-sfg* into). so you can use the cache variable to override the hint.
#### Automatically Find a Python Installation
If none of the above is provided, pystencils-sfg will invoke [FindPython](https://cmake.org/cmake/help/latest/module/FindPython.html)
to determine the Python interpreter it should use.
You can affect this process through any of the hints listed in the `FindPython` documentation.
#### Find pystencils-sfg
Finally, call `find_package( PystencilsSfg )` from your `CMakeLists.txt` to load the SFG module.
If SFG as a dependency is not optional, add the `REQUIRED` flag such that the call will fail if
the package cannot be found.
(cmake_add_generator_scripts)= (cmake_add_generator_scripts)=
### Add generator scripts ### Adding Generator Scripts
The primary interaction point in CMake is the function `pystencilssfg_generate_target_sources`, The primary interaction point in CMake is the function `pystencilssfg_generate_target_sources`,
with the following signature: with the following signature:
...@@ -83,10 +117,12 @@ with the following signature: ...@@ -83,10 +117,12 @@ with the following signature:
```CMake ```CMake
pystencilssfg_generate_target_sources( <target> pystencilssfg_generate_target_sources( <target>
SCRIPTS script1.py [script2.py ...] SCRIPTS script1.py [script2.py ...]
[SCRIPT_ARGS arg1 [arg2 ...]]
[DEPENDS dependency1.py [dependency2.py...]] [DEPENDS dependency1.py [dependency2.py...]]
[FILE_EXTENSIONS <header-extension> <impl-extension>] [FILE_EXTENSIONS <header-extension> <impl-extension>]
[OUTPUT_MODE <standalone|inline|header-only>]
[CONFIG_MODULE <path-to-config-module.py>] [CONFIG_MODULE <path-to-config-module.py>]
[OUTPUT_DIRECTORY <output-directory>]
[HEADER_ONLY]
) )
``` ```
...@@ -96,22 +132,33 @@ Any changes in the generator scripts, or any listed dependency, will trigger reg ...@@ -96,22 +132,33 @@ Any changes in the generator scripts, or any listed dependency, will trigger reg
The function takes the following options: The function takes the following options:
- `SCRIPTS`: A list of generator scripts - `SCRIPTS`: A list of generator scripts
- `SCRIPT_ARGS`: A list of custom command line arguments passed to the generator scripts; see [](#custom_cli_args)
- `DEPENDS`: A list of dependencies for the generator scripts - `DEPENDS`: A list of dependencies for the generator scripts
- `FILE_EXTENSION`: The desired extensions for the generated files - `FILE_EXTENSION`: The desired extensions for the generated files
- `OUTPUT_MODE`: Sets the output mode of the code generator; see {any}`SfgConfig.output_mode`.
- `CONFIG_MODULE`: Set the configuration module for all scripts registered with this call. - `CONFIG_MODULE`: Set the configuration module for all scripts registered with this call.
If set, this overrides the value of `PystencilsSfg_CONFIG_MODULE` If set, this overrides the value of `PystencilsSfg_CONFIG_MODULE`
in the current scope (see [](#cmake_set_config_module)) in the current scope (see [](#cmake_set_config_module))
- `OUTPUT_DIRECTORY`: Custom output directory for generated files. If `OUTPUT_DIRECTORY` is a relative path,
it will be interpreted relative to the current build directory.
- `HEADER_ONLY`: If this option is set, instruct the generator scripts to only generate header files
(see {any}`SfgConfig.header_only`).
### Include generated files If `OUTPUT_DIRECTORY` is *not* specified, any C++ header files generated by the above call
can be included in any files belonging to `target` via:
The `pystencils-sfg` CMake module creates a subfolder `sfg_sources/gen` at the root of the build tree
and writes all generated source files into it. The directory `sfg_sources` is added to the project's include
path, such that generated header files for a target `<target>` may be included via:
```C++ ```C++
#include "gen/<target>/kernels.h" #include "gen/<file1.hpp>"
#include "gen/<file2.hpp>"
/* ... */
``` ```
:::{attention}
If you change the code generator output directory using the `OUTPUT_DIRECTORY` argument,
you are yourself responsible for placing that directory--or any of its parents--on the
include path of your target.
:::
(cmake_set_config_module)= (cmake_set_config_module)=
### Set a Configuration Module ### Set a Configuration Module
......
from pystencils import Target, CreateKernelConfig, no_jit from pystencils import Target, CreateKernelConfig, no_jit
from lbmpy import create_lb_update_rule, LBMOptimisation from lbmpy import create_lb_update_rule, LBMOptimisation
from pystencilssfg import SourceFileGenerator, SfgConfig, OutputMode from pystencilssfg import SourceFileGenerator, SfgConfig
from pystencilssfg.lang.cpp.sycl_accessor import sycl_accessor_ref from pystencilssfg.lang.cpp.sycl_accessor import SyclAccessor
import pystencilssfg.extensions.sycl as sycl import pystencilssfg.extensions.sycl as sycl
from itertools import chain from itertools import chain
sfg_config = SfgConfig( sfg_config = SfgConfig(
output_directory="out/test_sycl_buffer", output_directory="out/test_sycl_buffer",
outer_namespace="gen_code", outer_namespace="gen_code",
output_mode=OutputMode.INLINE, header_only=True
) )
with SourceFileGenerator(sfg_config) as sfg: with SourceFileGenerator(sfg_config) as sfg:
...@@ -21,7 +21,7 @@ with SourceFileGenerator(sfg_config) as sfg: ...@@ -21,7 +21,7 @@ with SourceFileGenerator(sfg_config) as sfg:
cgh = sfg.sycl_handler("handler") cgh = sfg.sycl_handler("handler")
rang = sfg.sycl_range(update.method.dim, "range") rang = sfg.sycl_range(update.method.dim, "range")
mappings = [ mappings = [
sfg.map_field(field, sycl_accessor_ref(field)) sfg.map_field(field, SyclAccessor.from_field(field))
for field in chain(update.free_fields, update.bound_fields) for field in chain(update.free_fields, update.bound_fields)
] ]
......