The source code and dockerfile for the GSW2024 AI Lab.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

310 lines
14 KiB

2 months ago
  1. Eigen
  2. #####
  3. `Eigen <http://eigen.tuxfamily.org>`_ is C++ header-based library for dense and
  4. sparse linear algebra. Due to its popularity and widespread adoption, pybind11
  5. provides transparent conversion and limited mapping support between Eigen and
  6. Scientific Python linear algebra data types.
  7. To enable the built-in Eigen support you must include the optional header file
  8. :file:`pybind11/eigen.h`.
  9. Pass-by-value
  10. =============
  11. When binding a function with ordinary Eigen dense object arguments (for
  12. example, ``Eigen::MatrixXd``), pybind11 will accept any input value that is
  13. already (or convertible to) a ``numpy.ndarray`` with dimensions compatible with
  14. the Eigen type, copy its values into a temporary Eigen variable of the
  15. appropriate type, then call the function with this temporary variable.
  16. Sparse matrices are similarly copied to or from
  17. ``scipy.sparse.csr_matrix``/``scipy.sparse.csc_matrix`` objects.
  18. Pass-by-reference
  19. =================
  20. One major limitation of the above is that every data conversion implicitly
  21. involves a copy, which can be both expensive (for large matrices) and disallows
  22. binding functions that change their (Matrix) arguments. Pybind11 allows you to
  23. work around this by using Eigen's ``Eigen::Ref<MatrixType>`` class much as you
  24. would when writing a function taking a generic type in Eigen itself (subject to
  25. some limitations discussed below).
  26. When calling a bound function accepting a ``Eigen::Ref<const MatrixType>``
  27. type, pybind11 will attempt to avoid copying by using an ``Eigen::Map`` object
  28. that maps into the source ``numpy.ndarray`` data: this requires both that the
  29. data types are the same (e.g. ``dtype='float64'`` and ``MatrixType::Scalar`` is
  30. ``double``); and that the storage is layout compatible. The latter limitation
  31. is discussed in detail in the section below, and requires careful
  32. consideration: by default, numpy matrices and eigen matrices are *not* storage
  33. compatible.
  34. If the numpy matrix cannot be used as is (either because its types differ, e.g.
  35. passing an array of integers to an Eigen paramater requiring doubles, or
  36. because the storage is incompatible), pybind11 makes a temporary copy and
  37. passes the copy instead.
  38. When a bound function parameter is instead ``Eigen::Ref<MatrixType>`` (note the
  39. lack of ``const``), pybind11 will only allow the function to be called if it
  40. can be mapped *and* if the numpy array is writeable (that is
  41. ``a.flags.writeable`` is true). Any access (including modification) made to
  42. the passed variable will be transparently carried out directly on the
  43. ``numpy.ndarray``.
  44. This means you can can write code such as the following and have it work as
  45. expected:
  46. .. code-block:: cpp
  47. void scale_by_2(Eigen::Ref<Eigen::VectorXd> m) {
  48. v *= 2;
  49. }
  50. Note, however, that you will likely run into limitations due to numpy and
  51. Eigen's difference default storage order for data; see the below section on
  52. :ref:`storage_orders` for details on how to bind code that won't run into such
  53. limitations.
  54. .. note::
  55. Passing by reference is not supported for sparse types.
  56. Returning values to Python
  57. ==========================
  58. When returning an ordinary dense Eigen matrix type to numpy (e.g.
  59. ``Eigen::MatrixXd`` or ``Eigen::RowVectorXf``) pybind11 keeps the matrix and
  60. returns a numpy array that directly references the Eigen matrix: no copy of the
  61. data is performed. The numpy array will have ``array.flags.owndata`` set to
  62. ``False`` to indicate that it does not own the data, and the lifetime of the
  63. stored Eigen matrix will be tied to the returned ``array``.
  64. If you bind a function with a non-reference, ``const`` return type (e.g.
  65. ``const Eigen::MatrixXd``), the same thing happens except that pybind11 also
  66. sets the numpy array's ``writeable`` flag to false.
  67. If you return an lvalue reference or pointer, the usual pybind11 rules apply,
  68. as dictated by the binding function's return value policy (see the
  69. documentation on :ref:`return_value_policies` for full details). That means,
  70. without an explicit return value policy, lvalue references will be copied and
  71. pointers will be managed by pybind11. In order to avoid copying, you should
  72. explictly specify an appropriate return value policy, as in the following
  73. example:
  74. .. code-block:: cpp
  75. class MyClass {
  76. Eigen::MatrixXd big_mat = Eigen::MatrixXd::Zero(10000, 10000);
  77. public:
  78. Eigen::MatrixXd &getMatrix() { return big_mat; }
  79. const Eigen::MatrixXd &viewMatrix() { return big_mat; }
  80. };
  81. // Later, in binding code:
  82. py::class_<MyClass>(m, "MyClass")
  83. .def(py::init<>())
  84. .def("copy_matrix", &MyClass::getMatrix) // Makes a copy!
  85. .def("get_matrix", &MyClass::getMatrix, py::return_value_policy::reference_internal)
  86. .def("view_matrix", &MyClass::viewMatrix, py::return_value_policy::reference_internal)
  87. ;
  88. .. code-block:: python
  89. a = MyClass()
  90. m = a.get_matrix() # flags.writeable = True, flags.owndata = False
  91. v = a.view_matrix() # flags.writeable = False, flags.owndata = False
  92. c = a.copy_matrix() # flags.writeable = True, flags.owndata = True
  93. # m[5,6] and v[5,6] refer to the same element, c[5,6] does not.
  94. Note in this example that ``py::return_value_policy::reference_internal`` is
  95. used to tie the life of the MyClass object to the life of the returned arrays.
  96. You may also return an ``Eigen::Ref``, ``Eigen::Map`` or other map-like Eigen
  97. object (for example, the return value of ``matrix.block()`` and related
  98. methods) that map into a dense Eigen type. When doing so, the default
  99. behaviour of pybind11 is to simply reference the returned data: you must take
  100. care to ensure that this data remains valid! You may ask pybind11 to
  101. explicitly *copy* such a return value by using the
  102. ``py::return_value_policy::copy`` policy when binding the function. You may
  103. also use ``py::return_value_policy::reference_internal`` or a
  104. ``py::keep_alive`` to ensure the data stays valid as long as the returned numpy
  105. array does.
  106. When returning such a reference of map, pybind11 additionally respects the
  107. readonly-status of the returned value, marking the numpy array as non-writeable
  108. if the reference or map was itself read-only.
  109. .. note::
  110. Sparse types are always copied when returned.
  111. .. _storage_orders:
  112. Storage orders
  113. ==============
  114. Passing arguments via ``Eigen::Ref`` has some limitations that you must be
  115. aware of in order to effectively pass matrices by reference. First and
  116. foremost is that the default ``Eigen::Ref<MatrixType>`` class requires
  117. contiguous storage along columns (for column-major types, the default in Eigen)
  118. or rows if ``MatrixType`` is specifically an ``Eigen::RowMajor`` storage type.
  119. The former, Eigen's default, is incompatible with ``numpy``'s default row-major
  120. storage, and so you will not be able to pass numpy arrays to Eigen by reference
  121. without making one of two changes.
  122. (Note that this does not apply to vectors (or column or row matrices): for such
  123. types the "row-major" and "column-major" distinction is meaningless).
  124. The first approach is to change the use of ``Eigen::Ref<MatrixType>`` to the
  125. more general ``Eigen::Ref<MatrixType, 0, Eigen::Stride<Eigen::Dynamic,
  126. Eigen::Dynamic>>`` (or similar type with a fully dynamic stride type in the
  127. third template argument). Since this is a rather cumbersome type, pybind11
  128. provides a ``py::EigenDRef<MatrixType>`` type alias for your convenience (along
  129. with EigenDMap for the equivalent Map, and EigenDStride for just the stride
  130. type).
  131. This type allows Eigen to map into any arbitrary storage order. This is not
  132. the default in Eigen for performance reasons: contiguous storage allows
  133. vectorization that cannot be done when storage is not known to be contiguous at
  134. compile time. The default ``Eigen::Ref`` stride type allows non-contiguous
  135. storage along the outer dimension (that is, the rows of a column-major matrix
  136. or columns of a row-major matrix), but not along the inner dimension.
  137. This type, however, has the added benefit of also being able to map numpy array
  138. slices. For example, the following (contrived) example uses Eigen with a numpy
  139. slice to multiply by 2 all coefficients that are both on even rows (0, 2, 4,
  140. ...) and in columns 2, 5, or 8:
  141. .. code-block:: cpp
  142. m.def("scale", [](py::EigenDRef<Eigen::MatrixXd> m, double c) { m *= c; });
  143. .. code-block:: python
  144. # a = np.array(...)
  145. scale_by_2(myarray[0::2, 2:9:3])
  146. The second approach to avoid copying is more intrusive: rearranging the
  147. underlying data types to not run into the non-contiguous storage problem in the
  148. first place. In particular, that means using matrices with ``Eigen::RowMajor``
  149. storage, where appropriate, such as:
  150. .. code-block:: cpp
  151. using RowMatrixXd = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
  152. // Use RowMatrixXd instead of MatrixXd
  153. Now bound functions accepting ``Eigen::Ref<RowMatrixXd>`` arguments will be
  154. callable with numpy's (default) arrays without involving a copying.
  155. You can, alternatively, change the storage order that numpy arrays use by
  156. adding the ``order='F'`` option when creating an array:
  157. .. code-block:: python
  158. myarray = np.array(source, order='F')
  159. Such an object will be passable to a bound function accepting an
  160. ``Eigen::Ref<MatrixXd>`` (or similar column-major Eigen type).
  161. One major caveat with this approach, however, is that it is not entirely as
  162. easy as simply flipping all Eigen or numpy usage from one to the other: some
  163. operations may alter the storage order of a numpy array. For example, ``a2 =
  164. array.transpose()`` results in ``a2`` being a view of ``array`` that references
  165. the same data, but in the opposite storage order!
  166. While this approach allows fully optimized vectorized calculations in Eigen, it
  167. cannot be used with array slices, unlike the first approach.
  168. When *returning* a matrix to Python (either a regular matrix, a reference via
  169. ``Eigen::Ref<>``, or a map/block into a matrix), no special storage
  170. consideration is required: the created numpy array will have the required
  171. stride that allows numpy to properly interpret the array, whatever its storage
  172. order.
  173. Failing rather than copying
  174. ===========================
  175. The default behaviour when binding ``Eigen::Ref<const MatrixType>`` eigen
  176. references is to copy matrix values when passed a numpy array that does not
  177. conform to the element type of ``MatrixType`` or does not have a compatible
  178. stride layout. If you want to explicitly avoid copying in such a case, you
  179. should bind arguments using the ``py::arg().noconvert()`` annotation (as
  180. described in the :ref:`nonconverting_arguments` documentation).
  181. The following example shows an example of arguments that don't allow data
  182. copying to take place:
  183. .. code-block:: cpp
  184. // The method and function to be bound:
  185. class MyClass {
  186. // ...
  187. double some_method(const Eigen::Ref<const MatrixXd> &matrix) { /* ... */ }
  188. };
  189. float some_function(const Eigen::Ref<const MatrixXf> &big,
  190. const Eigen::Ref<const MatrixXf> &small) {
  191. // ...
  192. }
  193. // The associated binding code:
  194. using namespace pybind11::literals; // for "arg"_a
  195. py::class_<MyClass>(m, "MyClass")
  196. // ... other class definitions
  197. .def("some_method", &MyClass::some_method, py::arg().nocopy());
  198. m.def("some_function", &some_function,
  199. "big"_a.nocopy(), // <- Don't allow copying for this arg
  200. "small"_a // <- This one can be copied if needed
  201. );
  202. With the above binding code, attempting to call the the ``some_method(m)``
  203. method on a ``MyClass`` object, or attempting to call ``some_function(m, m2)``
  204. will raise a ``RuntimeError`` rather than making a temporary copy of the array.
  205. It will, however, allow the ``m2`` argument to be copied into a temporary if
  206. necessary.
  207. Note that explicitly specifying ``.noconvert()`` is not required for *mutable*
  208. Eigen references (e.g. ``Eigen::Ref<MatrixXd>`` without ``const`` on the
  209. ``MatrixXd``): mutable references will never be called with a temporary copy.
  210. Vectors versus column/row matrices
  211. ==================================
  212. Eigen and numpy have fundamentally different notions of a vector. In Eigen, a
  213. vector is simply a matrix with the number of columns or rows set to 1 at
  214. compile time (for a column vector or row vector, respectively). Numpy, in
  215. contast, has comparable 2-dimensional 1xN and Nx1 arrays, but *also* has
  216. 1-dimensional arrays of size N.
  217. When passing a 2-dimensional 1xN or Nx1 array to Eigen, the Eigen type must
  218. have matching dimensions: That is, you cannot pass a 2-dimensional Nx1 numpy
  219. array to an Eigen value expecting a row vector, or a 1xN numpy array as a
  220. column vector argument.
  221. On the other hand, pybind11 allows you to pass 1-dimensional arrays of length N
  222. as Eigen parameters. If the Eigen type can hold a column vector of length N it
  223. will be passed as such a column vector. If not, but the Eigen type constraints
  224. will accept a row vector, it will be passed as a row vector. (The column
  225. vector takes precendence when both are supported, for example, when passing a
  226. 1D numpy array to a MatrixXd argument). Note that the type need not be
  227. expicitly a vector: it is permitted to pass a 1D numpy array of size 5 to an
  228. Eigen ``Matrix<double, Dynamic, 5>``: you would end up with a 1x5 Eigen matrix.
  229. Passing the same to an ``Eigen::MatrixXd`` would result in a 5x1 Eigen matrix.
  230. When returning an eigen vector to numpy, the conversion is ambiguous: a row
  231. vector of length 4 could be returned as either a 1D array of length 4, or as a
  232. 2D array of size 1x4. When encoutering such a situation, pybind11 compromises
  233. by considering the returned Eigen type: if it is a compile-time vector--that
  234. is, the type has either the number of rows or columns set to 1 at compile
  235. time--pybind11 converts to a 1D numpy array when returning the value. For
  236. instances that are a vector only at run-time (e.g. ``MatrixXd``,
  237. ``Matrix<float, Dynamic, 4>``), pybind11 returns the vector as a 2D array to
  238. numpy. If this isn't want you want, you can use ``array.reshape(...)`` to get
  239. a view of the same data in the desired dimensions.
  240. .. seealso::
  241. The file :file:`tests/test_eigen.cpp` contains a complete example that
  242. shows how to pass Eigen sparse and dense data types in more detail.