From 8a0e6ed82cd77f087754bbe810cc118d34dfa46b Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sat, 23 Mar 2024 09:34:03 +0100 Subject: [PATCH] Extend the documentation of the nalgebra integration to discuss its likely surprising memory layout requirements. --- src/array.rs | 4 ++++ src/borrow/mod.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/untyped_array.rs | 2 +- tests/borrow.rs | 20 ++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/array.rs b/src/array.rs index f63455896..d93846961 100644 --- a/src/array.rs +++ b/src/array.rs @@ -879,6 +879,8 @@ where { /// Try to convert this array into a [`nalgebra::MatrixView`] using the given shape and strides. /// + /// See [`PyReadonlyArray::try_as_matrix`] for a discussion of the memory layout requirements. + /// /// # Safety /// /// Calling this method invalidates all exclusive references to the internal data, e.g. `ArrayViewMut` or `MatrixSliceMut`. @@ -901,6 +903,8 @@ where /// Try to convert this array into a [`nalgebra::MatrixViewMut`] using the given shape and strides. /// + /// See [`PyReadonlyArray::try_as_matrix`] for a discussion of the memory layout requirements. + /// /// # Safety /// /// Calling this method invalidates all other references to the internal data, e.g. `ArrayView`, `MatrixSlice`, `ArrayViewMut` or `MatrixSliceMut`. diff --git a/src/borrow/mod.rs b/src/borrow/mod.rs index 26b189936..b330fb68f 100644 --- a/src/borrow/mod.rs +++ b/src/borrow/mod.rs @@ -279,6 +279,43 @@ where D: Dimension, { /// Try to convert this array into a [`nalgebra::MatrixView`] using the given shape and strides. + /// + /// Note that nalgebra's types default to Fortan/column-major standard strides whereas NumPy creates C/row-major strides by default. + /// Furthermore, array views created by slicing into existing arrays will often have non-standard strides. + /// + /// If you do not fully control the memory layout of a given array, e.g. at your API entry points, + /// it can be useful to opt into nalgebra's support for [dynamic strides][nalgebra::Dyn], for example + /// + /// ```rust + /// # use pyo3::prelude::*; + /// use pyo3::py_run; + /// use numpy::{get_array_module, PyReadonlyArray2}; + /// use nalgebra::{MatrixView, Const, Dyn}; + /// + /// #[pyfunction] + /// fn sum_standard_layout<'py>(py: Python<'py>, array: PyReadonlyArray2<'py, f64>) -> Option { + /// let matrix: Option, Const<2>>> = array.try_as_matrix(); + /// matrix.map(|matrix| matrix.sum()) + /// } + /// + /// #[pyfunction] + /// fn sum_dynamic_strides<'py>(py: Python<'py>, array: PyReadonlyArray2<'py, f64>) -> Option { + /// let matrix: Option, Const<2>, Dyn, Dyn>> = array.try_as_matrix(); + /// matrix.map(|matrix| matrix.sum()) + /// } + /// + /// Python::with_gil(|py| { + /// let np = py.eval("__import__('numpy')", None, None).unwrap(); + /// let sum_standard_layout = wrap_pyfunction!(sum_standard_layout)(py).unwrap(); + /// let sum_dynamic_strides = wrap_pyfunction!(sum_dynamic_strides)(py).unwrap(); + /// + /// py_run!(py, np sum_standard_layout, r"assert sum_standard_layout(np.ones((2, 2), order='F')) == 4."); + /// py_run!(py, np sum_standard_layout, r"assert sum_standard_layout(np.ones((2, 2, 2))[:,:,0]) is None"); + /// + /// py_run!(py, np sum_dynamic_strides, r"assert sum_dynamic_strides(np.ones((2, 2), order='F')) == 4."); + /// py_run!(py, np sum_dynamic_strides, r"assert sum_dynamic_strides(np.ones((2, 2, 2))[:,:,0]) == 4."); + /// }); + /// ``` #[doc(alias = "nalgebra")] pub fn try_as_matrix( &self, @@ -466,6 +503,8 @@ where D: Dimension, { /// Try to convert this array into a [`nalgebra::MatrixViewMut`] using the given shape and strides. + /// + /// See [`PyReadonlyArray::try_as_matrix`] for a discussion of the memory layout requirements. #[doc(alias = "nalgebra")] pub fn try_as_matrix_mut( &self, diff --git a/src/untyped_array.rs b/src/untyped_array.rs index cf5bd385c..18a8ed112 100644 --- a/src/untyped_array.rs +++ b/src/untyped_array.rs @@ -13,7 +13,7 @@ use crate::cold; use crate::dtype::PyArrayDescr; use crate::npyffi; -/// A safe, untyped wrapper for NumPy's [`ndarray`][ndarray] class. +/// A safe, untyped wrapper for NumPy's [`ndarray`] class. /// /// Unlike [`PyArray`][crate::PyArray], this type does not constrain either element type `T` nor the dimensionality `D`. /// This can be useful to inspect function arguments, but it prevents operating on the elements without further downcasts. diff --git a/tests/borrow.rs b/tests/borrow.rs index 576eb9fc1..2ad8381cd 100644 --- a/tests/borrow.rs +++ b/tests/borrow.rs @@ -434,6 +434,26 @@ fn matrix_from_numpy() { assert!(matrix.is_none()); }); + Python::with_gil(|py| { + let array = numpy::pyarray![py, [[0, 1], [2, 3]], [[4, 5], [6, 7]]]; + let array: &PyArray2 = py + .eval("a[:,:,0]", Some([("a", array)].into_py_dict(py)), None) + .unwrap() + .downcast() + .unwrap(); + let array = array.readonly(); + + let matrix: nalgebra::MatrixView< + '_, + i32, + nalgebra::Const<2>, + nalgebra::Const<2>, + nalgebra::Dyn, + nalgebra::Dyn, + > = array.try_as_matrix().unwrap(); + assert_eq!(matrix, nalgebra::Matrix2::new(0, 2, 4, 6)); + }); + Python::with_gil(|py| { let array = numpy::pyarray![py, [0, 1, 2], [3, 4, 5], [6, 7, 8]]; let array = array.readonly();