diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9e4244d2..56f3cba4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v2 - - uses: conda-incubator/setup-miniconda@v2 + - uses: actions/checkout@v4 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true miniconda-version: "latest" @@ -48,7 +48,7 @@ jobs: working-directory: ${{github.workspace}}/build shell: bash -l {0} # Execute the build. You can specify a specific target with "--target " - run: cmake --build . --config $BUILD_TYPE + run: cmake --build . --verbose --config $BUILD_TYPE - name: Test working-directory: ${{github.workspace}}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 477666c7..b2d6fb39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,8 +17,9 @@ set (CMAKE_CXX_STANDARD 11) option(NDARRAY_TEST "Enable tests?" ON) option(NDARRAY_EIGEN "Enable Eigen tests?" ON) -option(NDARRAY_FFTW "Enable FFTW tests?" ON) -option(NDARRAY_PYBIND11 "Enable Pybind11 tests?" OFF) +option(NDARRAY_FFTW "Enable FFTW tests?" Off) +option(NDARRAY_PYBIND11 "Enable Pybind11 tests?" On) +option(NDARRAY_NANOBIND "Enable Nanobind tests?" On) add_subdirectory(include) diff --git a/etc/conda-forge-testing.yaml b/etc/conda-forge-testing.yaml index 2a4f1a3d..7892e12a 100644 --- a/etc/conda-forge-testing.yaml +++ b/etc/conda-forge-testing.yaml @@ -7,6 +7,7 @@ dependencies: - fftw - numpy - pybind11 + - nanobind - c-compiler - eigen - cmake diff --git a/include/ndarray/nanobind.h b/include/ndarray/nanobind.h new file mode 100644 index 00000000..e53d5572 --- /dev/null +++ b/include/ndarray/nanobind.h @@ -0,0 +1,273 @@ +/* + * LSST Data Management System + * Copyright 2008-2016 AURA/LSST. + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ + +#ifndef NDARRAY_nanobind_h_INCLUDED +#define NDARRAY_nanobind_h_INCLUDED + +/** + * @file ndarray/nanobind.h + * @brief Public header file for pybind11-based Python support. + * + * \warning Both the Numpy C-API headers "arrayobject.h" and + * "ufuncobject.h" must be included before ndarray/python.hpp + * or any of the files in ndarray/python. + * + * \note This file is not included by the main "ndarray.h" header file. + */ + +/** \defgroup ndarrayPythonGroup Python Support + * + * The ndarray Python support module provides conversion + * functions between ndarray objects, notably Array and + * Vector, and Python Numpy objects. + */ + +#include +#include +#include "ndarray.h" +#include "ndarray/eigen.h" +#include "ndarray/Array.h" + +#include + +namespace nb = nanobind; +namespace ndarray { +namespace detail { + + inline void destroyCapsule(PyObject *p) { + void *m = PyCapsule_GetPointer(p, "ndarray.Manager"); + Manager::Ptr *b = reinterpret_cast(m); + delete b; + } + +} // namespace ndarray::detail + +inline PyObject *makePyManager(Manager::Ptr const &m) { + return PyCapsule_New( + new Manager::Ptr(m), + "ndarray.Manager", + detail::destroyCapsule + ); +} + +template +struct +#ifdef __GNUG__ +// pybind11 hides all symbols in its namespace only when this is set, +// and in that case we should hide these classes too. + __attribute__((visibility("hidden"))) +#endif +NanobindHelper { +}; + +} // namespace ndarray +NAMESPACE_BEGIN(NB_NAMESPACE) +NAMESPACE_BEGIN(detail) +template +struct type_caster<::ndarray::Array> { + using Wrapper = std::remove_const_t>; + using ArrayType = nb::ndarray> ; + using Array = std::conditional_t, const ArrayType, ArrayType>; + using Element = typename std::remove_const_t; + static constexpr bool isConst = std::is_const::value; + + using Value = ::ndarray::Array; + static constexpr auto Name = const_name("ndarray"); + template using Cast = movable_cast_t; + + static handle from_cpp(Value *p, rv_policy policy, cleanup_list *list) { + if (!p)return none().release(); + return from_cpp(*p, policy, list); + } + + bool init(nb::handle src, cleanup_list *cleanup) { + isNone = src.is_none(); + if (isNone) { + return true; + } + + int64_t shape[N]; + ndarray_config config; + config.shape = shape; + wrapper = Wrapper(ndarray_import( + src.ptr(), &config, true, cleanup)); + return wrapper.is_valid(); + } + + bool check() const { + if (isNone) { + return true; + } + + if (wrapper.ndim() != N) { + return false; + } + if(wrapper.dtype().bits != sizeof(Element) * 8) { + return false; + } + switch(dlpack::dtype_code(wrapper.dtype().code)) { + case dlpack::dtype_code::Float: + if(!std::is_floating_point_v) return false; + break; + case dlpack::dtype_code::Int: + if(!(std::is_signed_v && std::is_integral_v)) return false; + break; + case dlpack::dtype_code::UInt: + if(!(std::is_unsigned_v && std::is_integral_v)) return false; + break; + case dlpack::dtype_code::Bool: + if(!std::is_same_v) return false; + break; + default: + return false; + } + + //if (!isConst && !wrapper.writeable()) { + // return false; + //} + + int64_t const * shape = wrapper.shape_ptr(); + int64_t const * strides = wrapper.stride_ptr(); + size_t const itemsize = wrapper.itemsize(); + if (C > 0) { + // If the shape is zero in any dimension, we don't + // worry about the strides. + for (int i = 0; i < C; ++i) { + if (shape[N-i-1] == 0) { + return true; + } + } + + int64_t requiredStride = 1;//itemsize; + for (int i = 0; i < C; ++i) { + if (strides[N-i-1] != requiredStride) { + return false; + } + requiredStride *= shape[N-i-1]; + } + } else if (C < 0) { + // If the shape is zero in any dimension, we don't + // worry about the strides. + for (int i = 0; i < -C; ++i) { + if (shape[i] == 0) { + return true; + } + } + size_t requiredStride = itemsize; + for (int i = 0; i < -C; ++i) { + if (strides[i] != requiredStride) { + return false; + } + requiredStride *= shape[i]; + } + } + return true; + } + + Value convert() const { + if (isNone) { + return Value(); + } + + //if (!wrapper.dtype().attr("isnative")) { + // throw nb::type_error("Only arrays with native byteorder can be converted to C++."); + //} + + ::ndarray::Vector<::ndarray::Size,N> nShape; + ::ndarray::Vector<::ndarray::Offset,N> nStrides; + int64_t const * pShape = wrapper.shape_ptr(); + int64_t const * pStrides = wrapper.stride_ptr(); + size_t itemsize = wrapper.itemsize(); + for (int i = 0; i < N; ++i) { + nShape[i] = pShape[i]; + nStrides[i] = pStrides[i]; + } + + auto *p = const_cast(wrapper.data()); + + return Value ( + ::ndarray::external(const_cast(wrapper.data()), + nShape, nStrides, wrapper) + ); + } + + void set_value() { + value = convert(); + } + + explicit operator Value * () { + if (isNone) { + return nullptr; + } else { + set_value(); + return &value; + } + } + + explicit operator Value &() { + set_value(); + return (Value &) value; + } + + explicit operator Value &&() { + set_value(); + return (Value &&) value; + } + + + bool from_python(nb::handle src, uint8_t flags, cleanup_list *cleanup) noexcept { + bool result = init(src, cleanup) && check(); + return result; + } + static nb::handle from_cpp(const ::ndarray::Array &src, rv_policy policy, + cleanup_list *cleanup) noexcept { + using ArrayType = nb::ndarray> ; + using Array = std::conditional_t, const ArrayType, ArrayType>; + using Element = typename std::remove_const_t; + ::ndarray::Vector<::ndarray::Size,N> nShape = src.getShape(); + ::ndarray::Vector<::ndarray::Offset,N> nStrides = src.getStrides(); + std::vector pShape(N); + std::vector pStrides(N); + for (int i = 0; i < N; ++i) { + pShape[i] = nShape[i]; + pStrides[i] = nStrides[i]; + } + nb::object base = nb::object(); + if (src.getManager()) { + base = nb::steal(::ndarray::makePyManager(src.getManager())); + } + Array array((Element*)src.getData(), N, pShape.data(), base, pStrides.data()); + + nb::handle result = ndarray_export(array.handle(), nb::numpy::value, policy, cleanup); + if (std::is_const_v) { + result.attr("flags")["WRITEABLE"] = false; + } + return result; + } +private: + bool isNone = false; + Value value; + Wrapper wrapper = Wrapper(); +}; +NAMESPACE_END(detail); +NAMESPACE_END(NB_NAMESPACE); +#endif diff --git a/include/ndarray/pybind11.h b/include/ndarray/pybind11.h index bd1eaeda..ee3aa55b 100644 --- a/include/ndarray/pybind11.h +++ b/include/ndarray/pybind11.h @@ -197,6 +197,7 @@ Pybind11Helper { if (src.getManager()) { base = pybind11::reinterpret_steal(ndarray::makePyManager(src.getManager())); } + Wrapper result(pShape, pStrides, src.getData(), base); if (std::is_const::value) { result.attr("flags")["WRITEABLE"] = false; @@ -220,9 +221,9 @@ class type_caster< ndarray::Array > { using Helper = ndarray::Pybind11Helper; public: - bool load(handle src, bool) { + bool load(handle src, bool) { return _helper.init(src) && _helper.check(); - } + } void set_value() { _value = _helper.convert(); @@ -238,7 +239,7 @@ class type_caster< ndarray::Array > { return cast(*src, policy, parent); } - operator ndarray::Array * () { + explicit operator ndarray::Array * () { if (_helper.isNone) { return nullptr; } else { @@ -247,7 +248,9 @@ class type_caster< ndarray::Array > { } } - operator ndarray::Array & () { set_value(); return _value; } + explicit operator ndarray::Array & () { + set_value(); return _value; + } template using cast_op_type = pybind11::detail::cast_op_type<_T>; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a36ae630..d84d6169 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,7 +8,7 @@ # (4) Addition of the test executable via add_test. ### Core tests, which rely only on boost-test and ndarray. -find_package(Boost COMPONENTS unit_test_framework REQUIRED) +find_package(Boost COMPONENTS headers unit_test_framework REQUIRED) include_directories( ${PROJECT_SOURCE_DIR}/include) @@ -64,3 +64,30 @@ if(NDARRAY_PYBIND11) message(STATUS "Skipping pybind11 tests as they depend on Eigen") endif(NDARRAY_EIGEN) endif(NDARRAY_PYBIND11) + +###Nanobind dependency tests (also depend on Eigen) +if(NDARRAY_NANOBIND) + if(NDARRAY_EIGEN) + find_package(Python + REQUIRED COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule) + execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR) + list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") + find_package(nanobind CONFIG REQUIRED) + + nanobind_add_module(nanobind_test_mod + nanobind_test_mod.cc + STABLE_ABI + NB_SHARED) + target_link_libraries(nanobind_test_mod PRIVATE Boost::headers Eigen3::Eigen) + configure_file(nanobind_test.py nanobind_test.py COPYONLY) + add_test(NAME nanobind_test + COMMAND ${Python_EXECUTABLE} + ${CMAKE_CURRENT_BINARY_DIR}/nanobind_test.py) + else(NDARRAY_EIGEN) + message(STATUS "Skipping nanobind tests as they depend on Eigen") + endif(NDARRAY_EIGEN) +endif(NDARRAY_NANOBIND) + diff --git a/tests/nanobind_test.py b/tests/nanobind_test.py new file mode 100644 index 00000000..ec70c97f --- /dev/null +++ b/tests/nanobind_test.py @@ -0,0 +1,75 @@ +# -*- python -*- +# +# Copyright (c) 2010-2012, Jim Bosch +# All rights reserved. +# +# ndarray is distributed under a simple BSD-like license; +# see the LICENSE file that should be present in the root +# of the source distribution, or alternately available at: +# https://github.com/ndarray/ndarray +# +import numpy +import unittest + +import nanobind_test_mod + + +class TestNumpyNanobind(unittest.TestCase): + + def testArray1(self): + a1 = nanobind_test_mod.returnArray1() + a2 = numpy.arange(6, dtype=float) + self.assertTrue((a1 == a2).all()) + self.assertTrue(nanobind_test_mod.acceptArray1(a2)) + a3 = nanobind_test_mod.returnConstArray1() + self.assertTrue((a1 == a3).all()) + self.assertFalse(a3.flags["WRITEABLE"]) + + def testArray3(self): + pass + a1 = nanobind_test_mod.returnArray3() + a2 = numpy.arange(4*3*2, dtype=float).reshape(4, 3, 2) + self.assertTrue((a1 == a2).all()) + self.assertTrue(nanobind_test_mod.acceptArray3(a2)) + a3 = nanobind_test_mod.returnConstArray3() + self.assertTrue((a1 == a3).all()) + self.assertFalse(a3.flags["WRITEABLE"]) + + def testStrideHandling(self): + pass + # in NumPy 1.8+ 1- and 0-sized arrays can have arbitrary strides; we should + # be able to handle those + array = numpy.zeros(1, dtype=float) + # the zero shape array tests are simply checking that nanobind can handle + # arbitrary strides (non-zero for length 1 array, zero for length 0 array + # for numpy >= 1.23). + nanobind_test_mod.acceptAnyArray10(array) + nanobind_test_mod.acceptAnyArray11(array) + array = numpy.zeros(0, dtype=float) + nanobind_test_mod.acceptAnyArray10(array) + nanobind_test_mod.acceptAnyArray11(array) + # test that we gracefully fail when the strides are no multiples of the itemsize + dtype = numpy.dtype([("f1", numpy.float64), ("f2", numpy.int16)]) + table = numpy.zeros(3, dtype=dtype) + self.assertRaises(TypeError, nanobind_test_mod.acceptAnyArray10, table['f1']) + self.assertRaises(TypeError, nanobind_test_mod.acceptAnyArray11, table['f1']) + + def testNone(self): + pass + array = numpy.zeros(10, dtype=float) + self.assertEqual(nanobind_test_mod.acceptNoneArray(array), 0) + self.assertEqual(nanobind_test_mod.acceptNoneArray(None), 1) + self.assertEqual(nanobind_test_mod.acceptNoneArray(), 1) + + def testNonNativeByteOrder(self): + pass + d1 = numpy.dtype("f8") + nonnative = d2 if d1 == numpy.dtype(float) else d1 + a = numpy.zeros(5, dtype=nonnative) + self.assertRaises(TypeError, nanobind_test_mod.acceptAnyArray10, a) + self.assertRaises(TypeError, nanobind_test_mod.acceptAnyArray11, a) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/nanobind_test_mod.cc b/tests/nanobind_test_mod.cc new file mode 100644 index 00000000..653bdddb --- /dev/null +++ b/tests/nanobind_test_mod.cc @@ -0,0 +1,77 @@ +#include "ndarray/nanobind.h" +#include + +namespace nb = nanobind; +using namespace nb::literals; + +::ndarray::Array returnArray1() { + ::ndarray::Array r(::ndarray::allocate(::ndarray::makeVector(6))); + for (int n = 0; n < r.getSize<0>(); ++n) { + r[n] = n; + } + return r; +} + +::ndarray::Array returnConstArray1() { + return returnArray1(); +} + +::ndarray::Array returnArray3() { + ::ndarray::Array r(::ndarray::allocate(::ndarray::makeVector(4,3,2))); + ::ndarray::Array f = ::ndarray::flatten<1>(r); + for (int n = 0; n < f.getSize<0>(); ++n) { + f[n] = n; + } + return r; +} + +::ndarray::Array returnConstArray3() { + return returnArray3(); +} + +bool acceptArray1(::ndarray::Array const & a1) { + ::ndarray::Array a2 = returnArray1(); +#ifndef GCC_45 + return ::ndarray::all(::ndarray::equal(a1, a2)); +#else + return std::equal(a1.begin(), a1.end(), a2.begin()); +#endif +} + +void acceptAnyArray10(::ndarray::Array const & a1) {} + +void acceptAnyArray11(::ndarray::Array const & a1) {} + +bool acceptArray3(::ndarray::Array const & a1) { + ::ndarray::Array a2 = returnArray3(); +#ifndef GCC_45 + return ::ndarray::all(ndarray::equal(a1, a2)); +#else + for (int i = 0; i < a1.getSize<0>(); ++i) { + for (int j = 0; j < a1.getSize<1>(); ++j) { + if (!std::equal(a1[i][j].begin(), a1[i][j].end(), a2[i][j].begin())) return false; + } + } + return true; +#endif +} + +int acceptNoneArray(::ndarray::Array const * array = nullptr) { + if (array) { + return 0; + } else { + return 1; + } +} + +NB_MODULE(nanobind_test_mod, mod) { + mod.def("returnArray1", returnArray1); + mod.def("returnConstArray1", returnConstArray1); + mod.def("returnArray3", returnArray3); + mod.def("returnConstArray3", returnConstArray3); + mod.def("acceptArray1", acceptArray1); + mod.def("acceptAnyArray10", acceptAnyArray10); + mod.def("acceptAnyArray11", acceptAnyArray11); + mod.def("acceptArray3", acceptArray3); + mod.def("acceptNoneArray", acceptNoneArray, "array"_a = nullptr); +}