Repository: stack-of-tasks/eigenpy (language: python)

Issue:
Add support for std::array
We currently only support `std::vector<T, Allocator>` vector types and `std::map<K, V>`.


Discussion / hints:

Fix patch:
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 <boost/python/suite/indexing/indexing_suite.hpp>
+#include "eigenpy/std-vector.hpp"
+
+#include <array>
+
+namespace eigenpy {
+
+template <typename Container, bool NoProxy, class SliceAllocator,
+          class DerivedPolicies>
+class array_indexing_suite;
+namespace details {
+
+template <typename Container, bool NoProxy, class SliceAllocator>
+class final_array_derived_policies
+    : public array_indexing_suite<
+          Container, NoProxy, SliceAllocator,
+          final_array_derived_policies<Container, NoProxy, SliceAllocator> > {};
+}  // namespace details
+
+template <typename Container, bool NoProxy = false,
+          class SliceAllocator = std::allocator<typename Container::value_type>,
+          class DerivedPolicies = details::final_array_derived_policies<
+              Container, NoProxy, SliceAllocator> >
+class array_indexing_suite
+    : public bp::vector_indexing_suite<Container, NoProxy, DerivedPolicies> {
+ 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<data_type, SliceAllocator> slice_vector_type;
+  static constexpr std::size_t Size = std::tuple_size<Container>{};
+
+  template <class Class>
+  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 <class Iter>
+  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 <typename array_type, bool NoProxy = false,
+          class SliceAllocator =
+              std::allocator<typename array_type::value_type> >
+struct StdArrayPythonVisitor {
+  typedef typename array_type::value_type value_type;
+
+  static ::boost::python::list tolist(array_type &self) {
+    return details::build_list<array_type, NoProxy>::run(self);
+  }
+
+  static void expose(const std::string &class_name,
+                     const std::string &doc_string = "") {
+    expose(class_name, doc_string, EmptyPythonVisitor());
+  }
+
+  template <typename DerivedVisitor>
+  static void expose(const std::string &class_name,
+                     const bp::def_visitor<DerivedVisitor> &visitor) {
+    expose(class_name, "", visitor);
+  }
+
+  template <typename DerivedVisitor>
+  static void expose(const std::string &class_name,
+                     const std::string &doc_string,
+                     const bp::def_visitor<DerivedVisitor> &visitor) {
+    if (!register_symbolic_link_to_registered_type<array_type>()) {
+      bp::class_<array_type> cl(class_name.c_str(), doc_string.c_str());
+      cl.def(bp::init<const array_type &>(bp::args("self", "other"),
+                                          "Copy constructor"));
+
+      array_indexing_suite<array_type, NoProxy, SliceAllocator> 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<MatrixType, Size>
+template <typename MatrixType, std::size_t Size>
+void exposeStdArrayEigenSpecificType(const char *name) {
+  std::ostringstream oss;
+  oss << "StdArr";
+  oss << Size << "_" << name;
+  typedef std::array<MatrixType, Size> array_type;
+  StdArrayPythonVisitor<array_type, false,
+                        Eigen::aligned_allocator<MatrixType> >::
+      expose(oss.str(),
+             details::overload_base_get_item_for_std_vector<array_type>());
+}
+
+}  // 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<std::vector<Type, Allocator> &>
 
 namespace eigenpy {
 
+namespace details {
+/// Defines traits for the container, used in \struct StdContainerFromPythonList
+template <class Container>
+struct container_traits {
+  // default behavior expects allocators
+  typedef typename Container::allocator_type Allocator;
+};
+
+template <typename _Tp, std::size_t Size>
+struct container_traits<std::array<_Tp, Size> > {
+  typedef void Allocator;
+};
+};  // namespace details
+
 ///
 /// \brief Register the conversion from a Python list to a std::vector
 ///
@@ -242,7 +256,7 @@ namespace eigenpy {
 template <typename vector_type, bool NoProxy>
 struct StdContainerFromPythonList {
   typedef typename vector_type::value_type T;
-  typedef typename vector_type::allocator_type Allocator;
+  typedef typename details::container_traits<vector_type>::Allocator Allocator;
 
   /// \brief Check if obj_ptr can be converted
   static void *convertible(PyObject *obj_ptr) {


Test patch:
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<int>:", 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<int, 3> get_arr_3_ints() { return {1, 2, 3}; }
+
+std::array<VectorXd, 3> get_arr_3_vecs() {
+  std::array<VectorXd, 3> out;
+  out[0].setOnes(4);
+  out[1].setZero(2);
+  out[2].setRandom(10);
+  return out;
+}
+
+struct test_struct {
+  std::array<int, 3> integs;
+  std::array<VectorXd, 2> 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<std::array<int, 3>, true>::expose("StdArr3_int");
+  StdVectorPythonVisitor<std::vector<int>, true>::expose("StdVec_int");
+
+  exposeStdArrayEigenSpecificType<VectorXd, 2>("VectorXd");
+  exposeStdArrayEigenSpecificType<VectorXd, 3>("VectorXd");
+  exposeStdVectorEigenSpecificType<VectorXd>("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>("test_struct", bp::init<>(bp::args("self")))
+      .def_readwrite("integs", &test_struct::integs)
+      .def_readwrite("vecs", &test_struct::vecs);
+}