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}