scirs2_core/python/
conversions.rs

1//! Type conversion utilities for Python integration
2//!
3//! This module provides comprehensive conversion utilities between Python types
4//! and scirs2-core types, with a focus on performance and correctness.
5
6#[cfg(feature = "python")]
7use pyo3::prelude::*;
8
9#[cfg(feature = "python")]
10use scirs2_numpy::{
11    Element, PyArray1, PyArray2, PyArrayDyn, PyArrayMethods, PyUntypedArrayMethods,
12};
13
14// IMPORTANT: Python integration uses scirs2-numpy with ndarray 0.17
15#[cfg(feature = "python")]
16use ::ndarray::{Array1, Array2, ArrayD};
17
18// ========================================
19// SCALAR CONVERSIONS
20// ========================================
21
22/// Convert Python float to Rust f32
23#[cfg(feature = "python")]
24pub fn py_float_to_f32(obj: &Bound<'_, PyAny>) -> PyResult<f32> {
25    obj.extract::<f64>().map(|v| v as f32)
26}
27
28/// Convert Python float to Rust f64
29#[cfg(feature = "python")]
30pub fn py_float_to_f64(obj: &Bound<'_, PyAny>) -> PyResult<f64> {
31    obj.extract::<f64>()
32}
33
34/// Convert Python int to Rust i32
35#[cfg(feature = "python")]
36pub fn py_int_to_i32(obj: &Bound<'_, PyAny>) -> PyResult<i32> {
37    obj.extract::<i64>().map(|v| v as i32)
38}
39
40/// Convert Python int to Rust i64
41#[cfg(feature = "python")]
42pub fn py_int_to_i64(obj: &Bound<'_, PyAny>) -> PyResult<i64> {
43    obj.extract::<i64>()
44}
45
46// ========================================
47// LIST/TUPLE CONVERSIONS
48// ========================================
49
50/// Convert Python list to Rust `Vec<f32>`
51#[cfg(feature = "python")]
52pub fn py_list_to_vec_f32(obj: &Bound<'_, PyAny>) -> PyResult<Vec<f32>> {
53    obj.extract::<Vec<f64>>()
54        .map(|v| v.into_iter().map(|x| x as f32).collect())
55}
56
57/// Convert Python list to Rust `Vec<i32>`
58#[cfg(feature = "python")]
59pub fn py_list_to_vec_i32(obj: &Bound<'_, PyAny>) -> PyResult<Vec<i32>> {
60    obj.extract::<Vec<i64>>()
61        .map(|v| v.into_iter().map(|x| x as i32).collect())
62}
63
64/// Convert Python list to Rust `Vec<usize>`
65#[cfg(feature = "python")]
66pub fn py_list_to_vec_usize(obj: &Bound<'_, PyAny>) -> PyResult<Vec<usize>> {
67    obj.extract::<Vec<i64>>()
68        .map(|v| v.into_iter().map(|x| x as usize).collect())
69}
70
71// ========================================
72// SHAPE CONVERSIONS
73// ========================================
74
75/// Convert Python shape tuple to Rust `Vec<usize>`
76#[cfg(feature = "python")]
77pub fn py_shape_to_vec(obj: &Bound<'_, PyAny>) -> PyResult<Vec<usize>> {
78    py_list_to_vec_usize(obj)
79}
80
81/// Convert Rust shape Vec to Python tuple
82#[cfg(feature = "python")]
83pub fn shape_to_py_tuple(shape: &[usize], py: Python<'_>) -> PyResult<Py<pyo3::types::PyTuple>> {
84    Ok(pyo3::types::PyTuple::new(py, shape)?.unbind())
85}
86
87// ========================================
88// NUMPY ARRAY CONVERSIONS (GENERIC)
89// ========================================
90
91/// Convert NumPy array to scirs2 Array1
92#[cfg(feature = "python")]
93pub fn numpy_to_scirs_array1<T: Element + Clone>(
94    array: &Bound<'_, scirs2_numpy::PyArray1<T>>,
95) -> PyResult<Array1<T>> {
96    let readonly = array.readonly();
97    Ok(readonly.as_array().to_owned())
98}
99
100/// Convert NumPy array to scirs2 Array2
101#[cfg(feature = "python")]
102pub fn numpy_to_scirs_array2<T: Element + Clone>(
103    array: &Bound<'_, scirs2_numpy::PyArray2<T>>,
104) -> PyResult<Array2<T>> {
105    let readonly = array.readonly();
106    Ok(readonly.as_array().to_owned())
107}
108
109/// Convert scirs2 Array1 to NumPy
110#[cfg(feature = "python")]
111pub fn scirs_array1_to_numpy<T: Element>(
112    array: Array1<T>,
113    py: Python<'_>,
114) -> PyResult<Py<scirs2_numpy::PyArray1<T>>> {
115    Ok(scirs2_numpy::PyArray1::from_owned_array(py, array).unbind())
116}
117
118/// Convert scirs2 Array2 to NumPy
119#[cfg(feature = "python")]
120pub fn scirs_array2_to_numpy<T: Element>(
121    array: Array2<T>,
122    py: Python<'_>,
123) -> PyResult<Py<scirs2_numpy::PyArray2<T>>> {
124    Ok(scirs2_numpy::PyArray2::from_owned_array(py, array).unbind())
125}
126
127// ========================================
128// DTYPE UTILITIES
129// ========================================
130
131/// Get NumPy dtype string for a Rust type
132#[cfg(feature = "python")]
133pub fn rust_dtype_to_numpy_str<T: 'static>() -> &'static str {
134    use std::any::TypeId;
135
136    match TypeId::of::<T>() {
137        t if t == TypeId::of::<f32>() => "float32",
138        t if t == TypeId::of::<f64>() => "float64",
139        t if t == TypeId::of::<i8>() => "int8",
140        t if t == TypeId::of::<i16>() => "int16",
141        t if t == TypeId::of::<i32>() => "int32",
142        t if t == TypeId::of::<i64>() => "int64",
143        t if t == TypeId::of::<u8>() => "uint8",
144        t if t == TypeId::of::<u16>() => "uint16",
145        t if t == TypeId::of::<u32>() => "uint32",
146        t if t == TypeId::of::<u64>() => "uint64",
147        _ => "unknown",
148    }
149}
150
151// ========================================
152// VALIDATION UTILITIES
153// ========================================
154
155/// Validate NumPy array meets scirs2 requirements
156#[cfg(feature = "python")]
157pub fn validate_numpy_array<T: Element>(array: &Bound<'_, PyArrayDyn<T>>) -> PyResult<()> {
158    let shape = array.shape();
159
160    // Check for valid dimensions
161    if shape.is_empty() {
162        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
163            "Array must have at least one dimension",
164        ));
165    }
166
167    // Check for zero-size dimensions
168    if shape.contains(&0) {
169        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
170            "Array dimensions must be non-zero",
171        ));
172    }
173
174    Ok(())
175}
176
177// ========================================
178// MEMORY LAYOUT UTILITIES
179// ========================================
180
181/// Information about NumPy array memory layout
182#[cfg(feature = "python")]
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct MemoryLayoutInfo {
185    pub is_c_contiguous: bool,
186    pub is_f_contiguous: bool,
187    pub itemsize: usize,
188    pub ndim: usize,
189}
190
191/// Get detailed memory layout information from NumPy array
192#[cfg(feature = "python")]
193pub fn get_memory_layout_info<T: Element>(array: &Bound<'_, PyArrayDyn<T>>) -> MemoryLayoutInfo {
194    MemoryLayoutInfo {
195        is_c_contiguous: array.is_c_contiguous(),
196        is_f_contiguous: array.is_fortran_contiguous(),
197        itemsize: std::mem::size_of::<T>(),
198        ndim: array.ndim(),
199    }
200}
201
202// ========================================
203// TESTS
204// ========================================
205
206#[cfg(all(test, feature = "python"))]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_dtype_strings() {
212        assert_eq!(rust_dtype_to_numpy_str::<f32>(), "float32");
213        assert_eq!(rust_dtype_to_numpy_str::<f64>(), "float64");
214        assert_eq!(rust_dtype_to_numpy_str::<i32>(), "int32");
215        assert_eq!(rust_dtype_to_numpy_str::<i64>(), "int64");
216    }
217
218    #[test]
219    fn test_shape_conversion() {
220        Python::with_gil(|py| {
221            let shape = vec![2, 3, 4];
222            let py_tuple = shape_to_py_tuple(&shape, py).expect("Operation failed");
223            let bound_tuple = py_tuple.bind(py);
224
225            assert_eq!(bound_tuple.len(), 3);
226            assert_eq!(
227                bound_tuple
228                    .get_item(0)
229                    .expect("Operation failed")
230                    .extract::<usize>()
231                    .expect("Operation failed"),
232                2
233            );
234            assert_eq!(
235                bound_tuple
236                    .get_item(1)
237                    .expect("Operation failed")
238                    .extract::<usize>()
239                    .expect("Operation failed"),
240                3
241            );
242            assert_eq!(
243                bound_tuple
244                    .get_item(2)
245                    .expect("Operation failed")
246                    .extract::<usize>()
247                    .expect("Operation failed"),
248                4
249            );
250        });
251    }
252}