From 0ba6eb67957f9098afb681e173d262d12d95c9ce Mon Sep 17 00:00:00 2001 From: Ralph Urlus Date: Sat, 21 Oct 2023 22:32:35 +0200 Subject: [PATCH] PKG: Restructure headers --- include/carma | 12 + include/carma.hpp | 98 -- include/carma_bits/array_view.hpp | 194 ---- include/carma_bits/base/config.hpp | 35 + include/carma_bits/base/converter_types.hpp | 76 ++ include/carma_bits/base/converters.hpp | 196 ++++ include/carma_bits/base/numpy_converters.hpp | 79 ++ include/carma_bits/base/outconv.hpp | 0 include/carma_bits/config.hpp | 40 + include/carma_bits/converter_types.hpp | 207 +++++ include/carma_bits/converters.hpp | 341 +------ include/carma_bits/extension/config.hpp | 57 ++ .../carma_bits/extension/converter_types.hpp | 315 +++++++ include/carma_bits/extension/converters.hpp | 369 ++++++++ .../{ => extension}/numpy_alloc.hpp | 14 +- .../carma_bits/extension/numpy_container.hpp | 139 +++ .../carma_bits/extension/numpy_converters.hpp | 178 ++++ include/carma_bits/extension/outconv.hpp | 0 .../carma_bits/internal/arma_container.hpp | 0 include/carma_bits/{ => internal}/common.hpp | 12 +- .../carma_bits/internal/converter_types.hpp | 98 ++ .../carma_bits/{ => internal}/numpy_api.hpp | 1 + .../carma_bits/internal/numpy_container.hpp | 144 +++ .../carma_bits/internal/numpy_converters.hpp | 117 +++ .../{to_numpy.hpp => internal/outconv.hpp} | 82 +- include/carma_bits/internal/type_traits.hpp | 193 ++++ include/carma_bits/to_arma.hpp | 837 ------------------ include/carma_bits/type_traits.hpp | 89 -- include/carma_bits/version.hpp | 26 + 29 files changed, 2379 insertions(+), 1570 deletions(-) create mode 100644 include/carma delete mode 100644 include/carma.hpp delete mode 100644 include/carma_bits/array_view.hpp create mode 100644 include/carma_bits/base/config.hpp create mode 100644 include/carma_bits/base/converter_types.hpp create mode 100644 include/carma_bits/base/converters.hpp create mode 100644 include/carma_bits/base/numpy_converters.hpp create mode 100644 include/carma_bits/base/outconv.hpp create mode 100644 include/carma_bits/config.hpp create mode 100644 include/carma_bits/converter_types.hpp create mode 100644 include/carma_bits/extension/config.hpp create mode 100644 include/carma_bits/extension/converter_types.hpp create mode 100644 include/carma_bits/extension/converters.hpp rename include/carma_bits/{ => extension}/numpy_alloc.hpp (83%) create mode 100644 include/carma_bits/extension/numpy_container.hpp create mode 100644 include/carma_bits/extension/numpy_converters.hpp create mode 100644 include/carma_bits/extension/outconv.hpp create mode 100644 include/carma_bits/internal/arma_container.hpp rename include/carma_bits/{ => internal}/common.hpp (95%) create mode 100644 include/carma_bits/internal/converter_types.hpp rename include/carma_bits/{ => internal}/numpy_api.hpp (98%) create mode 100644 include/carma_bits/internal/numpy_container.hpp create mode 100644 include/carma_bits/internal/numpy_converters.hpp rename include/carma_bits/{to_numpy.hpp => internal/outconv.hpp} (52%) create mode 100644 include/carma_bits/internal/type_traits.hpp delete mode 100644 include/carma_bits/to_arma.hpp delete mode 100644 include/carma_bits/type_traits.hpp create mode 100644 include/carma_bits/version.hpp diff --git a/include/carma b/include/carma new file mode 100644 index 00000000..994aaf8a --- /dev/null +++ b/include/carma @@ -0,0 +1,12 @@ +/* carma: Bidirectional coverter of Numpy arrays and Armadillo objects + * Copyright (c) 2023 Ralph Urlus + * All rights reserved. Use of this source code is governed by a + * Apache-2.0 license that can be found in the LICENSE file. + */ +#pragma once + +#include +// config should be included first +#include +#include +#include diff --git a/include/carma.hpp b/include/carma.hpp deleted file mode 100644 index 5c66d6fd..00000000 --- a/include/carma.hpp +++ /dev/null @@ -1,98 +0,0 @@ -/* carma/carma: Bidirectional coverter of Numpy arrays and Armadillo objects - * Copyright (c) 2023 Ralph Urlus - * All rights reserved. Use of this source code is governed by a - * Apache-2.0 license that can be found in the LICENSE file. - */ -#pragma once - -/* If the Numpy allocator/deallocator have not been set through - * the carma_armadillo target ARMA_ALIEN_MEM_ALLOC_FUNCTION and - * ARMA_ALIEN_MEM_FREE_FUNCTION need to be set. - * - * This requires that Armadillo wasn't included before carma - * The CMake script handles this by pre-compiling the numpy_alloc header - */ -#ifndef CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET -#if defined(ARMA_VERSION_MAJOR) -#error "|carma| please include the armadillo header after the carma header or use carma's CMake build" -#endif -#include -#endif - -#include - -#ifdef CARMA_EXTRA_DEBUG -#ifndef CARMA_DEBUG -#define CARMA_DEBUG true -#endif // CARMA_DEBUG - -#include -#endif // CARMA_EXTRA_DEBUG - -#include - -#if defined(CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET) -#if ((!defined(ARMA_ALIEN_MEM_ALLOC_FUNCTION)) || (!defined(ARMA_ALIEN_MEM_FREE_FUNCTION))) -#error \ - "|carma| ARMA_ALIEN_MEM_ALLOC_FUNCTION and or ARMA_ALIEN_MEM_FREE_FUNCTION not set while CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET is" -#endif -#endif - -#define CARMA_ARMA_VERSION (ARMA_VERSION_MAJOR * 10000 + ARMA_VERSION_MINOR * 100 + ARMA_VERSION_PATCH) -static_assert(CARMA_ARMA_VERSION > 100502, "|carma| minimum supported armadillo version is 10.5.2"); - -#ifndef CARMA_VERSION_MAJOR -#define CARMA_VERSION_MAJOR 0 -#define CARMA_VERSION_MINOR 7 -#define CARMA_VERSION_PATCH 0 -#define CARMA_VERSION_NAME "0.7.0 HO" -#endif - -namespace carma { - -struct carma_version { - static constexpr unsigned int major = CARMA_VERSION_MAJOR; - static constexpr unsigned int minor = CARMA_VERSION_MINOR; - static constexpr unsigned int patch = CARMA_VERSION_PATCH; - - static inline std::string as_string() { - std::ostringstream buffer; - buffer << carma_version::major << "." << carma_version::minor << "." << carma_version::patch; - return buffer.str(); - } -}; // carma_version - -#ifdef CARMA_EXTRA_DEBUG - -namespace anon { -class carma_config_debug_message { - public: - inline carma_config_debug_message() { - std::cout << "\n|----------------------------------------------------------|\n" - << "| CARMA CONFIGURATION |" - << "\n|----------------------------------------------------------|\n|\n"; - std::cout << "| Carma version: " + carma_version().as_string() << "\n|\n"; - std::cout << "| Default Numpy to Arma conversion config:\n" - << "| ----------------------------------------\n" - << "| * l-value converter: " << CARMA_DEFAULT_LVALUE_CONVERTER::name_ << "\n" - << "| * const l-value converter: " << CARMA_DEFAULT_CONST_LVALUE_CONVERTER::name_ << "\n" - << "| * resolution_policy: " << CARMA_DEFAULT_RESOLUTION::name_ << "\n" - << "| * memory_order_policy: " << CARMA_DEFAULT_MEMORY_ORDER::name_ << "\n"; - std::cout << "|\n| Converter Options:\n" - << "| ------------------\n" - << "| * enforce rvalue for MoveConverter: " -#ifndef CARMA_DONT_ENFORCE_RVALUE_MOVECONVERTER - << "true\n"; -#else - << "false\n"; -#endif // CARMA_DONT_ENFORCE_RVALUE_MOVECONVERTER - std::cout << "|\n|----------------------------------------------------------|\n\n"; - }; -}; - -static carma_config_debug_message carma_config_debug_message_print; -} // namespace anon - -#endif // CARMA_EXTRA_DEBUG - -} // namespace carma diff --git a/include/carma_bits/array_view.hpp b/include/carma_bits/array_view.hpp deleted file mode 100644 index b4663349..00000000 --- a/include/carma_bits/array_view.hpp +++ /dev/null @@ -1,194 +0,0 @@ -#pragma once - -// pybind11 include required even if not explicitly used -// to prevent link with pythonXX_d.lib on Win32 -// (cf Py_DEBUG defined in numpy headers and https://github.com/pybind/pybind11/issues/1295) -#include -// include order matters here -#include - -#define NPY_NO_DEPRECATED_API NPY_1_18_API_VERSION -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace carma { - -namespace internal { - -class ArrayView { - public: - std::array shape; - PyObject* obj; - PyArrayObject* arr; - void* mem; - arma::uword n_elem; - arma::uword n_rows = 0; - arma::uword n_cols = 0; - arma::uword n_slices = 0; - int n_dim; - // 0 is non-contigous; 1 is C order; 2 is F order - int contiguous; - //-1 for any order; 0 for C-order; 1 for F order - NPY_ORDER target_order = NPY_ANYORDER; - bool owndata; - bool writeable; - bool aligned; - bool ill_conditioned; - bool order_copy = false; - bool copy_in = false; - bool strict = true; - bool stolen_copy = false; - - template - explicit ArrayView(const py::array_t& src) - : obj{src.ptr()}, - arr{reinterpret_cast(obj)}, - mem{PyArray_DATA(arr)}, - n_elem{static_cast(src.size())}, - n_dim{static_cast(src.ndim())}, - contiguous{ - is_f_contiguous(arr) ? 2 - : is_c_contiguous(arr) ? 1 - : 0}, - owndata{src.owndata()}, - writeable{src.writeable()}, - aligned{is_aligned(arr)}, - ill_conditioned((!aligned) || (!static_cast(contiguous))) { - int clipped_n_dim = n_dim < 4 ? n_dim : 4; - std::memcpy(shape.data(), src.shape(), clipped_n_dim * sizeof(py::ssize_t)); - }; - - template - eT* data() const { - return static_cast(mem); - } - - void take_ownership() { - carma_extra_debug_print("taking ownership of array ", obj); - strict = false; - copy_in = n_elem <= arma::arma_config::mat_prealloc; - PyArray_CLEARFLAGS(arr, NPY_ARRAY_OWNDATA); - } - - /** - * \brief Give armadillo object ownership of memory - * - * \details Armadillo will free the memory during destruction when the `mem_state == 0` and - * when `n_alloc > arma_config::mat_prealloc`. - * In cases where the number of elements is below armadillo's pre-allocation limit - * the memory will be copied in. This means that we have to free the memory if a copy - * of an array was stolen. - * - * \param[in] dest arma object to be given ownership - * \return void - */ - template = 0> - inline void give_ownership(armaT& dest) { - carma_extra_debug_print("releasing ownership of array ", obj, " to ", (&dest)); - arma::access::rw(dest.n_alloc) = n_elem; - arma::access::rw(dest.mem_state) = 0; - release_if_copied_in(); - } - - void release_if_copied_in() { - if (copy_in) { - carma_extra_debug_print( - "array ", obj, " with size ", n_elem, " was copied in, as it does not exceed arma's prealloc size." - ); - if (stolen_copy) { - carma_extra_debug_print("freeing ", mem); - // We copied in because of the array's size in the CopyConverter - // we need to free the memory as we own it - npy_api::get().PyDataMem_FREE_(mem); - mem = nullptr; - } else { - carma_extra_debug_print("re-enabling owndata for array ", obj); - // We copied in because of the array's size in the MoveConterter - // if we free the memory any view or array that references this - // memory will segfault on the python side. - // We re-enable the owndata flag such that the memory is free'd - // by Python - PyArray_ENABLEFLAGS(arr, NPY_ARRAY_OWNDATA); - } - } - } - - /* Use Numpy's api to account for stride, order and steal the memory */ - void steal_copy() { -#ifdef CARMA_DEBUG - void* original_mem = mem; - std::cout << "|carma| a copy of array " << obj << " will moved into the arma object.\n"; -#endif - auto& api = npy_api::get(); - // build an PyArray to do F-order copy - auto dest = reinterpret_cast(api.PyArray_NewLikeArray_(arr, target_order, nullptr, 0)); - - // copy the array to a well behaved F-order - int ret_code = api.PyArray_CopyInto_(dest, arr); - if (ret_code != 0) { - throw std::runtime_error("|carma| Copy of array failed with ret_code: " + std::to_string(ret_code)); - } - - mem = PyArray_DATA(dest); -#ifdef CARMA_DEBUG - std::cout << "|carma| copied data " << original_mem << " to " << mem << "\n"; -#endif - // set OWNDATA to false such that the newly create - // memory is not freed when the array is cleared - PyArray_CLEARFLAGS(dest, NPY_ARRAY_OWNDATA); - // free the array but not the memory - api.PyArray_Free_(dest, nullptr); - // ensure that we don't clear the owndata flag from the original array - stolen_copy = true; - // arma owns thus not strict - strict = false; - // check if an additional copy is needed for arma to take ownership - copy_in = n_elem <= arma::arma_config::mat_prealloc; - } // steal_copy_array - - /* Use Numpy's api to account for stride, order and copy the new array in place*/ - void swap_copy() { -#ifdef CARMA_DEBUG - void* original_mem = mem; - std::cout << "|carma| array " << obj << " will be copied in-place.\n"; -#endif - auto& api = npy_api::get(); - auto tmp = reinterpret_cast(api.PyArray_NewLikeArray_(arr, target_order, nullptr, 0)); - - // copy the array to a well behaved target-order - int ret_code = api.PyArray_CopyInto_(tmp, arr); - if (ret_code != 0) { - throw std::runtime_error("|carma| Copy of numpy array failed with ret_code: " + std::to_string(ret_code)); - } - // swap copy into the original array - auto tmp_of = reinterpret_cast(tmp); - auto src_of = reinterpret_cast(arr); - std::swap(src_of->data, tmp_of->data); - - // fix strides - std::swap(src_of->strides, tmp_of->strides); - - PyArray_CLEARFLAGS(arr, NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_F_CONTIGUOUS); - PyArray_ENABLEFLAGS(arr, target_order | NPY_ARRAY_BEHAVED | NPY_ARRAY_OWNDATA); - - // clean up temporary which now contains the old memory - PyArray_ENABLEFLAGS(tmp, NPY_ARRAY_OWNDATA); - - mem = PyArray_DATA(arr); -#ifdef CARMA_DEBUG - std::cout << "|carma| copied " << mem << "into " << obj << "in place of " << original_mem << "\n"; - std::cout << "|carma| freeing " << PyArray_DATA(tmp) << "\n"; -#endif - api.PyArray_Free_(tmp, PyArray_DATA(tmp)); - } // swap_copy -}; - -} // namespace internal -} // namespace carma diff --git a/include/carma_bits/base/config.hpp b/include/carma_bits/base/config.hpp new file mode 100644 index 00000000..2eb7cf11 --- /dev/null +++ b/include/carma_bits/base/config.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#ifndef CARMA_DEFAULT_MEMORY_ORDER +#define CARMA_DEFAULT_MEMORY_ORDER carma::ColumnOrder +#endif // CARMA_DEFAULT_MEMORY_ORDER + +#ifdef CARMA_EXTRA_DEBUG +#include +#include +#include + +namespace carma { +namespace anon { +class carma_config_debug_message { + public: + inline carma_config_debug_message() { + std::cout << "\n|----------------------------------------------------------|\n" + << "| CARMA CONFIGURATION |" + << "\n|----------------------------------------------------------|\n|\n"; + std::cout << "| Carma version: " + carma_version().as_string() << "\n"; + std::cout << "| Carma mode: base\n|\n"; + std::cout << "| Default Numpy to Arma conversion config:\n" + << "| ----------------------------------------\n" + << "| * l-value converter: " << CopyInConverter::name_ << "\n" + << "| * const l-value converter: " << CopyInConverter::name_ << "\n" + << "| * memory_order_policy: " << CARMA_DEFAULT_MEMORY_ORDER::name_ << "\n"; + std::cout << "|\n|----------------------------------------------------------|\n\n"; + }; +}; + +static const carma_config_debug_message carma_config_debug_message_print; +} // namespace anon +} // namespace carma +#endif // CARMA_EXTRA_DEBUG diff --git a/include/carma_bits/base/converter_types.hpp b/include/carma_bits/base/converter_types.hpp new file mode 100644 index 00000000..29b82fd1 --- /dev/null +++ b/include/carma_bits/base/converter_types.hpp @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include // std::forward + +namespace carma { +/** + * \brief Convert by copying the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by copying the + * memory in using arma. The resulting arma object + * is _not_ strict and owns the data. + * + * The copy in converter requires that be + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct CopyInConverter { + template = 0> + armaT get(internal::NumpyContainer& src) { + src.copy_in = true; + auto dest = internal::to_arma(src); + return dest; + }; + +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "CopyInConverter"; +#endif +}; + +/** + * \brief Convert by copying the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by copying the + * memory. The resulting arma object is _not_ strict + * and owns the data. + * + * The copy converter does not have any requirements + * with regard to the memory + * + * if the array is not well-behaved we need to copy with Numpy + * If we copy in because of the pre-alloc size we need to free the memory again + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct CopyIntoConverter { + template = 0> + armaT get(internal::NumpyContainer& src) { + auto dest = internal::construct_arma(src); + src.copy_into(dest); + return dest; + }; + + template = 0> + armaT get(internal::NumpyContainer& src) { + src.copy_in = true; + src.make_arma_compatible(); + auto dest = internal::to_arma(src); + src.free(); + return dest; + }; + +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "CopyIntoConverter"; +#endif +}; + +} // namespace carma diff --git a/include/carma_bits/base/converters.hpp b/include/carma_bits/base/converters.hpp new file mode 100644 index 00000000..0337120e --- /dev/null +++ b/include/carma_bits/base/converters.hpp @@ -0,0 +1,196 @@ +#pragma once +#include +#include +#include +#include + +namespace py = pybind11; + +namespace carma { +/******************************************************************************* + * ARR_TO_ROW * + *******************************************************************************/ + +/** + * \brief Converter to arma::Row for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Row for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Row for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/******************************************************************************* + * ARR_TO_COL * + *******************************************************************************/ + +/** + * \brief Converter to arma::Col for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Col for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Col for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/******************************************************************************* + * ARR_TO_MAT * + *******************************************************************************/ + +/** + * \brief Converter to arma::Mat for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Mat for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Mat for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} +/******************************************************************************* + * ARR_TO_CUBE * + *******************************************************************************/ + +/** + * \brief Converter to arma::Cube for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Cube for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Cube for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} +} // namespace carma diff --git a/include/carma_bits/base/numpy_converters.hpp b/include/carma_bits/base/numpy_converters.hpp new file mode 100644 index 00000000..70b639ad --- /dev/null +++ b/include/carma_bits/base/numpy_converters.hpp @@ -0,0 +1,79 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include // std::forward + +namespace carma::internal { + +template = 0> +inline armaT construct_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Row(src.n_elem, arma::fill::none); +}; + +template = 1> +inline armaT construct_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Col(src.n_elem, arma::fill::none); +}; + +template = 2> +inline armaT construct_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Mat(src.n_rows, src.n_cols, arma::fill::none); +}; + +template = 3> +inline armaT construct_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Cube(src.n_rows, src.n_cols, src.n_slices, arma::fill::none); +}; + +// catch against unknown armaT with nicer to understand compile time issue +template ::value>> +inline armaT construct_arma(const NumpyContainer&) { + static_assert(!is_Arma::value, "|carma| encountered unhandled armaT."); +}; + +template +struct NumpyConverter { + template + armaT operator()(numpyT&& src) { + static_assert( + is_Numpy::value, + "|carma| `numpyT` must be a specialisation of `py::array_t`." + ); + static_assert( + is_Arma::value, + "|carma| `armaT` must be a (subclass of) `arma::Row`, `arma::Col`, " + "`arma::Mat` or `arma::Cube`." + ); + static_assert( + is_MemoryOrderPolicy::value, + "|carma| `memory_order_policy` must be one of: ColumnOrder, " + "TransposedRowOrder." + ); + NumpyContainer view(src); + FitsArmaType().check(view); + memory_order_policy().template check(view); + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy)) { + return CopyIntoConverter().get(src); + } + return CopyInConverter().get(src); + } +}; + +template +struct DefaultNumpyConverter { + template + armaT operator()(numpyT&& src) { + return internal::NumpyConverter().template operator( + )(std::forward(src)); + }; +}; + +} // namespace carma::internal diff --git a/include/carma_bits/base/outconv.hpp b/include/carma_bits/base/outconv.hpp new file mode 100644 index 00000000..e69de29b diff --git a/include/carma_bits/config.hpp b/include/carma_bits/config.hpp new file mode 100644 index 00000000..435df103 --- /dev/null +++ b/include/carma_bits/config.hpp @@ -0,0 +1,40 @@ +#pragma once + +#ifdef CARMA_EXTRA_DEBUG +#ifndef CARMA_DEBUG +#define CARMA_DEBUG true +#endif // CARMA_DEBUG +#endif // CARMA_EXTRA_DEBUG + +#ifdef CARMA_EXTENSION_MODE +/* If the Numpy allocator/deallocator have not been set through + * the carma_armadillo target ARMA_ALIEN_MEM_ALLOC_FUNCTION and + * ARMA_ALIEN_MEM_FREE_FUNCTION need to be set. + * + * This requires that Armadillo wasn't included before carma + * The CMake script handles this by pre-compiling the numpy_alloc header + */ +#ifndef CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET +#if defined(ARMA_VERSION_MAJOR) +#error "|carma| please include the armadillo header after the carma header or use carma's CMake build" +#endif +#include +#endif // CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET + +#ifdef CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET +#if ((!defined(ARMA_ALIEN_MEM_ALLOC_FUNCTION)) || (!defined(ARMA_ALIEN_MEM_FREE_FUNCTION))) +#error \ + "|carma| ARMA_ALIEN_MEM_ALLOC_FUNCTION and or ARMA_ALIEN_MEM_FREE_FUNCTION not set while CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET is" +#endif +#endif // CARMA_ARMA_ALIEN_MEM_FUNCTIONS_SET + +#include +#else +#include +#endif // CARMA_EXTENSION_MODE + +#include +#define CARMA_ARMA_VERSION (ARMA_VERSION_MAJOR * 10000 + ARMA_VERSION_MINOR * 100 + ARMA_VERSION_PATCH) +static_assert(CARMA_ARMA_VERSION > 100502, "|carma| minimum supported armadillo version is 10.5.2"); + +#include diff --git a/include/carma_bits/converter_types.hpp b/include/carma_bits/converter_types.hpp new file mode 100644 index 00000000..cb41f57c --- /dev/null +++ b/include/carma_bits/converter_types.hpp @@ -0,0 +1,207 @@ +#pragma once + +namespace carma { + +/* -------------------------------------------------------------- + Configs +-------------------------------------------------------------- */ + +/** + * \brief Create compile-time configuration object for Numpy to Armadillo + * conversion. + * + * \tparam converter the converter to be used options are: BorrowConverter, CopyConverter, MoveConverter, ViewConverter + * \tparam resolution_policy which resolution policy to use when the array cannot be converted directly, options are: + * RaiseResolution, CopyResolution, CopySwapResolution + * \tparam memory_order_policy which memory order policy to use, options are: ColumnOrder, TransposedRowOrder + */ +template +struct NumpyConversionConfig; + +/* -------------------------------------------------------------- + Converters +-------------------------------------------------------------- */ +/** + * \brief Borrow the memory s.t. the destination object is a mutable view. + * + * \details The destination object is a mutable view on the source object's memory. + * This requires that the lifetime of the source object is at least as long + * as that of the destination object. The source object keeps ownership of + * the data and is responsible for the memory management. + * + * Numpy -> Arma: + * The destination arma object is strict and does not own the data. + * Borrowing is a good choice when you want to set/change + * values but the shape of the object will not change. + * + * In order to borrow an array it's memory should + * be: + * * writeable + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * Arma -> Numpy: + * The destination array has Fortran order and is writeable + * but does not own the data. + */ +struct BorrowConverter; + +/** + * \brief Borrow the memory s.t. the destination object is a const view. + * + * \details The destination object is immutable view on the source object's memory. + * This requires that the lifetime of the source object is at least as long + * as that of the destination object. The source object keeps ownership of + * the data and is responsible for the memory management. + * + * Numpy -> Arma: + * The destination arma object is strict and does not own the data. + * Viewing is a good choice when you only need read access to the data. + * + * In order to create a const view of an array it's memory should + * be: + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * Arma -> Numpy: + * The destination array has Fortran order and is not writeable + * and does not own the data. + */ +struct ViewConverter; + +/** + * \brief Convert by copying the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by copying the + * memory. The resulting arma object is _not_ strict + * and owns the data. + * + * The copy converter does not have any requirements + * with regard to the memory + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct CopyConverter; + +/** + * \brief Convert by taking ownership of the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by transfering + * ownership of the memory to the armadillo object. + * The resulting arma object is _not_ strict + * and owns the data. + * + * After conversion the Numpy array will no longer own the + * memory, `owndata == false`. + * + * In order to take ownership, the array's memory order should + * be: + * * owned by the array, aka not a view or alias + * * writeable + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct MoveConverter; + +// /** +// * \brief Convert by copying the Numpy array's memory +// * +// * \details Convert the Numpy array to `armaT` by copying the +// * memory in using arma. The resulting arma object +// * is _not_ strict and owns the data. +// * +// * The copy in converter requires that be +// * * aligned and contiguous +// * * compatible with the specified `memory_order_policy` +// * +// * \param[in] src the view of the numpy array +// * \return arma object +// */ +// struct CopyInConverter; +// +// /** +// * \brief Convert by copying the Numpy array's memory +// * +// * \details Convert the Numpy array to `armaT` by copying the +// * memory. The resulting arma object is _not_ strict +// * and owns the data. +// * +// * The copy converter does not have any requirements +// * with regard to the memory +// * +// * if the array is not well-behaved we need to copy with Numpy +// * If we copy in because of the pre-alloc size we need to free the memory again +// * +// * \param[in] src the view of the numpy array +// * \return arma object +// */ +// struct CopyIntoConverter; + +/* -------------------------------------------------------------- + Memory order policies +-------------------------------------------------------------- */ + +/** + * \brief Memory order policy that looks for C-order contiguous arrays + * and transposes them. + * \details The TransposedRowOrder memory_order_policy expects + * that input arrays are row-major/C-order and converts them + * to column-major/F-order by transposing the array. + * If the array does not have the right order it is marked + * to be copied to the right order. + */ +struct TransposedRowOrder; + +/** + * \brief Memory order policy that looks for F-order contiguous arrays. + * \details The ColumnOrder memory_order_policy expects + * that input arrays are column-major/F-order. + * If the array does not have the right order it is marked + * to be copied to the right order. + */ +struct ColumnOrder; + +/* -------------------------------------------------------------- + Resolution policies +-------------------------------------------------------------- */ + +/** + * \brief Resolution policy that allows (silent) copying to meet the required + * conditions when required. \details The CopyResolution is the default + * resolution policy and will copy the input array when needed and possible. + * CopyResolution policy cannot resolve when the BorrowConverter is used, the + * CopySwapResolution policy can handle this scenario. + */ +struct CopyResolution; + +/** + * \brief Resolution policy that raises an runtime exception when the required + * conditions are not met. \details The RaiseResolution is the strictest policy + * and will raise an exception if any condition is not met, in contrast the + * CopyResolution will silently copy when it needs and can. This policy should + * be used when silent copies are undesired or prohibitively expensive. + */ +struct RaiseResolution; + +/** + * \brief Resolution policy that allows (silent) copying to meet the required + * conditions when required even with BorrowConverter. + * + * \details The CopySwapResolution is behaves identically to CopyResolution policy with the + * exception that it can handle ill conditioned and/or arrays with the wrong + * memory layout. An exception is raised when the array does not own it's memory + * or is marked as not writeable. + * + * \warning CopySwapResolution handles ill conditioned memory by copying the + * array's memory to the right state and swapping it in the place of the existing memory. + * This makes use of an deprecated numpy function to directly interface with the array fields. As + * such this resolution policy should be considered experimental. This policy + * will likely not work with Numpy >= v2.0 + */ +struct CopySwapResolution; + +} // namespace carma diff --git a/include/carma_bits/converters.hpp b/include/carma_bits/converters.hpp index b7155ce3..3eeb147c 100644 --- a/include/carma_bits/converters.hpp +++ b/include/carma_bits/converters.hpp @@ -1,336 +1,9 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace carma { - -namespace py = pybind11; - -/******************************************************************************* - * npConverter * - *******************************************************************************/ - -/** - * \brief Configurable Numpy to Armadillo converter. - * \details npConverter should be used when you want to configure a specific - * conversion strategy for specific arma or numpy types. - * - * \tparam armaT armadillo type - * \tparam numpyT pybind11::array_t specialisation - * \tparam config carma::ConversionConfig object - * \return armaT the created armadillo object - */ -template -struct npConverter { - using config_ = config; - armaT operator()(numpyT&& src) { - static_assert( - is_ConversionConfig::value, "|carma| config must be a specialisation of `ConversionConfig`" - ); - return internal::npConverterImpl< - armaT, - typename config::converter_, - typename config::resolution_, - typename config::mem_order_>() - .template operator()(std::forward(src)); - }; -}; - -/** - * \brief Compile time conversion assert. - * \details Should be used in combination with the npConverter. - * Certain correct usage cannot be enforced in npConverter. - * For example, we cannot enforce a const armaT return type - * for the ViewConverter which it assumes. - * - * \tparam armaT armadillo type - * \tparam numpyT pybind11::array_t - * \tparam converter the converter used, i.e. the npConverter functor instance - */ -template -inline void static_conversion_assert(armaT, numpyT, converter) { - static_assert( - not(is_ViewConverter::value - && (!std::is_const_v>)), - "numpyT should be const when using the ViewConverter." - ); -} - -/** - * \brief Generic Numpy to Armadillo converter. - * \details Default generic converter with support for Row, Col, Mat and Cube. - * The converter used is based on the the armaT and numpyT. - * If armaT is const qualified the ViewConverter is used. - * If numpyT is an r-value reference the MoveConverter is used. - * If numpyT is an l-value reference the CARMA_DEFAULT_LVALUE_CONVERTER is used, BorrowConverter by default. - * If numpyT is an const l-value reference the CARMA_DEFAULT_CONST_LVALUE_CONVERTER is used, CopyConverter by - * default. - * - * \tparam armaT armadillo type, cannot be deduced and must be specified - * \tparam numpyT pybind11::array_t specialisation, can often be deduced - * \param[in] src the numpy array to be converted - * \return armaT the created armadillo object - */ -template -armaT to_arma(numpyT&& src) { - return internal::toArma().template operator()(std::forward(src)); -} - -/******************************************************************************* - * ARR_TO_ROW * - *******************************************************************************/ - -/** - * \brief Converter to arma::Row for l-value references. - * \details By default the BorrowConverter is used which requires - * that the numpy array is mutable, and well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Row the created armadillo object - */ -template -auto arr_to_row(py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Default converter to arma::Row for const l-value references. - * \details By default the CopyConverter is used. - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Row the created armadillo object - */ -template -auto arr_to_row(const py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Converter to arma::Row for r-value references. - * \details By default the MoveConverter is used which requires - * that the numpy array is well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Row the created armadillo object - */ -template -auto arr_to_row(py::array_t&& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Configurable Numpy to arma::Row converter. - * \details this converter should be used when you want to use a specific - * configuration for pybind11::array_t specialisations. - * - * \tparam eT element type - * \tparam config carma::ConversionConfig object - * \tparam numpyT pybind11::array_t specialisation - * \return arma::Row the created armadillo object - */ -template -auto arr_to_row(numpyT arr) { - return npConverter, numpyT, config>()(arr); -} - -/******************************************************************************* - * ARR_TO_COL * - *******************************************************************************/ - -/** - * \brief Converter to arma::Col for l-value references. - * \details By default the BorrowConverter is used which requires - * that the numpy array is mutable, and well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Col the created armadillo object - */ -template -auto arr_to_col(py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Default converter to arma::Col for const l-value references. - * \details By default the CopyConverter is used. - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Col the created armadillo object - */ -template -auto arr_to_col(const py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Converter to arma::Col for r-value references. - * \details By default the MoveConverter is used which requires - * that the numpy array is well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Col the created armadillo object - */ -template -auto arr_to_col(py::array_t&& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Configurable Numpy to arma::Col converter. - * \details this converter should be used when you want to use a specific - * configuration for pybind11::array_t specialisations. - * - * \tparam eT element type - * \tparam config carma::ConversionConfig object - * \tparam numpyT pybind11::array_t specialisation - * \return arma::Col the created armadillo object - */ -template -auto arr_to_col(numpyT arr) { - return npConverter, numpyT, config>()(arr); -} - -/******************************************************************************* - * ARR_TO_MAT * - *******************************************************************************/ - -/** - * \brief Converter to arma::Mat for l-value references. - * \details By default the BorrowConverter is used which requires - * that the numpy array is mutable, and well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Mat the created armadillo object - */ -template -auto arr_to_mat(py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Default converter to arma::Mat for const l-value references. - * \details By default the CopyConverter is used. - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Mat the created armadillo object - */ -template -auto arr_to_mat(const py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Converter to arma::Mat for r-value references. - * \details By default the MoveConverter is used which requires - * that the numpy array is well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Mat the created armadillo object - */ -template -auto arr_to_mat(py::array_t&& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Configurable Numpy to arma::Mat converter. - * \details this converter should be used when you want to use a specific - * configuration for pybind11::array_t specialisations. - * - * \tparam eT element type - * \tparam config carma::ConversionConfig object - * \tparam numpyT pybind11::array_t specialisation - * \return arma::Mat the created armadillo object - */ -template -auto arr_to_mat(numpyT arr) { - return npConverter, numpyT, config>()(arr); -} - -/******************************************************************************* - * ARR_TO_CUBE * - *******************************************************************************/ - -/** - * \brief Converter to arma::Cube for l-value references. - * \details By default the BorrowConverter is used which requires - * that the numpy array is mutable, and well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Cube the created armadillo object - */ -template -auto arr_to_cube(py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Default converter to arma::Cube for const l-value references. - * \details By default the CopyConverter is used. - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Cube the created armadillo object - */ -template -auto arr_to_cube(const py::array_t& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Converter to arma::Cube for r-value references. - * \details By default the MoveConverter is used which requires - * that the numpy array is well-behaved. - * - * - * \tparam eT element type - * \param[in] arr pybind11 array to be converted - * \return arma::Cube the created armadillo object - */ -template -auto arr_to_cube(py::array_t&& arr) { - return internal::toArma>()(arr); -} - -/** - * \brief Configurable Numpy to arma::Cube converter. - * \details this converter should be used when you want to use a specific - * configuration for pybind11::array_t specialisations. - * - * \tparam eT element type - * \tparam config carma::ConversionConfig object - * \tparam numpyT pybind11::array_t specialisation - * \return arma::Cube the created armadillo object - */ -template -auto arr_to_cube(numpyT arr) { - return npConverter, numpyT, config>()(arr); -} - -} // namespace carma +#ifdef CARMA_EXTENSION_MODE +#include +#include +#else +#include +#include +#endif diff --git a/include/carma_bits/extension/config.hpp b/include/carma_bits/extension/config.hpp new file mode 100644 index 00000000..173663ae --- /dev/null +++ b/include/carma_bits/extension/config.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +/* -------------------------------------------------------------- + ConversionConfig +-------------------------------------------------------------- */ +#ifndef CARMA_DEFAULT_LVALUE_CONVERTER +#define CARMA_DEFAULT_LVALUE_CONVERTER carma::BorrowConverter +#endif // CARMA_DEFAULT_LVALUE_CONVERTER + +#ifndef CARMA_DEFAULT_CONST_LVALUE_CONVERTER +#define CARMA_DEFAULT_CONST_LVALUE_CONVERTER carma::CopyConverter +#endif // CARMA_DEFAULT_CONST_LVALUE_CONVERTER + +#ifndef CARMA_DEFAULT_RESOLUTION +#define CARMA_DEFAULT_RESOLUTION carma::CopyResolution +#endif // CARMA_DEFAULT_RESOLUTION + +#ifndef CARMA_DEFAULT_MEMORY_ORDER +#define CARMA_DEFAULT_MEMORY_ORDER carma::ColumnOrder +#endif // CARMA_DEFAULT_MEMORY_ORDER +// converters + +#ifdef CARMA_EXTRA_DEBUG + +#include +#include +namespace carma::anon { +class carma_config_debug_message { + public: + inline carma_config_debug_message() { + std::cout << "\n|----------------------------------------------------------|\n" + << "| CARMA CONFIGURATION |" + << "\n|----------------------------------------------------------|\n|\n"; + std::cout << "| Carma version: " + carma_version().as_string() << "\n"; + std::cout << "| Carma mode: extension\n|\n"; + std::cout << "| Default Numpy to Arma conversion config:\n" + << "| ----------------------------------------\n" + << "| * l-value converter: " << CARMA_DEFAULT_LVALUE_CONVERTER::name_ << "\n" + << "| * const l-value converter: " << CARMA_DEFAULT_CONST_LVALUE_CONVERTER::name_ << "\n" + << "| * resolution_policy: " << CARMA_DEFAULT_RESOLUTION::name_ << "\n" + << "| * memory_order_policy: " << CARMA_DEFAULT_MEMORY_ORDER::name_ << "\n"; + std::cout << "|\n| Converter Options:\n" + << "| ------------------\n" + << "| * enforce rvalue for MoveConverter: " +#ifndef CARMA_DONT_ENFORCE_RVALUE_MOVECONVERTER + << "true\n"; +#else + << "false\n"; +#endif // CARMA_DONT_ENFORCE_RVALUE_MOVECONVERTER + std::cout << "|\n|----------------------------------------------------------|\n\n"; + }; +}; + +static const carma_config_debug_message carma_config_debug_message_print; +} // namespace carma::anon +#endif // CARMA_EXTRA_DEBUG diff --git a/include/carma_bits/extension/converter_types.hpp b/include/carma_bits/extension/converter_types.hpp new file mode 100644 index 00000000..a792e5bd --- /dev/null +++ b/include/carma_bits/extension/converter_types.hpp @@ -0,0 +1,315 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace carma { + +/* -------------------------------------------------------------- + Converters +-------------------------------------------------------------- */ + +/** + * \brief Borrow the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by borrowing the + * memory, aka a mutable view on the memory. + * The resulting arma object is strict and does not + * own the data. + * Borrowing is a good choice when you want to set/change + * values but the shape of the object will not change + * + * In order to borrow an array it's memory order should + * be: + * * writeable + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct BorrowConverter { + template = 0> + armaT get(const internal::NumpyContainer& src) { + return internal::to_arma(src); + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "BorrowConverter"; +#endif +}; + +/** + * \brief Create const view on the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by borrowing the + * memory, aka a immutable view on the memory. + * The resulting arma object is strict and does not + * own the data. + * + * In order to create a view, the array's memory order should + * be: + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct ViewConverter { + template = 0> + armaT get(const internal::NumpyContainer& src) { + return internal::to_arma(src); + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "ViewConverter"; +#endif +}; + +/** + * \brief Convert by copying the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by copying the + * memory. The resulting arma object is _not_ strict + * and owns the data. + * + * The copy converter does not have any requirements + * with regard to the memory + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct CopyConverter { + template = 0> + armaT get(internal::NumpyContainer& src) { + src.steal_copy(); + auto dest = internal::to_arma(src); + src.give_ownership(dest); + return dest; + }; + +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "CopyConverter"; +#endif +}; + +/** + * \brief Convert by taking ownership of the Numpy array's memory + * + * \details Convert the Numpy array to `armaT` by transfering + * ownership of the memory to the armadillo object. + * The resulting arma object is _not_ strict + * and owns the data. + * + * After conversion the Numpy array will no longer own the + * memory, `owndata == false`. + * + * In order to take ownership, the array's memory order should + * be: + * * owned by the array, aka not a view or alias + * * writeable + * * aligned and contiguous + * * compatible with the specified `memory_order_policy` + * + * \param[in] src the view of the numpy array + * \return arma object + */ +struct MoveConverter { + template = 0> + armaT get(internal::NumpyContainer& src) { + src.take_ownership(); + auto dest = internal::to_arma(src); + src.give_ownership(dest); + return dest; + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "MoveConverter"; +#endif +}; + +/* -------------------------------------------------------------- + Resolution policies +-------------------------------------------------------------- */ + +namespace internal { + +#ifdef CARMA_DEBUG +template +inline void debug_print_conversion(const internal::NumpyContainer& src) { + if constexpr (is_MoveConverter::value) { + std::cout << "|carma| array " << src.arr << " does not meet MoveConverter conditions\n"; + } else if constexpr (is_ViewConverter::value) { + std::cout << "|carma| array " << src.arr << " does not meet ViewConverter conditions\n"; + } else if constexpr (is_BorrowConverter::value && CopySwapResolution == true) { + std::cout << "|carma| array " << src.arr << " requires copy-swap to meet BorrowConverter conditions\n"; + } +} +#else +template +inline void debug_print_conversion(const internal::NumpyContainer&){}; +#endif + +inline std::string get_array_address(const NumpyContainer& src) { + std::ostringstream stream; + stream << "|carma| array " << src.arr; + return stream.str(); +} + +} // namespace internal + +/** + * \brief Resolution policy that allows (silent) copying to meet the required + * conditions when required. \details The CopyResolution is the default + * resolution policy and will copy the input array when needed and possible. + * CopyResolution policy cannot resolve when the BorrowConverter is used, the + * CopySwapResolution policy can handle this scenario. + */ +struct CopyResolution { + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy || (!src.writeable))) { + throw std::runtime_error( + internal::get_array_address(src) + " does not meet BorrowConverter conditions and would require a copy" + ); + } + return BorrowConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + return CopyConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy || (!src.owndata) || (!src.writeable))) { + internal::debug_print_conversion(src); + return CopyConverter().get(src); + } + return MoveConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy)) { + internal::debug_print_conversion(src); + return CopyConverter().get(src); + } + return ViewConverter().get(src); + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "CopyResolution"; +#endif +}; + +/** + * \brief Resolution policy that raises an runtime exception when the required + * conditions are not met. \details The RaiseResolution is the strictest policy + * and will raise an exception if any condition is not met, in contrast the + * CopyResolution will silently copy when it needs and can. This policy should + * be used when silent copies are undesired or prohibitively expensive. + */ +struct RaiseResolution { + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy || (!src.writeable))) { + throw std::runtime_error( + internal::get_array_address(src) + " does not meet BorrowConverter conditions and would require a copy" + ); + } + return BorrowConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + return CopyConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy || (!src.owndata) || (!src.writeable))) { + throw std::runtime_error( + internal::get_array_address(src) + " does not meet MoveConverter conditions and would require a copy" + ); + } + return MoveConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy)) { + throw std::runtime_error( + internal::get_array_address(src) + " does not meet ViewConverter conditions and would require a copy" + ); + } + return ViewConverter().get(src); + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "RaiseResolution"; +#endif +}; + +/** + * \brief Resolution policy that allows (silent) copying to meet the required + * conditions when required even with BorrowConverter. + * + * \details The CopySwapResolution is behaves identically to CopyResolution policy with the + * exception that it can handle ill conditioned and/or arrays with the wrong + * memory layout. An exception is raised when the array does not own it's memory + * or is marked as not writeable. + * + * \warning CopySwapResolution handles ill conditioned memory by copying the + * array's memory to the right state and swapping it in the place of the existing memory. + * This makes use of an deprecated numpy function to directly interface with the array fields. As + * such this resolution policy should be considered experimental. This policy + * will likely not work with Numpy >= v2.0 + */ +struct CopySwapResolution { + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY((!src.writeable) || (!src.owndata))) { + throw std::runtime_error( + internal::get_array_address(src) + + " cannot copy-swapped as it does not own the data or is not writeable" + ); + } else if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy)) { + internal::debug_print_conversion(src); + src.swap_copy(); + } + return BorrowConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + return CopyConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy || (!src.owndata) || (!src.writeable))) { + internal::debug_print_conversion(src); + return CopyConverter().get(src); + } + return MoveConverter().get(src); + } + + template = 0> + armaT resolve(internal::NumpyContainer& src) { + if (CARMA_UNLIKELY(src.ill_conditioned || src.order_copy)) { + internal::debug_print_conversion(src); + return CopyConverter().get(src); + } + return ViewConverter().get(src); + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "CopySwapResolution"; +#endif +}; +} // namespace carma diff --git a/include/carma_bits/extension/converters.hpp b/include/carma_bits/extension/converters.hpp new file mode 100644 index 00000000..58e8bdd1 --- /dev/null +++ b/include/carma_bits/extension/converters.hpp @@ -0,0 +1,369 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include // std::forward + +namespace py = pybind11; + +namespace carma { + +/** + * \brief Create compile-time configuration object for Numpy to Armadillo + * conversion. + * + * \tparam converter the converter to be used options are: BorrowConverter, CopyConverter, MoveConverter, ViewConverter + * \tparam resolution_policy which resolution policy to use when the array cannot be converted directly, options are: + * RaiseResolution, CopyResolution, CopySwapResolution + * \tparam memory_order_policy which memory order policy to use, options are: ColumnOrder, TransposedRowOrder + */ +template < + class converter, + class resolution_policy = CARMA_DEFAULT_RESOLUTION, + class memory_order_policy = CARMA_DEFAULT_MEMORY_ORDER> +struct NumpyConversionConfig { + static_assert( + internal::is_Converter::value, + "|carma| `converter` must be one of: BorrowConverter, CopyConverter, " + "ViewConverter or MoveConverter." + ); + using converter_ = converter; + static_assert( + internal::is_ResolutionPolicy::value, + "|carma| `resolution_policy` must be one of: CopyResolution, " + "RaiseResolution, CopySwapResolution." + ); + using resolution_ = resolution_policy; + static_assert( + internal::is_MemoryOrderPolicy::value, + "|carma| `memory_order_policy` must be one of: ColumnOrder, " + "TransposedRowOrder." + ); + using mem_order_ = memory_order_policy; +}; + +/******************************************************************************* + * NumpyConverter * + *******************************************************************************/ + +/** + * \brief Configurable Numpy to Armadillo converter. + * \details npConverter should be used when you want to configure a specific + * conversion strategy for specific arma or numpy types. + * + * \tparam armaT armadillo type + * \tparam numpyT pybind11::array_t specialisation + * \tparam config carma::ConversionConfig object + * \return armaT the created armadillo object + */ +template +struct NumpyConverter { + using config_ = config; + armaT operator()(numpyT&& src) { + static_assert( + internal::is_NumpyConversionConfig::value, + "|carma| config must be a specialisation of `ConversionConfig`" + ); + return internal::NumpyConverter< + armaT, + typename config::converter_, + typename config::resolution_, + typename config::mem_order_>() + .template operator()(std::forward(src)); + }; +}; + +/** + * \brief Compile time conversion assert. + * \details Should be used in combination with the npConverter. + * Certain correct usage cannot be enforced in npConverter. + * For example, we cannot enforce a const armaT return type + * for the ViewConverter which it assumes. + * + * \tparam armaT armadillo type + * \tparam numpyT pybind11::array_t + * \tparam converter the converter used, i.e. the npConverter functor instance + */ +template +inline void static_conversion_assert(armaT, numpyT, converter) { + static_assert( + not(internal::is_ViewConverter::value + && (!std::is_const_v>)), + "numpyT should be const when using the ViewConverter." + ); +} + +/** + * \brief Generic Numpy to Armadillo converter. + * \details Default generic converter with support for Row, Col, Mat and Cube. + * The converter used is based on the the armaT and numpyT. + * If armaT is const qualified the ViewConverter is used. + * If numpyT is an r-value reference the MoveConverter is used. + * If numpyT is an l-value reference the CARMA_DEFAULT_LVALUE_CONVERTER is used, BorrowConverter by default. + * If numpyT is an const l-value reference the CARMA_DEFAULT_CONST_LVALUE_CONVERTER is used, CopyConverter by + * default. + * + * \tparam armaT armadillo type, cannot be deduced and must be specified + * \tparam numpyT pybind11::array_t specialisation, can often be deduced + * \param[in] src the numpy array to be converted + * \return armaT the created armadillo object + */ +template +armaT to_arma(numpyT&& src) { + return internal::DefaultNumpyConverter().template operator()(std::forward(src)); +} + +/******************************************************************************* + * ARR_TO_ROW * + *******************************************************************************/ + +/** + * \brief Converter to arma::Row for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Row for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Row for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Configurable Numpy to arma::Row converter. + * \details this converter should be used when you want to use a specific + * configuration for pybind11::array_t specialisations. + * + * \tparam eT element type + * \tparam config carma::ConversionConfig object + * \tparam numpyT pybind11::array_t specialisation + * \return arma::Row the created armadillo object + */ +template +auto arr_to_row(numpyT arr) { + return NumpyConverter, numpyT, config>()(arr); +} + +/******************************************************************************* + * ARR_TO_COL * + *******************************************************************************/ + +/** + * \brief Converter to arma::Col for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Col for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Col for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Configurable Numpy to arma::Col converter. + * \details this converter should be used when you want to use a specific + * configuration for pybind11::array_t specialisations. + * + * \tparam eT element type + * \tparam config carma::ConversionConfig object + * \tparam numpyT pybind11::array_t specialisation + * \return arma::Col the created armadillo object + */ +template +auto arr_to_col(numpyT arr) { + return NumpyConverter, numpyT, config>()(arr); +} + +/******************************************************************************* + * ARR_TO_MAT * + *******************************************************************************/ + +/** + * \brief Converter to arma::Mat for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Mat for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Mat for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Configurable Numpy to arma::Mat converter. + * \details this converter should be used when you want to use a specific + * configuration for pybind11::array_t specialisations. + * + * \tparam eT element type + * \tparam config carma::ConversionConfig object + * \tparam numpyT pybind11::array_t specialisation + * \return arma::Mat the created armadillo object + */ +template +auto arr_to_mat(numpyT arr) { + return NumpyConverter, numpyT, config>()(arr); +} + +/******************************************************************************* + * ARR_TO_CUBE * + *******************************************************************************/ + +/** + * \brief Converter to arma::Cube for l-value references. + * \details By default the BorrowConverter is used which requires + * that the numpy array is mutable, and well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Default converter to arma::Cube for const l-value references. + * \details By default the CopyConverter is used. + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(const py::array_t& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Converter to arma::Cube for r-value references. + * \details By default the MoveConverter is used which requires + * that the numpy array is well-behaved. + * + * + * \tparam eT element type + * \param[in] arr pybind11 array to be converted + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(py::array_t&& arr) { + return internal::DefaultNumpyConverter>()(arr); +} + +/** + * \brief Configurable Numpy to arma::Cube converter. + * \details this converter should be used when you want to use a specific + * configuration for pybind11::array_t specialisations. + * + * \tparam eT element type + * \tparam config carma::ConversionConfig object + * \tparam numpyT pybind11::array_t specialisation + * \return arma::Cube the created armadillo object + */ +template +auto arr_to_cube(numpyT arr) { + return NumpyConverter, numpyT, config>()(arr); +} + +} // namespace carma diff --git a/include/carma_bits/numpy_alloc.hpp b/include/carma_bits/extension/numpy_alloc.hpp similarity index 83% rename from include/carma_bits/numpy_alloc.hpp rename to include/carma_bits/extension/numpy_alloc.hpp index adb1c9c1..cbac69ad 100644 --- a/include/carma_bits/numpy_alloc.hpp +++ b/include/carma_bits/extension/numpy_alloc.hpp @@ -13,18 +13,16 @@ #include #include -#include +#include #include #ifdef CARMA_DEBUG #include #endif -namespace carma { -namespace alloc { +namespace carma::alloc { inline void* npy_malloc(size_t bytes) { - const auto& api = internal::npy_api::get(); - void* ptr = api.PyDataMem_NEW_(bytes); + void* ptr = internal::npy_api::get().PyDataMem_NEW_(bytes); #ifdef CARMA_EXTRA_DEBUG std::cout << "|carma| allocated " << ptr << "\n"; #endif // ARMA_EXTRA_DEBUG @@ -32,15 +30,13 @@ inline void* npy_malloc(size_t bytes) { } // npy_malloc inline void npy_free(void* ptr) { - const auto& api = internal::npy_api::get(); #ifdef CARMA_EXTRA_DEBUG std::cout << "|carma| freeing " << ptr << "\n"; #endif // CARMA_EXTRA_DEBUG - api.PyDataMem_FREE_(ptr); + internal::npy_api::get().PyDataMem_FREE_(ptr); } // npy_free -} // namespace alloc -} // namespace carma +} // namespace carma::alloc // carma makes use of the below Armadillo macros to enable // handing over memory ownership to armadillo objects. diff --git a/include/carma_bits/extension/numpy_container.hpp b/include/carma_bits/extension/numpy_container.hpp new file mode 100644 index 00000000..314b4a81 --- /dev/null +++ b/include/carma_bits/extension/numpy_container.hpp @@ -0,0 +1,139 @@ +#pragma once + +// pybind11 include required even if not explicitly used +// to prevent link with pythonXX_d.lib on Win32 +// (cf Py_DEBUG defined in numpy headers and https://github.com/pybind/pybind11/issues/1295) +#include +// include order matters here +#include + +#define NPY_NO_DEPRECATED_API NPY_1_18_API_VERSION +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace carma::internal { + +void NumpyContainer::take_ownership() { + carma_extra_debug_print("taking ownership of array ", obj); + strict = false; + copy_in = n_elem <= arma::arma_config::mat_prealloc; + PyArray_CLEARFLAGS(arr, NPY_ARRAY_OWNDATA); +} + +/** + * \brief Give armadillo object ownership of memory + * + * \details Armadillo will free the memory during destruction when the `mem_state == 0` and + * when `n_alloc > arma_config::mat_prealloc`. + * In cases where the number of elements is below armadillo's pre-allocation limit + * the memory will be copied in. This means that we have to free the memory if a copy + * of an array was stolen. + * + * \param[in] dest arma object to be given ownership + * \return void + */ +template = 0> +inline void NumpyContainer::give_ownership(armaT& dest) { + carma_extra_debug_print("releasing ownership of array ", obj, " to ", (&dest)); + arma::access::rw(dest.n_alloc) = n_elem; + arma::access::rw(dest.mem_state) = 0; + if (copy_in) { + carma_extra_debug_print( + "array ", obj, " with size ", n_elem, " was copied in, as it does not exceed arma's prealloc size." + ); + if (stolen_copy) { + carma_extra_debug_print("freeing ", mem); + // We copied in because of the array's size in the CopyConverter + // we need to free the memory as we own it + npy_api::get().PyDataMem_FREE_(mem); + mem = nullptr; + } else { + carma_extra_debug_print("re-enabling owndata for array ", obj); + // We copied in because of the array's size in the MoveConterter + // if we free the memory any view or array that references this + // memory will segfault on the python side. + // We re-enable the owndata flag such that the memory is free'd + // by Python + PyArray_ENABLEFLAGS(arr, NPY_ARRAY_OWNDATA); + } + } +} + +/* Use Numpy's api to account for stride, order and steal the memory */ +void NumpyContainer::steal_copy() { +#ifdef CARMA_DEBUG + void* original_mem = mem; + std::cout << "|carma| a copy of array " << obj << " will moved into the arma object.\n"; +#endif + auto& api = npy_api::get(); + // build an PyArray to do F-order copy + auto dest = reinterpret_cast(api.PyArray_NewLikeArray_(arr, target_order, nullptr, 0)); + + // copy the array to a well behaved F-order + int ret_code = api.PyArray_CopyInto_(dest, arr); + if (ret_code != 0) { + throw std::runtime_error("|carma| Copy of array failed with ret_code: " + std::to_string(ret_code)); + } + + mem = PyArray_DATA(dest); +#ifdef CARMA_DEBUG + std::cout << "|carma| copied data " << original_mem << " to " << mem << "\n"; +#endif + // set OWNDATA to false such that the newly create + // memory is not freed when the array is cleared + PyArray_CLEARFLAGS(dest, NPY_ARRAY_OWNDATA); + // free the array but not the memory + api.PyArray_Free_(dest, nullptr); + // ensure that we don't clear the owndata flag from the original array + stolen_copy = true; + // arma owns thus not strict + strict = false; + // check if an additional copy is needed for arma to take ownership + copy_in = n_elem <= arma::arma_config::mat_prealloc; +} // steal_copy_array + +/* Use Numpy's api to account for stride, order and copy the new array in place*/ +void NumpyContainer::swap_copy() { +#ifdef CARMA_DEBUG + void* original_mem = mem; + std::cout << "|carma| array " << obj << " will be copied in-place.\n"; +#endif + auto& api = npy_api::get(); + auto tmp = reinterpret_cast(api.PyArray_NewLikeArray_(arr, target_order, nullptr, 0)); + + // copy the array to a well behaved target-order + int ret_code = api.PyArray_CopyInto_(tmp, arr); + if (ret_code != 0) { + throw std::runtime_error("|carma| Copy of numpy array failed with ret_code: " + std::to_string(ret_code)); + } + // swap copy into the original array + auto tmp_of = reinterpret_cast(tmp); + auto src_of = reinterpret_cast(arr); + std::swap(src_of->data, tmp_of->data); + + // fix strides + std::swap(src_of->strides, tmp_of->strides); + + PyArray_CLEARFLAGS(arr, NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_F_CONTIGUOUS); + PyArray_ENABLEFLAGS(arr, target_order | NPY_ARRAY_BEHAVED | NPY_ARRAY_OWNDATA); + + // clean up temporary which now contains the old memory + PyArray_ENABLEFLAGS(tmp, NPY_ARRAY_OWNDATA); + + mem = PyArray_DATA(arr); +#ifdef CARMA_DEBUG + std::cout << "|carma| copied " << mem << "into " << obj << "in place of " << original_mem << "\n"; + std::cout << "|carma| freeing " << PyArray_DATA(tmp) << "\n"; +#endif + api.PyArray_Free_(tmp, PyArray_DATA(tmp)); +} // swap_copy + +} // namespace carma::internal diff --git a/include/carma_bits/extension/numpy_converters.hpp b/include/carma_bits/extension/numpy_converters.hpp new file mode 100644 index 00000000..7732f98a --- /dev/null +++ b/include/carma_bits/extension/numpy_converters.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace carma::internal { + +#if defined(CARMA_EXTRA_DEBUG) + +template +struct NumpyConverterInfo { + using numpyT_ = numpyT; + using armaT_ = armaT; + using converter_ = converter; + using resolution_ = resolution_policy; + using mem_order_ = memory_order_policy; + void operator()(const NumpyContainer& src) { + std::cout << "\n|----------------------------------------------------------|" + "\n" + << "| CARMA CONVERSION DEBUG |" + << "\n|----------------------------------------------------------|" + "\n|\n"; + std::cout << "| Array address: " << src.obj << "\n|\n"; + std::cout << "| Conversion configuration:\n" + << "| -------------------------\n" + << "| * from: " << get_full_typename() << "\n" + << "| * to: " << get_full_typename() << "\n" + << "| * converter: " << converter_::name_ << "\n" + << "| * resolution_policy: " << resolution_::name_ << "\n" + << "| * memory_order_policy: " << mem_order_::name_ << "\n|\n"; + + std::string shape; + shape.reserve(10); + shape = "("; + for (int i = 0; i < src.n_dim; i++) { + shape += std::to_string(src.shape[i]); + shape += ","; + } + shape += ")"; + std::cout << "| Array attributes:\n" + << "| -----------------\n" + << "| * data: " << src.mem << "\n" + << "| * size: " << src.n_elem << "\n" + << "| * shape: " << shape << "\n" + << "| * aligned: " << (src.aligned ? "true" : "false") << "\n" + << "| * owndata: " << (src.owndata ? "true" : "false") << "\n" + << "| * writeable: " << (src.writeable ? "true" : "false") << "\n" + << "| * memory order: " + << (src.contiguous == 2 ? "F-order" + : src.contiguous == 1 ? "C-order" + : "none") + << "\n|\n"; + + // needed as memory_order_policy runs after this, we can't move this + // forward without catching a potential exception regarding fit which we + // simple avoid here. + bool order_copy; + if constexpr (is_ColumnOrder::value) { + order_copy = src.contiguous != 2; + } else { + order_copy = src.contiguous != 1; + } + + if constexpr (!is_CopyConverter::value) { + std::cout << "| Copy if:\n" + << "| --------\n" + << "| * not aligned [" << (src.aligned ? "false" : "true") << "]\n" + << "| * not contiguous [" << (src.contiguous > 0 ? "false" : "true") << "]\n" + << "| * wrong memory order [" << (order_copy ? "true" : "false") << "]\n"; + if constexpr (is_BorrowConverter::value) { + std::cout << "| * not writeable [" << (src.writeable ? "false" : "true") << "]\n"; + } else if constexpr (is_MoveConverter::value) { + std::cout << "| * not owndata [" << (src.owndata ? "false" : "true") << "]\n" + << "| * not writeable [" << (src.writeable ? "false" : "true") << "]\n" + << "| * below pre-alloc size [" + << (src.n_elem <= arma::arma_config::mat_prealloc ? "true" : "false") << "]\n"; + } + } + std::cout << "|\n|-----------------------------------------------------" + "-----|\n\n"; + }; +}; + +#endif // CARMA_EXTRA_DEBUG + +/* + NumpyConverter +*/ + +template +struct NumpyConverter { + template + armaT operator()(numpyT&& src) { + static_assert( + is_Numpy::value, + "|carma| `numpyT` must be a specialisation of `py::array_t`." + ); + static_assert( + is_Arma::value, + "|carma| `armaT` must be a (subclass of) `arma::Row`, `arma::Col`, " + "`arma::Mat` or `arma::Cube`." + ); + static_assert( + is_Converter::value, + "|carma| `converter` must be one of: BorrowConverter, " + "CopyConverter, ViewConverter or " + "MoveConverter." + ); + static_assert( + is_ResolutionPolicy::value, + "|carma| `resolution_policy` must be one of: CopyResolution, " + "RaiseResolution, CopySwapResolution." + ); + static_assert( + is_MemoryOrderPolicy::value, + "|carma| `memory_order_policy` must be one of: ColumnOrder, " + "TransposedRowOrder." + ); + static_assert( + not((is_MoveConverter::value || is_BorrowConverter::value) + && std::is_const_v>), + "|carma| BorrowConverter and MoveConverter cannot be used with " + "`const py::array_t`." + ); +#ifndef CARMA_DONT_ENFORCE_RVALUE_MOVECONVERTER + static_assert( + not(is_MoveConverter::value && (!std::is_rvalue_reference_v)), + "|carma| [optional] `MoveConverter` is only enabled for r-value " + "references" + ); +#endif + NumpyContainer view(src); +#ifdef CARMA_EXTRA_DEBUG + NumpyConverterInfo()(view); +#endif // CARMA_EXTRA_DEBUG + FitsArmaType().check(view); + memory_order_policy().template check(view); + return resolution_policy().template resolve(view); + } +}; + +template < + typename armaT, + typename resolution_policy = CARMA_DEFAULT_RESOLUTION, + typename memory_order_policy = CARMA_DEFAULT_MEMORY_ORDER> +struct DefaultNumpyConverter { + template + armaT operator()(numpyT&& src) { + if constexpr (std::is_rvalue_reference_v) { + return internal::NumpyConverter() + .template operator()(std::forward(src)); + } else if constexpr (std::is_const_v>) { + return internal::NumpyConverter() + .template operator()(std::forward(src)); + } else if constexpr (std::is_const_v>) { + return internal:: + NumpyConverter()( + std::forward(src) + ); + } else { + return internal:: + NumpyConverter()( + std::forward(src) + ); + } + } +}; +} // namespace carma::internal diff --git a/include/carma_bits/extension/outconv.hpp b/include/carma_bits/extension/outconv.hpp new file mode 100644 index 00000000..e69de29b diff --git a/include/carma_bits/internal/arma_container.hpp b/include/carma_bits/internal/arma_container.hpp new file mode 100644 index 00000000..e69de29b diff --git a/include/carma_bits/common.hpp b/include/carma_bits/internal/common.hpp similarity index 95% rename from include/carma_bits/common.hpp rename to include/carma_bits/internal/common.hpp index c1c34d7b..1bf709cf 100644 --- a/include/carma_bits/common.hpp +++ b/include/carma_bits/internal/common.hpp @@ -4,12 +4,11 @@ #include #endif // __GNUG__ || __clang__ -#include -#include +#include #include #include -#ifndef CARMA_DEBUG +#ifdef CARMA_DEBUG #include #endif // CARMA_DEBUG @@ -18,12 +17,13 @@ #define OS_WIN #endif -// Fix for lack of ssize_t on Windows for >= CPython3.10 #if defined(_MSC_VER) #pragma warning(push) #pragma warning(disable : 4127) // warning C4127: Conditional expression is constant -#include -typedef SSIZE_T ssize_t; +// FIXME check if we need this... +// Fix for lack of ssize_t on Windows for >= CPython3.10 +// #include +// typedef SSIZE_T ssize_t; #endif #ifndef CARMA_DEFINED_EXPECT diff --git a/include/carma_bits/internal/converter_types.hpp b/include/carma_bits/internal/converter_types.hpp new file mode 100644 index 00000000..266710c7 --- /dev/null +++ b/include/carma_bits/internal/converter_types.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include + +namespace carma { +/* -------------------------------------------------------------- + Memory order policies +-------------------------------------------------------------- */ + +/** + * \brief Memory order policy that looks for C-order contiguous arrays + * and transposes them. + * \details The TransposedRowOrder memory_order_policy expects + * that input arrays are row-major/C-order and converts them + * to column-major/F-order by transposing the array. + * If the array does not have the right order it is marked + * to be copied to the right order. + */ +struct TransposedRowOrder { + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = 1; + src.n_cols = src.n_elem; + }; + + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = src.n_elem; + src.n_cols = 1; + }; + + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = src.shape[1]; + src.n_cols = src.shape[0]; + std::swap(src.shape[0], src.shape[1]); + src.order_copy = src.contiguous != 1; + src.target_order = NPY_CORDER; + }; + + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = src.shape[2]; + src.n_cols = src.shape[1]; + src.n_slices = src.shape[0]; + std::reverse(src.shape.begin(), src.shape.end()); + src.order_copy = src.contiguous != 1; + src.target_order = NPY_CORDER; + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "TransposedRowOrder"; +#endif +}; + +/** + * \brief Memory order policy that looks for F-order contiguous arrays. + * \details The ColumnOrder memory_order_policy expects + * that input arrays are column-major/F-order. + * If the array does not have the right order it is marked + * to be copied to the right order. + */ +struct ColumnOrder { + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = 1; + src.n_cols = src.n_elem; + }; + + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = src.n_elem; + src.n_cols = 1; + }; + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = src.shape[0]; + src.n_cols = src.shape[1]; + src.order_copy = src.contiguous != 2; + src.target_order = NPY_FORTRANORDER; + }; + + template = 0> + void check(internal::NumpyContainer& src) { + src.n_rows = src.shape[0]; + src.n_cols = src.shape[1]; + src.n_slices = src.shape[2]; + src.order_copy = src.contiguous != 2; + src.target_order = NPY_FORTRANORDER; + }; +#ifdef CARMA_DEBUG + static constexpr std::string_view name_ = "ColumnOrder"; +#endif +}; + +} // namespace carma diff --git a/include/carma_bits/numpy_api.hpp b/include/carma_bits/internal/numpy_api.hpp similarity index 98% rename from include/carma_bits/numpy_api.hpp rename to include/carma_bits/internal/numpy_api.hpp index cce07029..f8d20c2e 100644 --- a/include/carma_bits/numpy_api.hpp +++ b/include/carma_bits/internal/numpy_api.hpp @@ -34,6 +34,7 @@ struct npy_api { return api; } + PyTypeObject *PyArray_Type_; PyArray_Descr *(*PyArray_DescrFromType_)(int typenum); int (*PyArray_Size_)(PyObject *src); int (*PyArray_CopyInto_)(PyArrayObject *dest, PyArrayObject *src); diff --git a/include/carma_bits/internal/numpy_container.hpp b/include/carma_bits/internal/numpy_container.hpp new file mode 100644 index 00000000..751538db --- /dev/null +++ b/include/carma_bits/internal/numpy_container.hpp @@ -0,0 +1,144 @@ +#pragma once + +// pybind11 include required even if not explicitly used +// to prevent link with pythonXX_d.lib on Win32 +// (cf Py_DEBUG defined in numpy headers and https://github.com/pybind/pybind11/issues/1295) +#include +// include order matters here +#include + +#define NPY_NO_DEPRECATED_API NPY_1_18_API_VERSION +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace carma::internal { + +class NumpyContainer { + public: + std::array shape; + PyObject* obj; + PyArrayObject* arr; + void* mem; + arma::uword n_elem; + arma::uword n_rows = 0; + arma::uword n_cols = 0; + arma::uword n_slices = 0; + int n_dim; + // 0 is non-contigous; 1 is C order; 2 is F order + int contiguous; + //-1 for any order; 0 for C-order; 1 for F order + NPY_ORDER target_order = NPY_ANYORDER; + bool owndata; + bool writeable; + bool aligned; + bool ill_conditioned; + bool order_copy = false; + bool copy_in = false; + bool strict = true; + bool stolen_copy = false; + + template + explicit NumpyContainer(const py::array_t& src) + : obj{src.ptr()}, + arr{reinterpret_cast(obj)}, + mem{PyArray_DATA(arr)}, + n_elem{static_cast(src.size())}, + n_dim{static_cast(src.ndim())}, + contiguous{ + is_f_contiguous(arr) ? 2 + : is_c_contiguous(arr) ? 1 + : 0 + }, + owndata{src.owndata()}, + writeable{src.writeable()}, + aligned{is_aligned(arr)}, + ill_conditioned((!aligned) || (!static_cast(contiguous))) { + int clipped_n_dim = n_dim < 4 ? n_dim : 4; + std::memcpy(shape.data(), src.shape(), clipped_n_dim * sizeof(py::ssize_t)); + }; + + template + eT* data() const { + return static_cast(mem); + } + + /* Use Numpy's api to account for stride, order and copy into the arma object*/ + template = 0> + inline void copy_into(armaT& dest) { + using eT = typename armaT::elem_type; + carma_debug_print("Copying data of array ", obj, " to ", dest.memptr(), " using Numpy."); + auto api = npy_api::get(); + // make the temporary array writeable and mark the memory as aligned and give the order of the arma object + int flags + = (py::detail::npy_api::NPY_ARRAY_ALIGNED_ | py::detail::npy_api::NPY_ARRAY_WRITEABLE_ + | py::detail::npy_api::NPY_ARRAY_F_CONTIGUOUS_); + // get description from element type + auto dtype = py::dtype::of(); + // create Fortran order strides + auto strides = std::vector(n_dim, dtype.itemsize()); + for (int i = 1; i < n_dim; ++i) { + strides[i] = strides[i - 1] * shape[i - 1]; + } + auto tmp = api.PyArray_NewFromDescr_( + api.PyArray_Type_, dtype.release().ptr(), n_dim, shape.data(), strides.data(), dest.memptr(), flags, nullptr + ); + // copy the array to a well behaved target-order + int ret_code = api.PyArray_CopyInto_(tmp, arr); + if (ret_code != 0) { + throw std::runtime_error("|carma| Copy of numpy array failed with ret_code: " + std::to_string(ret_code)); + } + + // make sure to remove owndata flag to prevent memory being freed + PyArray_CLEARFLAGS(tmp, NPY_ARRAY_OWNDATA); + // clean up temporary array but not the memory it viewed + api.PyArray_Free_(tmp, nullptr); + } // copy_into + + inline void make_arma_compatible() { + carma_debug_print("Copying array ", obj, " to Arma compatible layout using Numpy."); + auto api = npy_api::get(); + PyObject* dest_obj = api.PyArray_NewLikeArray_(arr, NPY_FORTRANORDER, nullptr, 0); + auto dest_arr = reinterpret_cast(dest_obj); + + // copy the array to a well behaved target-order + int ret_code = api.PyArray_CopyInto_(dest_arr, arr); + if (ret_code != 0) { + throw std::runtime_error("|carma| Copy of numpy array failed with ret_code: " + std::to_string(ret_code)); + } + + obj = dest_obj; + arr = dest_arr; + mem = PyArray_DATA(arr); + contiguous = 2; + owndata = true; + writeable = true; + aligned = true; + ill_conditioned = false; + order_copy = false; + copy_in = true; + strict = false; + } // make_arma_compatible + + inline void free() { + carma_extra_debug_print("Freeing array ", arr); + npy_api::get().PyArray_Free_(arr, mem); + mem = nullptr; + } + + // alien methods; defined in carma/alien/array_view.hpp + void take_ownership(); + template > + void give_ownership(armaT& dest); + void steal_copy(); + void swap_copy(); +}; + +} // namespace carma::internal diff --git a/include/carma_bits/internal/numpy_converters.hpp b/include/carma_bits/internal/numpy_converters.hpp new file mode 100644 index 00000000..7c32fe77 --- /dev/null +++ b/include/carma_bits/internal/numpy_converters.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace carma::internal { + +template = 0> +inline armaT to_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Row(src.data(), src.n_elem, src.copy_in, src.strict); +}; + +template = 1> +inline armaT to_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Col(src.data(), src.n_elem, src.copy_in, src.strict); +}; + +template = 2> +inline armaT to_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Mat(src.data(), src.n_rows, src.n_cols, src.copy_in, src.strict); +}; + +template = 3> +inline armaT to_arma(const NumpyContainer& src) { + using eT = typename armaT::elem_type; + return arma::Cube(src.data(), src.n_rows, src.n_cols, src.n_slices, src.copy_in, src.strict); +}; + +// catch against unknown armaT with nicer to understand compile time issue +template ::value>> +inline armaT to_arma(const NumpyContainer&) { + static_assert(!is_Arma::value, "|carma| encountered unhandled armaT."); +}; + +/** + * \brief Check if array dimensions are compatible with arma type + */ +class FitsArmaType { + template = 0> + inline bool fits(const NumpyContainer& src) { + return (src.n_dim == 1) || ((src.n_dim == 2) && (src.shape[1] == 1 || src.shape[0] == 1)); + } + + template = 0> + inline bool fits(const NumpyContainer& src) { + return (src.n_dim == 2) || ((src.n_dim == 3) && (src.shape[2] == 1 || src.shape[1] == 1 || src.shape[0] == 1)); + } + + template = 0> + inline bool fits(const NumpyContainer& src) { + return (src.n_dim == 3) + || ((src.n_dim == 4) + && (src.shape[3] == 1 || src.shape[2] == 1 || src.shape[1] == 1 || src.shape[0] == 1)); + } + + public: + /** + * \brief Check if array dimensions are compatible with arma::Row, arma::Col + * + * \param[in] src the view of the numpy array + * \throws std::runtime_error if not compatible + * \return void + */ + template = 0> + void check(const NumpyContainer& src) { + if (CARMA_UNLIKELY((src.n_dim < 1) || (src.n_dim > 2) || (!fits(src)))) { + throw std::runtime_error( + "|carma| cannot convert array to arma::Vec with dimensions: " + std::to_string(src.n_dim) + ); + } + } + + /** + * \brief Check if array dimensions are compatible with arma::Mat + * + * \param[in] src the view of the numpy array + * \throws std::runtime_error if not compatible + * \return void + */ + template = 0> + void check(const NumpyContainer& src) { + if (CARMA_UNLIKELY((src.n_dim < 1) || (src.n_dim > 3) || (!fits(src)))) { + throw std::runtime_error( + "|carma| cannot convert array to arma::Mat with dimensions: " + std::to_string(src.n_dim) + ); + } + } + + /** + * \brief Check if array dimensions are compatible with arma::Cube + * + * \param[in] src the view of the numpy array + * \throws std::runtime_error if not compatible + * \return void + */ + template = 0> + void check(const NumpyContainer& src) { + if (CARMA_UNLIKELY((src.n_dim < 1) || (src.n_dim > 4) || (!fits(src)))) { + throw std::runtime_error( + "|carma| cannot convert array to arma::Mat with dimensions: " + std::to_string(src.n_dim) + ); + } + } +}; + +} // namespace carma::internal diff --git a/include/carma_bits/to_numpy.hpp b/include/carma_bits/internal/outconv.hpp similarity index 52% rename from include/carma_bits/to_numpy.hpp rename to include/carma_bits/internal/outconv.hpp index e19ea524..9874018f 100644 --- a/include/carma_bits/to_numpy.hpp +++ b/include/carma_bits/internal/outconv.hpp @@ -1,9 +1,11 @@ #pragma once +#define NPY_NO_DEPRECATED_API NPY_1_18_API_VERSION +#include + #include -#include -#include -#include +#include +#include namespace carma { @@ -99,3 +101,77 @@ py::array_t create_reference_array(const ArmaView& src); } // namespace internal } // namespace carma + +// std::vector shape = {size, 1}; +// std::vector strides = py::detail::c_strides(shape, sizeof(eT)); +// int flags +// = (py::detail::npy_api::NPY_ARRAY_OWNDATA_ | py::detail::npy_api::NPY_ARRAY_ALIGNED_ +// | py::detail::npy_api::NPY_ARRAY_WRITEABLE_ | py::detail::npy_api::NPY_ARRAY_C_CONTIGUOUS_ +// | py::detail::npy_api::NPY_ARRAY_F_CONTIGUOUS_); +// + +// template +// struct ArmaView { +// using eT = typename armaT::elem_type; +// static constexpr auto tsize = static_cast(sizeof(eT)); +// ssize_t n_rows; +// ssize_t n_cols; +// ssize_t n_slices; +// ssize_t* shape; +// ssize_t* strides; +// static constexpr int n_dim = is_Vec::value ? 1 : +// arma::is_Mat::value ? 2 : 3; eT* data = nullptr; armaT* obj = +// nullptr; +// }; + +// target_order | NPY_ARRAY_OWNDATA | NPY_ARRAY_BEHAVED | NPY_ARRAY_WRITEABLE + +// template struct armaConverter { +// using eT = typename armaT::elem_type; +// py::array_t operator()(armaT&& src) { +// auto view = ArmaView(); +// // check if arma owns the mem +// if constexpr (is_BorrowConverter::value) { +// } +// }; +// }; + +// template +// inline py::array_t to_numpy(const ArmaView& src) { +// return py::array_t(src.shape, // shape +// src.strides, // F-style contiguous +// strides src.data, // the data +// pointer create_capsule(src) // numpy array +// references this parent +// ); +// }; + +// template +// inline py::array_t to_numpy(arma::Col* src) { +// constexpr auto tsize = static_cast(sizeof(eT)); +// auto n_rows = static_cast(src->n_rows); + +// py::capsule base = create_capsule>(src); + +// return py::array_t({n_rows, static_cast(1)}, // shape +// {tsize, tsize}, // F-style +// contiguous strides src->memptr(), // the data +// pointer base // +// numpy array references this parent +// ); +// }; +// +// template = 2> +// inline armaT to_numpy(const ArrayView& src) { +// using eT = typename armaT::elem_type; +// return arma::Mat(src.data(), src.n_rows, src.n_cols, src.copy_in, +// src.strict); +// }; + +// template = 3> +// inline armaT to_numpy(const ArrayView& src) { +// using eT = typename armaT::elem_type; +// return arma::Cube(src.data(), src.n_rows, src.n_cols, +// src.n_slices, src.copy_in, src.strict); +// }; diff --git a/include/carma_bits/internal/type_traits.hpp b/include/carma_bits/internal/type_traits.hpp new file mode 100644 index 00000000..8b71cf98 --- /dev/null +++ b/include/carma_bits/internal/type_traits.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace carma { + +namespace py = pybind11; + +namespace internal { + +template typename> +// struct is_instance_impl : public std::false_type {}; +struct is_instance_impl { + static constexpr bool value = false; +}; + +template