Skip to main content

torsh_ffi/
numpy_compatibility.rs

1//! NumPy compatibility layer for seamless integration with ToRSh
2//!
3//! This module provides comprehensive compatibility with NumPy arrays,
4//! enabling zero-copy conversion, broadcasting compatibility, and familiar
5//! NumPy-style operations on ToRSh tensors.
6
7// Framework infrastructure - components designed for future use
8#![allow(dead_code)]
9use parking_lot::RwLock;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13
14#[cfg(feature = "python")]
15use numpy::PyArrayDyn;
16#[cfg(feature = "python")]
17use pyo3::prelude::*;
18
19/// NumPy data type mapping
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum NumpyDType {
22    Bool,
23    Int8,
24    Int16,
25    Int32,
26    Int64,
27    UInt8,
28    UInt16,
29    UInt32,
30    UInt64,
31    Float16,
32    Float32,
33    Float64,
34    Complex64,
35    Complex128,
36}
37
38impl NumpyDType {
39    /// Convert from NumPy dtype string
40    pub fn from_numpy_str(dtype_str: &str) -> Option<Self> {
41        match dtype_str {
42            "bool" | "bool_" => Some(Self::Bool),
43            "int8" => Some(Self::Int8),
44            "int16" => Some(Self::Int16),
45            "int32" => Some(Self::Int32),
46            "int64" => Some(Self::Int64),
47            "uint8" => Some(Self::UInt8),
48            "uint16" => Some(Self::UInt16),
49            "uint32" => Some(Self::UInt32),
50            "uint64" => Some(Self::UInt64),
51            "float16" => Some(Self::Float16),
52            "float32" => Some(Self::Float32),
53            "float64" => Some(Self::Float64),
54            "complex64" => Some(Self::Complex64),
55            "complex128" => Some(Self::Complex128),
56            _ => None,
57        }
58    }
59
60    /// Convert to NumPy dtype string
61    pub fn to_numpy_str(&self) -> &'static str {
62        match self {
63            Self::Bool => "bool",
64            Self::Int8 => "int8",
65            Self::Int16 => "int16",
66            Self::Int32 => "int32",
67            Self::Int64 => "int64",
68            Self::UInt8 => "uint8",
69            Self::UInt16 => "uint16",
70            Self::UInt32 => "uint32",
71            Self::UInt64 => "uint64",
72            Self::Float16 => "float16",
73            Self::Float32 => "float32",
74            Self::Float64 => "float64",
75            Self::Complex64 => "complex64",
76            Self::Complex128 => "complex128",
77        }
78    }
79
80    /// Get the size in bytes
81    pub fn size_bytes(&self) -> usize {
82        match self {
83            Self::Bool | Self::Int8 | Self::UInt8 => 1,
84            Self::Int16 | Self::UInt16 | Self::Float16 => 2,
85            Self::Int32 | Self::UInt32 | Self::Float32 => 4,
86            Self::Int64 | Self::UInt64 | Self::Float64 | Self::Complex64 => 8,
87            Self::Complex128 => 16,
88        }
89    }
90}
91
92/// NumPy array metadata
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct NumpyArrayInfo {
95    pub shape: Vec<usize>,
96    pub strides: Vec<isize>,
97    pub dtype: NumpyDType,
98    pub fortran_order: bool,
99    pub contiguous: bool,
100    pub writeable: bool,
101    pub aligned: bool,
102}
103
104/// NumPy-style broadcasting rules
105#[derive(Debug, Clone)]
106pub struct BroadcastingRules {
107    /// Enable automatic broadcasting
108    pub auto_broadcast: bool,
109    /// Maximum number of dimensions for broadcasting
110    pub max_dims: usize,
111    /// Strict NumPy compatibility mode
112    pub strict_numpy_compat: bool,
113}
114
115impl Default for BroadcastingRules {
116    fn default() -> Self {
117        Self {
118            auto_broadcast: true,
119            max_dims: 32,
120            strict_numpy_compat: true,
121        }
122    }
123}
124
125/// NumPy compatibility layer
126#[derive(Debug)]
127pub struct NumpyCompat {
128    broadcasting_rules: BroadcastingRules,
129    type_promotions: HashMap<(NumpyDType, NumpyDType), NumpyDType>,
130    #[allow(dead_code)]
131    conversion_cache: Arc<RwLock<HashMap<String, Vec<u8>>>>,
132}
133
134impl NumpyCompat {
135    /// Create a new NumPy compatibility layer
136    pub fn new() -> Self {
137        let mut compat = Self {
138            broadcasting_rules: BroadcastingRules::default(),
139            type_promotions: HashMap::new(),
140            conversion_cache: Arc::new(RwLock::new(HashMap::new())),
141        };
142        compat.init_type_promotions();
143        compat
144    }
145
146    /// Initialize NumPy-style type promotion rules
147    fn init_type_promotions(&mut self) {
148        // NumPy type promotion hierarchy
149        let promotions = vec![
150            // Boolean promotions
151            ((NumpyDType::Bool, NumpyDType::Int8), NumpyDType::Int8),
152            ((NumpyDType::Bool, NumpyDType::Float32), NumpyDType::Float32),
153            ((NumpyDType::Bool, NumpyDType::Float64), NumpyDType::Float64),
154            // Integer promotions
155            ((NumpyDType::Int8, NumpyDType::Int16), NumpyDType::Int16),
156            ((NumpyDType::Int8, NumpyDType::Int32), NumpyDType::Int32),
157            ((NumpyDType::Int8, NumpyDType::Int64), NumpyDType::Int64),
158            ((NumpyDType::Int16, NumpyDType::Int32), NumpyDType::Int32),
159            ((NumpyDType::Int16, NumpyDType::Int64), NumpyDType::Int64),
160            ((NumpyDType::Int32, NumpyDType::Int64), NumpyDType::Int64),
161            // Unsigned integer promotions
162            ((NumpyDType::UInt8, NumpyDType::UInt16), NumpyDType::UInt16),
163            ((NumpyDType::UInt8, NumpyDType::UInt32), NumpyDType::UInt32),
164            ((NumpyDType::UInt8, NumpyDType::UInt64), NumpyDType::UInt64),
165            ((NumpyDType::UInt16, NumpyDType::UInt32), NumpyDType::UInt32),
166            ((NumpyDType::UInt16, NumpyDType::UInt64), NumpyDType::UInt64),
167            ((NumpyDType::UInt32, NumpyDType::UInt64), NumpyDType::UInt64),
168            // Mixed signed/unsigned promotions
169            ((NumpyDType::Int8, NumpyDType::UInt8), NumpyDType::Int16),
170            ((NumpyDType::Int16, NumpyDType::UInt16), NumpyDType::Int32),
171            ((NumpyDType::Int32, NumpyDType::UInt32), NumpyDType::Int64),
172            // Float promotions
173            (
174                (NumpyDType::Float16, NumpyDType::Float32),
175                NumpyDType::Float32,
176            ),
177            (
178                (NumpyDType::Float16, NumpyDType::Float64),
179                NumpyDType::Float64,
180            ),
181            (
182                (NumpyDType::Float32, NumpyDType::Float64),
183                NumpyDType::Float64,
184            ),
185            // Integer to float promotions
186            ((NumpyDType::Int8, NumpyDType::Float16), NumpyDType::Float16),
187            ((NumpyDType::Int8, NumpyDType::Float32), NumpyDType::Float32),
188            ((NumpyDType::Int8, NumpyDType::Float64), NumpyDType::Float64),
189            (
190                (NumpyDType::Int16, NumpyDType::Float32),
191                NumpyDType::Float32,
192            ),
193            (
194                (NumpyDType::Int16, NumpyDType::Float64),
195                NumpyDType::Float64,
196            ),
197            (
198                (NumpyDType::Int32, NumpyDType::Float64),
199                NumpyDType::Float64,
200            ),
201            // Complex promotions
202            (
203                (NumpyDType::Float32, NumpyDType::Complex64),
204                NumpyDType::Complex64,
205            ),
206            (
207                (NumpyDType::Float64, NumpyDType::Complex128),
208                NumpyDType::Complex128,
209            ),
210            (
211                (NumpyDType::Complex64, NumpyDType::Complex128),
212                NumpyDType::Complex128,
213            ),
214        ];
215
216        for ((a, b), result) in promotions {
217            self.type_promotions
218                .insert((a.clone(), b.clone()), result.clone());
219            self.type_promotions.insert((b, a), result); // Commutative
220        }
221    }
222
223    /// Promote two data types according to NumPy rules
224    pub fn promote_types(&self, a: &NumpyDType, b: &NumpyDType) -> NumpyDType {
225        if a == b {
226            return a.clone();
227        }
228
229        if let Some(promoted) = self.type_promotions.get(&(a.clone(), b.clone())) {
230            promoted.clone()
231        } else {
232            // Default to the "larger" type
233            match (a, b) {
234                (NumpyDType::Float64, _) | (_, NumpyDType::Float64) => NumpyDType::Float64,
235                (NumpyDType::Float32, _) | (_, NumpyDType::Float32) => NumpyDType::Float32,
236                (NumpyDType::Int64, _) | (_, NumpyDType::Int64) => NumpyDType::Int64,
237                (NumpyDType::Int32, _) | (_, NumpyDType::Int32) => NumpyDType::Int32,
238                _ => a.clone(),
239            }
240        }
241    }
242
243    /// Check if two shapes can be broadcast together
244    pub fn can_broadcast(&self, shape1: &[usize], shape2: &[usize]) -> bool {
245        if !self.broadcasting_rules.auto_broadcast {
246            return shape1 == shape2;
247        }
248
249        let max_len = shape1.len().max(shape2.len());
250        if max_len > self.broadcasting_rules.max_dims {
251            return false;
252        }
253
254        // Pad with 1s on the left
255        let padded1: Vec<usize> = std::iter::repeat(1)
256            .take(max_len.saturating_sub(shape1.len()))
257            .chain(shape1.iter().cloned())
258            .collect();
259
260        let padded2: Vec<usize> = std::iter::repeat(1)
261            .take(max_len.saturating_sub(shape2.len()))
262            .chain(shape2.iter().cloned())
263            .collect();
264
265        // Check broadcasting rules
266        for (d1, d2) in padded1.iter().zip(padded2.iter()) {
267            if *d1 != *d2 && *d1 != 1 && *d2 != 1 {
268                return false;
269            }
270        }
271
272        true
273    }
274
275    /// Compute the broadcast shape for two shapes
276    pub fn broadcast_shapes(&self, shape1: &[usize], shape2: &[usize]) -> Option<Vec<usize>> {
277        if !self.can_broadcast(shape1, shape2) {
278            return None;
279        }
280
281        let max_len = shape1.len().max(shape2.len());
282
283        // Pad with 1s on the left
284        let padded1: Vec<usize> = std::iter::repeat(1)
285            .take(max_len.saturating_sub(shape1.len()))
286            .chain(shape1.iter().cloned())
287            .collect();
288
289        let padded2: Vec<usize> = std::iter::repeat(1)
290            .take(max_len.saturating_sub(shape2.len()))
291            .chain(shape2.iter().cloned())
292            .collect();
293
294        let result: Vec<usize> = padded1
295            .iter()
296            .zip(padded2.iter())
297            .map(|(d1, d2)| (*d1).max(*d2))
298            .collect();
299
300        Some(result)
301    }
302
303    /// Convert strides from NumPy format to ToRSh format
304    pub fn convert_strides(&self, strides: &[isize], dtype: &NumpyDType) -> Vec<usize> {
305        let element_size = dtype.size_bytes() as isize;
306        strides
307            .iter()
308            .map(|&stride| (stride / element_size) as usize)
309            .collect()
310    }
311
312    /// Check if array is C-contiguous (NumPy default)
313    pub fn is_c_contiguous(&self, shape: &[usize], strides: &[isize], dtype: &NumpyDType) -> bool {
314        if shape.is_empty() {
315            return true;
316        }
317
318        let element_size = dtype.size_bytes() as isize;
319        let mut expected_stride = element_size;
320
321        for i in (0..shape.len()).rev() {
322            if strides[i] != expected_stride {
323                return false;
324            }
325            expected_stride *= shape[i] as isize;
326        }
327
328        true
329    }
330
331    /// Check if array is Fortran-contiguous
332    pub fn is_fortran_contiguous(
333        &self,
334        shape: &[usize],
335        strides: &[isize],
336        dtype: &NumpyDType,
337    ) -> bool {
338        if shape.is_empty() {
339            return true;
340        }
341
342        let element_size = dtype.size_bytes() as isize;
343        let mut expected_stride = element_size;
344
345        for i in 0..shape.len() {
346            if strides[i] != expected_stride {
347                return false;
348            }
349            expected_stride *= shape[i] as isize;
350        }
351
352        true
353    }
354
355    /// Create NumPy-compatible array info
356    pub fn create_array_info(
357        &self,
358        shape: Vec<usize>,
359        dtype: NumpyDType,
360        order: Option<&str>,
361    ) -> NumpyArrayInfo {
362        let fortran_order = order == Some("F");
363        let strides = self.compute_strides(&shape, &dtype, fortran_order);
364        let contiguous = self.is_c_contiguous(&shape, &strides, &dtype)
365            || self.is_fortran_contiguous(&shape, &strides, &dtype);
366
367        NumpyArrayInfo {
368            shape,
369            strides,
370            dtype,
371            fortran_order,
372            contiguous,
373            writeable: true,
374            aligned: true,
375        }
376    }
377
378    /// Compute strides for given shape and memory order
379    fn compute_strides(
380        &self,
381        shape: &[usize],
382        dtype: &NumpyDType,
383        fortran_order: bool,
384    ) -> Vec<isize> {
385        if shape.is_empty() {
386            return vec![];
387        }
388
389        let element_size = dtype.size_bytes() as isize;
390        let mut strides = vec![0; shape.len()];
391
392        if fortran_order {
393            // Fortran order: stride increases from first to last dimension
394            let mut stride = element_size;
395            for i in 0..shape.len() {
396                strides[i] = stride;
397                stride *= shape[i] as isize;
398            }
399        } else {
400            // C order: stride decreases from last to first dimension
401            let mut stride = element_size;
402            for i in (0..shape.len()).rev() {
403                strides[i] = stride;
404                stride *= shape[i] as isize;
405            }
406        }
407
408        strides
409    }
410
411    /// NumPy-style array slicing
412    pub fn slice_array(&self, shape: &[usize], slice_spec: &[SliceSpec]) -> SliceResult {
413        let mut new_shape = Vec::new();
414        let mut new_strides = Vec::new();
415        let mut offset = 0;
416
417        for (i, &dim_size) in shape.iter().enumerate() {
418            let slice = if i < slice_spec.len() {
419                &slice_spec[i]
420            } else {
421                &SliceSpec::Full
422            };
423
424            match slice {
425                SliceSpec::Full => {
426                    new_shape.push(dim_size);
427                    new_strides.push(1);
428                }
429                SliceSpec::Index(idx) => {
430                    // Single index, dimension is removed
431                    let actual_idx = if *idx < 0 {
432                        (dim_size as isize + idx) as usize
433                    } else {
434                        *idx as usize
435                    };
436                    offset += actual_idx;
437                }
438                SliceSpec::Range { start, end, step } => {
439                    let actual_start = start.unwrap_or(0);
440                    let actual_end = end.unwrap_or(dim_size);
441                    let actual_step = step.unwrap_or(1);
442
443                    let slice_size = if actual_step > 0 {
444                        ((actual_end.saturating_sub(actual_start)) + actual_step - 1) / actual_step
445                    } else {
446                        0
447                    };
448
449                    new_shape.push(slice_size);
450                    new_strides.push(actual_step);
451                    offset += actual_start;
452                }
453            }
454        }
455
456        SliceResult {
457            shape: new_shape,
458            strides: new_strides,
459            offset,
460        }
461    }
462
463    /// Generate NumPy-compatible operation mapping
464    pub fn generate_operation_mapping(&self) -> HashMap<String, String> {
465        let mut mapping = HashMap::new();
466
467        // Basic operations
468        mapping.insert("np.add".to_string(), "tensor.add".to_string());
469        mapping.insert("np.subtract".to_string(), "tensor.sub".to_string());
470        mapping.insert("np.multiply".to_string(), "tensor.mul".to_string());
471        mapping.insert("np.divide".to_string(), "tensor.div".to_string());
472        mapping.insert("np.power".to_string(), "tensor.pow".to_string());
473        mapping.insert("np.sqrt".to_string(), "tensor.sqrt".to_string());
474        mapping.insert("np.exp".to_string(), "tensor.exp".to_string());
475        mapping.insert("np.log".to_string(), "tensor.log".to_string());
476
477        // Reduction operations
478        mapping.insert("np.sum".to_string(), "tensor.sum".to_string());
479        mapping.insert("np.mean".to_string(), "tensor.mean".to_string());
480        mapping.insert("np.std".to_string(), "tensor.std".to_string());
481        mapping.insert("np.var".to_string(), "tensor.var".to_string());
482        mapping.insert("np.min".to_string(), "tensor.min".to_string());
483        mapping.insert("np.max".to_string(), "tensor.max".to_string());
484
485        // Shape operations
486        mapping.insert("np.reshape".to_string(), "tensor.reshape".to_string());
487        mapping.insert("np.transpose".to_string(), "tensor.transpose".to_string());
488        mapping.insert("np.flatten".to_string(), "tensor.flatten".to_string());
489        mapping.insert("np.squeeze".to_string(), "tensor.squeeze".to_string());
490        mapping.insert("np.expand_dims".to_string(), "tensor.unsqueeze".to_string());
491
492        // Linear algebra
493        mapping.insert("np.dot".to_string(), "tensor.mm".to_string());
494        mapping.insert("np.matmul".to_string(), "tensor.matmul".to_string());
495        mapping.insert("np.linalg.norm".to_string(), "tensor.norm".to_string());
496
497        // Indexing and slicing
498        mapping.insert("np.take".to_string(), "tensor.index_select".to_string());
499        mapping.insert("np.where".to_string(), "tensor.where".to_string());
500
501        mapping
502    }
503
504    #[cfg(feature = "python")]
505    /// Convert NumPy array to ToRSh tensor (Python integration)
506    pub fn from_numpy_array(&self, _py_array: &PyArrayDyn<f32>) -> Result<Vec<f32>, String> {
507        // TODO: Fix PyArray compatibility issues
508        // Get array info
509        // let shape = py_array.shape().to_vec();
510        // let strides = py_array.strides().to_vec();
511
512        // Temporary placeholder implementation
513        Ok(vec![])
514
515        // Check if array is contiguous
516        // let is_contiguous = self.is_c_contiguous(&shape, &strides, &NumpyDType::Float32);
517
518        // if is_contiguous {
519        //     // Zero-copy conversion for contiguous arrays
520        //     let data = unsafe { py_array.as_slice() }
521        //         .map_err(|e| format!("Array conversion error: {}", e))?;
522        //     Ok(data.to_vec())
523        // } else {
524        //     // Copy with stride handling for non-contiguous arrays
525        //     let total_elements: usize = shape.iter().product();
526        //     let mut result = Vec::with_capacity(total_elements);
527
528        //     // Implement proper strided copying
529        //     self.copy_strided_array_to_contiguous(py_array, &shape, &strides, &mut result)?;
530
531        //     Ok(result)
532        // }
533    }
534
535    #[cfg(feature = "python")]
536    /// Convert ToRSh tensor to NumPy array (Python integration)
537    pub fn to_numpy_array(
538        &self,
539        _data: &[f32],
540        _shape: &[usize],
541    ) -> Result<Py<PyArrayDyn<f32>>, String> {
542        // TODO: Fix PyArray compatibility issues
543        Err("PyArray compatibility not implemented".to_string())
544
545        // Python::attach(|py| {
546        //     let array = PyArrayDyn::from_vec(py, data.to_vec())
547        //         .reshape(shape)
548        //         .map_err(|e| format!("Array creation error: {}", e))?;
549        //     Ok(array.to_owned())
550        // })
551    }
552
553    #[cfg(feature = "python")]
554    #[allow(dead_code)]
555    /// Copy strided array data to contiguous layout
556    fn copy_strided_array_to_contiguous(
557        &self,
558        _py_array: &PyArrayDyn<f32>,
559        _shape: &[usize],
560        _strides: &[isize],
561        _result: &mut Vec<f32>,
562    ) -> Result<(), String> {
563        // Get the raw data pointer
564        // let data_ptr = py_array.as_ptr();
565        return Err("PyArray compatibility not implemented".to_string());
566
567        // TODO: Fix PyArray compatibility issues
568        // Calculate total elements
569        // let total_elements: usize = shape.iter().product();
570        // result.reserve(total_elements);
571
572        // Create multi-dimensional index iterator
573        // let mut indices = vec![0usize; shape.len()];
574
575        // for _ in 0..total_elements {
576        //     // Calculate offset in strided layout
577        //     let mut offset = 0isize;
578        //     for (dim_idx, &index) in indices.iter().enumerate() {
579        //         offset += (index as isize) * strides[dim_idx];
580        //     }
581
582        //     // Safely read the element
583        //     let element = unsafe {
584        //         if offset < 0 {
585        //             return Err("Negative stride offset encountered".to_string());
586        //         }
587        //         *data_ptr.offset(offset / std::mem::size_of::<f32>() as isize)
588        //     };
589
590        //     result.push(element);
591
592        //     // Increment indices (like odometer)
593        //     let mut carry = 1;
594        //     for dim in (0..shape.len()).rev() {
595        //         indices[dim] += carry;
596        //         if indices[dim] < shape[dim] {
597        //             carry = 0;
598        //             break;
599        //         } else {
600        //             indices[dim] = 0;
601        //         }
602        //     }
603        // }
604
605        // Ok(())
606    }
607}
608
609impl Default for NumpyCompat {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615/// NumPy-style slice specification
616#[derive(Debug, Clone)]
617pub enum SliceSpec {
618    /// Full slice (:)
619    Full,
620    /// Single index
621    Index(isize),
622    /// Range slice (start:end:step)
623    Range {
624        start: Option<usize>,
625        end: Option<usize>,
626        step: Option<usize>,
627    },
628}
629
630/// Result of array slicing operation
631#[derive(Debug, Clone)]
632pub struct SliceResult {
633    pub shape: Vec<usize>,
634    pub strides: Vec<usize>,
635    pub offset: usize,
636}
637
638/// NumPy-style universal function (ufunc) implementation
639pub struct UniversalFunction {
640    pub name: String,
641    pub input_count: usize,
642    pub output_count: usize,
643    pub supports_broadcasting: bool,
644    pub supports_reduction: bool,
645}
646
647impl UniversalFunction {
648    /// Create a new universal function
649    pub fn new(name: String, input_count: usize, output_count: usize) -> Self {
650        Self {
651            name,
652            input_count,
653            output_count,
654            supports_broadcasting: true,
655            supports_reduction: false,
656        }
657    }
658
659    /// Apply the universal function with broadcasting
660    pub fn apply_with_broadcasting(
661        &self,
662        inputs: &[&NumpyArrayInfo],
663        compat: &NumpyCompat,
664    ) -> Result<Vec<usize>, String> {
665        if inputs.len() != self.input_count {
666            return Err(format!(
667                "Expected {} inputs, got {}",
668                self.input_count,
669                inputs.len()
670            ));
671        }
672
673        if !self.supports_broadcasting {
674            // All inputs must have the same shape
675            let first_shape = &inputs[0].shape;
676            for input in inputs.iter().skip(1) {
677                if input.shape != *first_shape {
678                    return Err("Shape mismatch for non-broadcasting function".to_string());
679                }
680            }
681            return Ok(first_shape.clone());
682        }
683
684        // Compute broadcast shape
685        let mut result_shape = inputs[0].shape.clone();
686        for input in inputs.iter().skip(1) {
687            if let Some(broadcast_shape) = compat.broadcast_shapes(&result_shape, &input.shape) {
688                result_shape = broadcast_shape;
689            } else {
690                return Err("Cannot broadcast input shapes".to_string());
691            }
692        }
693
694        Ok(result_shape)
695    }
696}
697
698/// Common NumPy universal functions
699pub struct NumpyUFuncs {
700    pub add: UniversalFunction,
701    pub subtract: UniversalFunction,
702    pub multiply: UniversalFunction,
703    pub divide: UniversalFunction,
704    pub power: UniversalFunction,
705    pub sqrt: UniversalFunction,
706    pub exp: UniversalFunction,
707    pub log: UniversalFunction,
708    pub sin: UniversalFunction,
709    pub cos: UniversalFunction,
710    pub tan: UniversalFunction,
711}
712
713impl Default for NumpyUFuncs {
714    fn default() -> Self {
715        Self {
716            add: UniversalFunction::new("add".to_string(), 2, 1),
717            subtract: UniversalFunction::new("subtract".to_string(), 2, 1),
718            multiply: UniversalFunction::new("multiply".to_string(), 2, 1),
719            divide: UniversalFunction::new("divide".to_string(), 2, 1),
720            power: UniversalFunction::new("power".to_string(), 2, 1),
721            sqrt: UniversalFunction::new("sqrt".to_string(), 1, 1),
722            exp: UniversalFunction::new("exp".to_string(), 1, 1),
723            log: UniversalFunction::new("log".to_string(), 1, 1),
724            sin: UniversalFunction::new("sin".to_string(), 1, 1),
725            cos: UniversalFunction::new("cos".to_string(), 1, 1),
726            tan: UniversalFunction::new("tan".to_string(), 1, 1),
727        }
728    }
729}
730
731#[cfg(test)]
732mod tests {
733    use super::*;
734
735    #[test]
736    fn test_numpy_dtype_conversion() {
737        assert_eq!(
738            NumpyDType::from_numpy_str("float32"),
739            Some(NumpyDType::Float32)
740        );
741        assert_eq!(NumpyDType::from_numpy_str("int64"), Some(NumpyDType::Int64));
742        assert_eq!(NumpyDType::from_numpy_str("bool"), Some(NumpyDType::Bool));
743
744        assert_eq!(NumpyDType::Float32.to_numpy_str(), "float32");
745        assert_eq!(NumpyDType::Int64.to_numpy_str(), "int64");
746        assert_eq!(NumpyDType::Bool.to_numpy_str(), "bool");
747    }
748
749    #[test]
750    fn test_type_promotion() {
751        let compat = NumpyCompat::new();
752
753        // Test basic promotions
754        assert_eq!(
755            compat.promote_types(&NumpyDType::Int32, &NumpyDType::Int64),
756            NumpyDType::Int64
757        );
758        assert_eq!(
759            compat.promote_types(&NumpyDType::Float32, &NumpyDType::Float64),
760            NumpyDType::Float64
761        );
762        assert_eq!(
763            compat.promote_types(&NumpyDType::Int32, &NumpyDType::Float32),
764            NumpyDType::Float32
765        );
766    }
767
768    #[test]
769    fn test_broadcasting() {
770        let compat = NumpyCompat::new();
771
772        // Test compatible shapes
773        assert!(compat.can_broadcast(&[3, 4], &[4]));
774        assert!(compat.can_broadcast(&[2, 1, 4], &[3, 4]));
775        assert!(compat.can_broadcast(&[1], &[8, 4, 5]));
776
777        // Test incompatible shapes
778        assert!(!compat.can_broadcast(&[3, 4], &[3, 5]));
779        assert!(!compat.can_broadcast(&[2, 4], &[3, 4]));
780
781        // Test broadcast shape computation
782        assert_eq!(compat.broadcast_shapes(&[3, 4], &[4]), Some(vec![3, 4]));
783        assert_eq!(
784            compat.broadcast_shapes(&[2, 1, 4], &[3, 4]),
785            Some(vec![2, 3, 4])
786        );
787    }
788
789    #[test]
790    fn test_contiguity() {
791        let compat = NumpyCompat::new();
792        let dtype = NumpyDType::Float32;
793
794        // C-contiguous array
795        let shape = vec![2, 3, 4];
796        let c_strides = vec![48, 16, 4]; // 3*4*4, 4*4, 4 bytes
797        assert!(compat.is_c_contiguous(&shape, &c_strides, &dtype));
798        assert!(!compat.is_fortran_contiguous(&shape, &c_strides, &dtype));
799
800        // Fortran-contiguous array
801        let f_strides = vec![4, 8, 24]; // 4, 2*4, 2*3*4 bytes
802        assert!(!compat.is_c_contiguous(&shape, &f_strides, &dtype));
803        assert!(compat.is_fortran_contiguous(&shape, &f_strides, &dtype));
804    }
805
806    #[test]
807    fn test_array_slicing() {
808        let compat = NumpyCompat::new();
809        let shape = vec![4, 6, 8];
810
811        // Full slice
812        let slice_spec = vec![SliceSpec::Full, SliceSpec::Full, SliceSpec::Full];
813        let result = compat.slice_array(&shape, &slice_spec);
814        assert_eq!(result.shape, vec![4, 6, 8]);
815        assert_eq!(result.offset, 0);
816
817        // Index slice (removes dimension)
818        let slice_spec = vec![SliceSpec::Index(1), SliceSpec::Full, SliceSpec::Full];
819        let result = compat.slice_array(&shape, &slice_spec);
820        assert_eq!(result.shape, vec![6, 8]);
821        assert_eq!(result.offset, 1);
822
823        // Range slice
824        let slice_spec = vec![
825            SliceSpec::Range {
826                start: Some(1),
827                end: Some(3),
828                step: Some(1),
829            },
830            SliceSpec::Full,
831            SliceSpec::Range {
832                start: Some(0),
833                end: Some(8),
834                step: Some(2),
835            },
836        ];
837        let result = compat.slice_array(&shape, &slice_spec);
838        assert_eq!(result.shape, vec![2, 6, 4]); // (3-1)/1, 6, 8/2
839    }
840
841    #[test]
842    fn test_universal_function() {
843        let compat = NumpyCompat::new();
844        let ufunc = UniversalFunction::new("add".to_string(), 2, 1);
845
846        let array1 = NumpyArrayInfo {
847            shape: vec![3, 4],
848            strides: vec![16, 4],
849            dtype: NumpyDType::Float32,
850            fortran_order: false,
851            contiguous: true,
852            writeable: true,
853            aligned: true,
854        };
855
856        let array2 = NumpyArrayInfo {
857            shape: vec![4],
858            strides: vec![4],
859            dtype: NumpyDType::Float32,
860            fortran_order: false,
861            contiguous: true,
862            writeable: true,
863            aligned: true,
864        };
865
866        let result = ufunc.apply_with_broadcasting(&[&array1, &array2], &compat);
867        assert_eq!(result.unwrap(), vec![3, 4]);
868    }
869
870    #[test]
871    fn test_operation_mapping() {
872        let compat = NumpyCompat::new();
873        let mapping = compat.generate_operation_mapping();
874
875        assert_eq!(mapping.get("np.add"), Some(&"tensor.add".to_string()));
876        assert_eq!(mapping.get("np.matmul"), Some(&"tensor.matmul".to_string()));
877        assert_eq!(
878            mapping.get("np.reshape"),
879            Some(&"tensor.reshape".to_string())
880        );
881    }
882}