Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ test.py
wheelhouse
test.ipynb
*.whl
# Ignore generated stubs (e.g. the nanobind-generated _core.pyi), but keep the
# hand-written public stub, which is a tracked source file and ships in the wheel.
*.pyi
!src/rayx/__init__.pyi
venv/
.venv/
**.so
__pycache__/
rayx/_version.py
src/rayx/_version.py
55 changes: 52 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,56 @@
cmake_minimum_required(VERSION 3.25.2)
# 3.26 is required for FindPython's Development.SABIModule component, which the
# STABLE_ABI extension build depends on (see nanobind_add_module below).
cmake_minimum_required(VERSION 3.26)
project(rayx-python LANGUAGES CXX)

project(rayx-python)
# rayx-core + nanobind. Provides the `rayx-core` target and the nanobind_add_*
# helpers (both stay available at this scope after the subdirectory is processed).
add_subdirectory(extern EXCLUDE_FROM_ALL)
add_subdirectory(rayx)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(alpaka_ACC_GPU_CUDA_ENABLE)
enable_language(CUDA)
set(CMAKE_CUDA_STANDARD 20)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
endif()
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF)

# Development.SABIModule provides the Python::SABIModule target that nanobind's
# STABLE_ABI build requires; without it nanobind silently falls back to a
# version-specific extension.
find_package(Python 3.12 COMPONENTS Interpreter Development.Module Development.SABIModule REQUIRED)

# The compiled extension is implementation-private (`_core`); the Python package
# `src/rayx` owns the public API. Following nanobind's recommended layout, the
# binding source sits next to the package as `src/rayx_ext.cpp`.
#
# STABLE_ABI builds against the CPython 3.12+ limited API (abi3) so a single wheel
# covers every supported interpreter (3.12, 3.13, 3.14, ...) instead of one per
# minor version. Paired with `wheel.py-api = "cp312"` in pyproject.toml for tagging.
nanobind_add_module(_core STABLE_ABI src/rayx_ext.cpp)
nanobind_add_stub(
_core_stub
MODULE _core
OUTPUT _core.pyi
PYTHON_PATH $<TARGET_FILE_DIR:_core>
DEPENDS _core
)
target_link_libraries(_core PRIVATE rayx-core)
target_include_directories(_core PRIVATE
$<TARGET_PROPERTY:rayx-core,INTERFACE_INCLUDE_DIRECTORIES>)

# Bundle rayx-core's runtime Data into the wheel. At import time the module
# registers its own package directory as the resource lookup path.
add_custom_command(
TARGET _core POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${RAYX_SOURCE_DIR}/Data"
"${CMAKE_CURRENT_BINARY_DIR}/share/RAYX/Data"
)

# Install native artifacts into the Python package. scikit-build-core installs
# the Python sources from src/rayx independently (see pyproject wheel.packages).
install(TARGETS _core DESTINATION rayx)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/_core.pyi" DESTINATION rayx)
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/share/RAYX/Data" DESTINATION rayx/share/RAYX)
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Python bindings for [RAYX](https://github.com/hz-b/rayx), the ray tracing engine
pip install rayx
```

> **Note:** The package includes a compiled C++ extension and is distributed as a pre-built wheel. Source builds are not supported via pip.
> **Note:** Standard installations use a pre-built wheel because the package includes a compiled C++ extension. The supported development source-build workflow is described below.

## Requirements

Expand All @@ -30,32 +30,42 @@ Make it permanent by adding that line to `~/.bashrc` (or `~/.zshrc`) and reloadi

## Development

The package can be built in two ways:

1. **CMake** — supports build caching; the result is usable immediately with no install step. See the [example notebook](./examples/metrix.ipynb).
2. **`uv build`** — builds the package as a wheel, which can then be installed into any Python environment.
Use an editable install for development; it builds the native extension and makes
the Python wrapper importable from its normal package location. `uv build` creates
a distributable wheel.

```bash
git submodule update --init --recursive

# Option 1: build in place with CMake (uv creates/manages the environment)
uv run cmake -S . -B build
uv run cmake --build build -j

# Option 2: editable install via uv
uv pip install -e .
uv sync --locked
uv pip install --no-build-isolation -e .
```

`uv run` puts the managed environment's Python first on `PATH`, so CMake's `find_package(Python)` resolves the right interpreter without pinning it manually.
Re-run the editable-install command after changing the native bindings. The
persistent `build/<wheel-tag>` directory and non-isolated development toolchain
retain CMake's build cache, so Ninja recompiles only affected targets in parallel.

`uv pip install -e .` only re-links the Python wrapper files. It does **not** rebuild the compiled bindings — if you change `rayx-core` or the nanobind glue in `rayxpy`, rerun the CMake commands (Option 1) to recompile, or the install will keep serving the stale extension.
Python-only changes are available to the next Python process. After changing
C++ or nanobind code, re-run the editable-install command above, then restart
the Python process or test run; extension modules cannot be safely replaced in
an already-running interpreter.

After the editable install has configured the build directory, `cmake --build
build/<wheel-tag> -j` remains available for a compile-only check. Re-run the
editable-install command to also copy the rebuilt extension into the active Python
environment.

A `tools/bootstrap.sh` helper script is also available, wrapping the steps above with CUDA on/off prompting.

### Running tests

Tests require a CMake build:
Tests require the editable install:

```bash
uv run pytest tests
```

## Releases

Release artifacts are built, tested, and published only by the wheel CI workflow.
It installs every produced wheel outside the checkout and runs the full test suite
before publication; do not publish a workstation-built wheel.
14 changes: 13 additions & 1 deletion extern/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
set(RAYX_STATIC_LIB ON)
set(RAYX_BUILD_RAYX_CLI NO)
set(RAYX_BUILD_RAYX_UI NO)
set(RAYX_BUILD_RAYX_TESTS NO)
# set(RAYX_CUSTOM_DATA_DIR "rayxdata/share" CACHE STRING "Set this val")

# rayx-core embeds CMAKE_INSTALL_PREFIX in every compilation unit. Packaging
# frontends use a new temporary wheel staging prefix for each invocation, which
# would otherwise invalidate the complete native build on every editable
# reinstall. The Python binding registers its module directory as the runtime
# resource lookup path, so rayx-core can use a stable build-local prefix while
# scikit-build-core still overrides the actual destination during installation.
set(RAYX_PYTHON_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/rayx-install")
add_subdirectory(rayx)
add_subdirectory(nanobind)
set(CMAKE_INSTALL_PREFIX "${RAYX_PYTHON_INSTALL_PREFIX}")
unset(RAYX_PYTHON_INSTALL_PREFIX)

add_subdirectory(nanobind)
36 changes: 26 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ dependencies = [
"matplotlib>=3.9.4",
"numpy >= 2.0.0",
"pandas>=2.3.3",
"pytest>=8.4.2",
]
requires-python = ">=3.12"
authors = [{ name = "RAYX team", email = "rayx-support@helmholtz-berlin.de" }]
Expand All @@ -21,31 +20,48 @@ dev = ["pytest>=7.0", "matplotlib>=3.5", "ipython"]
test = ["pytest>=7.0", "pytest-cov"]

[build-system]
requires = ["scikit-build-core", "nanobind"]
requires = ["scikit-build-core>=1.0", "nanobind>=2.13", "setuptools-scm>=10.2"]
build-backend = "scikit_build_core.build"

[tool.scikit-build]
wheel.packages = ["python/rayx"]
wheel.packages = ["src/rayx"]
# Keep CMake artifacts for fast local and editable rebuilds. The wheel tag
# prevents incompatible Python/ABI builds from sharing a cache.
build-dir = "build/{wheel_tag}"
cmake.build-type = "Release"
metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
sdist.include = ["rayx/_version.py"]
sdist.include = ["src/rayx/_version.py"]
# Tag wheels as cp312-abi3 to match the STABLE_ABI extension (see CMakeLists.txt):
# one wheel serves CPython 3.12+.
wheel.py-api = "cp312"

[[tool.dynamic-metadata]]
provider = "scikit_build_core.metadata.setuptools_scm"

[tool.setuptools_scm] # Section required
write_to = "rayx/_version.py"
write_to = "src/rayx/_version.py"

[tool.cibuildwheel]
archs = ["x86_64"]
build = ["*manylinux*"]
# Free-threaded CPython does not support the stable ABI used by this wheel.
skip = ["cp*t-*"]
manylinux-x86_64-image = "gitea.valentinstoecker.de/vls/manylinux_cuda"
test-requires = ["pytest", "pyright"]
test-command = "cd /tmp && python -m pytest -q {project}/tests"

[tool.cibuildwheel.linux]
environment = { SETUPTOOLS_SCM_OVERRIDES_FOR_RAYX = "{ local_scheme = 'no-local-version' }" }

[tool.pytest.ini_options]
pythonpath = ["python"]

[tool.uv]
package = false

[dependency-groups]
dev = ["setuptools-scm>=10.2.0", "twine>=6.2.0"]
dev = [
"cmake>=3.26",
"nanobind>=2.13.0",
"ninja>=1.13.0",
"pytest>=9.0.2",
"scikit-build-core>=1.0",
"setuptools-scm>=10.2.0",
"twine>=6.2.0",
]
52 changes: 0 additions & 52 deletions rayx/CMakeLists.txt

This file was deleted.

45 changes: 0 additions & 45 deletions rayx/__init__.py

This file was deleted.

35 changes: 35 additions & 0 deletions src/rayx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
RAY-X Python bindings
"""
# The compiled extension `_core` is an implementation detail; the public API of
# this package is the native bindings re-exported here plus the Python helpers below.
from . import _core
from ._core import *

from ._version import *

# Python helper layer built on top of the native bindings.
from .data import rays_to_df


def get_info():
"Get information about the RAYX installation"
# Imported locally so it does not leak into the package's public namespace.
from pathlib import Path

info = {
"version": __version__,
"python_wrapper": True,
"cpp_module": str(_core.__file__),
"module_path": str(Path(__file__).parent),
}
return info


# Advertise the full native API (everything `_core` exports) plus the Python
# helpers and version, so `from rayx import *` mirrors the real public surface.
__all__ = [name for name in dir(_core) if not name.startswith("_")] + [
"get_info",
"rays_to_df",
"__version__",
]
13 changes: 13 additions & 0 deletions src/rayx/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Type stub for the public ``rayx`` API.

The native bindings are re-exported from the private ``_core`` extension, which is
typed by the auto-generated ``_core.pyi``. Only the small Python helper layer is
declared here by hand. Keep this in sync with ``__init__.py`` — ``tests/test_stubs.py``
enforces it with ``stubtest``.
"""
from . import _core as _core
from ._core import *
from ._version import *
from .data import rays_to_df as rays_to_df

def get_info() -> dict[str, object]: ...
4 changes: 2 additions & 2 deletions rayx/data.py → src/rayx/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
RAY-X Python bindings
"""
import pandas as pd
from . import core
from . import _core

def rays_to_df(rays: core.Rays, columns: list | None = None) -> pd.DataFrame:
def rays_to_df(rays: _core.Rays, columns: list | None = None) -> pd.DataFrame:
if columns is None:
columns = [
"path_id",
Expand Down
Empty file added src/rayx/py.typed
Empty file.
Loading
Loading