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
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ test.py
cuda-*
wheelhouse
test.ipynb
rayx-*.whl
.venv
*.whl
*.pyi
venv/
.venv/
**.so
__pycache__/
14 changes: 10 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
cmake_minimum_required(VERSION 3.25.2)

project(rayx-python)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "Install prefix" FORCE)
endif()

project(rayx-python)
add_subdirectory(extern)
add_subdirectory(src)

# create __init__.py and install it in INSTALL_DIR/rayxdata
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/__init__.py" "")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/__init__.py" DESTINATION "rayxdata")
# Install Python package
install(DIRECTORY python/rayx DESTINATION .)

# Create and install rayxdata package (needed by C++ code)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/rayxdata/__init__.py" "")
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rayxdata" DESTINATION .)
93 changes: 84 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,84 @@
# rayx-py

## building
- Release: `pipx run cibuildwheel`
- Development (2 options):
1. `pip wheel .` and install the resulting wheel
2. build the project with CMake and either (both options require rayx to already be present in your environment as there are data files included in the full release that are not part of the .so file):
1. import the package like this `import build.src.rayx as rayx` (using the project root as CWD)
2. copy the built shared library (in build/src) to your CWD
# rayx-python

Python bindings for [RAYX](https://github.com/hz-b/rayx), the ray tracing engine for synchrotron optics.

## Installation

```bash
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.

## Development

The project has two separate concerns that use different tools:

- **uv** manages the Python virtual environment and dependencies (numpy, pytest, etc.)
- **setup_dev.sh** builds the C++ extension and wires everything up for a fast dev loop
- **cibuildwheel** builds the final distributable wheel for release

### Setup

```bash
./setup_dev.sh
source .venv/bin/activate
```

`setup_dev.sh` will:
1. Create a virtual environment with uv if one doesn't exist
2. Build the C++ extension via CMake
3. Symlink the compiled `_core.so` into `python/rayx/` so it can be imported directly
4. Symlink `rayxdata` from the build directory
5. Generate `python/rayx/_core.pyi` type stubs so your LSP can resolve symbols from the C++ module


### Project structure

```
python/rayx/
├── __init__.py # Python wrapper and public API
├── _core.so # Symlink to compiled C++ extension (generated by setup_dev.sh)
├── _core.pyi # Type stubs for LSP (generated by setup_dev.sh, do not edit)
└── [...].py # Additional files/directories for organization (need to be added to __init__.py)
```

### LSP / editor support

Type stubs are generated automatically by `setup_dev.sh`. Point your LSP at the venv Python interpreter (`.venv/bin/python3`) and symbol resolution for `_core` will work out of the box.

If you need to regenerate stubs without a full rebuild:

```bash
source .venv/bin/activate
python3 -c "
import sys
sys.path.insert(0, 'python')
import rayx._core as m
with open('python/rayx/_core.pyi', 'w') as f:
f.write('# Auto-generated stub - do not edit, regenerated by setup_dev.sh\n')
for name in dir(m):
if name.startswith('_'): continue
obj = getattr(m, name)
if callable(obj):
f.write(f'def {name}(*args, **kwargs): ...\n')
else:
f.write(f'{name}: object\n')
"
```

### Running tests

```bash
uv run pytest tests
```

## Release

Wheels are built using [cibuildwheel](https://cibuildwheel.readthedocs.io/) inside a custom manylinux container with CUDA support:

```bash
pipx run cibuildwheel
```

Built wheels are placed in `wheelhouse/`. See `pyproject.toml` for the build configuration.
2 changes: 1 addition & 1 deletion extern/pybind11
Submodule pybind11 updated 282 files
6,287 changes: 6,287 additions & 0 deletions log.txt

Large diffs are not rendered by default.

30 changes: 27 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,43 @@ version = "0.4.3"
description = "Python bindings for RAYX"
readme = "README.md"
license = { file = "LICENSE" }

dependencies = ["numpy >= 2.0.0"]
requires-python = ">=3.7"
dependencies = [
"numpy >= 2.0.0",
"pandas>=2.3.3",
"pytest>=8.4.2",
]
requires-python = ">=3.9"
authors = [{ name = "RAYX team", email = "[email protected]" }]
maintainers = [
{ name = "RAYX team", email = "[email protected]" },
]

[project.optional-dependencies]
dev = [
"pytest>=7.0",
"matplotlib>=3.5",
"ipython",
]
test = [
"pytest>=7.0",
"pytest-cov",
]

[build-system]
requires = ["scikit-build-core", "pybind11"]
build-backend = "scikit_build_core.build"

[tool.scikit-build]
wheel.packages = ["python/rayx"]
cmake.build-type = "Release"

[tool.cibuildwheel]
archs = ["x86_64"]
build = ["*manylinux*"]
manylinux-x86_64-image = "gitea.valentinstoecker.de/vls/manylinux_cuda"

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

[tool.uv]
package = false
37 changes: 37 additions & 0 deletions python/rayx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
RAY-X Python bindings
"""
import sys
from pathlib import Path

# Import the C++ extension module
try:
from . import _core
except ImportError:
# During development, try to find the built module
import os
build_dir = Path(__file__).parent.parent.parent / "build" / "install" / "rayx"
if build_dir.exists():
sys.path.insert(0, str(build_dir.parent))
from rayx import _core
else:
raise ImportError("Cannot find compiled _core module. Did you build the project?")

# Re-export everything from C++ module
from ._core import *

def get_info():
"""Get information about the RAYX installation"""
info = {
"version": __version__,
"python_wrapper": True,
"cpp_module": str(_core.__file__),
"module_path": str(Path(__file__).parent),
}
return info

# From other files
from .data import rays_to_df

__version__ = "0.4.3"
__all__ = ['get_info', 'rays_to_df']
34 changes: 34 additions & 0 deletions python/rayx/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
RAY-X Python bindings
"""
import sys
from pathlib import Path
import pandas as pd

# Import the C++ extension module
try:
from . import _core
except ImportError:
# During development, try to find the built module
import os
build_dir = Path(__file__).parent.parent.parent / "build" / "install" / "rayx"
if build_dir.exists():
sys.path.insert(0, str(build_dir.parent))
from rayx import _core
else:
raise ImportError("Cannot find compiled _core module. Did you build the project?")

def rays_to_df(rays, columns: list | None = None) -> pd.DataFrame:
if columns is None:
columns = [
"path_event_id",
"position_x", "position_y", "position_z",
"direction_x", "direction_y", "direction_z",
"electric_field_x", "electric_field_y", "electric_field_z",
"energy", "order",
"last_element_id", "source_id",
"event_type",
]

df = pd.DataFrame({col: getattr(rays, col) for col in columns})
return df
76 changes: 76 additions & 0 deletions setup_dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash
# setup_dev.sh - Development setup with virtual environment

set -e

# Ensure we're in the right directory
cd "$(dirname "$0")"

# Check if venv exists
if [ ! -d ".venv" ]; then
echo "Creating virtual environment with uv..."
uv venv
source .venv/bin/activate
echo "Installing dependencies..."
uv pip install numpy pytest matplotlib
else
source .venv/bin/activate
fi

# Build C++ extension
echo "Building C++ extension..."
mkdir -p build
cd build

# Configure with local install prefix
cmake -DCMAKE_INSTALL_PREFIX=./install ..

# Build
make -j$(nproc)

# Install to local directory
make install

cd ..

# Create symlink
BUILD_DIR="build/install/rayx"
PYTHON_DIR="python/rayx"

SO_FILE=$(find "$BUILD_DIR" -name "_core.cpython-*.so" 2>/dev/null | head -n 1)

if [ -f "$SO_FILE" ]; then
ln -sf "../../$SO_FILE" "$PYTHON_DIR/_core.so"
echo "✓ Created symlink: $PYTHON_DIR/_core.so"
else
echo "✗ Error: Could not find _core.cpython-*.so in $BUILD_DIR"
echo "Files in build/install:"
find build/install -type f 2>/dev/null || echo " (directory doesn't exist)"
exit 1
fi

# Add python directory to PYTHONPATH for development
export PYTHONPATH="$(pwd)/python:$PYTHONPATH"

source .venv/bin/activate
python3 -c "
import sys
sys.path.insert(0, 'python')
import rayx._core as m
with open('python/rayx/_core.pyi', 'w') as f:
f.write('# Auto-generated stub\n')
for name in dir(m):
if name.startswith('_'): continue
obj = getattr(m, name)
if callable(obj):
f.write(f'def {name}(*args, **kwargs): ...\n')
else:
f.write(f'{name}: object\n')
"
deactivate

echo "✓ Development environment ready!"
echo ""
echo "To use:"
echo " source .venv/bin/activate"
echo " cd python && python3 -c 'import rayx; print(rayx.__version__)'"
15 changes: 8 additions & 7 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
project(rayxpy LANGUAGES CXX CUDA)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CUDA_STANDARD 20)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)

set(PYBIND11_FINDPYTHON ON)

set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF)

pybind11_add_module(rayx main.cpp)
target_link_libraries(rayx PRIVATE rayx-core)
target_include_directories(rayx PRIVATE $<TARGET_PROPERTY:rayx-core,INTERFACE_INCLUDE_DIRECTORIES> ${CUDA_TOOLKIT_INCLUDE}/cccl)
# Changed from 'rayx' to '_core' to make it a private module
pybind11_add_module(_core main.cpp)
target_link_libraries(_core PRIVATE rayx-core)
target_include_directories(_core PRIVATE
$<TARGET_PROPERTY:rayx-core,INTERFACE_INCLUDE_DIRECTORIES>
${CUDA_TOOLKIT_INCLUDE}/cccl)

install(TARGETS rayx DESTINATION .)
# Install to rayx/ subdirectory
install(TARGETS _core LIBRARY DESTINATION rayx)
4 changes: 2 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ class Module {
}
};

PYBIND11_MODULE(rayx, m) {
PYBIND11_MODULE(_core, m) {
static Module module_instance;

m.doc() = "rayx module";
Expand Down Expand Up @@ -484,4 +484,4 @@ PYBIND11_MODULE(rayx, m) {

m.def(
"import_beamline", [](std::string path) { return rayx::importBeamline(path); }, "Import a beamline from an RML file", pybind11::arg("path"));
}
}
Loading