Skip to main content

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 NumPy array to scirs2 ArrayD (zero-copy when possible)
74///
75/// This function attempts zero-copy conversion when the NumPy array is C-contiguous.
76/// Otherwise, it creates a owned copy.
77#[cfg(feature = "python")]
78pub fn numpy_to_scirs_arrayd<'py, T>(array: &Bound<'py, PyArrayDyn<T>>) -> PyResult<ArrayD<T>>
79where
80    T: Element + Clone,
81{
82    let readonly = array.readonly();
83    let array_ref = readonly.as_array();
84
85    // Always create owned array for safety
86    // Zero-copy path: use `numpy_readonly_to_scirs_view` for a lifetime-bounded
87    // `ArrayViewD<'a, T>` that avoids the allocation here. This owned-copy path
88    // exists for callers that need owned data or mutable access.
89    Ok(array_ref.to_owned())
90}
91
92/// Convert NumPy readonly array to scirs2 ArrayD
93#[cfg(feature = "python")]
94pub fn numpy_readonly_to_scirs_arrayd<T>(array: PyReadonlyArrayDyn<T>) -> PyResult<ArrayD<T>>
95where
96    T: Element + Clone,
97{
98    Ok(array.as_array().to_owned())
99}
100
101/// Convert scirs2 ArrayD to NumPy array
102#[cfg(feature = "python")]
103pub fn scirs_to_numpy_arrayd<T: Element>(
104    array: ArrayD<T>,
105    py: Python<'_>,
106) -> PyResult<Py<PyArrayDyn<T>>> {
107    Ok(PyArrayDyn::from_owned_array(py, array).unbind())
108}
109
110/// Convert scirs2 Array1 to NumPy array
111#[cfg(feature = "python")]
112pub fn scirs_to_numpy_array1<T: Element>(
113    array: Array1<T>,
114    py: Python<'_>,
115) -> PyResult<Py<PyArray1<T>>> {
116    Ok(PyArray1::from_owned_array(py, array).unbind())
117}
118
119/// Convert scirs2 Array2 to NumPy array
120#[cfg(feature = "python")]
121pub fn scirs_to_numpy_array2<T: Element>(
122    array: Array2<T>,
123    py: Python<'_>,
124) -> PyResult<Py<PyArray2<T>>> {
125    Ok(PyArray2::from_owned_array(py, array).unbind())
126}
127
128// ========================================
129// BATCH CONVERSION UTILITIES
130// ========================================
131
132/// Convert a vector of NumPy arrays to scirs2 ArrayD arrays
133#[cfg(feature = "python")]
134pub fn numpy_batch_to_scirs<T: Element + Clone>(
135    arrays: Vec<PyReadonlyArrayDyn<T>>,
136) -> PyResult<Vec<ArrayD<T>>> {
137    arrays
138        .into_iter()
139        .map(|arr| Ok(arr.as_array().to_owned()))
140        .collect()
141}
142
143/// Convert a vector of scirs2 arrays to NumPy arrays
144#[cfg(feature = "python")]
145pub fn scirs_batch_to_numpy<T: Element>(
146    arrays: Vec<ArrayD<T>>,
147    py: Python<'_>,
148) -> PyResult<Vec<Py<PyArrayDyn<T>>>> {
149    arrays
150        .into_iter()
151        .map(|arr| Ok(PyArrayDyn::from_owned_array(py, arr).unbind()))
152        .collect()
153}
154
155// ========================================
156// VIEW CONVERSION (ZERO-COPY)
157// ========================================
158
159/// Convert NumPy readonly array to scirs2 ArrayView (zero-copy)
160///
161/// This provides true zero-copy access to NumPy arrays. The readonly guard
162/// must be kept alive for the lifetime of the view.
163///
164/// # Example
165///
166/// ```ignore
167/// let readonly = numpy_array.readonly();
168/// let view = numpy_readonly_to_scirs_view(&readonly);
169/// // Use view while readonly is in scope
170/// ```
171#[cfg(feature = "python")]
172pub fn numpy_readonly_to_scirs_view<'a, T: Element>(
173    array: &'a PyReadonlyArrayDyn<'a, T>,
174) -> ArrayViewD<'a, T> {
175    array.as_array()
176}
177
178// ========================================
179// TYPE INFORMATION UTILITIES
180// ========================================
181
182/// Check if a NumPy array is compatible with scirs2 operations
183#[cfg(feature = "python")]
184pub fn is_numpy_compatible<T: Element>(array: &Bound<'_, PyArrayDyn<T>>) -> bool {
185    // Check if array is well-formed (has valid shape and strides)
186    array.shape().iter().all(|&dim| dim > 0)
187}
188
189/// Get the memory layout of a NumPy array
190#[cfg(feature = "python")]
191pub enum MemoryLayout {
192    CContiguous,
193    FContiguous,
194    Neither,
195}
196
197#[cfg(feature = "python")]
198pub fn get_numpy_layout<T: Element>(array: &Bound<'_, PyArrayDyn<T>>) -> MemoryLayout {
199    if array.is_c_contiguous() {
200        MemoryLayout::CContiguous
201    } else if array.is_fortran_contiguous() {
202        MemoryLayout::FContiguous
203    } else {
204        MemoryLayout::Neither
205    }
206}
207
208// ========================================
209// TESTS
210// ========================================
211
212#[cfg(all(test, feature = "python"))]
213mod tests {
214    use super::*;
215    use pyo3::Python;
216
217    #[test]
218    #[allow(deprecated)]
219    fn test_numpy_scirs_roundtrip() {
220        pyo3::Python::attach(|py| {
221            // Create scirs2 array
222            let scirs_array = array![[1.0f32, 2.0], [3.0, 4.0]];
223            let scirs_arrayd = scirs_array.into_dyn();
224
225            // Convert to NumPy
226            let numpy_array =
227                scirs_to_numpy_arrayd(scirs_arrayd.clone(), py).expect("Operation failed");
228
229            // Convert back to scirs2
230            let result = numpy_to_scirs_arrayd(&numpy_array.bind(py)).expect("Operation failed");
231
232            // Verify equality
233            assert_eq!(result.shape(), scirs_arrayd.shape());
234            assert_eq!(result, scirs_arrayd);
235        });
236    }
237
238    #[test]
239    #[allow(deprecated)]
240    fn test_zero_copy_view() {
241        pyo3::Python::attach(|py| {
242            let scirs_array = array![[1.0f32, 2.0], [3.0, 4.0]].into_dyn();
243            let numpy_array =
244                scirs_to_numpy_arrayd(scirs_array.clone(), py).expect("Operation failed");
245
246            // Get zero-copy view
247            let readonly = numpy_array.bind(py).readonly();
248            let view = numpy_readonly_to_scirs_view(&readonly);
249
250            // Verify it's the same data
251            assert_eq!(view.shape(), scirs_array.shape());
252            assert_eq!(view[[0, 0]], 1.0f32);
253        });
254    }
255}