Skip to main content

torsh_tensor/
manipulation.rs

1//! Tensor manipulation operations
2//!
3//! This module provides PyTorch-compatible tensor manipulation operations including:
4//! - Stacking: stack
5//! - Splitting: chunk, split
6//! - Flipping: flip, fliplr, flipud
7//! - Rolling: roll, rot90
8//! - Tiling: tile, repeat_interleave
9//! - Utilities: unflatten, take_along_dim
10
11use crate::core_ops::Operation;
12use crate::{Tensor, TensorElement};
13use std::sync::Arc;
14use torsh_core::error::{Result, TorshError};
15
16impl<T: TensorElement + Copy + Default> Tensor<T> {
17    /// Stack tensors along a new dimension
18    ///
19    /// # PyTorch Compatibility
20    /// Equivalent to `torch.stack(tensors, dim)`
21    ///
22    /// # Arguments
23    /// * `tensors` - Sequence of tensors to stack
24    /// * `dim` - Dimension along which to stack
25    ///
26    /// # Examples
27    /// ```ignore
28    /// let a = Tensor::from_data(vec![1.0, 2.0], vec![2], DeviceType::Cpu)?;
29    /// let b = Tensor::from_data(vec![3.0, 4.0], vec![2], DeviceType::Cpu)?;
30    /// let result = Tensor::stack(&[a, b], 0)?; // shape: [2, 2]
31    /// ```
32    pub fn stack(tensors: &[Self], dim: isize) -> Result<Self> {
33        if tensors.is_empty() {
34            return Err(TorshError::InvalidArgument(
35                "stack requires at least one tensor".to_string(),
36            ));
37        }
38
39        // Verify all tensors have the same shape
40        let first_shape = tensors[0].shape().to_vec();
41        for tensor in tensors.iter().skip(1) {
42            if tensor.shape().dims() != first_shape.as_slice() {
43                return Err(TorshError::ShapeMismatch {
44                    expected: first_shape.clone(),
45                    got: tensor.shape().to_vec(),
46                });
47            }
48        }
49
50        let ndim = first_shape.len();
51        let dim = if dim < 0 {
52            ((ndim + 1) as isize + dim) as usize
53        } else {
54            dim as usize
55        };
56
57        if dim > ndim {
58            return Err(TorshError::InvalidArgument(format!(
59                "Dimension {} out of range for stacking {}-D tensors",
60                dim, ndim
61            )));
62        }
63
64        // Calculate output shape: insert new dimension at position `dim`
65        let mut output_shape = first_shape.to_vec();
66        output_shape.insert(dim, tensors.len());
67
68        // Stack tensors by interleaving data
69        let elem_count: usize = first_shape.iter().product();
70        let mut result_data = Vec::with_capacity(elem_count * tensors.len());
71
72        // Calculate strides for proper data layout
73        let outer_size: usize = first_shape[..dim].iter().product();
74        let inner_size: usize = first_shape[dim..].iter().product();
75
76        // Materialise every input exactly once (previously this happened once per
77        // (outer, tensor) pair, i.e. `outer_size` full copies of every input).
78        let sources: Vec<Vec<T>> = tensors
79            .iter()
80            .map(|tensor| tensor.to_vec())
81            .collect::<Result<Vec<_>>>()?;
82
83        for outer in 0..outer_size {
84            for source in &sources {
85                // One contiguous run per (outer, tensor) pair.
86                let start = outer * inner_size;
87                let run = source.get(start..start + inner_size).ok_or_else(|| {
88                    TorshError::IndexError {
89                        index: start + inner_size,
90                        size: source.len(),
91                    }
92                })?;
93                result_data.extend_from_slice(run);
94            }
95        }
96
97        let device = tensors[0].device.clone();
98        let mut result = Self::from_data(result_data, output_shape, device)?;
99
100        // Record the stack so each input's gradient is its slice of the seed
101        // along the newly-inserted axis `dim`.
102        let any_requires_grad = tensors.iter().any(|t| t.requires_grad);
103        if crate::should_record_grad(any_requires_grad) {
104            result.requires_grad = true;
105            result.operation = Operation::Stack {
106                inputs: tensors.iter().map(|t| Arc::new(t.clone())).collect(),
107                dim,
108            };
109        }
110
111        Ok(result)
112    }
113
114    /// Split tensor into chunks
115    ///
116    /// # PyTorch Compatibility
117    /// Equivalent to `torch.chunk(tensor, chunks, dim)`
118    pub fn chunk(&self, chunks: usize, dim: isize) -> Result<Vec<Self>> {
119        if chunks == 0 {
120            return Err(TorshError::InvalidArgument(
121                "chunks must be greater than 0".to_string(),
122            ));
123        }
124
125        let ndim = self.ndim();
126        let dim = if dim < 0 {
127            (ndim as isize + dim) as usize
128        } else {
129            dim as usize
130        };
131
132        if dim >= ndim {
133            return Err(TorshError::InvalidArgument(format!(
134                "Dimension {} out of range for {}-D tensor",
135                dim, ndim
136            )));
137        }
138
139        let dim_size = self.shape().dims()[dim];
140        let chunk_size = (dim_size + chunks - 1) / chunks; // Ceiling division
141
142        let mut result = Vec::new();
143        let mut start = 0;
144
145        while start < dim_size {
146            let end = (start + chunk_size).min(dim_size);
147            let slice_tensor = self.narrow(dim as i32, start as i64, end - start)?;
148            result.push(slice_tensor);
149            start = end;
150        }
151
152        Ok(result)
153    }
154
155    /// Split tensor into parts of given size
156    ///
157    /// # PyTorch Compatibility
158    /// Equivalent to `torch.split(tensor, split_size, dim)`
159    pub fn split(&self, split_size: usize, dim: isize) -> Result<Vec<Self>> {
160        if split_size == 0 {
161            return Err(TorshError::InvalidArgument(
162                "split_size must be greater than 0".to_string(),
163            ));
164        }
165
166        let ndim = self.ndim();
167        let dim = if dim < 0 {
168            (ndim as isize + dim) as usize
169        } else {
170            dim as usize
171        };
172
173        if dim >= ndim {
174            return Err(TorshError::InvalidArgument(format!(
175                "Dimension {} out of range for {}-D tensor",
176                dim, ndim
177            )));
178        }
179
180        let dim_size = self.shape().dims()[dim];
181        let mut result = Vec::new();
182        let mut start = 0;
183
184        while start < dim_size {
185            let size = split_size.min(dim_size - start);
186            let slice_tensor = self.narrow(dim as i32, start as i64, size)?;
187            result.push(slice_tensor);
188            start += split_size;
189        }
190
191        Ok(result)
192    }
193
194    /// Flip tensor along given dimensions
195    ///
196    /// # PyTorch Compatibility
197    /// Equivalent to `torch.flip(tensor, dims)`
198    pub fn flip(&self, dims: &[isize]) -> Result<Self> {
199        if dims.is_empty() {
200            return Ok(self.clone());
201        }
202
203        let ndim = self.ndim();
204
205        // Normalize dimensions
206        let mut norm_dims = Vec::new();
207        for &dim in dims {
208            let d = if dim < 0 {
209                (ndim as isize + dim) as usize
210            } else {
211                dim as usize
212            };
213
214            if d >= ndim {
215                return Err(TorshError::InvalidArgument(format!(
216                    "Dimension {} out of range for {}-D tensor",
217                    dim, ndim
218                )));
219            }
220            norm_dims.push(d);
221        }
222
223        // Flip data
224        let data = self.to_vec()?;
225        let shape = self.shape().to_vec();
226        let mut result_data = vec![T::default(); data.len()];
227
228        // Calculate strides
229        let mut strides = vec![1; ndim];
230        for i in (0..ndim - 1).rev() {
231            strides[i] = strides[i + 1] * shape[i + 1];
232        }
233
234        // Copy data with flipped indices
235        for i in 0..data.len() {
236            let mut indices = vec![0; ndim];
237            let mut remainder = i;
238
239            for d in 0..ndim {
240                indices[d] = remainder / strides[d];
241                remainder %= strides[d];
242            }
243
244            // Flip specified dimensions
245            for &flip_dim in &norm_dims {
246                indices[flip_dim] = shape[flip_dim] - 1 - indices[flip_dim];
247            }
248
249            // Calculate flipped index
250            let mut flipped_idx = 0;
251            for d in 0..ndim {
252                flipped_idx += indices[d] * strides[d];
253            }
254
255            result_data[flipped_idx] = data[i];
256        }
257
258        Self::from_data(result_data, shape.to_vec(), self.device)
259    }
260
261    /// Flip tensor left-right (last dimension)
262    ///
263    /// # PyTorch Compatibility
264    /// Equivalent to `torch.fliplr(tensor)`
265    pub fn fliplr(&self) -> Result<Self> {
266        if self.ndim() < 2 {
267            return Err(TorshError::InvalidArgument(
268                "fliplr requires at least 2 dimensions".to_string(),
269            ));
270        }
271        self.flip(&[-1])
272    }
273
274    /// Flip tensor up-down (first dimension)
275    ///
276    /// # PyTorch Compatibility
277    /// Equivalent to `torch.flipud(tensor)`
278    pub fn flipud(&self) -> Result<Self> {
279        if self.ndim() < 1 {
280            return Err(TorshError::InvalidArgument(
281                "flipud requires at least 1 dimension".to_string(),
282            ));
283        }
284        self.flip(&[0])
285    }
286
287    /// Roll tensor elements along given dimensions
288    ///
289    /// # PyTorch Compatibility
290    /// Equivalent to `torch.roll(tensor, shifts, dims)`
291    pub fn roll(&self, shifts: &[isize], dims: &[isize]) -> Result<Self> {
292        if shifts.len() != dims.len() {
293            return Err(TorshError::InvalidArgument(
294                "shifts and dims must have the same length".to_string(),
295            ));
296        }
297
298        if dims.is_empty() {
299            // Roll flattened tensor
300            let data = self.to_vec()?;
301            let shift = if shifts.is_empty() { 0 } else { shifts[0] };
302            let n = data.len();
303            let shift = ((shift % n as isize) + n as isize) as usize % n;
304
305            let mut result_data = vec![T::default(); n];
306            for (i, &val) in data.iter().enumerate() {
307                result_data[(i + shift) % n] = val;
308            }
309
310            return Self::from_data(result_data, self.shape().dims().to_vec(), self.device);
311        }
312
313        let ndim = self.ndim();
314
315        // Normalize dimensions
316        let mut norm_dims = Vec::new();
317        for &dim in dims {
318            let d = if dim < 0 {
319                (ndim as isize + dim) as usize
320            } else {
321                dim as usize
322            };
323
324            if d >= ndim {
325                return Err(TorshError::InvalidArgument(format!(
326                    "Dimension {} out of range for {}-D tensor",
327                    dim, ndim
328                )));
329            }
330            norm_dims.push(d);
331        }
332
333        let data = self.to_vec()?;
334        let shape = self.shape().to_vec();
335        let mut result_data = vec![T::default(); data.len()];
336
337        // Calculate strides
338        let mut strides = vec![1; ndim];
339        for i in (0..ndim - 1).rev() {
340            strides[i] = strides[i + 1] * shape[i + 1];
341        }
342
343        // Copy data with rolled indices
344        for i in 0..data.len() {
345            let mut indices = vec![0; ndim];
346            let mut remainder = i;
347
348            for d in 0..ndim {
349                indices[d] = remainder / strides[d];
350                remainder %= strides[d];
351            }
352
353            // Roll specified dimensions
354            for (dim_idx, &roll_dim) in norm_dims.iter().enumerate() {
355                let shift = shifts[dim_idx];
356                let dim_size = shape[roll_dim] as isize;
357                let rolled =
358                    ((indices[roll_dim] as isize + shift) % dim_size + dim_size) % dim_size;
359                indices[roll_dim] = rolled as usize;
360            }
361
362            // Calculate rolled index
363            let mut rolled_idx = 0;
364            for d in 0..ndim {
365                rolled_idx += indices[d] * strides[d];
366            }
367
368            result_data[rolled_idx] = data[i];
369        }
370
371        Self::from_data(result_data, shape.to_vec(), self.device)
372    }
373
374    /// Rotate tensor 90 degrees
375    ///
376    /// # PyTorch Compatibility
377    /// Equivalent to `torch.rot90(tensor, k, dims)`
378    pub fn rot90(&self, k: isize, dims: &[isize]) -> Result<Self> {
379        if dims.len() != 2 {
380            return Err(TorshError::InvalidArgument(
381                "dims must contain exactly 2 dimensions".to_string(),
382            ));
383        }
384
385        let ndim = self.ndim();
386        if ndim < 2 {
387            return Err(TorshError::InvalidArgument(
388                "rot90 requires at least 2 dimensions".to_string(),
389            ));
390        }
391
392        // Normalize dimensions
393        let dim0 = if dims[0] < 0 {
394            (ndim as isize + dims[0]) as usize
395        } else {
396            dims[0] as usize
397        };
398
399        let dim1 = if dims[1] < 0 {
400            (ndim as isize + dims[1]) as usize
401        } else {
402            dims[1] as usize
403        };
404
405        if dim0 >= ndim || dim1 >= ndim {
406            return Err(TorshError::InvalidArgument("dims out of range".to_string()));
407        }
408
409        if dim0 == dim1 {
410            return Err(TorshError::InvalidArgument(
411                "dims must be different".to_string(),
412            ));
413        }
414
415        // Normalize k to [0, 4)
416        let k = ((k % 4) + 4) % 4;
417
418        let mut result = self.clone();
419        for _ in 0..k {
420            // Transpose and flip
421            result = result.transpose_view(dim0, dim1)?;
422            result = result.flip(&[dim1 as isize])?;
423        }
424
425        Ok(result)
426    }
427
428    /// Tile tensor by repeating
429    ///
430    /// # PyTorch Compatibility
431    /// Equivalent to `torch.tile(tensor, repeats)`
432    pub fn tile(&self, repeats: &[usize]) -> Result<Self> {
433        if repeats.is_empty() {
434            return Ok(self.clone());
435        }
436
437        let shape = self.shape().to_vec();
438        let ndim = shape.len();
439
440        // Extend shape if needed
441        let mut new_shape = shape.to_vec();
442        if repeats.len() > ndim {
443            let diff = repeats.len() - ndim;
444            for _ in 0..diff {
445                new_shape.insert(0, 1);
446            }
447        }
448
449        // Calculate output shape
450        let mut output_shape = new_shape.clone();
451        let repeat_offset = if repeats.len() < output_shape.len() {
452            output_shape.len() - repeats.len()
453        } else {
454            0
455        };
456
457        for (i, &rep) in repeats.iter().enumerate() {
458            let idx = repeat_offset + i;
459            if idx < output_shape.len() {
460                output_shape[idx] *= rep;
461            }
462        }
463
464        // Tile using existing repeat method from data_ops
465        self.repeat(repeats)
466    }
467
468    /// Repeat elements of a tensor along a dimension
469    ///
470    /// # PyTorch Compatibility
471    /// Equivalent to `torch.repeat_interleave(tensor, repeats, dim)`
472    ///
473    /// # Arguments
474    /// * `repeats` - Number of times to repeat each element
475    /// * `dim` - Dimension along which to repeat (None = flatten first)
476    ///
477    /// # Examples
478    /// ```ignore
479    /// let x = Tensor::from_data(vec![1.0, 2.0, 3.0], vec![3], DeviceType::Cpu)?;
480    /// let y = x.repeat_interleave(2, None)?; // [1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
481    /// ```
482    pub fn repeat_interleave(&self, repeats: usize, dim: Option<isize>) -> Result<Self> {
483        if repeats == 0 {
484            return Err(TorshError::InvalidArgument(
485                "repeats must be positive".to_string(),
486            ));
487        }
488
489        match dim {
490            None => {
491                // Flatten and repeat each element
492                let data = self.to_vec()?;
493                let mut result_data = Vec::with_capacity(data.len() * repeats);
494
495                for &val in data.iter() {
496                    for _ in 0..repeats {
497                        result_data.push(val);
498                    }
499                }
500
501                Self::from_data(result_data, vec![data.len() * repeats], self.device)
502            }
503            Some(d) => {
504                let ndim = self.ndim();
505                let dim = if d < 0 {
506                    (ndim as isize + d) as usize
507                } else {
508                    d as usize
509                };
510
511                if dim >= ndim {
512                    return Err(TorshError::InvalidArgument(format!(
513                        "Dimension {} out of range for {}-D tensor",
514                        d, ndim
515                    )));
516                }
517
518                let shape = self.shape().to_vec();
519                let data = self.to_vec()?;
520
521                // Calculate output shape
522                let mut output_shape = shape.clone();
523                output_shape[dim] *= repeats;
524
525                // Repeat along the specified dimension
526                let dim_size = shape[dim];
527                let outer_size: usize = shape[..dim].iter().product();
528                let inner_size: usize = shape[dim + 1..].iter().product();
529
530                let mut result_data = Vec::with_capacity(data.len() * repeats);
531
532                for outer in 0..outer_size {
533                    for d in 0..dim_size {
534                        for _ in 0..repeats {
535                            for inner in 0..inner_size {
536                                let idx = outer * dim_size * inner_size + d * inner_size + inner;
537                                result_data.push(data[idx]);
538                            }
539                        }
540                    }
541                }
542
543                Self::from_data(result_data, output_shape, self.device)
544            }
545        }
546    }
547
548    /// Unflatten a dimension into multiple dimensions
549    ///
550    /// # PyTorch Compatibility
551    /// Equivalent to `torch.unflatten(tensor, dim, sizes)`
552    ///
553    /// # Arguments
554    /// * `dim` - Dimension to unflatten
555    /// * `sizes` - Target sizes for the unflattened dimensions
556    ///
557    /// # Examples
558    /// ```ignore
559    /// let x = Tensor::from_data(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![6], DeviceType::Cpu)?;
560    /// let y = x.unflatten(0, &[2, 3])?; // Shape becomes [2, 3]
561    /// ```
562    pub fn unflatten(&self, dim: isize, sizes: &[usize]) -> Result<Self> {
563        if sizes.is_empty() {
564            return Err(TorshError::InvalidArgument(
565                "sizes cannot be empty".to_string(),
566            ));
567        }
568
569        let shape = self.shape().to_vec();
570        let ndim = shape.len();
571
572        // Normalize dimension
573        let dim = if dim < 0 {
574            (ndim as isize + dim) as usize
575        } else {
576            dim as usize
577        };
578
579        if dim >= ndim {
580            return Err(TorshError::InvalidArgument(format!(
581                "Dimension {} out of range for {}-D tensor",
582                dim, ndim
583            )));
584        }
585
586        // Verify that the product of sizes equals the dimension size
587        let sizes_product: usize = sizes.iter().product();
588        if sizes_product != shape[dim] {
589            return Err(TorshError::InvalidArgument(format!(
590                "sizes product {} does not match dimension size {}",
591                sizes_product, shape[dim]
592            )));
593        }
594
595        // Build new shape
596        let mut new_shape = Vec::new();
597        new_shape.extend_from_slice(&shape[..dim]);
598        new_shape.extend_from_slice(sizes);
599        new_shape.extend_from_slice(&shape[dim + 1..]);
600
601        // Reshape (data layout doesn't change, only shape interpretation)
602        let data = self.to_vec()?;
603        Self::from_data(data, new_shape, self.device)
604    }
605
606    /// Gather values along a dimension using indices
607    ///
608    /// # PyTorch Compatibility
609    /// Equivalent to `torch.take_along_dim(tensor, indices, dim)`
610    ///
611    /// # Arguments
612    /// * `indices` - Indices to gather
613    /// * `dim` - Dimension along which to gather (None = flatten first)
614    ///
615    /// # Examples
616    /// ```ignore
617    /// let x = Tensor::from_data(vec![1.0, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)?;
618    /// let indices = Tensor::from_data(vec![0i64, 2], vec![2], DeviceType::Cpu)?;
619    /// let y = x.take_along_dim(&indices, None)?; // [1.0, 3.0]
620    /// ```
621    pub fn take_along_dim(&self, indices: &Tensor<i64>, dim: Option<isize>) -> Result<Self> {
622        match dim {
623            None => {
624                // Flatten both tensors and use simple indexing
625                let data = self.to_vec()?;
626                let idx_data = indices.to_vec()?;
627
628                let mut result = Vec::with_capacity(idx_data.len());
629
630                for &idx in idx_data.iter() {
631                    if idx < 0 || idx as usize >= data.len() {
632                        return Err(TorshError::InvalidArgument(format!(
633                            "Index {} out of range for tensor with {} elements",
634                            idx,
635                            data.len()
636                        )));
637                    }
638                    result.push(data[idx as usize]);
639                }
640
641                Self::from_data(result, indices.shape().to_vec(), self.device)
642            }
643            Some(d) => {
644                let ndim = self.ndim();
645                let dim = if d < 0 {
646                    (ndim as isize + d) as usize
647                } else {
648                    d as usize
649                };
650
651                if dim >= ndim {
652                    return Err(TorshError::InvalidArgument(format!(
653                        "Dimension {} out of range for {}-D tensor",
654                        d, ndim
655                    )));
656                }
657
658                let self_shape = self.shape().to_vec();
659                let indices_shape = indices.shape().to_vec();
660
661                // Verify shapes match except at the gather dimension
662                if self_shape.len() != indices_shape.len() {
663                    return Err(TorshError::ShapeMismatch {
664                        expected: self_shape.clone(),
665                        got: indices_shape.clone(),
666                    });
667                }
668
669                for (i, (&s, &idx_s)) in self_shape.iter().zip(indices_shape.iter()).enumerate() {
670                    if i != dim && s != idx_s {
671                        return Err(TorshError::ShapeMismatch {
672                            expected: self_shape.clone(),
673                            got: indices_shape.clone(),
674                        });
675                    }
676                }
677
678                let data = self.to_vec()?;
679                let idx_data = indices.to_vec()?;
680
681                let dim_size = self_shape[dim];
682                let outer_size: usize = self_shape[..dim].iter().product();
683                let inner_size: usize = self_shape[dim + 1..].iter().product();
684
685                let indices_dim_size = indices_shape[dim];
686                let mut result = Vec::with_capacity(idx_data.len());
687
688                for outer in 0..outer_size {
689                    for d in 0..indices_dim_size {
690                        for inner in 0..inner_size {
691                            let idx_flat =
692                                outer * indices_dim_size * inner_size + d * inner_size + inner;
693                            let gather_idx = idx_data[idx_flat];
694
695                            if gather_idx < 0 || gather_idx as usize >= dim_size {
696                                return Err(TorshError::InvalidArgument(format!(
697                                    "Index {} out of range for dimension size {}",
698                                    gather_idx, dim_size
699                                )));
700                            }
701
702                            let src_idx = outer * dim_size * inner_size
703                                + (gather_idx as usize) * inner_size
704                                + inner;
705
706                            result.push(data[src_idx]);
707                        }
708                    }
709                }
710
711                Self::from_data(result, indices_shape, self.device)
712            }
713        }
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use torsh_core::device::DeviceType;
721
722    // Stack tests
723    #[test]
724    fn test_stack_1d() {
725        let a = Tensor::from_data(vec![1.0f32, 2.0], vec![2], DeviceType::Cpu)
726            .expect("failed to create tensor a");
727        let b = Tensor::from_data(vec![3.0f32, 4.0], vec![2], DeviceType::Cpu)
728            .expect("failed to create tensor b");
729
730        let result = Tensor::stack(&[a, b], 0).expect("stack should succeed for 1d tensors");
731
732        assert_eq!(result.shape().dims(), &[2, 2]);
733        let data = result.data().expect("failed to get stacked tensor data");
734        assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
735    }
736
737    #[test]
738    fn test_stack_negative_dim() {
739        let a = Tensor::from_data(vec![1.0f32, 2.0], vec![2], DeviceType::Cpu)
740            .expect("failed to create tensor a");
741        let b = Tensor::from_data(vec![3.0f32, 4.0], vec![2], DeviceType::Cpu)
742            .expect("failed to create tensor b");
743
744        let result = Tensor::stack(&[a, b], -1).expect("stack should succeed with negative dim");
745        assert_eq!(result.shape().dims(), &[2, 2]);
746    }
747
748    // Chunk tests
749    #[test]
750    fn test_chunk_even() {
751        let tensor = Tensor::from_data(
752            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
753            vec![6],
754            DeviceType::Cpu,
755        )
756        .expect("failed to create tensor for chunk_even");
757
758        let chunks = tensor.chunk(3, 0).expect("chunk into 3 should succeed");
759        assert_eq!(chunks.len(), 3);
760        assert_eq!(chunks[0].shape().dims(), &[2]);
761        assert_eq!(
762            chunks[0].data().expect("failed to get chunk 0 data"),
763            vec![1.0, 2.0]
764        );
765        assert_eq!(
766            chunks[1].data().expect("failed to get chunk 1 data"),
767            vec![3.0, 4.0]
768        );
769        assert_eq!(
770            chunks[2].data().expect("failed to get chunk 2 data"),
771            vec![5.0, 6.0]
772        );
773    }
774
775    #[test]
776    fn test_chunk_uneven() {
777        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0, 5.0], vec![5], DeviceType::Cpu)
778            .expect("failed to create tensor for chunk_uneven");
779
780        let chunks = tensor.chunk(2, 0).expect("uneven chunk should succeed");
781        assert_eq!(chunks.len(), 2);
782        assert_eq!(chunks[0].shape().dims(), &[3]);
783        assert_eq!(chunks[1].shape().dims(), &[2]);
784    }
785
786    // Split tests
787    #[test]
788    fn test_split_even() {
789        let tensor = Tensor::from_data(
790            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
791            vec![6],
792            DeviceType::Cpu,
793        )
794        .expect("failed to create tensor for split_even");
795
796        let splits = tensor.split(2, 0).expect("split by 2 should succeed");
797        assert_eq!(splits.len(), 3);
798        assert_eq!(
799            splits[0].data().expect("failed to get split 0 data"),
800            vec![1.0, 2.0]
801        );
802        assert_eq!(
803            splits[1].data().expect("failed to get split 1 data"),
804            vec![3.0, 4.0]
805        );
806    }
807
808    #[test]
809    fn test_split_uneven() {
810        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0, 5.0], vec![5], DeviceType::Cpu)
811            .expect("failed to create tensor for split_uneven");
812
813        let splits = tensor.split(2, 0).expect("uneven split should succeed");
814        assert_eq!(splits.len(), 3);
815        assert_eq!(splits[0].shape().dims(), &[2]);
816        assert_eq!(splits[1].shape().dims(), &[2]);
817        assert_eq!(splits[2].shape().dims(), &[1]); // Last one smaller
818    }
819
820    // Flip tests
821    #[test]
822    fn test_flip_1d() {
823        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
824            .expect("failed to create 1d tensor for flip");
825
826        let result = tensor.flip(&[0]).expect("flip dim 0 should succeed");
827        assert_eq!(
828            result.data().expect("failed to get flipped data"),
829            vec![4.0, 3.0, 2.0, 1.0]
830        );
831    }
832
833    #[test]
834    fn test_flip_2d() {
835        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
836            .expect("failed to create 2d tensor for flip");
837
838        let result = tensor.flip(&[0]).expect("flip 2d dim 0 should succeed");
839        assert_eq!(
840            result.data().expect("failed to get 2d flipped data"),
841            vec![3.0, 4.0, 1.0, 2.0]
842        );
843    }
844
845    #[test]
846    fn test_fliplr() {
847        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
848            .expect("failed to create tensor for fliplr");
849
850        let result = tensor.fliplr().expect("fliplr should succeed");
851        assert_eq!(
852            result.data().expect("failed to get fliplr data"),
853            vec![2.0, 1.0, 4.0, 3.0]
854        );
855    }
856
857    #[test]
858    fn test_flipud() {
859        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
860            .expect("failed to create tensor for flipud");
861
862        let result = tensor.flipud().expect("flipud should succeed");
863        assert_eq!(
864            result.data().expect("failed to get flipud data"),
865            vec![3.0, 4.0, 1.0, 2.0]
866        );
867    }
868
869    // Roll tests
870    #[test]
871    fn test_roll_1d() {
872        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
873            .expect("failed to create tensor for roll");
874
875        let result = tensor.roll(&[1], &[0]).expect("roll by 1 should succeed");
876        assert_eq!(
877            result.data().expect("failed to get rolled data"),
878            vec![4.0, 1.0, 2.0, 3.0]
879        );
880    }
881
882    #[test]
883    fn test_roll_negative() {
884        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
885            .expect("failed to create tensor for negative roll");
886
887        let result = tensor
888            .roll(&[-1], &[0])
889            .expect("negative roll should succeed");
890        assert_eq!(
891            result.data().expect("failed to get negatively rolled data"),
892            vec![2.0, 3.0, 4.0, 1.0]
893        );
894    }
895
896    // Rot90 tests
897    #[test]
898    fn test_rot90_once() {
899        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
900            .expect("failed to create tensor for rot90");
901
902        let result = tensor.rot90(1, &[0, 1]).expect("rot90 once should succeed");
903        assert_eq!(result.shape().dims(), &[2, 2]);
904        // After 90° rotation: [[1,2],[3,4]] -> [[2,4],[1,3]]
905    }
906
907    #[test]
908    fn test_rot90_twice() {
909        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
910            .expect("failed to create tensor for rot90 twice");
911
912        let result = tensor
913            .rot90(2, &[0, 1])
914            .expect("rot90 twice should succeed");
915        assert_eq!(result.shape().dims(), &[2, 2]);
916        // After 180° rotation: [[1,2],[3,4]] -> [[4,3],[2,1]]
917        assert_eq!(
918            result.data().expect("failed to get rot90 twice data"),
919            vec![4.0, 3.0, 2.0, 1.0]
920        );
921    }
922
923    // Tile tests
924    #[test]
925    fn test_tile_1d() {
926        let tensor = Tensor::from_data(vec![1.0f32, 2.0], vec![2], DeviceType::Cpu)
927            .expect("failed to create tensor for tile 1d");
928
929        let result = tensor.tile(&[2]).expect("tile 1d should succeed");
930        assert_eq!(result.shape().dims(), &[4]);
931        assert_eq!(
932            result.data().expect("failed to get tiled 1d data"),
933            vec![1.0, 2.0, 1.0, 2.0]
934        );
935    }
936
937    #[test]
938    fn test_tile_2d() {
939        let tensor = Tensor::from_data(vec![1.0f32, 2.0], vec![1, 2], DeviceType::Cpu)
940            .expect("failed to create tensor for tile 2d");
941
942        let result = tensor.tile(&[2, 1]).expect("tile 2d should succeed");
943        assert_eq!(result.shape().dims(), &[2, 2]);
944    }
945
946    // Repeat interleave tests
947    #[test]
948    fn test_repeat_interleave_flatten() {
949        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu)
950            .expect("failed to create tensor for repeat_interleave");
951
952        let result = tensor
953            .repeat_interleave(2, None)
954            .expect("repeat_interleave flatten should succeed");
955        assert_eq!(result.shape().dims(), &[6]);
956        assert_eq!(
957            result.data().expect("failed to get repeat_interleave data"),
958            vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
959        );
960    }
961
962    #[test]
963    fn test_repeat_interleave_dim() {
964        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
965            .expect("failed to create tensor for repeat_interleave dim");
966
967        let result = tensor
968            .repeat_interleave(2, Some(0))
969            .expect("repeat_interleave along dim 0 should succeed");
970        assert_eq!(result.shape().dims(), &[4, 2]);
971    }
972
973    // Unflatten tests
974    #[test]
975    fn test_unflatten_basic() {
976        let tensor = Tensor::from_data(
977            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
978            vec![6],
979            DeviceType::Cpu,
980        )
981        .expect("failed to create tensor for unflatten");
982
983        let result = tensor
984            .unflatten(0, &[2, 3])
985            .expect("unflatten to [2,3] should succeed");
986        assert_eq!(result.shape().dims(), &[2, 3]);
987        assert_eq!(
988            result.data().expect("failed to get unflattened data"),
989            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
990        );
991    }
992
993    #[test]
994    fn test_unflatten_multiple_dims() {
995        let tensor = Tensor::from_data(vec![1.0f32; 24], vec![24], DeviceType::Cpu)
996            .expect("failed to create tensor for unflatten multiple dims");
997
998        let result = tensor
999            .unflatten(0, &[2, 3, 4])
1000            .expect("unflatten to [2,3,4] should succeed");
1001        assert_eq!(result.shape().dims(), &[2, 3, 4]);
1002    }
1003
1004    // Take along dim tests
1005    #[test]
1006    fn test_take_along_dim_flatten() {
1007        let tensor = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
1008            .expect("failed to create tensor for take_along_dim");
1009
1010        let indices = Tensor::from_data(vec![0i64, 2, 1], vec![3], DeviceType::Cpu)
1011            .expect("failed to create indices tensor");
1012
1013        let result = tensor
1014            .take_along_dim(&indices, None)
1015            .expect("take_along_dim flatten should succeed");
1016        assert_eq!(result.shape().dims(), &[3]);
1017        assert_eq!(
1018            result
1019                .data()
1020                .expect("failed to get take_along_dim flatten data"),
1021            vec![1.0, 3.0, 2.0]
1022        );
1023    }
1024
1025    #[test]
1026    fn test_take_along_dim_2d() {
1027        let tensor = Tensor::from_data(
1028            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
1029            vec![2, 3],
1030            DeviceType::Cpu,
1031        )
1032        .expect("failed to create 2d tensor for take_along_dim");
1033
1034        let indices = Tensor::from_data(vec![0i64, 2, 1, 1, 0, 2], vec![2, 3], DeviceType::Cpu)
1035            .expect("failed to create 2d indices tensor");
1036
1037        let result = tensor
1038            .take_along_dim(&indices, Some(1))
1039            .expect("take_along_dim 2d should succeed");
1040        assert_eq!(result.shape().dims(), &[2, 3]);
1041        // Row 0: [1.0, 2.0, 3.0] indexed by [0, 2, 1] = [1.0, 3.0, 2.0]
1042        // Row 1: [4.0, 5.0, 6.0] indexed by [1, 0, 2] = [5.0, 4.0, 6.0]
1043        assert_eq!(
1044            result.data().expect("failed to get take_along_dim 2d data"),
1045            vec![1.0, 3.0, 2.0, 5.0, 4.0, 6.0]
1046        );
1047    }
1048}