1#![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#[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 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 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 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#[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#[derive(Debug, Clone)]
106pub struct BroadcastingRules {
107 pub auto_broadcast: bool,
109 pub max_dims: usize,
111 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#[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 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 fn init_type_promotions(&mut self) {
148 let promotions = vec![
150 ((NumpyDType::Bool, NumpyDType::Int8), NumpyDType::Int8),
152 ((NumpyDType::Bool, NumpyDType::Float32), NumpyDType::Float32),
153 ((NumpyDType::Bool, NumpyDType::Float64), NumpyDType::Float64),
154 ((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 ((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 ((NumpyDType::Int8, NumpyDType::UInt8), NumpyDType::Int16),
170 ((NumpyDType::Int16, NumpyDType::UInt16), NumpyDType::Int32),
171 ((NumpyDType::Int32, NumpyDType::UInt32), NumpyDType::Int64),
172 (
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 ((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 (
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); }
221 }
222
223 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn generate_operation_mapping(&self) -> HashMap<String, String> {
465 let mut mapping = HashMap::new();
466
467 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 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 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 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 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 pub fn from_numpy_array(&self, _py_array: &PyArrayDyn<f32>) -> Result<Vec<f32>, String> {
507 Ok(vec![])
514
515 }
534
535 #[cfg(feature = "python")]
536 pub fn to_numpy_array(
538 &self,
539 _data: &[f32],
540 _shape: &[usize],
541 ) -> Result<Py<PyArrayDyn<f32>>, String> {
542 Err("PyArray compatibility not implemented".to_string())
544
545 }
552
553 #[cfg(feature = "python")]
554 #[allow(dead_code)]
555 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 return Err("PyArray compatibility not implemented".to_string());
566
567 }
607}
608
609impl Default for NumpyCompat {
610 fn default() -> Self {
611 Self::new()
612 }
613}
614
615#[derive(Debug, Clone)]
617pub enum SliceSpec {
618 Full,
620 Index(isize),
622 Range {
624 start: Option<usize>,
625 end: Option<usize>,
626 step: Option<usize>,
627 },
628}
629
630#[derive(Debug, Clone)]
632pub struct SliceResult {
633 pub shape: Vec<usize>,
634 pub strides: Vec<usize>,
635 pub offset: usize,
636}
637
638pub 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 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 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 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 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
698pub 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 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 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 assert!(!compat.can_broadcast(&[3, 4], &[3, 5]));
779 assert!(!compat.can_broadcast(&[2, 4], &[3, 4]));
780
781 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 let shape = vec![2, 3, 4];
796 let c_strides = vec![48, 16, 4]; assert!(compat.is_c_contiguous(&shape, &c_strides, &dtype));
798 assert!(!compat.is_fortran_contiguous(&shape, &c_strides, &dtype));
799
800 let f_strides = vec![4, 8, 24]; 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 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 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 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]); }
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}