-
Frederik Hennig authoredFrederik Hennig authored
jupytext:
formats: md:myst
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.16.4
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
(getting_started_guide)=
Getting Started
: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:
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:
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:
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:
:tags: [remove-cell]
DocsPatchedGenerator.scriptname = "add_kernel_demo"
DocsPatchedGenerator.glue = True
DocsPatchedGenerator.display = False
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, 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:
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:
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:
:tags: [remove-cell]
DocsPatchedGenerator.setup("generated", False, True, "mdspan_demo")
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, layout_policy="layout_left")
dst_mdspan = std.mdspan.from_field(dst, layout_policy="layout_left")
# 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);
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.
:::
Integrating and Compiling the Generated Code
To use the generated functions from handwritten code, include the generated header file into your C++ application, compile the generated translation unit to an object file, and link the two together.
Here is an example application frame:
:tags: [remove-input]
from IPython.display import Markdown
output_dir = Path("mdspan_demo")
output_dir.mkdir(exist_ok=True)
code = r"""
#include <mdspan>
#include <memory>
#include <iostream>
// Include the generated header
#include "generated.hpp"
using field_t = std::mdspan< double, std::dextents< uint64_t, 1 >, std::layout_left >;
int main(void){
constexpr size_t N = 4;
auto data_src = std::make_unique< double[] >(N);
auto data_dst = std::make_unique< double[] >(N);
field_t src { data_src.get(), N };
field_t dst { data_dst.get(), N };
for(size_t i = 0; i < N; ++i)
src[i] = 2.0 * double(i) + 1.0;
const double c { 4.5 };
// Call generated function
scale(c, dst, src);
// Print output
for(size_t i = 0; i < N; ++i)
std::cout << dst[i] << std::endl;
return 0;
}
"""
(output_dir / "app.cpp").write_text(code)
md = f""":::{{code-block}} C++
:caption: app.cpp
{code}
:::
"""
Markdown(md)
The application sets up the data arrays and mdspan
views for the src
and dst
fields, and initializes src
.
Then, it invokes the scale
kernel defined in the above generator script
on these mdspan
objects and prints the result to stdout.
Save the code into a file app.cpp
.
We can now compile application frame and generated code together, and link them into an executable,
using the following compiler command:
clang++ -std=c++23 -stdlib=libc++ -I $(python -m pystencils.include -s) generated.cpp app.cpp
Let's briefly take a look at the compiler options:
-
std=c++23
ensures the C++23 standard, required forstd::mdspan
; -
-stdlib=libc++
instructsclang
to link against the LLVM C++ standard library, which implementsstd::mdspan
; -
-I $(python -m pystencils.include -s)
adds the location of the pystencils runtime headers to the compiler's include path; the header path is obtained by executing thepython -m pystencils.include -s
subcommand.
:::{note}
The above command requires that at least clang 18 and libc++
are installed.
On Ubuntu >= 24, you can install these via apt-get install clang libc++-dev
.
On older systems, install clang-18 libc++-18-dev
instead.
:::
After succesful compilation, running the executable should yield the following output:
./a.out
:tags: [remove-input]
!cd mdspan_demo; clang++ -std=c++23 -stdlib=libc++ -I $(python -m pystencils.include -s) generated.cpp app.cpp
!./mdspan_demo/a.out
That's it! We've now gone through all the basic steps of generating code and integrating it into an application using pystencils-sfg.
Next Steps
To learn more about using pystencils-sfg's composer API, read . For integrating reflection of your own C++ APIs and field classes in pystencils-sfg, refer to . At , you can find more information about integrating pystencils-sfg with your project and build system to run code generation on-the-fly.