diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea829eca..188e37fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Support for C++11 `std::array` types ([#412](https://github.com/stack-of-tasks/pull/412)) + ## [3.1.4] - 2023-11-27 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 96a80995f..3d06700a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,7 @@ set(${PROJECT_NAME}_HEADERS include/eigenpy/user-type.hpp include/eigenpy/ufunc.hpp include/eigenpy/register.hpp + include/eigenpy/std-array.hpp include/eigenpy/std-map.hpp include/eigenpy/std-vector.hpp include/eigenpy/optional.hpp diff --git a/include/eigenpy/std-array.hpp b/include/eigenpy/std-array.hpp new file mode 100644 index 000000000..c6261de7e --- /dev/null +++ b/include/eigenpy/std-array.hpp @@ -0,0 +1,161 @@ +/// Copyright (c) 2023 CNRS INRIA + +#ifndef __eigenpy_utils_std_array_hpp__ +#define __eigenpy_utils_std_array_hpp__ + +#include +#include "eigenpy/std-vector.hpp" + +#include + +namespace eigenpy { + +template +class array_indexing_suite; +namespace details { + +template +class final_array_derived_policies + : public array_indexing_suite< + Container, NoProxy, SliceAllocator, + final_array_derived_policies > {}; +} // namespace details + +template , + class DerivedPolicies = details::final_array_derived_policies< + Container, NoProxy, SliceAllocator> > +class array_indexing_suite + : public bp::vector_indexing_suite { + public: + typedef typename Container::value_type data_type; + typedef typename Container::value_type key_type; + typedef typename Container::size_type index_type; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef std::vector slice_vector_type; + static constexpr std::size_t Size = std::tuple_size{}; + + template + static void extension_def(Class &) {} + + // throws exception + static void delete_item(Container &, index_type) { + PyErr_SetString(PyExc_NotImplementedError, + "Cannot delete item from std::array type."); + bp::throw_error_already_set(); + } + + // throws exception + static void delete_slice(Container &, index_type, index_type) { + PyErr_SetString(PyExc_NotImplementedError, + "Cannot delete slice from std::array type."); + bp::throw_error_already_set(); + } + + static void set_slice(Container &container, index_type from, index_type to, + data_type const &v) { + if (from >= to) { + PyErr_SetString(PyExc_NotImplementedError, + "Setting this slice would insert into an std::array, " + "which is not supported."); + bp::throw_error_already_set(); + } else { + std::fill(container.begin() + from, container.begin() + to, v); + } + } + + template + static void set_slice(Container &container, index_type from, index_type to, + Iter first, Iter last) { + if (from >= to) { + PyErr_SetString(PyExc_NotImplementedError, + "Setting this slice would insert into an std::array, " + "which is not supported."); + bp::throw_error_already_set(); + } else { + if (long(to - from) == std::distance(first, last)) { + std::copy(first, last, container.begin() + from); + } else { + PyErr_SetString(PyExc_NotImplementedError, + "Size of std::array slice and size of right-hand side " + "iterator are incompatible."); + bp::throw_error_already_set(); + } + } + } + + static bp::object get_slice(Container &container, index_type from, + index_type to) { + if (from > to) return bp::object(slice_vector_type()); + slice_vector_type out; + for (size_t i = from; i < to; i++) { + out.push_back(container[i]); + } + return bp::object(std::move(out)); + } +}; + +/// \brief Expose an std::array (a C++11 fixed-size array) from a given type +/// \tparam array_type std::array type to expose +/// \tparam NoProxy When set to false, the elements will be copied when +/// returned to Python. +/// \tparam SliceAllocator Allocator type to use for slices of std::array type +/// accessed using e.g. __getitem__[0:4] in Python. These slices are returned as +/// std::vector (dynamic size). +template > +struct StdArrayPythonVisitor { + typedef typename array_type::value_type value_type; + + static ::boost::python::list tolist(array_type &self) { + return details::build_list::run(self); + } + + static void expose(const std::string &class_name, + const std::string &doc_string = "") { + expose(class_name, doc_string, EmptyPythonVisitor()); + } + + template + static void expose(const std::string &class_name, + const bp::def_visitor &visitor) { + expose(class_name, "", visitor); + } + + template + static void expose(const std::string &class_name, + const std::string &doc_string, + const bp::def_visitor &visitor) { + if (!register_symbolic_link_to_registered_type()) { + bp::class_ cl(class_name.c_str(), doc_string.c_str()); + cl.def(bp::init(bp::args("self", "other"), + "Copy constructor")); + + array_indexing_suite indexing_suite; + cl.def(indexing_suite) + .def(visitor) + .def("tolist", tolist, bp::arg("self"), + "Returns the std::array as a Python list."); + } + } +}; + +/// Exposes std::array +template +void exposeStdArrayEigenSpecificType(const char *name) { + std::ostringstream oss; + oss << "StdArr"; + oss << Size << "_" << name; + typedef std::array array_type; + StdArrayPythonVisitor >:: + expose(oss.str(), + details::overload_base_get_item_for_std_vector()); +} + +} // namespace eigenpy + +#endif // ifndef __eigenpy_utils_std_array_hpp__ diff --git a/include/eigenpy/std-vector.hpp b/include/eigenpy/std-vector.hpp index c69887b85..783307465 100644 --- a/include/eigenpy/std-vector.hpp +++ b/include/eigenpy/std-vector.hpp @@ -234,6 +234,20 @@ struct reference_arg_from_python &> namespace eigenpy { +namespace details { +/// Defines traits for the container, used in \struct StdContainerFromPythonList +template +struct container_traits { + // default behavior expects allocators + typedef typename Container::allocator_type Allocator; +}; + +template +struct container_traits > { + typedef void Allocator; +}; +}; // namespace details + /// /// \brief Register the conversion from a Python list to a std::vector /// @@ -242,7 +256,7 @@ namespace eigenpy { template struct StdContainerFromPythonList { typedef typename vector_type::value_type T; - typedef typename vector_type::allocator_type Allocator; + typedef typename details::container_traits::Allocator Allocator; /// \brief Check if obj_ptr can be converted static void *convertible(PyObject *obj_ptr) { diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 7da4556f6..7737f07ed 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -38,6 +38,7 @@ if(NOT NUMPY_WITH_BROKEN_UFUNC_SUPPORT) add_lib_unit_test(user_type) endif() add_lib_unit_test(std_vector) +add_lib_unit_test(std_array) add_lib_unit_test(user_struct) function(config_bind_optional tagname opttype) @@ -110,6 +111,10 @@ add_python_unit_test("py-std-vector" "unittest/python/test_std_vector.py" "python;unittest") set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP}) +add_python_unit_test("py-std-array" "unittest/python/test_std_array.py" + "python;unittest") +set_tests_properties("py-std-array" PROPERTIES DEPENDS ${PYWRAP}) + add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py" "python;unittest") set_tests_properties("py-user-struct" PROPERTIES DEPENDS ${PYWRAP}) diff --git a/unittest/python/test_std_array.py b/unittest/python/test_std_array.py new file mode 100644 index 000000000..20d366e27 --- /dev/null +++ b/unittest/python/test_std_array.py @@ -0,0 +1,143 @@ +import std_array + + +ints = std_array.get_arr_3_ints() +print(ints[0]) +print(ints[1]) +print(ints[2]) +print(ints.tolist()) +assert ints.tolist() == [1, 2, 3] + +_ints_slice = ints[1:3] +print("Printing slice...") +for el in _ints_slice: + print(el) + +ref = [1, 2, 3] +assert len(ref[1:2]) == 1 # sanity check + +assert len(_ints_slice) == 2, "Slice size should be 1, got %d" % len(_ints_slice) +assert _ints_slice[0] == 2 +assert _ints_slice[1] == 3 + +# Test that insert/delete is impossible with the slice operator + +# prepend +try: + ints[0:0] = [0, 1] +except NotImplementedError: + pass +else: + assert False, "Insert value with slice operator should be impossible" + +# append +try: + ints[10:12] = [0] +except NotImplementedError: + pass +else: + assert False, "Insert value with slice operator should be impossible" + +# append +try: + ints[3:3] = [0] +except NotImplementedError: + pass +else: + assert False, "Insert value with slice operator should be impossible" + +# Erase two elements and replace by one +try: + ints[1:3] = [0] +except NotImplementedError: + pass +else: + assert False, "Insert value with slice operator should be impossible" + +# Erase two elements and replace by three +try: + ints[1:3] = [0, 1, 2] +except NotImplementedError: + pass +else: + assert False, "Insert value with slice operator should be impossible" + +# Test that delete operator is not implemented +# Index delete +try: + del ints[0] +except NotImplementedError: + pass +else: + assert False, "del is not implemented" + +# Slice delete +try: + del ints[1:3] +except NotImplementedError: + pass +else: + assert False, "del is not implemented" + +# Slice delete +try: + del ints[1:3] +except NotImplementedError: + pass +else: + assert False, "del is not implemented" + +# Test that append/extend are not implemented +# append +try: + ints.append(4) +except AttributeError: + pass +else: + assert False, "append is not implemented" + +# extend +try: + ints.extend([4, 5]) +except AttributeError: + pass +else: + assert False, "extend is not implemented" + +# Test set_slice nominal case +ints[1:3] = [10, 20] +assert ints[1] == 10 +assert ints[2] == 20 + +# print(ints.tolist()) + +vecs = std_array.get_arr_3_vecs() +assert len(vecs) == 3 +print(vecs[0]) +print(vecs[1]) +print(vecs[2]) + +# slices do not work for Eigen objects... + +# v2 = vecs[:] +# assert isinstance(v2, std_array.StdVec_VectorXd) +# assert len(v2) == 3 +# print(v2.tolist()) +# print(v2[0]) + +ts = std_array.test_struct() +assert len(ts.integs) == 3 +assert len(ts.vecs) == 2 +print(ts.integs[:].tolist()) +print(ts.vecs[0]) +print(ts.vecs[1]) + +ts.integs[:] = 111 +print("Test of set_slice for std::array:", ts.integs[:].tolist()) +for el in ts.integs: + assert el == 111 + +ts.vecs[0][0] = 0.0 +ts.vecs[1][0] = -243 +print(ts.vecs[0]) +print(ts.vecs[1]) diff --git a/unittest/std_array.cpp b/unittest/std_array.cpp new file mode 100644 index 000000000..fb753b520 --- /dev/null +++ b/unittest/std_array.cpp @@ -0,0 +1,46 @@ +/// @file +/// @copyright Copyright 2023 CNRS INRIA + +#include "eigenpy/std-array.hpp" + +using Eigen::VectorXd; + +std::array get_arr_3_ints() { return {1, 2, 3}; } + +std::array get_arr_3_vecs() { + std::array out; + out[0].setOnes(4); + out[1].setZero(2); + out[2].setRandom(10); + return out; +} + +struct test_struct { + std::array integs; + std::array vecs; + test_struct() { + integs = {42, 3, -1}; + vecs[0].setRandom(4); // 4 randoms between [-1,1] + vecs[1].setZero(11); // 11 zeroes + } +}; + +BOOST_PYTHON_MODULE(std_array) { + using namespace eigenpy; + + enableEigenPy(); + + StdArrayPythonVisitor, true>::expose("StdArr3_int"); + StdVectorPythonVisitor, true>::expose("StdVec_int"); + + exposeStdArrayEigenSpecificType("VectorXd"); + exposeStdArrayEigenSpecificType("VectorXd"); + exposeStdVectorEigenSpecificType("VectorXd"); + + bp::def("get_arr_3_ints", get_arr_3_ints); + bp::def("get_arr_3_vecs", get_arr_3_vecs); + + bp::class_("test_struct", bp::init<>(bp::args("self"))) + .def_readwrite("integs", &test_struct::integs) + .def_readwrite("vecs", &test_struct::vecs); +}