scirs2_core/python/numpy_compat.rs
1//! NumPy compatibility layer for scirs2-core
2//!
3//! This module ensures that scirs2_core::ndarray types are fully compatible with
4//! PyO3's numpy crate, enabling seamless Python integration.
5//!
6//! # Problem Statement
7//!
8//! The PyO3 `numpy` crate (v0.28.3) delegates NumPy bindings to `scirs2-numpy` v0.5.0. When scirs2-core
9//! uses `ndarray` v0.17 by default, there's a type incompatibility. Python integration
10//! modules must explicitly use `ndarray16` types to ensure compatibility with numpy.
11//!
12//! # Solution
13//!
14//! This module provides:
15//! 1. Explicit re-exports of ndarray types that numpy needs
16//! 2. Type aliases that guarantee compatibility
17//! 3. Conversion utilities for zero-copy operations
18//!
19//! # Usage in Python Bindings
20//!
21//! ```ignore
22//! use pyo3::prelude::*;
23//! use scirs2_core::python::numpy_compat::*;
24//!
25//! #[pyfunction]
26//! fn process_array(array: PyReadonlyArrayDyn<f32>) -> PyResult<Py<PyArrayDyn<f32>>> {
27//! // Convert from NumPy to scirs2 array
28//! let scirs_array = numpy_to_scirs_arrayd(array)?;
29//!
30//! // Process with scirs2-core
31//! let result = scirs_array.map(|x| x * 2.0);
32//!
33//! // Convert back to NumPy
34//! scirs_to_numpy_arrayd(result, array.py())
35//! }
36//! ```
37
38#[cfg(feature = "python")]
39use pyo3::prelude::*;
40
41#[cfg(feature = "python")]
42use scirs2_numpy::{
43 Element, PyArray, PyArray1, PyArray2, PyArrayDyn, PyArrayMethods, PyReadonlyArray,
44 PyReadonlyArrayDyn, PyUntypedArrayMethods,
45};
46
47// Re-export ndarray types for Python compatibility
48// IMPORTANT: Python integration uses scirs2-numpy with ndarray 0.17 support
49#[cfg(feature = "python")]
50pub use ::ndarray::{
51 arr1, arr2, array, s, Array, Array0, Array1, Array2, Array3, Array4, ArrayBase, ArrayD,
52 ArrayView, ArrayView1, ArrayView2, ArrayViewD, ArrayViewMut, ArrayViewMut1, ArrayViewMut2,
53 ArrayViewMutD, Axis, Data, DataMut, DataOwned, Dim, Dimension, Ix0, Ix1, Ix2, Ix3, Ix4, IxDyn,
54 IxDynImpl, OwnedRepr, RawData, ViewRepr, Zip,
55};
56
57/// Type alias for numpy-compatible dynamic arrays
58#[cfg(feature = "python")]
59pub type NumpyCompatArrayD<T> = ArrayD<T>;
60
61/// Type alias for numpy-compatible 1D arrays
62#[cfg(feature = "python")]
63pub type NumpyCompatArray1<T> = Array1<T>;
64
65/// Type alias for numpy-compatible 2D arrays
66#[cfg(feature = "python")]
67pub type NumpyCompatArray2<T> = Array2<T>;
68
69// ========================================
70// CONVERSION FUNCTIONS
71// ========================================
72
73/// Convert a NumPy array to an owned scirs2 `ArrayD`
74///
75/// This function always copies -- it cannot be zero-copy. `.readonly()` produces a
76/// `PyReadonlyArrayDyn` guard that is local to this call and is dropped when the
77/// function returns, so a view borrowed from it could never be returned soundly by
78/// this `ArrayD<T>`-returning signature. `.to_owned()` is required here, not merely
79/// an optimization choice.
80///
81/// For true zero-copy access, use [`numpy_readonly_to_scirs_view`] instead: it takes
82/// the `PyReadonlyArrayDyn` guard by reference from the *caller*, so the caller keeps
83/// the guard (and therefore the returned view) alive for as long as it's needed.
84#[cfg(feature = "python")]
85pub fn numpy_to_scirs_arrayd<'py, T>(array: &Bound<'py, PyArrayDyn<T>>) -> PyResult<ArrayD<T>>
86where
87 T: Element + Clone,
88{
89 let readonly = array.readonly();
90 let array_ref = readonly.as_array();
91
92 // `readonly` is a local guard dropped when this function returns, so copying
93 // here is required for soundness -- see the zero-copy alternative documented
94 // above.
95 Ok(array_ref.to_owned())
96}
97
98/// Convert NumPy readonly array to scirs2 ArrayD
99#[cfg(feature = "python")]
100pub fn numpy_readonly_to_scirs_arrayd<T>(array: PyReadonlyArrayDyn<T>) -> PyResult<ArrayD<T>>
101where
102 T: Element + Clone,
103{
104 Ok(array.as_array().to_owned())
105}
106
107/// Convert scirs2 ArrayD to NumPy array
108#[cfg(feature = "python")]
109pub fn scirs_to_numpy_arrayd<T: Element>(
110 array: ArrayD<T>,
111 py: Python<'_>,
112) -> PyResult<Py<PyArrayDyn<T>>> {
113 Ok(PyArrayDyn::from_owned_array(py, array).unbind())
114}
115
116/// Convert scirs2 Array1 to NumPy array
117#[cfg(feature = "python")]
118pub fn scirs_to_numpy_array1<T: Element>(
119 array: Array1<T>,
120 py: Python<'_>,
121) -> PyResult<Py<PyArray1<T>>> {
122 Ok(PyArray1::from_owned_array(py, array).unbind())
123}
124
125/// Convert scirs2 Array2 to NumPy array
126#[cfg(feature = "python")]
127pub fn scirs_to_numpy_array2<T: Element>(
128 array: Array2<T>,
129 py: Python<'_>,
130) -> PyResult<Py<PyArray2<T>>> {
131 Ok(PyArray2::from_owned_array(py, array).unbind())
132}
133
134// ========================================
135// BATCH CONVERSION UTILITIES
136// ========================================
137
138/// Convert a vector of NumPy arrays to scirs2 ArrayD arrays
139#[cfg(feature = "python")]
140pub fn numpy_batch_to_scirs<T: Element + Clone>(
141 arrays: Vec<PyReadonlyArrayDyn<T>>,
142) -> PyResult<Vec<ArrayD<T>>> {
143 arrays
144 .into_iter()
145 .map(|arr| Ok(arr.as_array().to_owned()))
146 .collect()
147}
148
149/// Convert a vector of scirs2 arrays to NumPy arrays
150#[cfg(feature = "python")]
151pub fn scirs_batch_to_numpy<T: Element>(
152 arrays: Vec<ArrayD<T>>,
153 py: Python<'_>,
154) -> PyResult<Vec<Py<PyArrayDyn<T>>>> {
155 arrays
156 .into_iter()
157 .map(|arr| Ok(PyArrayDyn::from_owned_array(py, arr).unbind()))
158 .collect()
159}
160
161// ========================================
162// VIEW CONVERSION (ZERO-COPY)
163// ========================================
164
165/// Convert NumPy readonly array to scirs2 ArrayView (zero-copy)
166///
167/// This provides true zero-copy access to NumPy arrays. The readonly guard
168/// must be kept alive for the lifetime of the view.
169///
170/// # Example
171///
172/// ```ignore
173/// let readonly = numpy_array.readonly();
174/// let view = numpy_readonly_to_scirs_view(&readonly);
175/// // Use view while readonly is in scope
176/// ```
177#[cfg(feature = "python")]
178pub fn numpy_readonly_to_scirs_view<'a, T: Element>(
179 array: &'a PyReadonlyArrayDyn<'a, T>,
180) -> ArrayViewD<'a, T> {
181 array.as_array()
182}
183
184// ========================================
185// TYPE INFORMATION UTILITIES
186// ========================================
187
188/// Check if a NumPy array is compatible with scirs2 operations
189#[cfg(feature = "python")]
190pub fn is_numpy_compatible<T: Element>(array: &Bound<'_, PyArrayDyn<T>>) -> bool {
191 // Check if array is well-formed (has valid shape and strides)
192 array.shape().iter().all(|&dim| dim > 0)
193}
194
195/// Get the memory layout of a NumPy array
196#[cfg(feature = "python")]
197pub enum MemoryLayout {
198 CContiguous,
199 FContiguous,
200 Neither,
201}
202
203#[cfg(feature = "python")]
204pub fn get_numpy_layout<T: Element>(array: &Bound<'_, PyArrayDyn<T>>) -> MemoryLayout {
205 if array.is_c_contiguous() {
206 MemoryLayout::CContiguous
207 } else if array.is_fortran_contiguous() {
208 MemoryLayout::FContiguous
209 } else {
210 MemoryLayout::Neither
211 }
212}
213
214// ========================================
215// TESTS
216// ========================================
217
218#[cfg(all(test, feature = "python"))]
219mod tests {
220 use super::*;
221 use pyo3::Python;
222
223 #[test]
224 #[allow(deprecated)]
225 fn test_numpy_scirs_roundtrip() {
226 pyo3::Python::attach(|py| {
227 // Create scirs2 array
228 let scirs_array = array![[1.0f32, 2.0], [3.0, 4.0]];
229 let scirs_arrayd = scirs_array.into_dyn();
230
231 // Convert to NumPy
232 let numpy_array =
233 scirs_to_numpy_arrayd(scirs_arrayd.clone(), py).expect("Operation failed");
234
235 // Convert back to scirs2
236 let result = numpy_to_scirs_arrayd(&numpy_array.bind(py)).expect("Operation failed");
237
238 // Verify equality
239 assert_eq!(result.shape(), scirs_arrayd.shape());
240 assert_eq!(result, scirs_arrayd);
241 });
242 }
243
244 #[test]
245 #[allow(deprecated)]
246 fn test_zero_copy_view() {
247 pyo3::Python::attach(|py| {
248 let scirs_array = array![[1.0f32, 2.0], [3.0, 4.0]].into_dyn();
249 let numpy_array =
250 scirs_to_numpy_arrayd(scirs_array.clone(), py).expect("Operation failed");
251
252 // Get zero-copy view
253 let readonly = numpy_array.bind(py).readonly();
254 let view = numpy_readonly_to_scirs_view(&readonly);
255
256 // Verify it's the same data
257 assert_eq!(view.shape(), scirs_array.shape());
258 assert_eq!(view[[0, 0]], 1.0f32);
259 });
260 }
261}