From ff5634e2ba585db6013fcc428ff7d4a1d61b2cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20St=C3=B6cker?= Date: Tue, 28 Apr 2026 16:11:16 +0200 Subject: [PATCH 1/2] switch-to-nanobind --- .gitmodules | 3 + CMakeLists.txt | 13 +-- extern/CMakeLists.txt | 4 +- extern/nanobind | 1 + extern/pybind11 | 1 - extern/rayx | 2 +- setup_dev.sh | 76 ------------ src/CMakeLists.txt | 46 ++++++-- src/__init__.py | 27 +++++ src/data.py | 25 ++++ src/main.cpp | 264 +++++++++++++++++++----------------------- src/reflection.hpp | 75 +++++++----- 12 files changed, 270 insertions(+), 267 deletions(-) create mode 160000 extern/nanobind delete mode 160000 extern/pybind11 delete mode 100755 setup_dev.sh create mode 100644 src/__init__.py create mode 100644 src/data.py diff --git a/.gitmodules b/.gitmodules index 23098b9..46ad45b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "extern/rayx"] path = extern/rayx url = https://github.com/hz-b/rayx +[submodule "extern/nanobind"] + path = extern/nanobind + url = https://github.com/wjakob/nanobind diff --git a/CMakeLists.txt b/CMakeLists.txt index 30410c4..170cc84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,13 @@ cmake_minimum_required(VERSION 3.25.2) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "Install prefix" FORCE) -endif() +# 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) +add_subdirectory(src rayx) # Install Python package -install(DIRECTORY python/rayx DESTINATION .) +# 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 .) diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 37656ba..45451b4 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -1,7 +1,7 @@ set(RAYX_STATIC_LIB ON) set(RAYX_BUILD_RAYX_CLI NO) set(RAYX_BUILD_RAYX_UI NO) -set(RAYX_CUSTOM_DATA_DIR "rayxdata/share" CACHE STRING "Set this val") +# set(RAYX_CUSTOM_DATA_DIR "rayxdata/share" CACHE STRING "Set this val") add_subdirectory(rayx) -add_subdirectory(pybind11) \ No newline at end of file +add_subdirectory(nanobind) \ No newline at end of file diff --git a/extern/nanobind b/extern/nanobind new file mode 160000 index 0000000..4bfeca0 --- /dev/null +++ b/extern/nanobind @@ -0,0 +1 @@ +Subproject commit 4bfeca03aa67acb211a8a1355a3b11c05fbee8a9 diff --git a/extern/pybind11 b/extern/pybind11 deleted file mode 160000 index 58c382a..0000000 --- a/extern/pybind11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 58c382a8e3d7081364d2f5c62e7f429f0412743b diff --git a/extern/rayx b/extern/rayx index 8ab595f..7965b98 160000 --- a/extern/rayx +++ b/extern/rayx @@ -1 +1 @@ -Subproject commit 8ab595ff046414b206140a8ef952b1351172ae9c +Subproject commit 7965b98c7fa5fe36d99159cd02aa94a5c6e55a8f diff --git a/setup_dev.sh b/setup_dev.sh deleted file mode 100755 index 490bc41..0000000 --- a/setup_dev.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/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__)'" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ab29de8..307ffc4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,15 +3,47 @@ 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) -# 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 +if (CMAKE_VERSION VERSION_LESS 3.18) + set(DEV_MODULE Development) +else() + set(DEV_MODULE Development.Module) +endif() + +find_package(Python 3.9 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) + +# Changed from 'rayx' to 'core' to make it a private module +nanobind_add_module(core main.cpp) +nanobind_add_stub( + core_stub + MODULE core + OUTPUT core.pyi + PYTHON_PATH $ + DEPENDS core +) +target_link_libraries(core PRIVATE rayx-core) +target_include_directories(core PRIVATE $ - ${CUDA_TOOLKIT_INCLUDE}/cccl) + ${CUDA_TOOLKIT_INCLUDE}) + +add_custom_command( + TARGET core + COMMAND ${CMAKE_COMMAND} -E copy_directory + #"${CMAKE_CURRENT_SOURCE_DIR}/extern/rayx/Data" + "${RAYX_SOURCE_DIR}/Data" + "${CMAKE_CURRENT_BINARY_DIR}/share/RAYX/Data" +) + +file(GLOB_RECURSE PY_SRC *.py) +# file(COPY PY_SRC ".") + +add_custom_command( + TARGET core + COMMAND ${CMAKE_COMMAND} -E copy + ${PY_SRC} + "${CMAKE_CURRENT_BINARY_DIR}" +) # Install to rayx/ subdirectory -install(TARGETS _core LIBRARY DESTINATION rayx) +install(TARGETS core LIBRARY DESTINATION rayx) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..c25b9b9 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,27 @@ +""" +RAY-X Python bindings +""" +import sys +from pathlib import Path + +# Import the C++ extension module +from . import core + +# 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'] diff --git a/src/data.py b/src/data.py new file mode 100644 index 0000000..9eda627 --- /dev/null +++ b/src/data.py @@ -0,0 +1,25 @@ +""" +RAY-X Python bindings +""" +import sys +from pathlib import Path +import pandas as pd + +# Import the C++ extension module + +def rays_to_df(rays, columns: list | None = None) -> pd.DataFrame: + if columns is None: + columns = [ + "path_id", + "path_event_id", + "position_x", "position_y", "position_z", + "direction_x", "direction_y", "direction_z", + "electric_field_x", "electric_field_y", "electric_field_z", + "optical_path_length", + "energy", "order", + "object_id", "source_id", + "event_type", + ] + + df = pd.DataFrame({col: getattr(rays, col) for col in columns}) + return df diff --git a/src/main.cpp b/src/main.cpp index 9864c8f..737ef65 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,23 +4,22 @@ #include #include #include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include "reflection.hpp" -using namespace pybind11::literals; - std::complex toStdComplex(const rayx::complex::Complex& c) { return std::complex(c.real(), c.imag()); } -std::filesystem::path getModulePath() { - pybind11::gil_scoped_acquire acquire; - pybind11::object rayx = pybind11::module::import("rayxdata"); - return std::filesystem::path(rayx.attr("__file__").cast()).parent_path().parent_path(); +std::filesystem::path getModulePath(py::module_ m) { + py::gil_scoped_acquire acquire; + return std::filesystem::path(py::cast(m.attr("__file__"))).parent_path(); } namespace reflect { @@ -202,7 +201,7 @@ struct info { prop_info{&rayx::DesignElement::getCurvatureType, &rayx::DesignElement::setCurvatureType, "curvatureType"}, prop_info{&rayx::DesignElement::getBehaviourType, &rayx::DesignElement::setBehaviourType, "behaviourType"}, prop_info{&rayx::DesignElement::getCrystalType, &rayx::DesignElement::setCrystalType, "crystalType"}, - // prop_info{&rayx::DesignElement::getCrystalMaterial, &rayx::DesignElement::setCrystalMaterial, "crystalMaterial"}, + prop_info{&rayx::DesignElement::getCrystalMaterial, &rayx::DesignElement::setCrystalMaterial, "crystalMaterial"}, prop_info{&rayx::DesignElement::getOffsetAngle, &rayx::DesignElement::setOffsetAngle, "offsetAngle"}, prop_info{&rayx::DesignElement::getStructureFactorReF0, &rayx::DesignElement::setStructureFactorReF0, "structureFactorReF0"}, prop_info{&rayx::DesignElement::getStructureFactorImF0, &rayx::DesignElement::setStructureFactorImF0, "structureFactorImF0"}, @@ -270,123 +269,100 @@ static_assert(Structure); } // namespace reflect -template -struct data_buffer { - std::vector data; - - pybind11::buffer_info buffer_info() { return pybind11::buffer_info(data.data(), data.size()); } +// TODO: dmat4x4 +// TODO: rays struct +// TODO: LayerCoating +// TODO: CurvatureType - pybind11::object as_numpy_array() const { - pybind11::gil_scoped_acquire acquire; - pybind11::object np = pybind11::module::import("numpy"); - return np.attr("array")(this, "copy"_a = false); - } -}; - -struct Rays { - data_buffer position_x; - data_buffer position_y; - data_buffer position_z; - data_buffer direction_x; - data_buffer direction_y; - data_buffer direction_z; - data_buffer energy; - data_buffer> electric_field_x; - data_buffer> electric_field_y; - data_buffer> electric_field_z; - data_buffer path_length; - data_buffer order; - data_buffer event_type; - data_buffer last_element_id; - data_buffer source_id; - data_buffer ray_id; - data_buffer path_event_id; - - Rays(rayx::Rays&& rays) { - position_x.data = std::move(rays.position_x); - position_y.data = std::move(rays.position_y); - position_z.data = std::move(rays.position_z); - direction_x.data = std::move(rays.direction_x); - direction_y.data = std::move(rays.direction_y); - direction_z.data = std::move(rays.direction_z); - energy.data = std::move(rays.energy); - electric_field_x.data.resize(rays.electric_field_x.size()); - electric_field_y.data.resize(rays.electric_field_y.size()); - electric_field_z.data.resize(rays.electric_field_z.size()); - for (size_t i = 0; i < rays.electric_field_x.size(); i++) { - electric_field_x.data[i] = toStdComplex(rays.electric_field_x[i]); - electric_field_y.data[i] = toStdComplex(rays.electric_field_y[i]); - electric_field_z.data[i] = toStdComplex(rays.electric_field_z[i]); - } - path_length.data = std::move(rays.optical_path_length); - order.data = std::move(rays.order); - event_type.data = std::move(rays.event_type); - last_element_id.data = std::move(rays.object_id); - source_id.data = std::move(rays.source_id); - ray_id.data = std::move(rays.path_id); - path_event_id.data = std::move(rays.path_event_id); - } +template <> +struct py::detail::dtype_traits { + static constexpr dlpack::dtype value = py::detail::dtype_traits::value; + static constexpr auto name = py::detail::dtype_traits::name; }; -class Module { - public: - Module() { - std::filesystem::path data_dir = getModulePath(); - rayx::ResourceHandler::getInstance().addLookUpPath(data_dir); - } -}; +template +py::ndarray> to_numpy(std::vector& v) { + return py::ndarray>(v.data(), {v.size()}); +} -PYBIND11_MODULE(_core, m) { - static Module module_instance; +NB_MODULE(core, m) { + std::filesystem::path module_path = getModulePath(m); + rayx::ResourceHandler::getInstance().addLookUpPath(module_path); m.doc() = "rayx module"; - m.def("get_module_path", []() { return getModulePath().string(); }, "Get the path to the rayx module"); - - // element.cutout.width = 10.0 + m.def("get_module_path", [=]() { return module_path.string(); }, "Get the path to the rayx module"); reflect::register_type(m); reflect::register_type(m); - pybind11::enum_(m, "Material") - .value("VACUUM", rayx::Material::VACUUM) - .value("REFLECTIVE", rayx::Material::REFLECTIVE) + py::enum_(m, "Material").value("VACUUM", rayx::Material::VACUUM).value("REFLECTIVE", rayx::Material::REFLECTIVE) #define X(e, z, a, rho) .value(#e, rayx::Material::e) #include #undef X - .export_values(); - - pybind11::enum_(m, "SourceDist") + ; + py::enum_(m, "SourceDist") .value("UNIFORM", rayx::SourceDist::Uniform) .value("GAUSSIAN", rayx::SourceDist::Gaussian) .value("THIRDS", rayx::SourceDist::Thirds) - .value("CIRCLE", rayx::SourceDist::Circle) - .export_values(); + .value("CIRCLE", rayx::SourceDist::Circle); - pybind11::enum_(m, "SpreadType") + py::enum_(m, "SpreadType") .value("HARD_EDGE", rayx::SpreadType::HardEdge) .value("SOFT_EDGE", rayx::SpreadType::SoftEdge) - .value("SEPARATE_ENERGIES", rayx::SpreadType::SeparateEnergies) - .export_values(); + .value("SEPARATE_ENERGIES", rayx::SpreadType::SeparateEnergies); - pybind11::enum_(m, "EnergyDistributionType") + py::enum_(m, "EnergyDistributionType") .value("FILE", rayx::EnergyDistributionType::File) .value("VALUES", rayx::EnergyDistributionType::Values) .value("TOTAL", rayx::EnergyDistributionType::Total) - .value("PARAM", rayx::EnergyDistributionType::Param) - .export_values(); + .value("PARAM", rayx::EnergyDistributionType::Param); - pybind11::enum_(m, "EnergySpreadUnit") + py::enum_(m, "EnergySpreadUnit") .value("EV", rayx::EnergySpreadUnit::EU_eV) - .value("PERCENT", rayx::EnergySpreadUnit::EU_PERCENT) - .export_values(); + .value("PERCENT", rayx::EnergySpreadUnit::EU_PERCENT); - pybind11::enum_(m, "ElectronEnergyOrientation") + py::enum_(m, "ElectronEnergyOrientation") .value("Clockwise", rayx::ElectronEnergyOrientation::Clockwise) - .value("Counterclockwise", rayx::ElectronEnergyOrientation::Counterclockwise) - .export_values(); - - pybind11::enum_(m, "ElementType") + .value("Counterclockwise", rayx::ElectronEnergyOrientation::Counterclockwise); + + py::enum_(m, "ToroidType").value("Convex", rayx::ToroidType::Convex).value("Concave", rayx::ToroidType::Concave); + + py::enum_(m, "CutoutType") + .value("Unlimited", rayx::CutoutType::Unlimited) + .value("Rect", rayx::CutoutType::Rect) + .value("Trapezoid", rayx::CutoutType::Trapezoid) + .value("Elliptical", rayx::CutoutType::Elliptical); + + py::enum_(m, "CentralBeamstop") + .value("None", rayx::CentralBeamstop::None) + .value("Rectangle", rayx::CentralBeamstop::Rectangle) + .value("Elliptical", rayx::CentralBeamstop::Elliptical); + + py::enum_(m, "CylinderDirection") + .value("LongRadiusR", rayx::CylinderDirection::LongRadiusR) + .value("ShortRadiusRho", rayx::CylinderDirection::ShortRadiusRho); + + py::enum_(m, "FigureRotation") + .value("Yes", rayx::FigureRotation::Yes) + .value("Plane", rayx::FigureRotation::Plane) + .value("A11", rayx::FigureRotation::A11); + + py::enum_(m, "CurvatureType") + .value("Plane", rayx::CurvatureType::Plane) + .value("Toroidal", rayx::CurvatureType::Toroidal) + .value("Spherical", rayx::CurvatureType::Spherical) + .value("Cubic", rayx::CurvatureType::Cubic) + .value("Cone", rayx::CurvatureType::Cone) + .value("Cylinder", rayx::CurvatureType::Cylinder) + .value("Ellipsoid", rayx::CurvatureType::Ellipsoid) + .value("Paraboloid", rayx::CurvatureType::Paraboloid) + .value("Quadric", rayx::CurvatureType::Quadric) + .value("RzpSphere", rayx::CurvatureType::RzpSphere); + + py::enum_(m, "DesignPlane").value("XY", rayx::DesignPlane::XY).value("XZ", rayx::DesignPlane::XZ); + + py::enum_(m, "ElementType") .value("UNDEFINED", rayx::ElementType::Undefined) .value("IMAGE_PLANE", rayx::ElementType::ImagePlane) .value("CONE_MIRROR", rayx::ElementType::ConeMirror) @@ -411,54 +387,53 @@ PYBIND11_MODULE(_core, m) { .value("PIXEL_SOURCE", rayx::ElementType::PixelSource) .value("CIRCLE_SOURCE", rayx::ElementType::CircleSource) .value("SIMPLE_UNDULATOR_SOURCE", rayx::ElementType::SimpleUndulatorSource) - .value("RAY_LIST_SOURCE", rayx::ElementType::RayListSource) - .export_values(); + .value("RAY_LIST_SOURCE", rayx::ElementType::RayListSource); - pybind11::enum_(m, "EventType") + py::enum_(m, "EventType") .value("UNINITIALIZED", rayx::EventType::Uninitialized) .value("EMITTED", rayx::EventType::Emitted) .value("HIT_ELEMENT", rayx::EventType::HitElement) .value("FATAL_ERROR", rayx::EventType::FatalError) .value("ABSORBED", rayx::EventType::Absorbed) .value("BEYOND_HORIZON", rayx::EventType::BeyondHorizon) - .value("TOO_MANY_EVENTS", rayx::EventType::TooManyEvents) - .export_values(); - - pybind11::class_>(m, "Array[double]", pybind11::buffer_protocol()) - .def_buffer([](data_buffer& db) { return db.buffer_info(); }) - .def("as_numpy", &data_buffer::as_numpy_array); - pybind11::class_>>(m, "Array[complex[double]]", pybind11::buffer_protocol()) - .def_buffer([](data_buffer>& db) { return db.buffer_info(); }) - .def("as_numpy", &data_buffer>::as_numpy_array); - pybind11::class_>(m, "Array[int]", pybind11::buffer_protocol()) - .def_buffer([](data_buffer& db) { return db.buffer_info(); }) - .def("as_numpy", &data_buffer::as_numpy_array); - pybind11::class_>(m, "Array[event_type]", pybind11::buffer_protocol()) - .def_buffer([](data_buffer& db) { return db.buffer_info(); }) - .def("as_numpy", &data_buffer::as_numpy_array); - - pybind11::class_(m, "Rays") - .def_property_readonly("position_x", [](const Rays& r) { return r.position_x.as_numpy_array(); }) - .def_property_readonly("position_y", [](const Rays& r) { return r.position_y.as_numpy_array(); }) - .def_property_readonly("position_z", [](const Rays& r) { return r.position_z.as_numpy_array(); }) - .def_property_readonly("direction_x", [](const Rays& r) { return r.direction_x.as_numpy_array(); }) - .def_property_readonly("direction_y", [](const Rays& r) { return r.direction_y.as_numpy_array(); }) - .def_property_readonly("direction_z", [](const Rays& r) { return r.direction_z.as_numpy_array(); }) - .def_property_readonly("energy", [](const Rays& r) { return r.energy.as_numpy_array(); }) - .def_property_readonly("electric_field_x", [](const Rays& r) { return r.electric_field_x.as_numpy_array(); }) - .def_property_readonly("electric_field_y", [](const Rays& r) { return r.electric_field_y.as_numpy_array(); }) - .def_property_readonly("electric_field_z", [](const Rays& r) { return r.electric_field_z.as_numpy_array(); }) - .def_property_readonly("path_length", [](const Rays& r) { return r.path_length.as_numpy_array(); }) - .def_property_readonly("order", [](const Rays& r) { return r.order.as_numpy_array(); }) - .def_property_readonly("event_type", [](const Rays& r) { return r.event_type.as_numpy_array(); }) - .def_property_readonly("last_element_id", [](const Rays& r) { return r.last_element_id.as_numpy_array(); }) - .def_property_readonly("source_id", [](const Rays& r) { return r.source_id.as_numpy_array(); }) - .def_property_readonly("ray_id", [](const Rays& r) { return r.ray_id.as_numpy_array(); }) - .def_property_readonly("path_event_id", [](const Rays& r) { return r.path_event_id.as_numpy_array(); }); - - pybind11::class_(m, "Beamline") - .def_property_readonly("elements", &rayx::Beamline::getElements) - .def_property_readonly("sources", &rayx::Beamline::getSources) + .value("TOO_MANY_EVENTS", rayx::EventType::TooManyEvents); + + py::class_(m, "Rays") + .def_prop_ro("path_id", [](rayx::Rays& rays) { return to_numpy(rays.path_id); }) + .def_prop_ro("path_event_id", [](rayx::Rays& rays) { return to_numpy(rays.path_event_id); }) + .def_prop_ro( + "position_x", [](rayx::Rays& rays) { return to_numpy(rays.position_x); }, py::rv_policy::reference_internal) + .def_prop_ro( + "position_y", [](rayx::Rays& rays) { return to_numpy(rays.position_y); }, py::rv_policy::reference_internal) + .def_prop_ro( + "position_z", [](rayx::Rays& rays) { return to_numpy(rays.position_z); }, py::rv_policy::reference_internal) + .def_prop_ro( + "direction_x", [](rayx::Rays& rays) { return to_numpy(rays.direction_x); }, py::rv_policy::reference_internal) + .def_prop_ro( + "direction_y", [](rayx::Rays& rays) { return to_numpy(rays.direction_y); }, py::rv_policy::reference_internal) + .def_prop_ro( + "direction_z", [](rayx::Rays& rays) { return to_numpy(rays.direction_z); }, py::rv_policy::reference_internal) + .def_prop_ro( + "electric_field_x", [](rayx::Rays& rays) { return to_numpy(rays.electric_field_x); }, py::rv_policy::reference_internal) + .def_prop_ro( + "electric_field_y", [](rayx::Rays& rays) { return to_numpy(rays.electric_field_y); }, py::rv_policy::reference_internal) + .def_prop_ro( + "electric_field_z", [](rayx::Rays& rays) { return to_numpy(rays.electric_field_z); }, py::rv_policy::reference_internal) + .def_prop_ro( + "optical_path_length", [](rayx::Rays& rays) { return to_numpy(rays.optical_path_length); }, py::rv_policy::reference_internal) + .def_prop_ro( + "energy", [](rayx::Rays& rays) { return to_numpy(rays.energy); }, py::rv_policy::reference_internal) + .def_prop_ro( + "order", [](rayx::Rays& rays) { return to_numpy(rays.order); }, py::rv_policy::reference_internal) + .def_prop_ro( + "object_id", [](rayx::Rays& rays) { return to_numpy(rays.object_id); }, py::rv_policy::reference_internal) + .def_prop_ro( + "source_id", [](rayx::Rays& rays) { return to_numpy(rays.source_id); }, py::rv_policy::reference_internal) + .def_prop_ro("event_type", [](rayx::Rays& rays) { return to_numpy(rays.event_type); }, py::rv_policy::reference_internal); + + py::class_(m, "Beamline") + .def_prop_ro("elements", &rayx::Beamline::getElements) + .def_prop_ro("sources", &rayx::Beamline::getSources) .def("trace", [](rayx::Beamline& bl) { rayx::DeviceConfig deviceConfig = rayx::DeviceConfig().enableBestDevice(); @@ -466,22 +441,21 @@ PYBIND11_MODULE(_core, m) { rayx::ObjectMask obj_mask = rayx::ObjectMask::all(); rayx::RayAttrMask attr_mask = rayx::RayAttrMask::All; rayx::Rays rays = tracer.trace(bl, rayx::Sequential::No, obj_mask, attr_mask, std::nullopt, std::nullopt); - return Rays(std::move(rays)); + return rays; }) .def("__getitem__", [](rayx::Beamline& bl, const std::string& name) { for (auto element : bl.getElements()) { if (element->getName() == name) { - return pybind11::cast(element); + return py::cast(element); } } for (auto source : bl.getSources()) { if (source->getName() == name) { - return pybind11::cast(source); + return py::cast(source); } } throw std::runtime_error("No element or source with name '" + name + "' found in beamline."); }); - m.def( - "import_beamline", [](std::string path) { return rayx::importBeamline(path); }, "Import a beamline from an RML file", pybind11::arg("path")); + m.def("import_beamline", [](std::string path) { return rayx::importBeamline(path); }, "Import a beamline from an RML file", py::arg("path")); } diff --git a/src/reflection.hpp b/src/reflection.hpp index c6f91c1..4856eed 100644 --- a/src/reflection.hpp +++ b/src/reflection.hpp @@ -1,13 +1,15 @@ #pragma once #include -#include +#include +#include +#include #include #include #include -namespace py = pybind11; +namespace py = nanobind; namespace reflect { @@ -70,16 +72,38 @@ concept Variant = requires(T v) { v.visit(any_callable{}); }; +template +py::sig property_sig(const char* name) { + py::handle type = py::type(); + std::string s = std::string("def ") + name + "(self): -> " + py::type_name(type).c_str(); + return py::sig(s.c_str()); +} + +template +struct pytype_t { + using value_t = T; +}; + +template +struct pytype_t { + using value_t = Ref; +}; + +template +struct pytype_t> { + using value_t = std::variant...>; +}; + template void bind(py::class_& cls, const field_info& field) { - cls.def_property( + cls.def_prop_rw( field.name, - [field](S& self) { + [field](S& self) -> py::typed::value_t> { if constexpr (Structure) { return py::cast(Ref{[&self, field]() { return self.*(field.member); }, [&self, field](M value) { self.*(field.member) = value; }}); } else if constexpr (Variant) { M m = self.*(field.member); - return m.visit([field, &self](U&& value) { + return m.visit([field, &self](U&& value) -> py::object { return py::cast(Ref>{ [&self, field] { return self.*(field.member).template get>(); }, [&self, field](U new_value) { self.*(field.member).template get>() = new_value; }}); @@ -92,15 +116,15 @@ void bind(py::class_& cls, const field_info& field) { template void bind(py::class_& cls, const prop_info& prop) { - cls.def_property( + cls.def_prop_rw( prop.name, - [prop](S& self) { + [prop](S& self) -> py::typed::value_t> { if constexpr (Structure) { return py::cast( Ref{[&self, prop]() { return (self.*(prop.getter))(); }, [&self, prop](M value) { (self.*(prop.setter))(value); }}); } else if constexpr (Variant) { M m = (self.*(prop.getter))(); - return m.visit([prop, &self](U&& value) { + return m.visit([prop, &self](U&& value) -> py::object { return py::cast(Ref>{[&self, prop] { M m_local = (self.*(prop.getter))(); return m_local.template get>(); @@ -119,9 +143,9 @@ void bind(py::class_& cls, const prop_info& prop) { template void bind_ref(py::class_>& cls, const field_info& field) { - cls.def_property( + cls.def_prop_rw( field.name, - [field](Ref& self) { + [field](Ref& self) -> py::typed::value_t> { if constexpr (Structure) { return py::cast(Ref{[&self, field]() { return self.get().*(field.member); }, [&self, field](M value) { @@ -132,7 +156,7 @@ void bind_ref(py::class_>& cls, const field_info& field) { } else if constexpr (Variant) { S s = self.get(); M m = s.*(field.member); - return m.visit([field, &self, s](U&& value) { + return m.visit([field, &self, s](U&& value) -> py::object { return py::cast(Ref>{[&self, field] { S s_local = self.get(); return s_local.*(field.member); @@ -156,9 +180,9 @@ void bind_ref(py::class_>& cls, const field_info& field) { template void bind_ref(py::class_>& cls, const prop_info& prop) { - cls.def_property( + cls.def_prop_rw( prop.name, - [prop](Ref& self) { + [prop](Ref& self) -> py::typed::value_t> { if constexpr (Structure) { return py::cast(Ref{[&self, prop]() { S s = self.get(); @@ -172,7 +196,7 @@ void bind_ref(py::class_>& cls, const prop_info& prop) { } else if constexpr (Variant) { S s = self.get(); M m = (s.*(prop.getter))(); - return m.visit([prop, &self, s](U&& value) { + return m.visit([prop, &self, s](U&& value) -> py::object { return py::cast(Ref>{[&self, prop] { S s_local = self.get(); M m_local = (s_local.*(prop.getter))(); @@ -210,6 +234,11 @@ concept is_primitive = std::is_arithmetic_v || std::is_same_v template void register_type(py::module_& m) {}; +/* template +void register_type(py::module_& m) { + register_type(m); +}; */ + template void register_type(py::module_& m); @@ -237,13 +266,10 @@ void register_type(py::module_& m) { // add conversion from Ref to T if T is copy constructible if constexpr (std::is_copy_constructible_v) { - py::class_> ref_cls(m, ("Ref[" + std::string(name) + "]").c_str()); + py::class_> ref_cls(m, (std::string(name) + "Ref").c_str()); std::apply([&](auto&&... field) { (bind_ref(ref_cls, field), ...); }, info::fields); - cls.def(py::init([](const Ref& ref) { - T value = ref.get(); - return new T(value); - })); + cls.def("__init__", [](T* res, const Ref& ref) { *res = ref.get(); }); py::implicitly_convertible, T>(); } } @@ -256,9 +282,7 @@ void register_alternative(py::module_& m, py::class_ cls) { register_type(m); - cls.def(py::init([](const U& u) { return new T(u); })); - - py::implicitly_convertible(); + cls.def(py::init_implicit()); } template @@ -277,12 +301,9 @@ void register_type(py::module_& m) { py::class_ cls(m, name); cls.def(py::init<>()); - py::class_> ref_cls(m, ("Ref[" + std::string(name) + "]").c_str()); + py::class_> ref_cls(m, (std::string(name) + "Ref").c_str()); - cls.def(py::init([](const Ref& ref) { - T value = ref.get(); - return new T(value); - })); + cls.def("__init__", [](T* res, const Ref& ref) { *res = ref.get(); }); py::implicitly_convertible, T>(); From 4f2d4b06966c394078dbcd6429457d6f3b9f6cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20St=C3=B6cker?= Date: Tue, 28 Apr 2026 16:25:17 +0200 Subject: [PATCH 2/2] add remaining enums --- python/rayx/__init__.py | 37 ------------------------------------- python/rayx/data.py | 34 ---------------------------------- src/main.cpp | 16 ++++++++++++++++ 3 files changed, 16 insertions(+), 71 deletions(-) delete mode 100644 python/rayx/__init__.py delete mode 100644 python/rayx/data.py diff --git a/python/rayx/__init__.py b/python/rayx/__init__.py deleted file mode 100644 index f7f01f0..0000000 --- a/python/rayx/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -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'] diff --git a/python/rayx/data.py b/python/rayx/data.py deleted file mode 100644 index bceba80..0000000 --- a/python/rayx/data.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -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 diff --git a/src/main.cpp b/src/main.cpp index 737ef65..fd2369f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -362,6 +362,22 @@ NB_MODULE(core, m) { py::enum_(m, "DesignPlane").value("XY", rayx::DesignPlane::XY).value("XZ", rayx::DesignPlane::XZ); + py::enum_(m, "BehaviourType") + .value("Mirror", rayx::BehaviourType::Mirror) + .value("Grating", rayx::BehaviourType::Grating) + .value("Slit", rayx::BehaviourType::Slit) + .value("Rzp", rayx::BehaviourType::Rzp) + .value("ImagePlane", rayx::BehaviourType::ImagePlane) + .value("Crystal", rayx::BehaviourType::Crystal) + .value("Foil", rayx::BehaviourType::Foil); + + py::enum_(m, "SurfaceCoatingType") + .value("SubstrateOnly", rayx::SurfaceCoatingType::SubstrateOnly) + .value("OneCoating", rayx::SurfaceCoatingType::OneCoating) + .value("MultipleCoatings", rayx::SurfaceCoatingType::MultipleCoatings); + + py::enum_(m, "SigmaType").value("Standard", rayx::SigmaType::ST_STANDARD).value("Accurate", rayx::SigmaType::ST_ACCURATE); + py::enum_(m, "ElementType") .value("UNDEFINED", rayx::ElementType::Undefined) .value("IMAGE_PLANE", rayx::ElementType::ImagePlane)