diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7340d091065e0a1de1cf7510914f0021827c6a93..d501e83f223767a186a1fac6c5beccf5e144a4d0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,6 +4,23 @@ stages:
  - Deploy
 
 
+testsuite-clang18:
+  image: i10git.cs.fau.de:5005/walberla/buildenvs/clang-18:latest
+  tags:
+    - docker
+  stage: "Test"
+  needs: []
+  variables:
+    CXX: clang++
+    CC: clang
+  before_script:
+    - cd tests
+    - cmake -G Ninja -S . -B build
+    - cd build
+    - cmake --build . --target SfgTests
+  script:
+    - ctest
+
 build-examples:
   image: i10git.cs.fau.de:5005/walberla/buildenvs/clang-18:latest
   tags:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 16854db8f42545bcf0434a71bf60939b9eb91b2a..f9e705726123ca02a17edeab80e8f9ebe52c6ecb 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 0000000000000000000000000000000000000000..c887b8e7bd55d437b4a0d83160c47b7fe66682a7
--- /dev/null
+++ b/include/sfg/IrregularFreeSlip.hpp
@@ -0,0 +1,185 @@
+#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_offset_x;
+        int32_t source_offset_y;
+        int32_t source_offset_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_offset_x{sourceCell.x() - fluidCell.x()},
+              source_offset_y{sourceCell.y() - fluidCell.y()}, source_offset_z{sourceCell.z() - fluidCell.z()},
+              source_dir{int32_t(sourceDir)} {}
+
+        bool operator==(const IrregularFreeSlipLinkInfo &other) const
+        {
+            return std::tie(x, y, z, dir, source_offset_x, source_offset_y, source_offset_z, source_dir) ==
+                   std::tie(other.x, other.y, other.z, other.dir, other.source_offset_x,
+                            other.source_offset_y, other.source_offset_z, other.source_dir);
+        }
+    };
+
+    namespace detail
+    {
+        template <typename Stencil, typename FlagField_T>
+        class FreeSlipLinksFromFlagField
+        {
+        public:
+            using IndexList = SparseIndexList<IrregularFreeSlipLinkInfo>;
+            using flag_t = typename FlagField_T::flag_t;
+
+            FreeSlipLinksFromFlagField(
+                StructuredBlockForest &sbfs,
+                BlockDataID 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_);
+                    flag_t boundaryFlag{flagField->getFlag(boundaryFlagUID_)};
+                    flag_t fluidFlag{flagField->getFlag(fluidFlagUID_)};
+
+                    auto &idxVector = indexList.get(block);
+
+                    for (auto it = flagField->beginXYZ(); it != flagField->end(); ++it)
+                    {
+                        Cell c{it.cell()};
+                        if (!isFlagSet(it, fluidFlag))
+                        {
+                            continue;
+                        }
+
+                        for (auto dIt = Stencil::beginNoCenter(); dIt != Stencil::end(); ++dIt)
+                        {
+                            stencil::Direction dir{*dIt};
+                            Cell neighbor{c + dir};
+                            if (flagField->isFlagSet(neighbor, boundaryFlag))
+                            {
+                                idxVector.emplace_back(createLink(flagField, c, dir));
+                            }
+                        }
+                    }
+                }
+
+                return indexList;
+            }
+
+        private:
+            IrregularFreeSlipLinkInfo createLink(FlagField_T *flagField, const Cell &fluidCell, const stencil::Direction dir)
+            {
+                const flag_t fluidFlag{flagField->getFlag(fluidFlagUID_)};
+                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(), fluidFlag))
+                {
+                    wnx = ix;
+                    sourceDir = stencil::mirrorX[sourceDir];
+                }
+                if (flagField->isFlagSet(wallCell.x(), wallCell.y() + iy, wallCell.z(), fluidFlag))
+                {
+                    wny = iy;
+                    sourceDir = stencil::mirrorY[sourceDir];
+                }
+                if (flagField->isFlagSet(wallCell.x(), wallCell.y(), wallCell.z() + iz, fluidFlag))
+                {
+                    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 BlockDataID flagFieldID_;
+            const field::FlagUID boundaryFlagUID_;
+            const field::FlagUID fluidFlagUID_;
+        };
+    }
+
+    template <typename Impl>
+    class IrregularFreeSlipFactory
+    {
+    public:
+        IrregularFreeSlipFactory(const shared_ptr<StructuredBlockForest> blocks, BlockDataID pdfFieldID)
+            : blocks_{blocks}, pdfFieldID_{pdfFieldID} {}
+
+        template <typename FlagField_T>
+        auto fromFlagField(BlockDataID flagFieldID, field::FlagUID boundaryFlagUID, field::FlagUID fluidFlagUID)
+        {
+            detail::FreeSlipLinksFromFlagField<typename Impl::Stencil, FlagField_T> linksFromFlagField{*blocks_, flagFieldID, boundaryFlagUID, fluidFlagUID};
+            auto indexVector = linksFromFlagField.collectLinks();
+            return impl().irregularFromIndexVector(indexVector);
+        }
+
+    protected:
+        shared_ptr<StructuredBlockForest> blocks_;
+        BlockDataID pdfFieldID_;
+
+    private:
+        Impl &impl()
+        {
+            return *static_cast<Impl *>(this);
+        }
+
+        const Impl &impl() const
+        {
+            return *static_cast<const Impl *>(this);
+        }
+    };
+}
diff --git a/src/sfg_walberla/api.py b/src/sfg_walberla/api.py
index 0be30b8dc161a51ba386b036ec897fbc019f5b5c..207c433230b72df39c1927664e7878c010cb896f 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 9a732bf3f6131d110ec614b49d0996bbcf5919f5..baeed122b07f2f2e9f07e36b6d63c5392688134f 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
 
-__all__ = ["SimpleHbbBoundary"]
+__all__ = ["SimpleHbbBoundary", "FreeSlip"]
diff --git a/src/sfg_walberla/boundaries/boundary_utils.py b/src/sfg_walberla/boundaries/boundary_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..6618ddf1a50ceb2e5f00d90cb832130316c7287e
--- /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 0000000000000000000000000000000000000000..863d49eb13c812721d96b5541d14654f7247888e
--- /dev/null
+++ b/src/sfg_walberla/boundaries/freeslip.py
@@ -0,0 +1,129 @@
+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__ = ["FreeSlip"]
+
+
+class _IrregularSentinel:
+    def __repr__(self) -> str:
+        return "IRREGULAR"
+
+
+class FreeSlip(CustomGenerator):
+    """Free-Slip boundary condition"""
+
+    IRREGULAR = _IrregularSentinel()
+
+    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 == self.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=[f"public {factory_crtp_base}"])(
+            sfg.public(
+                f"using Base = {factory_crtp_base};",
+                f"friend class {factory_crtp_base};",
+                f"using Stencil = walberla::stencil::{stencil_name};",
+                f"using Sweep = {sweep_type.get_dtype().c_string()};",
+                "using Base::Base;",
+            ),
+            sfg.private(
+                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_offset_x", BoundaryIndexType),
+            ("source_offset_y", BoundaryIndexType),
+            ("source_offset_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_offset_x"),
+            index_field("source_offset_y"),
+            index_field("source_offset_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 fdc1683ccbfe3c1b010e5d47de0fd5ec16dfe8b4..d041f1b19a67df5da2107d4563c5b3b560b2d6f5 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 0000000000000000000000000000000000000000..13e422d94cf2329d339a23077a51a13553928652
--- /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 5c16a448fef6d95daf36fa4d5097756365a348b0..a79d133f4381deea94d06e96ff57c33531e73e26 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 59a075a72448d40f6743ba8e7bfbcd15e8e71bc8..72acde9c5dc7ca88b51038b3798c79810a812883 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,6 +1,11 @@
 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_SHOWCASES OFF CACHE BOOL "")
+set(WALBERLA_BUILD_TUTORIALS OFF CACHE BOOL "")
+
 include(FetchContent)
 
 FetchContent_Declare(
@@ -15,3 +20,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/.. ${CMAKE_BINARY_DIR}/sfg-walberla)
 
 #   Test Directories
 include(CTest)
+
+add_custom_target( SfgTests )
+
+add_subdirectory( FreeSlip )
diff --git a/tests/CMakePresets.json b/tests/CMakePresets.json
index 11b6c5797a4a848db2ef82c3e601e403848fb15a..48449c2d856200adfa38b7019aa2bcff44144615 100644
--- a/tests/CMakePresets.json
+++ b/tests/CMakePresets.json
@@ -11,6 +11,7 @@
             "binaryDir": "${sourceDir}/build/${presetName}",
             "generator": "Ninja",
             "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Debug",
                 "WALBERLA_BUILD_TESTS": false
             }
         }
diff --git a/tests/FreeSlip/CMakeLists.txt b/tests/FreeSlip/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e47cae2e1bef502e62a8b64dd2dea7f9fa396151
--- /dev/null
+++ b/tests/FreeSlip/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_executable( TestIrregularFreeSlip TestIrregularFreeSlip.cpp )
+walberla_generate_sources( TestIrregularFreeSlip SCRIPTS IrregularFreeSlip.py )
+target_link_libraries( TestIrregularFreeSlip core blockforest field geometry sfg_walberla )
+add_test( NAME TestIrregularFreeSlip COMMAND TestIrregularFreeSlip )
+
+add_dependencies( SfgTests TestIrregularFreeSlip )
diff --git a/tests/FreeSlip/IrregularFreeSlip.py b/tests/FreeSlip/IrregularFreeSlip.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fa69cf2042e054618db15ec1779945b9198214a
--- /dev/null
+++ b/tests/FreeSlip/IrregularFreeSlip.py
@@ -0,0 +1,54 @@
+import sympy as sp
+from pystencilssfg import SourceFileGenerator
+
+from pystencils import fields, Field
+from lbmpy import LBStencil, Stencil, LBMConfig, LBMOptimisation, create_lb_update_rule
+from lbmpy.macroscopic_value_kernels import macroscopic_values_setter
+
+
+from sfg_walberla import Sweep
+from sfg_walberla.boundaries import FreeSlip
+
+with SourceFileGenerator() as sfg:
+    sfg.namespace("TestIrregularFreeSlip::gen")
+
+    stencil = LBStencil(Stencil.D3Q19)
+    d, q = stencil.D, stencil.Q
+    f: Field
+    f_tmp: Field
+    f, f_tmp, rho, u = fields(
+        f"f({q}), f_tmp({q}), rho, u({d}): double[{d}D]", layout="fzyx"
+    )  # type: ignore
+
+    stencil_name = stencil.name
+    sfg.include(f"stencil/{stencil_name}.h")
+    sfg.code(f"using LbStencil = walberla::stencil::{stencil_name};")
+
+    lbm_config = LBMConfig(
+        stencil=stencil,
+        output={"density": rho, "velocity": u},
+    )
+    lbm_opt = LBMOptimisation(
+        symbolic_field=f,
+        symbolic_temporary_field=f_tmp,
+    )
+
+    lb_update = create_lb_update_rule(lbm_config=lbm_config, lbm_optimisation=lbm_opt)
+    assert lb_update is not None
+
+    lb_update_sweep = Sweep("LbStreamCollide", lb_update)
+    lb_update_sweep.swap_fields(f, f_tmp)
+    sfg.generate(lb_update_sweep)
+
+    lb_init = macroscopic_values_setter(
+        lb_update.method,
+        density=sp.Symbol("density"),
+        velocity=u.center_vector,
+        pdfs=f,
+    )
+    lb_init_sweep = Sweep("LbInit", lb_init)
+    sfg.generate(lb_init_sweep)
+
+    irreg_freeslip = FreeSlip("FreeSlipIrregular", lb_update.method, f, wall_normal=FreeSlip.IRREGULAR)
+    sfg.generate(irreg_freeslip)
+
diff --git a/tests/FreeSlip/TestIrregularFreeSlip.cpp b/tests/FreeSlip/TestIrregularFreeSlip.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c08df34308cc4e8c64d02717cfe1c091c0ecb5a4
--- /dev/null
+++ b/tests/FreeSlip/TestIrregularFreeSlip.cpp
@@ -0,0 +1,147 @@
+
+#include "core/all.h"
+#include "blockforest/all.h"
+
+#include "field/all.h"
+#include "field/communication/PackInfo.h"
+#include "field/communication/StencilRestrictedPackInfo.h"
+
+#include "geometry/all.h"
+
+#include "vtk/all.h"
+
+#include <array>
+
+#include "gen/IrregularFreeSlip.hpp"
+
+namespace TestIrregularFreeSlip
+{
+    using namespace walberla;
+
+    using PdfField_T = field::GhostLayerField<real_t, gen::LbStencil::Q>;
+    using ScalarField_T = field::GhostLayerField<real_t, 1>;
+    using VectorField_T = field::GhostLayerField<real_t, gen::LbStencil::D>;
+    using FlagField_T = FlagField<uint8_t>;
+    using CommScheme = blockforest::communication::UniformBufferedScheme<gen::LbStencil>;
+    using PdfsPackInfo = field::communication::StencilRestrictedPackInfo<PdfField_T, gen::LbStencil>;
+
+    /**
+     * Pipe flow with circular cross-section and free-slip walls.
+     * The pipe flow is initialized with a constant velocity in x-direction.
+     * As free-slip walls do not impose any friction, velocity should remain constant in time.
+     */
+    void freeSlipPipe(Environment &env)
+    {
+        std::array<uint_t, 3> blocks{1, 1, 1};
+        std::array<uint_t, 3> cpb{4, 32, 32};
+        std::array<bool, 3> periodic{true, false, false};
+
+        auto sbfs = blockforest::createUniformBlockGrid(
+            blocks[0], blocks[1], blocks[2],
+            cpb[0], cpb[1], cpb[2],
+            1.0,
+            true,
+            periodic[0], periodic[1], periodic[2]
+        );
+
+        BlockDataID pdfsId = field::addToStorage<PdfField_T>(sbfs, "f", real_c(0.0));
+        BlockDataID rhoId = field::addToStorage<ScalarField_T>(sbfs, "rho", real_c(0.0));
+        BlockDataID uId = field::addToStorage<VectorField_T>(sbfs, "u", real_c(0.0));
+
+        const BlockDataID flagFieldId = field::addFlagFieldToStorage<FlagField_T>(sbfs, "flagField");
+        const FlagUID fluidFlagUid{"Fluid"};
+        const FlagUID freeSlipFlagUID{"FreeSlip"};
+
+        const CellInterval allCells{{0, 0, 0}, {sbfs->getNumberOfXCellsPerBlock() - 1, sbfs->getNumberOfYCellsPerBlock() - 1, sbfs->getNumberOfZCellsPerBlock() - 1}};
+        const CellInterval allCellsWithGl{{-1, 0, 0}, {sbfs->getNumberOfXCellsPerBlock(), sbfs->getNumberOfYCellsPerBlock() - 1, sbfs->getNumberOfZCellsPerBlock() - 1}};
+
+        const real_t pipeRadius{14.0};
+        const Vector3<real_t> pipeAnchor{0.0, 16.0, 16.0};
+        const real_t maxVelocity{0.02};
+
+        for (auto &block : *sbfs)
+        {
+            FlagField_T &flagField = *block.getData<FlagField_T>(flagFieldId);
+            const uint8_t freeSlipFlag{flagField.getOrRegisterFlag(freeSlipFlagUID)};
+
+            VectorField_T &velField = *block.getData<VectorField_T>(uId);
+
+            for (Cell c : allCellsWithGl)
+            {
+                Cell globalCell{c};
+                sbfs->transformBlockLocalToGlobalCell(globalCell, block);
+                Vector3<real_t> cellCenter{sbfs->getCellCenter(globalCell)};
+                cellCenter[0] = 0.0;
+
+                Vector3<real_t> initVelocity;
+                real_t radialDistance = (cellCenter - pipeAnchor).length();
+                if (radialDistance > pipeRadius)
+                {
+                    flagField.addFlag(c, freeSlipFlag);
+                    initVelocity = Vector3<real_t>{NAN};
+                }
+                else
+                {
+                    initVelocity = Vector3<real_t>{maxVelocity, 0., 0.};
+                }
+
+                velField.get(c, 0) = initVelocity[0];
+                velField.get(c, 1) = initVelocity[1];
+                velField.get(c, 2) = initVelocity[2];
+            }
+        }
+
+        geometry::setNonBoundaryCellsToDomain<FlagField_T>(*sbfs, flagFieldId, fluidFlagUid);
+
+        auto flagsOutput = field::createVTKOutput<FlagField_T>(flagFieldId, *sbfs, "flags");
+        flagsOutput();
+
+        gen::LbInit initialize { pdfsId, uId, 1.0 };
+
+        for(auto& b : *sbfs){
+            initialize(&b);
+        }
+
+        gen::FreeSlipIrregular freeSlip{
+            gen::FreeSlipIrregularFactory(sbfs, pdfsId).fromFlagField<FlagField_T>(flagFieldId, freeSlipFlagUID, fluidFlagUid)
+        };
+
+        gen::LbStreamCollide streamCollide { pdfsId, rhoId, uId, 1.0 };
+
+        CommScheme comm{sbfs};
+        auto pdfsPackInfo = std::make_shared<PdfsPackInfo>(pdfsId);
+        comm.addPackInfo(pdfsPackInfo);
+
+        auto velOutput = field::createVTKOutput<VectorField_T>(uId, *sbfs, "vel");
+
+        for(uint_t i = 0; i < 10; ++i){
+            comm();
+            for (auto &block : *sbfs)
+            {
+                velOutput();
+                freeSlip(&block);
+                streamCollide(&block);
+
+                VectorField_T & velField = *block.getData< VectorField_T >(uId);
+                FlagField_T & flagField = *block.getData< FlagField_T >(flagFieldId);
+                uint8_t fluidFlag = flagField.getFlag(fluidFlagUid);
+                
+                for(Cell c : allCells){
+                    if( flagField.isFlagSet(c, fluidFlag) ){
+                        WALBERLA_CHECK_FLOAT_EQUAL( velField.get(c, 0), maxVelocity );
+                        WALBERLA_CHECK_FLOAT_EQUAL( velField.get(c, 1), 0. );
+                        WALBERLA_CHECK_FLOAT_EQUAL( velField.get(c, 2), 0. );
+                    }
+                }
+            }
+        }
+
+        velOutput();
+    }
+}
+
+int main(int argc, char **argv)
+{
+    walberla::Environment env{argc, argv};
+    TestIrregularFreeSlip::freeSlipPipe(env);
+}
diff --git a/user_manual/conf.py b/user_manual/conf.py
index 2bc06da4f2d273c00770914d3e7fd0e7555755e0..6dfd366ee11f1cc57897e825c1ac61bcd85f3bd7 100644
--- a/user_manual/conf.py
+++ b/user_manual/conf.py
@@ -21,7 +21,7 @@ extensions = [
 ]
 
 # templates_path = ['_templates']
-exclude_patterns = ["build"]
+exclude_patterns = ["*/build"]
 
 myst_enable_extensions = [
     "colon_fence",
diff --git a/user_manual/examples/CMakeLists.txt b/user_manual/examples/CMakeLists.txt
index f498cc3603a44d597f611da0530ef297a238f97a..499f1f083bf4419e3f4c8d72c7ccb55123500ad4 100644
--- a/user_manual/examples/CMakeLists.txt
+++ b/user_manual/examples/CMakeLists.txt
@@ -16,6 +16,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/../.. ${CMAKE_BINARY_DIR}/sfg-walberla)
 add_subdirectory( GeneratorScriptBasics )
 add_subdirectory( ForceDrivenChannel )
 add_subdirectory( SparseSpiral )
+add_subdirectory( FreeSlipBubble )
 
 add_custom_target( Examples )
 add_dependencies( 
@@ -23,4 +24,5 @@ add_dependencies(
     Ex_GeneratorScriptBasics
     Ex_ForceDrivenChannel
     SparseSpiral
+    FreeSlipBubble
 )
diff --git a/user_manual/examples/FreeSlipBubble/CMakeLists.txt b/user_manual/examples/FreeSlipBubble/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cd3e7af346973a31ce122c55531821489e74ce14
--- /dev/null
+++ b/user_manual/examples/FreeSlipBubble/CMakeLists.txt
@@ -0,0 +1,6 @@
+walberla_link_files_to_builddir( *.prm )
+
+add_executable( FreeSlipBubble FreeSlipBubble.cpp )
+
+walberla_generate_sources( FreeSlipBubble SCRIPTS LbmAlgorithms.py )
+target_link_libraries( FreeSlipBubble core blockforest field geometry stencil sfg_walberla )
diff --git a/user_manual/examples/FreeSlipBubble/FreeSlipBubble.cpp b/user_manual/examples/FreeSlipBubble/FreeSlipBubble.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be5f93da51d38ebd445127201ba36a31d74f1900
--- /dev/null
+++ b/user_manual/examples/FreeSlipBubble/FreeSlipBubble.cpp
@@ -0,0 +1,168 @@
+#include "blockforest/all.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/all.h"
+
+#include "domain_decomposition/SharedSweep.h"
+
+#include "field/all.h"
+#include "field/communication/PackInfo.h"
+#include "field/communication/StencilRestrictedPackInfo.h"
+
+#include "geometry/InitBoundaryHandling.h"
+
+#include "stencil/all.h"
+
+#include "timeloop/all.h"
+
+#include "vtk/all.h"
+
+#include "gen/LbmAlgorithms.hpp"
+
+namespace FreeSlipBubble
+{
+    using namespace walberla;
+    using namespace blockforest;
+
+    using std::make_unique;
+    using std::shared_ptr;
+
+    using ScalarField_T = field::GhostLayerField<real_t, 1>;
+    using VectorField_T = field::GhostLayerField<real_t, 3>;
+
+    using LbStencil = stencil::D3Q19;
+    using PdfField_T = field::GhostLayerField<real_t, LbStencil::Q>;
+
+    using CommScheme = blockforest::communication::UniformBufferedScheme<LbStencil>;
+    using PdfsPackInfo = field::communication::StencilRestrictedPackInfo<PdfField_T, LbStencil>;
+
+    using FlagField_T = FlagField<uint8_t>;
+
+    void run(const shared_ptr<Config> &config)
+    {
+        auto blocks = createUniformBlockGridFromConfig(config);
+
+        Config::BlockHandle simParams = config->getBlock("Parameters");
+        const real_t omega{simParams.getParameter<real_t>("omega")};
+        const real_t density{simParams.getParameter<real_t>("density", 1.0)};
+        const Vector3<real_t> velocity = simParams.getParameter<Vector3<real_t>>("velocity", Vector3<real_t>{0.05, 0.0, 0.0});
+
+        BlockDataID pdfsId = field::addToStorage<PdfField_T>(blocks, "pdfs", real_c(0.0), field::fzyx, 1);
+        BlockDataID rhoId = field::addToStorage<ScalarField_T>(blocks, "rho", real_c(1.0), field::fzyx, 0);
+        BlockDataID uId = field::addToStorage<VectorField_T>(blocks, "u", real_c(0.0), field::fzyx, 0);
+
+        gen::LbInit lbInit{pdfsId, density, velocity};
+
+        for (auto &block : *blocks)
+        {
+            lbInit(&block);
+        }
+
+        //  Stream-Collide
+
+        auto streamCollide = make_shared<gen::LbStreamCollide>(pdfsId, rhoId, uId, omega);
+
+        //  Communication Setup
+
+        CommScheme comm{blocks};
+        auto pdfsPackInfo = std::make_shared<PdfsPackInfo>(pdfsId);
+        comm.addPackInfo(pdfsPackInfo);
+
+        //  Boundary Conditions
+
+        const BlockDataID flagFieldId = field::addFlagFieldToStorage<FlagField_T>(blocks, "flagField");
+        const FlagUID fluidFlagUid{"Fluid"};
+        const FlagUID boundaryFlagUID{"FreeSlip"};
+
+        const CellInterval allCells{{0, 0, 0}, {blocks->getNumberOfXCellsPerBlock() - 1, blocks->getNumberOfYCellsPerBlock() - 1, blocks->getNumberOfZCellsPerBlock() - 1}};
+        const Vector3< real_t > sphereCenter { 16., 16., 16. };
+        const real_t sphereRadius { 8. };
+
+        for (auto &block : *blocks)
+        {
+            FlagField_T &flagField = *block.getData<FlagField_T>(flagFieldId);
+            const uint8_t freeSlipFlag{flagField.getOrRegisterFlag(boundaryFlagUID)};
+
+            PdfField_T & pdfField = *block.getData< PdfField_T >(pdfsId);
+
+            for (Cell c: allCells){
+                Cell globalCell { c };
+                blocks->transformBlockLocalToGlobalCell( globalCell, block );
+                Vector3< real_t > cellCenter { blocks->getCellCenter(globalCell) };
+
+                if((cellCenter - sphereCenter).sqrLength() < sphereRadius * sphereRadius){
+                    flagField.addFlag(c, freeSlipFlag);
+                    for( uint_t q = 0; q < 19; ++q ){
+                        pdfField.get(c, q) = NAN;
+                    }
+                }
+            }
+        }
+
+        geometry::setNonBoundaryCellsToDomain<FlagField_T>(*blocks, flagFieldId, fluidFlagUid);
+
+        auto flagFieldOutput = field::createVTKOutput<FlagField_T>(flagFieldId, *blocks, "flagField", 1, 0);
+        flagFieldOutput();
+
+        // begin freeSlipFactory
+        gen::FreeSlipFactory freeSlipFactory{ blocks, pdfsId };
+        // end freeSlipFactory
+        // begin freeSlipFromFlagField
+        gen::FreeSlip freeSlip {
+            freeSlipFactory.fromFlagField<FlagField_T>(
+                flagFieldId,
+                boundaryFlagUID,
+                fluidFlagUid
+            )
+        };
+        // end freeSlipFromFlagField
+
+        //  Timeloop
+        const uint_t numTimesteps{simParams.getParameter<uint_t>("timesteps")};
+        SweepTimeloop loop{blocks->getBlockStorage(), numTimesteps};
+
+        loop.add() << BeforeFunction(comm) << Sweep(freeSlip);
+        loop.add() << Sweep(makeSharedSweep(streamCollide));
+
+        RemainingTimeLogger logger{numTimesteps};
+        loop.addFuncAfterTimeStep(logger);
+
+        //  VTK Output
+
+        Config::BlockHandle outputParams = config->getBlock("Output");
+
+        const uint_t vtkWriteFrequency = outputParams.getParameter<uint_t>("vtkWriteFrequency", 0);
+        if (vtkWriteFrequency > 0)
+        {
+            auto vtkOutput = vtk::createVTKOutput_BlockData(*blocks, "vtk", vtkWriteFrequency, 0, false, "vtk_out",
+                                                            "simulation_step", false, true, true, false, 0);
+
+            auto densityWriter = make_shared<field::VTKWriter<ScalarField_T, float32>>(rhoId, "density");
+            vtkOutput->addCellDataWriter(densityWriter);
+
+            auto velWriter = make_shared<field::VTKWriter<VectorField_T, float32>>(uId, "velocity");
+            vtkOutput->addCellDataWriter(velWriter);
+
+            const cell_idx_t xCells = cell_idx_c(blocks->getNumberOfXCells());
+            const cell_idx_t yCells = cell_idx_c(blocks->getNumberOfYCells());
+            const cell_idx_t zCells = cell_idx_c(blocks->getNumberOfZCells());
+
+            loop.addFuncAfterTimeStep(vtk::writeFiles(vtkOutput), "VTK Output");
+        }
+
+        //  Run the Simulation
+
+        WALBERLA_LOG_INFO_ON_ROOT("Commencing simulation with " << numTimesteps << " timesteps")
+
+        loop.run();
+    }
+}
+
+int main(int argc, char **argv)
+{
+    walberla::Environment env{argc, argv};
+
+    FreeSlipBubble::run(env.config());
+
+    return EXIT_SUCCESS;
+}
diff --git a/user_manual/examples/FreeSlipBubble/LbmAlgorithms.py b/user_manual/examples/FreeSlipBubble/LbmAlgorithms.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2975f2617a73d9b129dc7bd55b1a308e9a37b2a
--- /dev/null
+++ b/user_manual/examples/FreeSlipBubble/LbmAlgorithms.py
@@ -0,0 +1,59 @@
+import sympy as sp
+from pystencilssfg import SourceFileGenerator
+
+from pystencils import fields, Field
+from lbmpy import LBStencil, Stencil, LBMConfig, LBMOptimisation, create_lb_update_rule
+from lbmpy.macroscopic_value_kernels import macroscopic_values_setter
+
+
+from sfg_walberla import Sweep
+
+with SourceFileGenerator() as sfg:
+    sfg.namespace("FreeSlipBubble::gen")
+
+    stencil = LBStencil(Stencil.D3Q19)
+    d, q = stencil.D, stencil.Q
+    pdfs: Field
+    pdfs_tmp: Field
+    pdfs, pdfs_tmp, rho, u = fields(
+        f"f({q}), f_tmp({q}), rho, u({d}): double[{d}D]", layout="fzyx"
+    )  # type: ignore
+
+    lbm_config = LBMConfig(
+        stencil=Stencil.D3Q19,
+        output={"density": rho, "velocity": u},
+    )
+    lbm_opt = LBMOptimisation(
+        symbolic_field=pdfs,
+        symbolic_temporary_field=pdfs_tmp,
+    )
+
+    lb_update = create_lb_update_rule(lbm_config=lbm_config, lbm_optimisation=lbm_opt)
+    lb_method = lb_update.method
+    assert lb_update is not None
+
+    lb_update_sweep = Sweep("LbStreamCollide", lb_update)
+    lb_update_sweep.swap_fields(pdfs, pdfs_tmp)
+    sfg.generate(lb_update_sweep)
+
+    lb_init = macroscopic_values_setter(
+        lb_update.method,
+        density=sp.Symbol("density"),
+        velocity=sp.symbols(f"velocity_:{d}"),
+        pdfs=pdfs,
+    )
+    lb_init_sweep = Sweep("LbInit", lb_init)
+    sfg.generate(lb_init_sweep)
+
+    #   begin irregular-freeslip
+    from sfg_walberla.boundaries import FreeSlip
+
+    freeslip = FreeSlip(
+        "FreeSlip",
+        lb_method,
+        pdfs,
+        wall_normal=FreeSlip.IRREGULAR,
+    )
+
+    sfg.generate(freeslip)
+    #   end irregular-freeslip
diff --git a/user_manual/examples/FreeSlipBubble/Periodic.prm b/user_manual/examples/FreeSlipBubble/Periodic.prm
new file mode 100644
index 0000000000000000000000000000000000000000..2fd9fbd63f7836cce2ffb43723485ca4e01fdbc9
--- /dev/null
+++ b/user_manual/examples/FreeSlipBubble/Periodic.prm
@@ -0,0 +1,17 @@
+DomainSetup
+{
+    blocks < 1, 1, 1 >;
+    cellsPerBlock < 32, 32, 32 >;
+    periodic < 1, 1, 1 >;
+}
+
+Parameters
+{
+    omega       1.0;
+    timesteps   2000;
+}
+
+Output
+{
+    vtkWriteFrequency   100;
+}
diff --git a/user_manual/guides/BoundaryConditions.md b/user_manual/guides/BoundaryConditions.md
new file mode 100644
index 0000000000000000000000000000000000000000..7480c64f5648fd34511320785d1e450dea36bdb2
--- /dev/null
+++ b/user_manual/guides/BoundaryConditions.md
@@ -0,0 +1,56 @@
+(guide-boundary-conditions)=
+# Boundary Conditions
+
+## Sparse Boundary Sweeps for Irregular Geometries
+
+To cover the boundary conditions at surfaces with irregular geometries
+(i.e. surfaces not aligned with the cell grid),
+we need to use a sparse implementation of the boundary sweep.
+
+### Generating Sparse Link-Wise LBM Boundary Sweeps
+
+For link-wise lattice Boltzmann boundary conditions, sparsity is enabled by
+setting the `wall_normal` parameter to `IRREGULAR`:
+
+```{literalinclude} ../examples/FreeSlipBubble/LbmAlgorithms.py
+:language: Python
+:start-after: "#   begin irregular-freeslip"
+:end-before: "#   end irregular-freeslip"
+:linenos:
+:dedent: 4
+:emphasize-lines: 7
+```
+
+The above code will generate two classes:
+ - The `FreeSlip` sweep class, which is a [sparse sweep](#guide-sparse-sweeps) that applies
+   the free-slip boundary condition at each lattice link crossing the boundary surface;
+ - and the `FreeSlipFactory` class; which produces instances of the `FreeSlip` sweep and populates its
+   index vector from user-provided geometry information.
+
+### Populating the Index Vector
+
+Sparse boundary sweeps are created, and their index vectors populated, by their associated factory.
+Create the factory object by passing it any required parameters:
+
+```{literalinclude} ../examples/FreeSlipBubble/FreeSlipBubble.cpp
+:language: C++
+:start-after: "// begin freeSlipFactory"
+:end-before: "// end freeSlipFactory"
+:dedent: 8
+```
+
+#### From a Flag Field
+
+One possibility to populate the sparse sweep is to extract boundary information stored in a *flag field*.
+This requires that each *solid* cell considered by the boundary condition is marked with a *boundary flag*,
+and each valid *fluid* cell is marked with the *fluid* flag.
+Assuming that this information is stored in a flag field of type `FlagField_T` identified by `flagFieldId`,
+and `boundaryFlagUID` and `fluidFlagUID` respectively identify the boundary and fluid flags,
+we can create a sparse boundary sweep using the `fromFlagField` method of the factory:
+
+```{literalinclude} ../examples/FreeSlipBubble/FreeSlipBubble.cpp
+:language: C++
+:start-after: "// begin freeSlipFromFlagField"
+:end-before: "// end freeSlipFromFlagField"
+:dedent: 8
+```
diff --git a/user_manual/guides/SparseSweeps.md b/user_manual/guides/SparseSweeps.md
index 159313f0c8542839d117a651bacd9d8165128bdf..822d92fbc3894a07e09def2e2797b95d0f349e46 100644
--- a/user_manual/guides/SparseSweeps.md
+++ b/user_manual/guides/SparseSweeps.md
@@ -1,3 +1,4 @@
+(guide-sparse-sweeps)=
 # Sparse Sweeps
 
 :::{admonition} Example Code
diff --git a/user_manual/index.md b/user_manual/index.md
index a48e3fe493164d216d069af31192de6f64a37b51..29564ac46045ef4171c75bd23f1ee63d1e19985c 100644
--- a/user_manual/index.md
+++ b/user_manual/index.md
@@ -24,6 +24,7 @@ examples/GeneratorScriptBasics/GeneratorScriptBasics
 :maxdepth: 1
 
 guides/SparseSweeps
+guides/BoundaryConditions
 :::
 
 :::{toctree}