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