Skip to main content

torsh_tensor/
shape_ops.rs

1//! Shape and view operations for tensors
2//!
3//! This module provides comprehensive tensor shape manipulation and view operations
4//! including reshaping, transposing, slicing, squeezing, unsqueezing, and permuting.
5//!
6//! # Features
7//!
8//! - **Zero-copy views**: Efficient view operations that share underlying data
9//! - **Safe reshaping**: Comprehensive validation and overflow checking
10//! - **Dimension manipulation**: Squeeze, unsqueeze, transpose, and permute operations
11//! - **Slicing operations**: Flexible tensor slicing with stride computation
12//! - **Broadcasting support**: Expand operations for broadcasting compatibility
13//! - **Contiguity checking**: Efficient memory layout validation
14
15use std::sync::{Arc, RwLock};
16use torsh_core::{
17    dtype::TensorElement,
18    error::{Result, TorshError},
19    shape::Shape,
20};
21
22use crate::core_ops::{Operation, Tensor};
23use crate::memory_pool::global_acquire_uninit;
24
25impl<T: TensorElement + Copy> Tensor<T> {
26    /// Get size of a specific dimension
27    pub fn size(&self, dim: i32) -> Result<usize> {
28        self.shape().size(dim)
29    }
30
31    /// Reshapes the tensor to a new shape (creates a view or copy if needed).
32    ///
33    /// This is equivalent to PyTorch's `view()` operation. The total number of elements
34    /// must remain the same. You can use `-1` for one dimension to have it inferred automatically.
35    ///
36    /// # Arguments
37    ///
38    /// * `shape` - The new shape as a slice of dimensions. Use `-1` to infer one dimension.
39    ///
40    /// # Returns
41    ///
42    /// A reshaped tensor, or an error if the reshape is invalid.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use torsh_tensor::creation::zeros;
48    ///
49    /// // Reshape a 1D tensor to 2D
50    /// let t = zeros::<f32>(&[6]).expect("tensor creation should succeed");
51    /// let reshaped = t.view(&[2, 3]).expect("view should succeed");
52    /// assert_eq!(reshaped.shape().dims(), &[2, 3]);
53    ///
54    /// // Use -1 to infer a dimension
55    /// let t2 = zeros::<f32>(&[12]).expect("tensor creation should succeed");
56    /// let auto = t2.view(&[-1, 4]).expect("view should succeed");  // Infers 3 for first dimension
57    /// assert_eq!(auto.shape().dims(), &[3, 4]);
58    ///
59    /// // Flatten to 1D
60    /// let matrix = zeros::<f32>(&[3, 4, 5]).expect("tensor creation should succeed");
61    /// let flat = matrix.view(&[-1]).expect("view should succeed");
62    /// assert_eq!(flat.shape().dims(), &[60]);
63    /// ```
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if:
68    /// - More than one dimension is `-1`
69    /// - The total number of elements doesn't match
70    /// - Any dimension would overflow
71    ///
72    /// # See Also
73    ///
74    /// * [`Self::reshape`] - Alias for `view()`
75    /// * [`Self::view_as`] - Zero-copy view for compatible shapes
76    /// * [`Self::contiguous`] - Make tensor contiguous in memory
77    pub fn view(&self, shape: &[i32]) -> Result<Self> {
78        // Validate that there's at most one -1 in the shape
79        let infer_count = shape.iter().filter(|&&x| x == -1).count();
80        if infer_count > 1 {
81            return Err(TorshError::InvalidShape(
82                "Only one dimension can be inferred (only one -1 allowed)".to_string(),
83            ));
84        }
85
86        let new_shape: Result<Vec<usize>> = shape
87            .iter()
88            .map(|&d| {
89                if d == -1 {
90                    // Infer dimension - first validate all other dimensions are valid
91                    let known_dims: Result<Vec<usize>> = shape
92                        .iter()
93                        .filter(|&&x| x != -1)
94                        .map(|&x| {
95                            if x < 0 {
96                                Err(TorshError::InvalidShape(format!(
97                                    "Invalid dimension size: {x} (negative dimensions not allowed except -1)"
98                                )))
99                            } else {
100                                Ok(x as usize)
101                            }
102                        })
103                        .collect();
104
105                    let known_dims = known_dims?;
106
107                    // Check for overflow in product calculation
108                    let known_product = known_dims.iter().try_fold(1usize, |acc, &dim| {
109                        acc.checked_mul(dim).ok_or_else(|| {
110                            TorshError::InvalidShape(
111                                "Shape dimensions too large (would overflow)".to_string()
112                            )
113                        })
114                    })?;
115
116                    if known_product == 0 {
117                        return Err(TorshError::InvalidShape(
118                            "Cannot infer dimension with zero-sized dimensions".to_string(),
119                        ));
120                    }
121
122                    let total = self.numel();
123                    if total % known_product != 0 {
124                        return Err(TorshError::InvalidShape(
125                            "Cannot infer dimension: size is not divisible".to_string(),
126                        ));
127                    }
128
129                    Ok(total / known_product)
130                } else if d < 0 {
131                    Err(TorshError::InvalidShape(format!(
132                        "Invalid dimension size: {d}"
133                    )))
134                } else {
135                    Ok(d as usize)
136                }
137            })
138            .collect();
139
140        let new_shape = new_shape?;
141
142        // Check for overflow in total elements calculation
143        let new_numel = new_shape.iter().try_fold(1usize, |acc, &dim| {
144            acc.checked_mul(dim).ok_or_else(|| {
145                TorshError::InvalidShape(
146                    "Reshaped tensor would be too large (would overflow)".to_string(),
147                )
148            })
149        })?;
150
151        if new_numel != self.numel() {
152            return Err(TorshError::InvalidShape(format!(
153                "Shape {:?} is invalid for tensor of size {}",
154                new_shape,
155                self.numel()
156            )));
157        }
158
159        // Create a new tensor with the same data but different shape
160        let data = self.to_vec()?;
161        Self::from_data(data, new_shape, self.device)
162    }
163
164    /// Create an efficient view with different shape (shares data, no copying)
165    /// This is the zero-copy version of view() for compatible shapes
166    pub fn view_as(&self, shape: &[usize]) -> Result<Self> {
167        // Validate that the total number of elements is the same
168        let new_numel = shape.iter().product::<usize>();
169        if new_numel != self.numel() {
170            return Err(TorshError::InvalidShape(format!(
171                "Shape {:?} is invalid for tensor of size {}",
172                shape,
173                self.numel()
174            )));
175        }
176
177        // Only create efficient views for contiguous tensors or existing views
178        // that are still relatively simple
179        if !self.is_contiguous() {
180            return Err(TorshError::InvalidShape(
181                "Cannot create efficient view of non-contiguous tensor".to_string(),
182            ));
183        }
184
185        // Create new tensor sharing the same storage
186        Ok(Self {
187            storage: self.storage.clone(),
188            shape: Shape::new(shape.to_vec()),
189            device: self.device,
190            requires_grad: self.requires_grad,
191            grad: Arc::new(RwLock::new(None)), // Views don't share gradients
192            operation: Operation::Leaf,        // Views reset operation tracking
193            strides: None,                     // Use default contiguous strides for simple reshapes
194            storage_offset: self.storage_offset,
195            base_tensor: if self.is_view() {
196                // If this is already a view, keep reference to the original base
197                self.base_tensor.clone()
198            } else {
199                // This is a base tensor, so create a weak reference to it
200                Some(Arc::downgrade(&Arc::new(self.clone())))
201            },
202        })
203    }
204
205    /// Create a view of a slice along a dimension (shares data, no copying)
206    pub fn slice_tensor(&self, dim: usize, start: usize, end: usize) -> Result<Self> {
207        if dim >= self.ndim() {
208            return Err(TorshError::InvalidArgument(format!(
209                "Dimension {} out of range for tensor with {} dimensions",
210                dim,
211                self.ndim()
212            )));
213        }
214
215        let shape = self.shape.dims();
216        if start >= shape[dim] || end > shape[dim] || start >= end {
217            return Err(TorshError::InvalidArgument(format!(
218                "Invalid slice range [{}:{}] for dimension {} of size {}",
219                start, end, dim, shape[dim]
220            )));
221        }
222
223        // Calculate new shape
224        let mut new_shape = shape.to_vec();
225        new_shape[dim] = end - start;
226
227        // Calculate new strides and offset
228        let current_strides = self.strides();
229        let offset_adjustment = start * current_strides[dim];
230
231        Ok(Self {
232            storage: self.storage.clone(),
233            shape: Shape::new(new_shape),
234            device: self.device,
235            requires_grad: self.requires_grad,
236            grad: Arc::new(RwLock::new(None)),
237            operation: Operation::Leaf,
238            strides: Some(current_strides),
239            storage_offset: self.storage_offset + offset_adjustment,
240            base_tensor: if self.is_view() {
241                self.base_tensor.clone()
242            } else {
243                Some(Arc::downgrade(&Arc::new(self.clone())))
244            },
245        })
246    }
247
248    /// Create a transposed view (shares data, no copying)
249    pub fn transpose_view(&self, dim0: usize, dim1: usize) -> Result<Self> {
250        if dim0 >= self.ndim() || dim1 >= self.ndim() {
251            return Err(TorshError::InvalidArgument(format!(
252                "Dimensions {} and {} out of range for tensor with {} dimensions",
253                dim0,
254                dim1,
255                self.ndim()
256            )));
257        }
258
259        if dim0 == dim1 {
260            return Ok(self.clone());
261        }
262
263        // Create new shape and strides
264        let mut new_shape = self.shape.dims().to_vec();
265        let mut new_strides = self.strides();
266
267        // Swap dimensions
268        new_shape.swap(dim0, dim1);
269        new_strides.swap(dim0, dim1);
270
271        Ok(Self {
272            storage: self.storage.clone(),
273            shape: Shape::new(new_shape),
274            device: self.device,
275            requires_grad: self.requires_grad,
276            grad: Arc::new(RwLock::new(None)),
277            operation: Operation::Leaf,
278            strides: Some(new_strides),
279            storage_offset: self.storage_offset,
280            base_tensor: if self.is_view() {
281                self.base_tensor.clone()
282            } else {
283                Some(Arc::downgrade(&Arc::new(self.clone())))
284            },
285        })
286    }
287
288    /// Squeeze a tensor along a specific dimension (removes dimension of size 1)
289    pub fn squeeze_tensor(&self, dim: usize) -> Result<Self> {
290        if dim >= self.ndim() {
291            return Err(TorshError::InvalidArgument(format!(
292                "Dimension {} out of range for tensor with {} dimensions",
293                dim,
294                self.ndim()
295            )));
296        }
297
298        let shape = self.shape.dims();
299        if shape[dim] != 1 {
300            return Err(TorshError::InvalidArgument(format!(
301                "Cannot squeeze dimension {} of size {}",
302                dim, shape[dim]
303            )));
304        }
305
306        // Remove the dimension from shape and strides
307        let mut new_shape = shape.to_vec();
308        new_shape.remove(dim);
309
310        let mut new_strides = self.strides();
311        new_strides.remove(dim);
312
313        Ok(Self {
314            storage: self.storage.clone(),
315            shape: Shape::new(new_shape),
316            device: self.device,
317            requires_grad: self.requires_grad,
318            grad: Arc::new(RwLock::new(None)),
319            operation: Operation::Leaf,
320            strides: Some(new_strides),
321            storage_offset: self.storage_offset,
322            base_tensor: if self.is_view() {
323                self.base_tensor.clone()
324            } else {
325                Some(Arc::downgrade(&Arc::new(self.clone())))
326            },
327        })
328    }
329
330    /// Unsqueeze a tensor at a specific dimension (adds dimension of size 1)
331    pub fn unsqueeze_tensor(&self, dim: usize) -> Result<Self> {
332        if dim > self.ndim() {
333            return Err(TorshError::InvalidArgument(format!(
334                "Dimension {} out of range for insertion in tensor with {} dimensions",
335                dim,
336                self.ndim()
337            )));
338        }
339
340        // Insert new dimension into shape and strides
341        let mut new_shape = self.shape.dims().to_vec();
342        new_shape.insert(dim, 1);
343
344        let mut new_strides = self.strides();
345        // For the new dimension, stride should be the product of all dimensions to the right
346        let new_stride = if dim == new_shape.len() - 1 {
347            1 // Last dimension always has stride 1
348        } else {
349            new_strides[dim] // Use the stride that was at this position
350        };
351        new_strides.insert(dim, new_stride);
352
353        Ok(Self {
354            storage: self.storage.clone(),
355            shape: Shape::new(new_shape),
356            device: self.device,
357            requires_grad: self.requires_grad,
358            grad: Arc::new(RwLock::new(None)),
359            operation: Operation::Leaf,
360            strides: Some(new_strides),
361            storage_offset: self.storage_offset,
362            base_tensor: if self.is_view() {
363                self.base_tensor.clone()
364            } else {
365                Some(Arc::downgrade(&Arc::new(self.clone())))
366            },
367        })
368    }
369
370    /// Transposes two dimensions of the tensor.
371    ///
372    /// Swaps the specified dimensions, creating a new tensor. For 2D tensors, calling
373    /// `transpose(0, 1)` produces the standard matrix transpose operation.
374    ///
375    /// # Arguments
376    ///
377    /// * `dim0` - The first dimension to swap. Negative values count from the end.
378    /// * `dim1` - The second dimension to swap. Negative values count from the end.
379    ///
380    /// # Returns
381    ///
382    /// A tensor with the specified dimensions transposed.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use torsh_tensor::creation::{zeros, arange};
388    ///
389    /// // Standard matrix transpose
390    /// let matrix = zeros::<f32>(&[3, 4]).expect("tensor creation should succeed");
391    /// let transposed = matrix.transpose(0, 1).expect("transpose should succeed");
392    /// assert_eq!(transposed.shape().dims(), &[4, 3]);
393    ///
394    /// // Transpose in 3D tensor
395    /// let cube = zeros::<f32>(&[2, 3, 4]).expect("tensor creation should succeed");
396    /// let swapped = cube.transpose(0, 2).expect("transpose should succeed");
397    /// assert_eq!(swapped.shape().dims(), &[4, 3, 2]);
398    ///
399    /// // Use negative indexing
400    /// let t = zeros::<f32>(&[5, 6, 7]).expect("tensor creation should succeed");
401    /// let result = t.transpose(-2, -1).expect("transpose should succeed");
402    /// assert_eq!(result.shape().dims(), &[5, 7, 6]);
403    ///
404    /// // Practical use: convert between row-major and column-major
405    /// let data = arange(0, 12, 1).expect("arange should succeed");
406    /// let row_major = data.reshape(&[3, 4]).expect("reshape should succeed");
407    /// let col_major = row_major.transpose(0, 1).expect("transpose should succeed");
408    /// ```
409    ///
410    /// # See Also
411    ///
412    /// * [`Self::permute`] - Rearrange dimensions in arbitrary order
413    /// * [`Self::view`] - Reshape to different dimensions
414    pub fn transpose(&self, dim0: i32, dim1: i32) -> Result<Self> {
415        let ndim = self.ndim();
416        let dim0 = if dim0 < 0 {
417            (ndim as i32 + dim0) as usize
418        } else {
419            dim0 as usize
420        };
421        let dim1 = if dim1 < 0 {
422            (ndim as i32 + dim1) as usize
423        } else {
424            dim1 as usize
425        };
426
427        if dim0 >= ndim || dim1 >= ndim {
428            return Err(TorshError::InvalidArgument(format!(
429                "Dimensions {} and {} out of range for tensor with {} dimensions",
430                dim0, dim1, ndim
431            )));
432        }
433
434        if ndim == 2 && dim0 != dim1 {
435            self.transpose_2d()
436        } else {
437            self.transpose_view(dim0, dim1)
438        }
439    }
440
441    /// 2D transpose implementation
442    fn transpose_2d(&self) -> Result<Self> {
443        let shape = self.shape.dims();
444        if shape.len() != 2 {
445            return Err(TorshError::InvalidArgument(
446                "transpose_2d only works with 2D tensors".to_string(),
447            ));
448        }
449
450        let (rows, cols) = (shape[0], shape[1]);
451        let data = self.to_vec()?;
452        let n = data.len();
453        let mut buf = global_acquire_uninit::<T>(n);
454        let uninit = buf.as_uninit_slice_mut();
455        let mut count = 0;
456
457        for col in 0..cols {
458            for row in 0..rows {
459                uninit[count].write(data[row * cols + col]);
460                count += 1;
461            }
462        }
463
464        let transposed_data = buf.into_vec(count);
465        Self::from_data(transposed_data, vec![cols, rows], self.device)
466    }
467
468    /// Permute dimensions according to the given order
469    pub fn permute(&self, dims: &[i32]) -> Result<Self> {
470        let ndim = self.ndim();
471
472        if dims.len() != ndim {
473            return Err(TorshError::InvalidArgument(format!(
474                "Number of dimensions in permutation ({}) doesn't match tensor dimensions ({})",
475                dims.len(),
476                ndim
477            )));
478        }
479
480        // Convert negative indices and validate
481        let perm_dims: Result<Vec<usize>> = dims
482            .iter()
483            .map(|&d| {
484                let dim = if d < 0 { ndim as i32 + d } else { d } as usize;
485                if dim >= ndim {
486                    Err(TorshError::InvalidArgument(format!(
487                        "Dimension {} out of range for tensor with {} dimensions",
488                        d, ndim
489                    )))
490                } else {
491                    Ok(dim)
492                }
493            })
494            .collect();
495
496        let perm_dims = perm_dims?;
497
498        // Check for duplicates
499        let mut sorted_dims = perm_dims.clone();
500        sorted_dims.sort_unstable();
501        for i in 0..ndim {
502            if sorted_dims[i] != i {
503                return Err(TorshError::InvalidArgument(
504                    "Permutation must contain each dimension exactly once".to_string(),
505                ));
506            }
507        }
508
509        // Create new shape and strides
510        let old_shape = self.shape.dims();
511        let old_strides = self.strides();
512
513        let new_shape: Vec<usize> = perm_dims.iter().map(|&i| old_shape[i]).collect();
514        let new_strides: Vec<usize> = perm_dims.iter().map(|&i| old_strides[i]).collect();
515
516        Ok(Self {
517            storage: self.storage.clone(),
518            shape: Shape::new(new_shape),
519            device: self.device,
520            requires_grad: self.requires_grad,
521            grad: Arc::new(RwLock::new(None)),
522            operation: Operation::Leaf,
523            strides: Some(new_strides),
524            storage_offset: self.storage_offset,
525            base_tensor: if self.is_view() {
526                self.base_tensor.clone()
527            } else {
528                Some(Arc::downgrade(&Arc::new(self.clone())))
529            },
530        })
531    }
532
533    /// Removes a dimension of size 1 at the specified position.
534    ///
535    /// This operation reduces the dimensionality of the tensor by removing dimensions
536    /// that have size 1. Commonly used to remove singleton dimensions after reductions
537    /// or to match tensor shapes for operations.
538    ///
539    /// # Arguments
540    ///
541    /// * `dim` - The dimension to squeeze. Negative values count from the end.
542    ///
543    /// # Returns
544    ///
545    /// A tensor with the specified dimension removed, or an error if the dimension
546    /// doesn't have size 1.
547    ///
548    /// # Examples
549    ///
550    /// ```
551    /// use torsh_tensor::creation::zeros;
552    ///
553    /// // Remove a singleton dimension
554    /// let t = zeros::<f32>(&[3, 1, 4]).expect("tensor creation should succeed");
555    /// let squeezed = t.squeeze(1).expect("squeeze should succeed");
556    /// assert_eq!(squeezed.shape().dims(), &[3, 4]);
557    ///
558    /// // Use negative indexing
559    /// let t2 = zeros::<f32>(&[2, 3, 1]).expect("tensor creation should succeed");
560    /// let squeezed2 = t2.squeeze(-1).expect("squeeze should succeed");
561    /// assert_eq!(squeezed2.shape().dims(), &[2, 3]);
562    ///
563    /// // After a reduction operation
564    /// let matrix = zeros::<f32>(&[5, 10]).expect("tensor creation should succeed");
565    /// let reduced = matrix.sum_dim(&[1], true).expect("sum_dim should succeed");  // Shape: [5, 1]
566    /// let final_result = reduced.squeeze(1).expect("squeeze should succeed");  // Shape: [5]
567    /// ```
568    ///
569    /// # See Also
570    ///
571    /// * [`Self::squeeze_all`] - Remove all singleton dimensions
572    /// * [`Self::unsqueeze`] - Add a singleton dimension
573    pub fn squeeze(&self, dim: i32) -> Result<Self> {
574        let ndim = self.ndim();
575        let dim = if dim < 0 {
576            (ndim as i32 + dim) as usize
577        } else {
578            dim as usize
579        };
580
581        self.squeeze_tensor(dim)
582    }
583
584    /// Squeeze all dimensions with size 1
585    pub fn squeeze_all(&self) -> Result<Self> {
586        let shape = self.shape.dims();
587        let new_shape: Vec<usize> = shape.iter().copied().filter(|&s| s != 1).collect();
588
589        if new_shape.is_empty() {
590            // If all dimensions were 1, result should be a scalar (0-dimensional tensor)
591            let data = self.to_vec()?;
592            Self::from_data(data, vec![], self.device)
593        } else {
594            let data = self.to_vec()?;
595            Self::from_data(data, new_shape, self.device)
596        }
597    }
598
599    /// Adds a dimension of size 1 at the specified position.
600    ///
601    /// This operation increases the dimensionality of the tensor by inserting a new
602    /// dimension of size 1. Commonly used to add batch dimensions or to match tensor
603    /// shapes for broadcasting operations.
604    ///
605    /// # Arguments
606    ///
607    /// * `dim` - The position to insert the new dimension. Negative values count from the end.
608    ///
609    /// # Returns
610    ///
611    /// A tensor with an additional dimension of size 1 inserted.
612    ///
613    /// # Examples
614    ///
615    /// ```
616    /// use torsh_tensor::creation::zeros;
617    ///
618    /// // Add a batch dimension at the beginning
619    /// let t = zeros::<f32>(&[3, 4]).expect("tensor creation should succeed");
620    /// let batched = t.unsqueeze(0).expect("unsqueeze should succeed");
621    /// assert_eq!(batched.shape().dims(), &[1, 3, 4]);
622    ///
623    /// // Add a dimension at the end
624    /// let t2 = zeros::<f32>(&[5]).expect("tensor creation should succeed");
625    /// let expanded = t2.unsqueeze(-1).expect("unsqueeze should succeed");
626    /// assert_eq!(expanded.shape().dims(), &[5, 1]);
627    ///
628    /// // Prepare for broadcasting
629    /// let weights = zeros::<f32>(&[64]).expect("tensor creation should succeed");
630    /// let weights_2d = weights.unsqueeze(0).expect("unsqueeze should succeed");  // Shape: [1, 64]
631    /// // Now can broadcast with shape [batch_size, 64]
632    /// ```
633    ///
634    /// # See Also
635    ///
636    /// * [`Self::squeeze`] - Remove a singleton dimension
637    /// * [`Self::view`] - Reshape to arbitrary shape
638    pub fn unsqueeze(&self, dim: i32) -> Result<Self> {
639        let ndim = self.ndim();
640        let dim = if dim < 0 {
641            (ndim as i32 + dim + 1) as usize
642        } else {
643            dim as usize
644        };
645
646        self.unsqueeze_tensor(dim)
647    }
648
649    /// Reshapes the tensor to a new shape.
650    ///
651    /// This is an alias for [`view()`](Self::view) and provides the same functionality.
652    /// The total number of elements must remain the same.
653    ///
654    /// # Arguments
655    ///
656    /// * `shape` - The new shape as a slice of dimensions. Use `-1` to infer one dimension.
657    ///
658    /// # Returns
659    ///
660    /// A reshaped tensor, or an error if the reshape is invalid.
661    ///
662    /// # Examples
663    ///
664    /// ```
665    /// use torsh_tensor::creation::arange;
666    ///
667    /// // Reshape a sequence to a matrix
668    /// let t = arange(0, 12, 1).expect("arange should succeed");
669    /// let matrix = t.reshape(&[3, 4]).expect("reshape should succeed");
670    /// assert_eq!(matrix.shape().dims(), &[3, 4]);
671    ///
672    /// // Reshape with automatic dimension inference
673    /// let cube = t.reshape(&[2, -1, 3]).expect("reshape should succeed");  // Infers 2 for middle dimension
674    /// assert_eq!(cube.shape().dims(), &[2, 2, 3]);
675    /// ```
676    ///
677    /// # See Also
678    ///
679    /// * [`Self::view`] - The underlying implementation
680    pub fn reshape(&self, shape: &[i32]) -> Result<Self> {
681        self.view(shape)
682    }
683
684    /// Check if tensor is contiguous in memory
685    pub fn is_contiguous(&self) -> bool {
686        // A tensor is contiguous if its strides match the default strides for its shape
687        let default_strides = self.compute_default_strides();
688        let current_strides = self.strides();
689
690        current_strides == default_strides
691    }
692
693    /// Make tensor contiguous if it isn't already
694    pub fn contiguous(&self) -> Result<Self> {
695        if self.is_contiguous() {
696            Ok(self.clone())
697        } else {
698            // Need to copy data to make it contiguous
699            let data = self.to_vec()?;
700            Self::from_data(data, self.shape.dims().to_vec(), self.device)
701        }
702    }
703
704    /// Expand tensor to a larger size
705    pub fn expand(&self, shape: &[usize]) -> Result<Self> {
706        let old_shape = self.shape.dims();
707
708        // Validate that expansion is possible
709        if shape.len() < old_shape.len() {
710            return Err(TorshError::InvalidShape(
711                "Cannot expand to smaller number of dimensions".to_string(),
712            ));
713        }
714
715        // Check dimension compatibility (broadcasting rules) and build the strides
716        // for a zero-copy broadcast view.
717        //
718        // A dimension of size 1 that is expanded to size N is given stride 0, so all
719        // N logical indices resolve to the same storage element (PyTorch `expand`
720        // semantics). Newly-introduced leading dimensions are likewise stride 0, and
721        // dimensions that keep their size retain their original (possibly
722        // non-contiguous) stride. No tensor data is copied: the result shares the
723        // source storage via `Arc`, so an expanded tensor never duplicates data.
724        let offset = shape.len() - old_shape.len();
725        let current_strides = self.strides();
726        let mut new_strides = vec![0usize; shape.len()];
727        for (i, &old_dim) in old_shape.iter().enumerate() {
728            let new_dim = shape[offset + i];
729            if old_dim == new_dim {
730                new_strides[offset + i] = current_strides[i];
731            } else if old_dim == 1 {
732                new_strides[offset + i] = 0;
733            } else {
734                return Err(TorshError::InvalidShape(format!(
735                    "Cannot expand dimension {} from {} to {}",
736                    i, old_dim, new_dim
737                )));
738            }
739        }
740        // Any leading broadcast dimensions already have stride 0 from initialization.
741
742        Ok(Self {
743            storage: self.storage.clone(),
744            shape: Shape::new(shape.to_vec()),
745            device: self.device,
746            requires_grad: false,
747            grad: Arc::new(RwLock::new(None)),
748            operation: Operation::Leaf,
749            strides: Some(new_strides),
750            storage_offset: self.storage_offset,
751            base_tensor: if self.is_view() {
752                self.base_tensor.clone()
753            } else {
754                Some(Arc::downgrade(&Arc::new(self.clone())))
755            },
756        })
757    }
758
759    /// Move dimensions from source positions to destination positions
760    ///
761    /// # PyTorch Compatibility
762    /// Equivalent to `torch.movedim(tensor, source, destination)`
763    ///
764    /// # Arguments
765    /// * `source` - Original positions of dimensions to move
766    /// * `destination` - Target positions for the dimensions
767    ///
768    /// # Examples
769    /// ```ignore
770    /// let x = Tensor::from_data(vec![1.0; 24], vec![2, 3, 4], DeviceType::Cpu)?;
771    /// let y = x.movedim(&[0, 1], &[2, 0])?; // [2,3,4] -> [3,4,2]
772    /// ```
773    pub fn movedim(&self, source: &[isize], destination: &[isize]) -> Result<Self> {
774        if source.len() != destination.len() {
775            return Err(TorshError::InvalidArgument(
776                "source and destination must have the same length".to_string(),
777            ));
778        }
779
780        let ndim = self.ndim();
781
782        // Normalize source and destination dimensions
783        let norm_source: Result<Vec<usize>> = source
784            .iter()
785            .map(|&d| {
786                let dim = if d < 0 {
787                    (ndim as isize + d) as usize
788                } else {
789                    d as usize
790                };
791                if dim >= ndim {
792                    Err(TorshError::InvalidArgument(format!(
793                        "Dimension {} out of range for {}-D tensor",
794                        d, ndim
795                    )))
796                } else {
797                    Ok(dim)
798                }
799            })
800            .collect();
801        let norm_source = norm_source?;
802
803        let norm_dest: Result<Vec<usize>> = destination
804            .iter()
805            .map(|&d| {
806                let dim = if d < 0 {
807                    (ndim as isize + d) as usize
808                } else {
809                    d as usize
810                };
811                if dim >= ndim {
812                    Err(TorshError::InvalidArgument(format!(
813                        "Dimension {} out of range for {}-D tensor",
814                        d, ndim
815                    )))
816                } else {
817                    Ok(dim)
818                }
819            })
820            .collect();
821        let norm_dest = norm_dest?;
822
823        // Check for duplicates in source
824        for i in 0..norm_source.len() {
825            for j in i + 1..norm_source.len() {
826                if norm_source[i] == norm_source[j] {
827                    return Err(TorshError::InvalidArgument(
828                        "repeated dim in source".to_string(),
829                    ));
830                }
831            }
832        }
833
834        // Check for duplicates in destination
835        for i in 0..norm_dest.len() {
836            for j in i + 1..norm_dest.len() {
837                if norm_dest[i] == norm_dest[j] {
838                    return Err(TorshError::InvalidArgument(
839                        "repeated dim in destination".to_string(),
840                    ));
841                }
842            }
843        }
844
845        // Build permutation array by placing dims in final positions
846        let mut result_perm = vec![0; ndim];
847        let mut used = vec![false; ndim];
848
849        // Place source dims at destination positions
850        for (&src, &dst) in norm_source.iter().zip(norm_dest.iter()) {
851            result_perm[dst] = src;
852            used[dst] = true;
853        }
854
855        // Fill remaining positions with remaining dims in order
856        let remaining_dims: Vec<usize> = (0..ndim).filter(|d| !norm_source.contains(d)).collect();
857
858        let mut remaining_idx = 0;
859        for i in 0..ndim {
860            if !used[i] {
861                result_perm[i] = remaining_dims[remaining_idx];
862                remaining_idx += 1;
863            }
864        }
865
866        // Convert usize to i32 for permute
867        let perm_i32: Vec<i32> = result_perm.iter().map(|&d| d as i32).collect();
868        self.permute(&perm_i32)
869    }
870
871    /// Move axis from source position to destination position (alias for movedim)
872    ///
873    /// # PyTorch Compatibility
874    /// Equivalent to `torch.moveaxis(tensor, source, destination)`
875    ///
876    /// # Arguments
877    /// * `source` - Original positions of axes to move
878    /// * `destination` - Target positions for the axes
879    pub fn moveaxis(&self, source: &[isize], destination: &[isize]) -> Result<Self> {
880        self.movedim(source, destination)
881    }
882
883    /// Swap two dimensions
884    ///
885    /// # PyTorch Compatibility
886    /// Equivalent to `torch.swapaxes(tensor, axis0, axis1)` or `torch.swapdims(tensor, dim0, dim1)`
887    ///
888    /// # Arguments
889    /// * `axis0` - First dimension
890    /// * `axis1` - Second dimension
891    ///
892    /// # Examples
893    /// ```ignore
894    /// let x = Tensor::from_data(vec![1.0; 12], vec![2, 3, 2], DeviceType::Cpu)?;
895    /// let y = x.swapaxes(0, 2)?; // [2,3,2] -> [2,3,2] with dims 0 and 2 swapped
896    /// ```
897    pub fn swapaxes(&self, axis0: isize, axis1: isize) -> Result<Self> {
898        let ndim = self.ndim();
899
900        // Normalize dimensions
901        let dim0 = if axis0 < 0 {
902            (ndim as isize + axis0) as usize
903        } else {
904            axis0 as usize
905        };
906        let dim1 = if axis1 < 0 {
907            (ndim as isize + axis1) as usize
908        } else {
909            axis1 as usize
910        };
911
912        if dim0 >= ndim {
913            return Err(TorshError::InvalidArgument(format!(
914                "Dimension {} out of range for {}-D tensor",
915                axis0, ndim
916            )));
917        }
918        if dim1 >= ndim {
919            return Err(TorshError::InvalidArgument(format!(
920                "Dimension {} out of range for {}-D tensor",
921                axis1, ndim
922            )));
923        }
924
925        // Build permutation: swap dim0 and dim1
926        let mut perm: Vec<i32> = (0..ndim as i32).collect();
927        perm.swap(dim0, dim1);
928
929        self.permute(&perm)
930    }
931
932    /// Swap two dimensions (alias for swapaxes)
933    ///
934    /// # PyTorch Compatibility
935    /// Equivalent to `torch.swapdims(tensor, dim0, dim1)`
936    pub fn swapdims(&self, dim0: isize, dim1: isize) -> Result<Self> {
937        self.swapaxes(dim0, dim1)
938    }
939
940    /// Broadcast tensor to a new shape
941    ///
942    /// # PyTorch Compatibility
943    /// Equivalent to `torch.broadcast_to(tensor, shape)`
944    ///
945    /// # Arguments
946    /// * `shape` - Target shape for broadcasting
947    ///
948    /// # Examples
949    /// ```ignore
950    /// let x = Tensor::from_data(vec![1.0, 2.0], vec![2], DeviceType::Cpu)?;
951    /// let y = x.broadcast_to(&[3, 2])?; // Broadcast [2] to [3, 2]
952    /// ```
953    pub fn broadcast_to(&self, shape: &[usize]) -> Result<Self> {
954        // Use the existing expand method which handles broadcasting
955        self.expand(shape)
956    }
957
958    /// Expand tensor to match another tensor's shape
959    ///
960    /// # PyTorch Compatibility
961    /// Equivalent to `torch.expand_as(tensor, other)`
962    ///
963    /// # Arguments
964    /// * `other` - Target tensor whose shape to match
965    ///
966    /// # Examples
967    /// ```ignore
968    /// let x = Tensor::from_data(vec![1.0, 2.0], vec![2], DeviceType::Cpu)?;
969    /// let y = Tensor::from_data(vec![0.0; 6], vec![3, 2], DeviceType::Cpu)?;
970    /// let z = x.expand_as(&y)?; // Expand x to match y's shape [3, 2]
971    /// ```
972    pub fn expand_as(&self, other: &Self) -> Result<Self> {
973        self.broadcast_to(other.shape().dims())
974    }
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980    use torsh_core::device::DeviceType;
981
982    #[test]
983    fn test_tensor_view() {
984        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
985        let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu)
986            .expect("tensor creation should succeed");
987
988        let reshaped = tensor.view(&[3, 2]).expect("view should succeed");
989        assert_eq!(reshaped.shape().dims(), &[3, 2]);
990        assert_eq!(reshaped.numel(), 6);
991    }
992
993    #[test]
994    fn test_tensor_view_with_inference() {
995        let data = vec![1.0f32; 24];
996        let tensor = Tensor::from_data(data, vec![2, 3, 4], DeviceType::Cpu)
997            .expect("tensor creation should succeed");
998
999        let reshaped = tensor.view(&[6, -1]).expect("view should succeed");
1000        assert_eq!(reshaped.shape().dims(), &[6, 4]);
1001    }
1002
1003    #[test]
1004    fn test_tensor_slice() {
1005        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1006        let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu)
1007            .expect("tensor creation should succeed");
1008
1009        let slice = tensor.slice_tensor(1, 1, 3).expect("slice should succeed");
1010        assert_eq!(slice.shape().dims(), &[2, 2]);
1011    }
1012
1013    #[test]
1014    fn test_tensor_transpose() {
1015        let data = vec![1.0f32, 2.0, 3.0, 4.0];
1016        let tensor = Tensor::from_data(data, vec![2, 2], DeviceType::Cpu)
1017            .expect("tensor creation should succeed");
1018
1019        let transposed = tensor.transpose(0, 1).expect("transpose should succeed");
1020        assert_eq!(transposed.shape().dims(), &[2, 2]);
1021        assert_eq!(
1022            transposed.get(&[0, 1]).expect("data access should succeed"),
1023            3.0
1024        );
1025        assert_eq!(
1026            transposed.get(&[1, 0]).expect("data access should succeed"),
1027            2.0
1028        );
1029    }
1030
1031    #[test]
1032    fn test_tensor_squeeze_unsqueeze() {
1033        let data = vec![1.0f32, 2.0, 3.0];
1034        let tensor = Tensor::from_data(data, vec![1, 3], DeviceType::Cpu)
1035            .expect("tensor creation should succeed");
1036
1037        let squeezed = tensor.squeeze(0).expect("squeeze should succeed");
1038        assert_eq!(squeezed.shape().dims(), &[3]);
1039
1040        let unsqueezed = squeezed.unsqueeze(0).expect("unsqueeze should succeed");
1041        assert_eq!(unsqueezed.shape().dims(), &[1, 3]);
1042    }
1043
1044    #[test]
1045    fn test_tensor_permute() {
1046        let data = vec![1.0f32; 24];
1047        let tensor = Tensor::from_data(data, vec![2, 3, 4], DeviceType::Cpu)
1048            .expect("tensor creation should succeed");
1049
1050        let permuted = tensor.permute(&[2, 0, 1]).expect("permute should succeed");
1051        assert_eq!(permuted.shape().dims(), &[4, 2, 3]);
1052    }
1053
1054    #[test]
1055    fn test_is_contiguous() {
1056        let data = vec![1.0f32, 2.0, 3.0, 4.0];
1057        let tensor = Tensor::from_data(data, vec![2, 2], DeviceType::Cpu)
1058            .expect("tensor creation should succeed");
1059        assert!(tensor.is_contiguous());
1060
1061        let transposed = tensor
1062            .transpose_view(0, 1)
1063            .expect("transpose view should succeed");
1064        assert!(!transposed.is_contiguous());
1065
1066        let contiguous = transposed.contiguous().expect("contiguous should succeed");
1067        assert!(contiguous.is_contiguous());
1068    }
1069
1070    #[test]
1071    fn test_expand() {
1072        let data = vec![1.0f32, 2.0];
1073        let tensor = Tensor::from_data(data, vec![1, 2], DeviceType::Cpu)
1074            .expect("tensor creation should succeed");
1075
1076        let expanded = tensor.expand(&[3, 2]).expect("expand should succeed");
1077        assert_eq!(expanded.shape().dims(), &[3, 2]);
1078        assert_eq!(expanded.numel(), 6);
1079
1080        // Correctness: broadcasting the size-1 leading dimension repeats the row.
1081        assert_eq!(
1082            expanded.to_vec().expect("to_vec should succeed"),
1083            vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]
1084        );
1085
1086        // The size-1 dimension is broadcast with stride 0; the kept dimension
1087        // retains its contiguous stride of 1, so the result is a non-contiguous view.
1088        assert_eq!(expanded.strides(), vec![0, 1]);
1089        assert!(!expanded.is_contiguous());
1090        assert!(expanded.is_view());
1091    }
1092
1093    #[test]
1094    fn test_expand_new_leading_dimension() {
1095        // Expanding [2] -> [3, 2] introduces a brand new leading dimension.
1096        let tensor = Tensor::from_data(vec![7.0f32, 8.0], vec![2], DeviceType::Cpu)
1097            .expect("tensor creation should succeed");
1098
1099        let expanded = tensor.expand(&[3, 2]).expect("expand should succeed");
1100        assert_eq!(expanded.shape().dims(), &[3, 2]);
1101
1102        // New leading dimension -> stride 0; the original dimension keeps stride 1.
1103        assert_eq!(expanded.strides(), vec![0, 1]);
1104        assert_eq!(
1105            expanded.to_vec().expect("to_vec should succeed"),
1106            vec![7.0, 8.0, 7.0, 8.0, 7.0, 8.0]
1107        );
1108
1109        // Element access flows through the strided view.
1110        assert_eq!(expanded.get(&[0, 1]).expect("get should succeed"), 8.0);
1111        assert_eq!(expanded.get(&[2, 0]).expect("get should succeed"), 7.0);
1112    }
1113
1114    #[test]
1115    fn test_expand_zero_copy_no_data_duplication() {
1116        // A single element expanded to one million logical elements must NOT copy
1117        // data: the resulting view shares the source's one-element storage.
1118        let source = Tensor::from_data(vec![5.0f32], vec![1], DeviceType::Cpu)
1119            .expect("tensor creation should succeed");
1120        let source_memory = source.memory_usage();
1121
1122        let expanded = source.expand(&[1024, 1024]).expect("expand should succeed");
1123        assert_eq!(expanded.numel(), 1024 * 1024);
1124
1125        // Shared storage => identical reported memory, far below a materialized copy.
1126        assert_eq!(expanded.memory_usage(), source_memory);
1127        assert!(expanded.memory_usage() < expanded.numel() * std::mem::size_of::<f32>());
1128
1129        // Fully broadcast view: both dimensions have stride 0.
1130        assert_eq!(expanded.strides(), vec![0, 0]);
1131        assert!(!expanded.is_contiguous());
1132        assert!(expanded.is_view());
1133
1134        // Every logical element resolves to the single stored value.
1135        assert_eq!(expanded.get(&[0, 0]).expect("get should succeed"), 5.0);
1136        assert_eq!(expanded.get(&[500, 700]).expect("get should succeed"), 5.0);
1137        assert_eq!(
1138            expanded.get(&[1023, 1023]).expect("get should succeed"),
1139            5.0
1140        );
1141    }
1142
1143    #[test]
1144    fn test_expand_rejects_incompatible_dimension() {
1145        // Expanding a non-unit dimension to a different size is invalid.
1146        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu)
1147            .expect("tensor creation should succeed");
1148        assert!(tensor.expand(&[5]).is_err());
1149        // Fewer dimensions than the source is also invalid.
1150        let matrix = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1151            .expect("tensor creation should succeed");
1152        assert!(matrix.expand(&[4]).is_err());
1153    }
1154
1155    #[test]
1156    fn test_view_error_handling() {
1157        let data = vec![1.0f32, 2.0, 3.0];
1158        let tensor = Tensor::from_data(data, vec![3], DeviceType::Cpu)
1159            .expect("tensor creation should succeed");
1160
1161        // Should fail - wrong total size
1162        assert!(tensor.view(&[2, 2]).is_err());
1163
1164        // Should fail - multiple -1
1165        assert!(tensor.view(&[-1, -1]).is_err());
1166    }
1167
1168    #[test]
1169    fn test_movedim_single() {
1170        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1171            .expect("tensor creation should succeed");
1172
1173        // Move dim 0 to position 2: [2,3,4] -> [3,4,2]
1174        let result = tensor.movedim(&[0], &[2]).expect("movedim should succeed");
1175        assert_eq!(result.shape().dims(), &[3, 4, 2]);
1176    }
1177
1178    #[test]
1179    fn test_movedim_multiple() {
1180        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1181            .expect("tensor creation should succeed");
1182
1183        // Move dims [0, 1] to positions [2, 0]: [2,3,4] -> [3,4,2]
1184        let result = tensor
1185            .movedim(&[0, 1], &[2, 0])
1186            .expect("movedim should succeed");
1187        assert_eq!(result.shape().dims(), &[3, 4, 2]);
1188    }
1189
1190    #[test]
1191    fn test_movedim_negative_indices() {
1192        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1193            .expect("tensor creation should succeed");
1194
1195        // Move last dim to first position: [2,3,4] -> [4,2,3]
1196        let result = tensor.movedim(&[-1], &[0]).expect("movedim should succeed");
1197        assert_eq!(result.shape().dims(), &[4, 2, 3]);
1198    }
1199
1200    #[test]
1201    fn test_moveaxis_alias() {
1202        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1203            .expect("tensor creation should succeed");
1204
1205        let result1 = tensor.movedim(&[0], &[2]).expect("movedim should succeed");
1206        let result2 = tensor
1207            .moveaxis(&[0], &[2])
1208            .expect("moveaxis should succeed");
1209        assert_eq!(result1.shape().dims(), result2.shape().dims());
1210    }
1211
1212    #[test]
1213    fn test_swapaxes_simple() {
1214        let tensor = Tensor::from_data(
1215            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
1216            vec![2, 3],
1217            DeviceType::Cpu,
1218        )
1219        .expect("tensor creation should succeed");
1220
1221        // Swap dims 0 and 1: [2,3] -> [3,2]
1222        let result = tensor.swapaxes(0, 1).expect("swapaxes should succeed");
1223        assert_eq!(result.shape().dims(), &[3, 2]);
1224    }
1225
1226    #[test]
1227    fn test_swapaxes_3d() {
1228        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1229            .expect("tensor creation should succeed");
1230
1231        // Swap dims 0 and 2: [2,3,4] -> [4,3,2]
1232        let result = tensor.swapaxes(0, 2).expect("swapaxes should succeed");
1233        assert_eq!(result.shape().dims(), &[4, 3, 2]);
1234    }
1235
1236    #[test]
1237    fn test_swapaxes_negative_indices() {
1238        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1239            .expect("tensor creation should succeed");
1240
1241        // Swap last two dims: [2,3,4] -> [2,4,3]
1242        let result = tensor.swapaxes(-1, -2).expect("swapaxes should succeed");
1243        assert_eq!(result.shape().dims(), &[2, 4, 3]);
1244    }
1245
1246    #[test]
1247    fn test_swapdims_alias() {
1248        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![2, 3, 4], DeviceType::Cpu)
1249            .expect("tensor creation should succeed");
1250
1251        let result1 = tensor.swapaxes(0, 2).expect("swapaxes should succeed");
1252        let result2 = tensor.swapdims(0, 2).expect("swapdims should succeed");
1253        assert_eq!(result1.shape().dims(), result2.shape().dims());
1254    }
1255
1256    #[test]
1257    fn test_broadcast_to_same_shape() {
1258        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1259            .expect("tensor creation should succeed");
1260
1261        let result = tensor
1262            .broadcast_to(&[2, 2])
1263            .expect("broadcast_to should succeed");
1264        assert_eq!(result.shape().dims(), &[2, 2]);
1265    }
1266
1267    #[test]
1268    fn test_broadcast_to_expand_dim() {
1269        let tensor = Tensor::from_data(vec![1.0f32, 2.0], vec![1, 2], DeviceType::Cpu)
1270            .expect("tensor creation should succeed");
1271
1272        // Broadcast [1, 2] to [3, 2]
1273        let result = tensor
1274            .broadcast_to(&[3, 2])
1275            .expect("broadcast_to should succeed");
1276        assert_eq!(result.shape().dims(), &[3, 2]);
1277    }
1278
1279    #[test]
1280    fn test_expand_as_basic() {
1281        let tensor = Tensor::from_data(vec![1.0f32, 2.0], vec![1, 2], DeviceType::Cpu)
1282            .expect("tensor creation should succeed");
1283
1284        let target = Tensor::from_data(vec![0.0f32; 6], vec![3, 2], DeviceType::Cpu)
1285            .expect("tensor creation should succeed");
1286
1287        let result = tensor.expand_as(&target).expect("expand_as should succeed");
1288        assert_eq!(result.shape().dims(), target.shape().dims());
1289        assert_eq!(result.shape().dims(), &[3, 2]);
1290    }
1291}