From f5b4f809a2d9325f57ba8984ab1a4317d8d14136 Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Mon, 3 Feb 2025 15:14:43 +0100
Subject: [PATCH] irregular free-slip implementation

---
 CMakeLists.txt                                |   5 +-
 include/sfg/IrregularFreeSlip.hpp             | 170 ++++++++++++++++++
 src/sfg_walberla/api.py                       |  60 ++++++-
 src/sfg_walberla/boundaries/__init__.py       |   3 +-
 src/sfg_walberla/boundaries/boundary_utils.py |  90 ++++++++++
 src/sfg_walberla/boundaries/freeslip.py       | 125 +++++++++++++
 src/sfg_walberla/boundaries/hbb.py            |  14 +-
 src/sfg_walberla/reflection.py                |  29 +++
 src/sfg_walberla/sweep.py                     |  51 +++++-
 tests/CMakeLists.txt                          |   6 +
 tests/boundary_sweeps/CMakeLists.txt          |   5 +
 tests/boundary_sweeps/FreeSlip.py             |  16 ++
 tests/boundary_sweeps/TestBoundarySweeps.cpp  |   6 +
 13 files changed, 554 insertions(+), 26 deletions(-)
 create mode 100644 include/sfg/IrregularFreeSlip.hpp
 create mode 100644 src/sfg_walberla/boundaries/boundary_utils.py
 create mode 100644 src/sfg_walberla/boundaries/freeslip.py
 create mode 100644 src/sfg_walberla/reflection.py
 create mode 100644 tests/boundary_sweeps/CMakeLists.txt
 create mode 100644 tests/boundary_sweeps/FreeSlip.py
 create mode 100644 tests/boundary_sweeps/TestBoundarySweeps.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 16854db..f9e7057 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -10,7 +10,10 @@ include( PrepareSFG )
 add_library( sfg_walberla INTERFACE )
 
 target_sources( sfg_walberla
-    INTERFACE include/sfg/SparseIteration.hpp include/sfg/GenericHbbBoundary.hpp
+    INTERFACE
+    include/sfg/SparseIteration.hpp
+    include/sfg/GenericHbbBoundary.hpp
+    include/sfg/IrregularFreeSlip.hpp
 )
 
 target_include_directories( sfg_walberla INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include )
diff --git a/include/sfg/IrregularFreeSlip.hpp b/include/sfg/IrregularFreeSlip.hpp
new file mode 100644
index 0000000..54fecce
--- /dev/null
+++ b/include/sfg/IrregularFreeSlip.hpp
@@ -0,0 +1,170 @@
+#pragma once
+
+#include "core/DataTypes.h"
+#include "core/cell/Cell.h"
+#include "domain_decomposition/BlockDataID.h"
+#include "domain_decomposition/IBlock.h"
+#include "field/GhostLayerField.h"
+#include "field/FlagField.h"
+#include "sfg/SparseIteration.hpp"
+#include "stencil/Directions.h"
+#include <tuple>
+
+#include "SparseIteration.hpp"
+
+namespace walberla::sfg
+{
+    struct IrregularFreeSlipLinkInfo
+    {
+        int32_t x;
+        int32_t y;
+        int32_t z;
+        int32_t dir;
+        int32_t source_x;
+        int32_t source_y;
+        int32_t source_z;
+        int32_t source_dir;
+        IrregularFreeSlipLinkInfo(
+            walberla::Cell fluidCell, walberla::stencil::Direction linkDir,
+            walberla::Cell sourceCell, walberla::stencil::Direction sourceDir)
+            : x{fluidCell.x()}, y{fluidCell.y()}, z{fluidCell.z()},
+              dir{int32_t(linkDir)}, source_x{sourceCell.x()},
+              source_y{sourceCell.y()}, source_z{sourceCell.z()},
+              source_dir{int32_t(sourceDir)} {}
+
+        bool operator==(const IrregularFreeSlipLinkInfo &other)
+        {
+            return std::tie(x, y, z, dir, source_x, source_y, source_z, source_dir) ==
+                   std::tie(other.x, other.y, other.z, other.dir, other.source_x,
+                            other.source_y, other.source_z, other.source_dir);
+        }
+    };
+
+    namespace detail
+    {
+        template <typename Stencil, typename FlagField_T>
+        class FreeSlipLinksFromFlagField
+        {
+        public:
+            using IndexList = SparseIndexList<IrregularFreeSlipLinkInfo>;
+
+            FreeSlipLinksFromFlagField(
+                StructuredBlockForest &sbfs,
+                ConstBlockDataID flagFieldID,
+                field::FlagUID boundaryFlagUID,
+                field::FlagUID fluidFlagUID) : sbfs_{sbfs}, flagFieldID_(flagFieldID), boundaryFlagUID_(boundaryFlagUID), fluidFlagUID_(fluidFlagUID) {}
+
+            IndexList collectLinks()
+            {
+                IndexList indexList{sbfs_};
+
+                for (auto &block : sbfs_)
+                {
+                    FlagField_T *flagField = block.getData<FlagField_T>(flagFieldID_);
+                    auto &idxVector = indexList.get(block);
+
+                    for (auto it = flagField->beginXYZ(); it != flagField->end(); ++it)
+                    {
+                        Cell c{it.cell()};
+                        if (!isFlagSet(it, fluidFlagUID_))
+                        {
+                            continue;
+                        }
+
+                        for (auto dIt = Stencil::beginNoCenter(); dIt != Stencil::end(); ++dIt)
+                        {
+                            stencil::Direction dir{*dIt};
+                            Cell neighbor{c + dir};
+                            if (flagField->isFlagSet(neighbor, boundaryFlagUID_))
+                            {
+                                idxVector.emplace_back(createLink(flagField, c, dir));
+                            }
+                        }
+                    }
+                }
+            }
+
+        private:
+            IrregularFreeSlipLinkInfo createLink(FlagField_T *flagField, const Cell &fluidCell, const stencil::Direction dir)
+            {
+                const Cell wallCell{fluidCell + dir};
+
+                // inverse direction of 'dir' as lattice vector
+
+                const cell_idx_t ix = stencil::cx[stencil::inverseDir[dir]];
+                const cell_idx_t iy = stencil::cy[stencil::inverseDir[dir]];
+                const cell_idx_t iz = stencil::cz[stencil::inverseDir[dir]];
+
+                stencil::Direction sourceDir = stencil::inverseDir[dir]; // compute reflected (mirrored) of inverse direction of 'dir'
+
+                cell_idx_t wnx = 0; // compute "normal" vector of free slip wall
+                cell_idx_t wny = 0;
+                cell_idx_t wnz = 0;
+
+                if (flagField->isFlagSet(wallCell.x() + ix, wallCell.y(), wallCell.z(), fluidFlagUID_))
+                {
+                    wnx = ix;
+                    sourceDir = stencil::mirrorX[sourceDir];
+                }
+                if (flagField->isFlagSet(wallCell.x(), wallCell.y() + iy, wallCell.z(), fluidFlagUID_))
+                {
+                    wny = iy;
+                    sourceDir = stencil::mirrorY[sourceDir];
+                }
+                if (flagField->isFlagSet(wallCell.x(), wallCell.y(), wallCell.z() + iz, fluidFlagUID_))
+                {
+                    wnz = iz;
+                    sourceDir = stencil::mirrorZ[sourceDir];
+                }
+
+                // concave corner (neighbors are non-fluid)
+                if (wnx == 0 && wny == 0 && wnz == 0)
+                {
+                    wnx = ix;
+                    wny = iy;
+                    wnz = iz;
+                    sourceDir = dir;
+                }
+
+                const Cell sourceCell{
+                    wallCell.x() + wnx,
+                    wallCell.y() + wny,
+                    wallCell.z() + wnz};
+
+                return {
+                    fluidCell,
+                    dir,
+                    sourceCell,
+                    sourceDir};
+            }
+
+            StructuredBlockForest &sbfs_;
+            const ConstBlockDataID flagFieldID_;
+            const field::FlagUID boundaryFlagUID_;
+            const field::FlagUID fluidFlagUID_;
+        };
+    }
+
+    template <typename Impl>
+    class IrregularFreeSlipFactory
+    {
+        using Stencil = typename Impl::Stencil;
+        using Sweep = typename Impl::Sweep;
+
+    public:
+        IrregularFreeSlipFactory(const shared_ptr<StructuredBlockForest> blocks, BlockDataID pdfFieldID)
+            : blocks_{blocks}, pdfFieldID_{pdfFieldID} {}
+
+        template <typename FlagField_T>
+        Sweep fromFlagField(BlockDataID flagFieldID, field::FlagUID boundaryFlagUID, field::FlagUID fluidFlagUID)
+        {
+            detail::FreeSlipLinksFromFlagField< Stencil, FlagField_T > linksFromFlagField{ *blocks_, flagFieldID, boundaryFlagUID, fluidFlagUID};
+            auto indexVector = linksFromFlagField.collectLinks();
+            return Impl::irregularFromIndexVector(indexVector);
+        }
+
+    protected:
+        shared_ptr<StructuredBlockForest> blocks_;
+        BlockDataID pdfFieldID_;
+    };
+}
diff --git a/src/sfg_walberla/api.py b/src/sfg_walberla/api.py
index 0be30b8..207c433 100644
--- a/src/sfg_walberla/api.py
+++ b/src/sfg_walberla/api.py
@@ -19,9 +19,10 @@ from pystencilssfg.lang import (
     Ref,
     ExprLike,
     cpptype,
-    CppClass
+    CppClass,
 )
 from pystencilssfg.lang.types import CppTypeFactory, CppType
+from pystencilssfg.lang.cpp import std
 
 
 real_t = PsCustomType("walberla::real_t")
@@ -84,14 +85,41 @@ class AABB(_PlainCppClass):
         return Vector3(real_t).bind("{}.max()", self)
 
 
+class Cell(_PlainCppClass):
+    _type = cpptype("walberla::Cell", "core/cell/Cell.h")
+
+    def x(self) -> AugExpr:
+        return AugExpr.format("{}.x()", self)
+
+    def y(self) -> AugExpr:
+        return AugExpr.format("{}.y()", self)
+
+    def z(self) -> AugExpr:
+        return AugExpr.format("{}.z()", self)
+
+
 class CellInterval(_PlainCppClass):
     _type = cpptype("walberla::CellInterval", "core/cell/CellInterval.h")
 
 
+class Direction(_PlainCppClass):
+    _type = cpptype("walberla::stencil::Direction", "stencil/Directions.h")
+
+
 class BlockDataID(_PlainCppClass):
     _type = cpptype("walberla::BlockDataID", "domain_decomposition/BlockDataID.h")
 
 
+class IBlock(_PlainCppClass):
+    _type = cpptype("walberla::IBlock", "domain_decomposition/IBlock.h")
+
+    def getData(self, dtype: str | PsType, id: BlockDataID) -> AugExpr:
+        return AugExpr.format("{}.template getData< {} >({})", self, dtype, id)
+
+    def getAABB(self) -> AABB:
+        return AABB().bind("{}.getAABB()", self)
+
+
 class IBlockPtr(_PlainCppClass):
     _type = cpptype("walberla::IBlock *", "domain_decomposition/IBlock.h")
 
@@ -110,7 +138,9 @@ class SharedPtr(CppClass):
 
 
 class StructuredBlockForest(AugExpr):
-    typename = cpptype("walberla::StructuredBlockForest", "blockforest/StructuredBlockForest.h")
+    typename = cpptype(
+        "walberla::StructuredBlockForest", "blockforest/StructuredBlockForest.h"
+    )
 
     def __init__(self, ref: bool = True, const: bool = False):
         dtype = self.typename(const=const, ref=ref)
@@ -303,9 +333,29 @@ def glfield(field: Field, ci: str | AugExpr | None = None):
     return GhostLayerFieldExtraction(field_ptr, ci)
 
 
+class SparseIndexList(AugExpr):
+    _template = cpptype(
+        "walberla::sfg::SparseIndexList< {IndexStruct} >", "sfg/SparseIteration.hpp"
+    )
+
+    def __init__(
+        self, idx_struct: PsStructType, const: bool = False, ref: bool = False
+    ):
+        self._idx_struct = idx_struct
+        dtype = self._template(IndexStruct=idx_struct, const=const, ref=ref)
+        super().__init__(dtype)
+
+    def get(self, block: IBlock) -> std.vector:
+        return std.vector(self._idx_struct, ref=True).bind("{}.get({})", self, block)
+
+    def bufferId(self) -> BlockDataID:
+        return BlockDataID().bind("{}.bufferId()", self)
+
+
 class IndexListBufferPtr(SrcField):
     _template = cpptype(
-        "walberla::sfg::internal::IndexListBuffer< {IndexStruct} >", "sfg/SparseIteration.hpp"
+        "walberla::sfg::internal::IndexListBuffer< {IndexStruct} >",
+        "sfg/SparseIteration.hpp",
     )
 
     def __init__(self, idx_struct: PsStructType):
@@ -350,7 +400,7 @@ CellIdx = PsStructType(
     (
         ("x", create_type("int64_t")),
         ("y", create_type("int64_t")),
-        ("z", create_type("int64_t"))
+        ("z", create_type("int64_t")),
     ),
-    "walberla::sfg::CellIdx"
+    "walberla::sfg::CellIdx",
 )
diff --git a/src/sfg_walberla/boundaries/__init__.py b/src/sfg_walberla/boundaries/__init__.py
index 9a732bf..716bcb0 100644
--- a/src/sfg_walberla/boundaries/__init__.py
+++ b/src/sfg_walberla/boundaries/__init__.py
@@ -1,3 +1,4 @@
 from .hbb import SimpleHbbBoundary
+from .freeslip import FreeSlip, IRREGULAR
 
-__all__ = ["SimpleHbbBoundary"]
+__all__ = ["SimpleHbbBoundary", "FreeSlip", "IRREGULAR"]
diff --git a/src/sfg_walberla/boundaries/boundary_utils.py b/src/sfg_walberla/boundaries/boundary_utils.py
new file mode 100644
index 0000000..6618ddf
--- /dev/null
+++ b/src/sfg_walberla/boundaries/boundary_utils.py
@@ -0,0 +1,90 @@
+from functools import cache
+
+from pystencils import Field, FieldType, TypedSymbol, Assignment
+from pystencils.types import PsStructType, create_type
+
+from lbmpy import LBStencil
+from lbmpy.methods import AbstractLbMethod
+from lbmpy.boundaries.boundaryconditions import LbBoundary
+from lbmpy.advanced_streaming import Timestep
+
+from pystencilssfg import SfgComposer
+from pystencilssfg.composer.custom import CustomGenerator
+
+BoundaryIndexType = create_type("int32")
+
+HbbLinkType = PsStructType(
+    [
+        ("x", BoundaryIndexType),
+        ("y", BoundaryIndexType),
+        ("z", BoundaryIndexType),
+        ("dir", BoundaryIndexType),
+    ],
+    "walberla::sfg::HbbLink",
+)
+
+
+class WalberlaLbmBoundary:
+    idx_struct_type: PsStructType
+
+    @classmethod
+    @cache
+    def get_index_field(cls):
+        return Field(
+            "indexVector",
+            FieldType.INDEXED,
+            cls.idx_struct_type,
+            (0,),
+            (TypedSymbol("indexVectorLength", BoundaryIndexType), 1),
+            (1, 1),
+        )
+
+    def get_assignments(
+        self,
+        lb_method: AbstractLbMethod,
+        pdf_field: Field,
+        prev_timestep: Timestep = Timestep.BOTH,
+        streaming_pattern="pull",
+    ) -> list[Assignment]:
+        assert isinstance(self, LbBoundary)
+
+        index_field = self.get_index_field()
+
+        from lbmpy.advanced_streaming.indexing import BetweenTimestepsIndexing
+
+        indexing = BetweenTimestepsIndexing(
+            pdf_field,
+            lb_method.stencil,
+            prev_timestep,
+            streaming_pattern,
+            BoundaryIndexType,
+            BoundaryIndexType,
+        )
+
+        f_out, f_in = indexing.proxy_fields
+        dir_symbol = indexing.dir_symbol
+        inv_dir = indexing.inverse_dir_symbol
+
+        boundary_assignments = self(
+            f_out,
+            f_in,
+            dir_symbol,
+            inv_dir,
+            lb_method,
+            index_field,
+            None,  # TODO: Fix force vector
+        )
+        boundary_assignments = indexing.substitute_proxies(boundary_assignments)
+
+        elements: list[Assignment] = []
+
+        index_arrs_node = indexing.create_code_node()
+        elements += index_arrs_node.get_array_declarations()
+
+        for node in self.get_additional_code_nodes(lb_method)[::-1]:
+            elements += node.get_array_declarations()
+
+        elements += [Assignment(dir_symbol, index_field[0]("dir"))]
+        elements += boundary_assignments.all_assignments
+
+        return elements
diff --git a/src/sfg_walberla/boundaries/freeslip.py b/src/sfg_walberla/boundaries/freeslip.py
new file mode 100644
index 0000000..0f0b1b8
--- /dev/null
+++ b/src/sfg_walberla/boundaries/freeslip.py
@@ -0,0 +1,125 @@
+from pystencils import Field, Assignment, CreateKernelConfig
+from pystencils.types import PsStructType
+
+from lbmpy.methods import AbstractLbMethod
+from lbmpy.boundaries.boundaryconditions import LbBoundary
+
+from pystencilssfg import SfgComposer
+from pystencilssfg.composer.custom import CustomGenerator
+
+from .hbb import BoundaryIndexType
+from .boundary_utils import WalberlaLbmBoundary
+from ..sweep import Sweep
+from ..api import SparseIndexList
+
+__all__ = ["IRREGULAR", "FreeSlip"]
+
+
+class _IrregularSentinel:
+    def __repr__(self) -> str:
+        return "IRREGULAR"
+
+
+IRREGULAR = _IrregularSentinel()
+
+
+class FreeSlip(CustomGenerator):
+    """Free-Slip boundary condition"""
+
+    def __init__(
+        self,
+        name: str,
+        lb_method: AbstractLbMethod,
+        pdf_field: Field,
+        wall_normal: tuple[int, int, int] | _IrregularSentinel,
+    ):
+        self._name = name
+        self._method = lb_method
+        self._pdf_field = pdf_field
+        self._wall_normal = wall_normal
+
+    def generate(self, sfg: SfgComposer) -> None:
+        if self._wall_normal == IRREGULAR:
+            self._generate_irregular(sfg)
+        else:
+            self._generate_regular(sfg)
+
+    def _generate_irregular(self, sfg: SfgComposer):
+        sfg.include("sfg/IrregularFreeSlip.hpp")
+
+        #   Get waLBerla build config
+        bc_obj = WalberlaIrregularFreeSlip()
+
+        #   Get assignments for bc
+        bc_asm = bc_obj.get_assignments(self._method, self._pdf_field)
+
+        #   Build generator config
+        bc_cfg = CreateKernelConfig()
+        bc_cfg.index_dtype = BoundaryIndexType
+        index_field = bc_obj.get_index_field()
+        bc_cfg.index_field = index_field
+
+        #   Prepare sweep
+        bc_sweep = Sweep(self._name, bc_asm, bc_cfg)
+
+        #   Emit code
+        sfg.generate(bc_sweep)
+
+        #   Build factory
+        factory_name = f"{self._name}Factory"
+        factory_crtp_base = f"walberla::sfg::IrregularFreeSlipFactory< {factory_name} >"
+        index_vector = SparseIndexList(
+            WalberlaIrregularFreeSlip.idx_struct_type, ref=True
+        ).var("indexVector")
+
+        sweep_type = bc_sweep.generated_class()
+        sweep_ctor_args = {
+            f"{self._pdf_field.name}Id": "this->pdfFieldID_",
+            f"{index_field.name}Id": index_vector.bufferId(),
+        }
+
+        stencil_name = self._method.stencil.name
+        sfg.include(f"stencil/{stencil_name}.h")
+
+        sfg.klass(factory_name, bases=[factory_crtp_base])(
+            sfg.private(
+                f"using Base = {factory_crtp_base};",
+                "friend class Base;",
+                f"using Stencil = walberla::stencil::{stencil_name};",
+                f"using Sweep = {sweep_type.get_dtype().c_string()};",
+                sfg.method("irregularFromIndexVector", returns=sweep_type.get_dtype(), inline=True)(
+                    sfg.expr("return {};", sweep_type.ctor(**sweep_ctor_args))
+                ),
+            )
+        )
+
+    def _generate_regular(self, sfg: SfgComposer):
+        raise NotImplementedError("Regular-geometry free slip is not implemented yet")
+
+
+class WalberlaIrregularFreeSlip(LbBoundary, WalberlaLbmBoundary):
+    idx_struct_type = PsStructType(
+        (
+            ("x", BoundaryIndexType),
+            ("y", BoundaryIndexType),
+            ("z", BoundaryIndexType),
+            ("dir", BoundaryIndexType),
+            ("source_x", BoundaryIndexType),
+            ("source_y", BoundaryIndexType),
+            ("source_z", BoundaryIndexType),
+            ("source_dir", BoundaryIndexType),
+        ),
+        "walberla::sfg::IrregularFreeSlipLinkInfo",
+    )
+
+    def __call__(
+        self, f_out, f_in, dir_symbol, inv_dir, lb_method, index_field, force_vector
+    ):
+        source_cell = (
+            index_field("source_x"),
+            index_field("source_y"),
+            index_field("source_z"),
+        )
+        source_dir = index_field("source_dir")
+
+        return Assignment(f_in(inv_dir[dir_symbol]), f_out[source_cell](source_dir))
diff --git a/src/sfg_walberla/boundaries/hbb.py b/src/sfg_walberla/boundaries/hbb.py
index fdc1683..d041f1b 100644
--- a/src/sfg_walberla/boundaries/hbb.py
+++ b/src/sfg_walberla/boundaries/hbb.py
@@ -9,6 +9,7 @@ from pystencilssfg.composer.class_composer import SfgClassComposer
 from pystencilssfg.composer.custom import CustomGenerator
 from pystencilssfg.lang import AugExpr
 
+from lbmpy import LBStencil
 from lbmpy.methods import AbstractLbMethod
 from lbmpy.boundaries.boundaryconditions import LbBoundary
 from lbmpy.advanced_streaming.indexing import Timestep
@@ -20,18 +21,7 @@ from ..sweep import (
     combine_vectors,
 )
 from ..api import GhostLayerFieldPtr, BlockDataID, IBlockPtr, StructuredBlockForest, IndexListBufferPtr
-
-BoundaryIndexType = create_type("int32")
-
-HbbLinkType = PsStructType(
-    [
-        ("x", BoundaryIndexType),
-        ("y", BoundaryIndexType),
-        ("z", BoundaryIndexType),
-        ("dir", BoundaryIndexType),
-    ],
-    "walberla::sfg::HbbLink",
-)
+from .boundary_utils import HbbLinkType, BoundaryIndexType
 
 
 class HbbBoundaryProperties(SweepClassProperties):
diff --git a/src/sfg_walberla/reflection.py b/src/sfg_walberla/reflection.py
new file mode 100644
index 0000000..13e422d
--- /dev/null
+++ b/src/sfg_walberla/reflection.py
@@ -0,0 +1,29 @@
+from pystencilssfg.lang import CppClass, AugExpr
+from pystencilssfg.lang.types import cpptype
+from pystencilssfg.ir import SfgClass
+
+
+class GeneratedClassWrapperBase(CppClass):
+    _class: SfgClass
+    _namespace: str | None
+
+    def __init_subclass__(cls) -> None:
+        typename = (
+            f"{cls._namespace}::{cls._class.class_name}"
+            if cls._namespace is not None
+            else cls._class.class_name
+        )
+        cls.template = cpptype(typename)
+
+    def ctor(self, **kwargs) -> AugExpr:
+        for candidate_ctor in self._class.constructors():
+            ctor_argnames = [param.name for param in candidate_ctor.parameters]
+            if set(ctor_argnames) == set(kwargs.keys()):
+                break
+        else:
+            raise Exception(
+                f"No constructor of class {self._class.class_name} matches the argument names {kwargs.keys()}"
+            )
+
+        ctor_args = [kwargs[name] for name in ctor_argnames]
+        return self.ctor_bind(*ctor_args)
diff --git a/src/sfg_walberla/sweep.py b/src/sfg_walberla/sweep.py
index 5c16a44..a79d133 100644
--- a/src/sfg_walberla/sweep.py
+++ b/src/sfg_walberla/sweep.py
@@ -31,6 +31,8 @@ from pystencilssfg.lang import (
     SrcVector,
     strip_ptr_ref,
 )
+from pystencilssfg.lang.types import CppTypeFactory, cpptype
+from .reflection import GeneratedClassWrapperBase
 from .api import (
     StructuredBlockForest,
     GenericWalberlaField,
@@ -422,12 +424,18 @@ class Sweep(CustomGenerator):
         config: CreateKernelConfig | None = None,
     ):
         if config is not None:
-            if config.ghost_layers is not None:
+            config = config.copy()
+
+            if config.get_option("ghost_layers") is not None:
                 raise ValueError(
                     "Specifying `ghost_layers` in your codegen config is invalid when generating a waLBerla sweep."
                 )
-            elif config.iteration_slice is None:
-                config = replace(config, ghost_layers=0)
+
+            if (
+                config.get_option("iteration_slice") is None
+                and config.get_option("index_field") is None
+            ):
+                config.ghost_layers = 0
         else:
             config = CreateKernelConfig(ghost_layers=0)
 
@@ -436,7 +444,7 @@ class Sweep(CustomGenerator):
         if isinstance(assignments, AssignmentCollection):
             self._assignments = assignments
         else:
-            self._assignments = AssignmentCollection(assignments)
+            self._assignments = AssignmentCollection(assignments)  # type: ignore
         self._gen_config = config
 
         if self._gen_config.get_target() == Target.CUDA:
@@ -451,13 +459,21 @@ class Sweep(CustomGenerator):
         #   Map from shadow field to shadowed field
         self._shadow_fields: dict[Field, Field] = dict()
 
+        #   RESULTS - unset at this point
+        self._generated_class: type[GeneratedClassWrapperBase] | None = None
+
+    #   CONFIGURATION
+
     @property
     def sparse(self) -> bool:
-        return self._gen_config.index_field is not None
+        return self._gen_config.get_option("index_field") is not None
 
     @sparse.setter
     def sparse(self, sparse_iteration: bool):
         if sparse_iteration:
+            if self._gen_config.get_option("index_field") is not None:
+                return
+
             if self._gen_config.get_option("index_dtype") != create_type("int64"):
                 raise ValueError(
                     "Sparse sweeps require `int64_t` as their index data type. Check your code generator config."
@@ -471,8 +487,6 @@ class Sweep(CustomGenerator):
             self._gen_config.index_field = None
             self._gen_config.ghost_layers = 0
 
-    #   CONFIGURATION
-
     def swap_fields(self, field: Field, shadow_field: Field):
         if field in self._shadow_fields:
             raise ValueError(f"Field swap for {field} was already registered.")
@@ -503,6 +517,8 @@ class Sweep(CustomGenerator):
         knamespace = sfg.kernel_namespace(f"{self._name}_kernels")
 
         assignments = BlockforestParameters.process(self._assignments)
+        #   TODO: Get default config from waLBerla build system and override its entries
+        #   from the user-provided config
         khandle = knamespace.create(assignments, self._name, self._gen_config)
 
         all_fields: dict[str, FieldInfo] = {
@@ -601,3 +617,24 @@ class Sweep(CustomGenerator):
             sfg.public(*methods),
             *shadows_cache.render(sfg),
         )
+
+        gen_class = sfg.context.get_class(self._name)
+        assert gen_class is not None
+
+        class GenClassWrapper(GeneratedClassWrapperBase):
+            _class = gen_class
+            _namespace = sfg.context.fully_qualified_namespace
+
+        self._generated_class = GenClassWrapper
+
+    #   CODE-GENERATION RESULTS
+    #   These properties are only available after the sweep was generated
+
+    @property
+    def generated_class(self) -> type[GeneratedClassWrapperBase]:
+        if self._generated_class is None:
+            raise AttributeError(
+                "Generated class is unavailable - code generation was not run yet."
+            )
+
+        return self._generated_class
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 59a075a..22ae8c4 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,6 +1,10 @@
 cmake_minimum_required( VERSION 3.24 )
 project( sfg-walberla-testsuite )
 
+set(WALBERLA_BUILD_TESTS OFF CACHE BOOL "")
+set(WALBERLA_BUILD_BENCHMARKS OFF CACHE BOOL "")
+set(WALBERLA_BUILD_TUTORIALS OFF CACHE BOOL "")
+
 include(FetchContent)
 
 FetchContent_Declare(
@@ -15,3 +19,5 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/.. ${CMAKE_BINARY_DIR}/sfg-walberla)
 
 #   Test Directories
 include(CTest)
+
+add_subdirectory( boundary_sweeps )
diff --git a/tests/boundary_sweeps/CMakeLists.txt b/tests/boundary_sweeps/CMakeLists.txt
new file mode 100644
index 0000000..91aa645
--- /dev/null
+++ b/tests/boundary_sweeps/CMakeLists.txt
@@ -0,0 +1,5 @@
+
+add_executable( TestBoundarySweeps TestBoundarySweeps.cpp )
+
+walberla_generate_sources( TestBoundarySweeps SCRIPTS FreeSlip.py )
+target_link_libraries( TestBoundarySweeps core blockforest field sfg_walberla )
diff --git a/tests/boundary_sweeps/FreeSlip.py b/tests/boundary_sweeps/FreeSlip.py
new file mode 100644
index 0000000..abf2f03
--- /dev/null
+++ b/tests/boundary_sweeps/FreeSlip.py
@@ -0,0 +1,16 @@
+from pystencilssfg import SourceFileGenerator
+
+from pystencils import fields
+from lbmpy import create_lb_method, Stencil, LBMConfig
+
+from sfg_walberla.boundaries import FreeSlip, IRREGULAR
+
+with SourceFileGenerator() as sfg:
+    sfg.namespace("gen")
+
+    lb_config = LBMConfig(stencil=Stencil.D3Q19)
+    lb_method = create_lb_method(lbm_config=lb_config)
+    pdf_field = fields("f(19): double[3D]")
+
+    freeslip = FreeSlip("FreeSlip", lb_method, pdf_field, wall_normal=IRREGULAR)
+    sfg.generate(freeslip)
diff --git a/tests/boundary_sweeps/TestBoundarySweeps.cpp b/tests/boundary_sweeps/TestBoundarySweeps.cpp
new file mode 100644
index 0000000..734d702
--- /dev/null
+++ b/tests/boundary_sweeps/TestBoundarySweeps.cpp
@@ -0,0 +1,6 @@
+
+#include "gen/FreeSlip.hpp"
+
+int main(void) {
+
+}
-- 
GitLab