Skip to main content

torsh_tensor/
indexing.rs

1//! Tensor indexing and slicing operations
2
3use crate::core_ops::{Operation, ViewKind};
4use crate::{Tensor, TensorElement};
5use std::sync::Arc;
6use torsh_core::error::{Result, TorshError};
7
8/// Index type for tensor indexing
9#[derive(Debug, Clone)]
10pub enum TensorIndex {
11    /// Single index
12    Index(i64),
13    /// Range of indices
14    Range(Option<i64>, Option<i64>, Option<i64>), // start, stop, step
15    /// All indices (:)
16    All,
17    /// List of indices (fancy indexing)
18    List(Vec<i64>),
19    /// Boolean mask
20    Mask(Tensor<bool>),
21    /// Ellipsis (...) - represents multiple ':' to fill remaining dimensions
22    Ellipsis,
23    /// Newaxis (None) - adds a dimension of size 1
24    NewAxis,
25}
26
27impl TensorIndex {
28    /// Create a range index
29    pub fn range(start: Option<i64>, stop: Option<i64>) -> Self {
30        TensorIndex::Range(start, stop, None)
31    }
32
33    /// Create a range index with step
34    pub fn range_step(start: Option<i64>, stop: Option<i64>, step: i64) -> Self {
35        TensorIndex::Range(start, stop, Some(step))
36    }
37}
38
39/// Indexing implementation
40impl<T: TensorElement> Tensor<T> {
41    /// Index into the tensor
42    pub fn index(&self, indices: &[TensorIndex]) -> Result<Self> {
43        // Validate number of indices (NewAxis and Ellipsis don't consume tensor dimensions)
44        let consuming_indices = indices
45            .iter()
46            .filter(|idx| !matches!(idx, TensorIndex::NewAxis | TensorIndex::Ellipsis))
47            .count();
48
49        if consuming_indices > self.ndim() {
50            return Err(TorshError::InvalidArgument(format!(
51                "Too many indices for tensor: tensor has {} dimensions but {} consuming indices were provided",
52                self.ndim(),
53                consuming_indices
54            )));
55        }
56
57        // Handle ellipsis by expanding indices first
58        let expanded_indices = self.expand_ellipsis(indices)?;
59
60        // Process each expanded index to determine the output shape and extraction logic
61        let mut output_shape = Vec::new();
62        let mut slices = Vec::new();
63        let mut input_dim_idx = 0; // Track which input tensor dimension we're accessing
64
65        for index in expanded_indices.iter() {
66            if let TensorIndex::NewAxis = index {
67                // NewAxis doesn't consume input dimensions, just adds a new dimension of size 1
68                output_shape.push(1);
69                slices.push((0, 1, 1));
70                // Don't increment input_dim_idx for NewAxis
71                continue;
72            }
73
74            // For all other indices, we need to get the dimension size from the input tensor
75            let dim_size = if input_dim_idx < self.ndim() {
76                self.shape().dims()[input_dim_idx]
77            } else {
78                return Err(TorshError::InvalidArgument(format!(
79                    "Index {} beyond tensor dimensions (tensor has {} dimensions)",
80                    input_dim_idx,
81                    self.ndim()
82                )));
83            };
84
85            match index {
86                TensorIndex::Index(idx) => {
87                    // Single index - this dimension is removed
88                    let idx = if *idx < 0 {
89                        (dim_size as i64 + idx) as usize
90                    } else {
91                        *idx as usize
92                    };
93
94                    if idx >= dim_size {
95                        return Err(TorshError::IndexOutOfBounds {
96                            index: idx,
97                            size: dim_size,
98                        });
99                    }
100
101                    slices.push((idx, idx + 1, 1));
102                    // Single index doesn't add an output dimension, but consumes input dimension
103                    input_dim_idx += 1;
104                }
105                TensorIndex::Range(start, stop, step) => {
106                    let step = step.unwrap_or(1);
107                    if step == 0 {
108                        return Err(TorshError::InvalidArgument(
109                            "Step cannot be zero".to_string(),
110                        ));
111                    }
112
113                    let start = start
114                        .map(|s| {
115                            if s < 0 {
116                                (dim_size as i64 + s).max(0) as usize
117                            } else {
118                                s.min(dim_size as i64) as usize
119                            }
120                        })
121                        .unwrap_or(0);
122
123                    let stop = stop
124                        .map(|s| {
125                            if s < 0 {
126                                (dim_size as i64 + s).max(0) as usize
127                            } else {
128                                s.min(dim_size as i64) as usize
129                            }
130                        })
131                        .unwrap_or(dim_size);
132
133                    let size = if step > 0 {
134                        ((stop as i64 - start as i64 + step - 1) / step).max(0) as usize
135                    } else {
136                        ((stop as i64 - start as i64 + step + 1) / step).max(0) as usize
137                    };
138
139                    output_shape.push(size);
140                    slices.push((start, stop, step as usize));
141                    input_dim_idx += 1;
142                }
143                TensorIndex::All => {
144                    output_shape.push(dim_size);
145                    slices.push((0, dim_size, 1));
146                    input_dim_idx += 1;
147                }
148                TensorIndex::List(indices_list) => {
149                    // Fancy indexing with list of indices
150                    for &idx in indices_list {
151                        let normalized_idx = if idx < 0 {
152                            (dim_size as i64 + idx) as usize
153                        } else {
154                            idx as usize
155                        };
156
157                        if normalized_idx >= dim_size {
158                            return Err(TorshError::IndexOutOfBounds {
159                                index: normalized_idx,
160                                size: dim_size,
161                            });
162                        }
163                    }
164
165                    output_shape.push(indices_list.len());
166                    // Store list indices as a special slice marker
167                    slices.push((0, indices_list.len(), 0)); // step=0 indicates list indexing
168                    input_dim_idx += 1;
169                }
170                TensorIndex::Mask(mask) => {
171                    // Boolean mask indexing - dimension is flattened
172                    if mask.ndim() != 1 {
173                        return Err(TorshError::InvalidArgument(
174                            "Boolean mask must be 1D for single dimension indexing".to_string(),
175                        ));
176                    }
177
178                    if mask.numel() != dim_size {
179                        return Err(TorshError::ShapeMismatch {
180                            expected: vec![dim_size],
181                            got: mask.shape().dims().to_vec(),
182                        });
183                    }
184
185                    // Count True values to determine output size
186                    let mask_data = mask.to_vec()?;
187                    let true_count = mask_data.iter().filter(|&&x| x).count();
188
189                    output_shape.push(true_count);
190                    // Store mask as special slice marker
191                    slices.push((0, true_count, 0)); // step=0 indicates mask indexing
192                    input_dim_idx += 1;
193                }
194                TensorIndex::NewAxis => {
195                    // This should not happen since NewAxis is handled earlier
196                    return Err(TorshError::InvalidArgument(
197                        "NewAxis should be handled before this point".to_string(),
198                    ));
199                }
200                TensorIndex::Ellipsis => {
201                    // This should not happen since ellipsis is expanded earlier
202                    return Err(TorshError::InvalidArgument(
203                        "Ellipsis should be expanded before processing".to_string(),
204                    ));
205                }
206            }
207        }
208
209        // If all indices were single indices, we need at least one dimension
210        if output_shape.is_empty() {
211            output_shape.push(1);
212        }
213
214        // A contiguous single-axis slice is pure geometry: it records a view and
215        // skips the index map entirely (see `narrow_geometry`).
216        let narrow = self.narrow_geometry(&expanded_indices, &slices);
217
218        // Use specialized extraction logic for advanced indexing
219        let (mut result, index_map) = if expanded_indices
220            .iter()
221            .any(|idx| matches!(idx, TensorIndex::List(_) | TensorIndex::Mask(_)))
222        {
223            self.extract_advanced_indexing(&expanded_indices, &output_shape)?
224        } else {
225            self.extract_basic_indexing(
226                &expanded_indices,
227                &output_shape,
228                &slices,
229                narrow.is_none(),
230            )?
231        };
232        match narrow {
233            // The slab's geometry determines the backward scatter on its own.
234            Some((dim, start)) => self.record_view(&mut result, ViewKind::Narrow { dim, start }),
235            // Record the gather so gradients scatter back into the indexed
236            // tensor (this is what makes fancy indexing differentiable).
237            // `index_map[o]` is the input logical index that fed output
238            // position `o`.
239            None => self.record_gather(&mut result, index_map),
240        }
241        Ok(result)
242    }
243
244    /// Recognise a contiguous single-axis slice among already-normalised indices.
245    ///
246    /// Returns `Some((dim, start))` when every axis is taken whole except at
247    /// most one, which is a step-1 range — the same geometry
248    /// [`Tensor::narrow`] and [`Tensor::slice_tensor`] build directly as an
249    /// aliasing view, reached here through `slice_with_step` with `step == 1`
250    /// and through plain range indexing. Such a slice needs no index map:
251    /// [`ViewKind::Narrow`] scatters the gradient back as one zero-padded slab
252    /// instead of one element at a time.
253    ///
254    /// `slices[axis]` is read rather than re-derived from the `TensorIndex`
255    /// because the caller has already folded negative bounds and clamped the
256    /// range against the axis extent. Anything that re-indexes (single indices,
257    /// lists, masks, `NewAxis`) or strides (`step != 1`, including a negative
258    /// step, which wraps to a large `usize`) keeps the gather path.
259    fn narrow_geometry(
260        &self,
261        indices: &[TensorIndex],
262        slices: &[(usize, usize, usize)],
263    ) -> Option<(usize, usize)> {
264        let ndim = self.ndim();
265        if ndim == 0 || indices.len() != ndim || slices.len() != ndim {
266            return None;
267        }
268        let shape = self.shape();
269        let dims = shape.dims();
270
271        let mut narrowed: Option<(usize, usize)> = None;
272        for (axis, index) in indices.iter().enumerate() {
273            if !matches!(index, TensorIndex::All | TensorIndex::Range(..)) {
274                return None;
275            }
276            let (start, stop, step) = slices[axis];
277            if step != 1 {
278                return None;
279            }
280            if start == 0 && stop == dims[axis] {
281                continue;
282            }
283            if narrowed.is_some() {
284                // Two narrowed axes are not a single contiguous slab.
285                return None;
286            }
287            narrowed = Some((axis, start));
288        }
289        // Every axis taken whole is still a slab — the full one.
290        Some(narrowed.unwrap_or((0, 0)))
291    }
292
293    /// Record `result` as a gather of `self` for autograd. No-op when gradients
294    /// are not being tracked, so inference keeps building plain leaves.
295    pub(crate) fn record_gather(&self, result: &mut Self, index_map: Vec<usize>) {
296        if crate::should_record_grad(self.requires_grad) {
297            result.requires_grad = true;
298            result.operation = Operation::Gather {
299                input: Arc::new(self.clone()),
300                index_map: Arc::new(index_map),
301            };
302        }
303    }
304
305    /// Extract data using basic indexing (ranges, single indices, all).
306    ///
307    /// Returns the gathered tensor and, alongside it, the map from each output
308    /// logical position to the input logical index it was copied from — the
309    /// exact information the backward pass scatters through. `record_index_map`
310    /// is cleared by callers that record the slice's geometry instead (see
311    /// [`ViewKind::Narrow`]), which skips the one-usize-per-output allocation.
312    fn extract_basic_indexing(
313        &self,
314        indices: &[TensorIndex],
315        output_shape: &[usize],
316        slices: &[(usize, usize, usize)],
317        record_index_map: bool,
318    ) -> Result<(Self, Vec<usize>)> {
319        let input_data = self.to_vec()?;
320
321        let output_size = output_shape.iter().product();
322        let mut output_data = Vec::with_capacity(output_size);
323        // Only the autograd path needs the output→input map; inference skips the
324        // allocation entirely.
325        let record = record_index_map && crate::should_record_grad(self.requires_grad);
326        let mut index_map = Vec::with_capacity(if record { output_size } else { 0 });
327
328        let input_strides = self.compute_strides();
329        let output_strides = compute_strides_from_shape(output_shape);
330
331        for out_idx in 0..output_size {
332            // Convert flat index to multi-dimensional indices
333            let mut out_indices = vec![0; output_shape.len()];
334            let mut remaining = out_idx;
335            for (i, &stride) in output_strides.iter().enumerate() {
336                out_indices[i] = remaining / stride;
337                remaining %= stride;
338            }
339
340            // Map output indices to input indices using slices
341            let mut input_flat_idx = 0;
342            let mut out_dim = 0;
343            let mut input_dim = 0;
344
345            for (slice_idx, &(start, _, step)) in slices.iter().enumerate() {
346                // Skip NewAxis dimensions in input tensor
347                if slice_idx < indices.len() && matches!(indices[slice_idx], TensorIndex::NewAxis) {
348                    out_dim += 1;
349                    continue;
350                }
351
352                // Ensure we don't exceed input dimensions
353                if input_dim >= input_strides.len() {
354                    break;
355                }
356
357                let idx = if slice_idx < indices.len()
358                    && matches!(indices[slice_idx], TensorIndex::Index(_))
359                {
360                    start
361                } else {
362                    start + out_indices[out_dim] * step
363                };
364                input_flat_idx += idx * input_strides[input_dim];
365
366                if !(slice_idx < indices.len()
367                    && matches!(indices[slice_idx], TensorIndex::Index(_)))
368                {
369                    out_dim += 1;
370                }
371                input_dim += 1;
372            }
373
374            output_data.push(input_data[input_flat_idx]);
375            if record {
376                index_map.push(input_flat_idx);
377            }
378        }
379
380        let result = Self::from_data(output_data, output_shape.to_vec(), self.device)?;
381        Ok((result, index_map))
382    }
383
384    /// Extract data using advanced indexing (lists, masks).
385    ///
386    /// Also returns the output→input logical index map for the backward scatter;
387    /// fancy indexing that reads one input element several times produces
388    /// duplicate entries, and the scatter accumulates them (PyTorch semantics).
389    fn extract_advanced_indexing(
390        &self,
391        indices: &[TensorIndex],
392        output_shape: &[usize],
393    ) -> Result<(Self, Vec<usize>)> {
394        let input_data = self.to_vec()?;
395
396        let output_size = output_shape.iter().product();
397        let mut output_data = Vec::with_capacity(output_size);
398        let record = crate::should_record_grad(self.requires_grad);
399        let mut index_map = Vec::with_capacity(if record { output_size } else { 0 });
400
401        let input_strides = self.compute_strides();
402        let output_strides = compute_strides_from_shape(output_shape);
403
404        for out_idx in 0..output_size {
405            // Convert flat index to multi-dimensional indices
406            let mut out_indices = vec![0; output_shape.len()];
407            let mut remaining = out_idx;
408            for (i, &stride) in output_strides.iter().enumerate() {
409                out_indices[i] = remaining / stride;
410                remaining %= stride;
411            }
412
413            // Map output indices to input indices using advanced indexing
414            let mut input_flat_idx = 0;
415            let mut out_dim = 0;
416
417            for (dim_idx, index) in indices.iter().enumerate() {
418                if dim_idx >= self.ndim() {
419                    break;
420                }
421
422                let input_idx = match index {
423                    TensorIndex::Index(idx) => {
424                        let dim_size = self.shape().dims()[dim_idx];
425
426                        if *idx < 0 {
427                            (dim_size as i64 + idx) as usize
428                        } else {
429                            *idx as usize
430                        }
431                    }
432                    TensorIndex::Range(start, _stop, step) => {
433                        let dim_size = self.shape().dims()[dim_idx];
434                        let step = step.unwrap_or(1);
435                        let start = start
436                            .map(|s| {
437                                if s < 0 {
438                                    (dim_size as i64 + s).max(0) as usize
439                                } else {
440                                    s.min(dim_size as i64) as usize
441                                }
442                            })
443                            .unwrap_or(0);
444
445                        start + out_indices[out_dim] * (step as usize)
446                    }
447                    TensorIndex::All => out_indices[out_dim],
448                    TensorIndex::List(indices_list) => {
449                        // Fancy indexing: use the list index
450                        let list_idx = out_indices[out_dim];
451                        if list_idx >= indices_list.len() {
452                            return Err(TorshError::IndexOutOfBounds {
453                                index: list_idx,
454                                size: indices_list.len(),
455                            });
456                        }
457
458                        let actual_idx = indices_list[list_idx];
459                        let dim_size = self.shape().dims()[dim_idx];
460
461                        if actual_idx < 0 {
462                            (dim_size as i64 + actual_idx) as usize
463                        } else {
464                            actual_idx as usize
465                        }
466                    }
467                    TensorIndex::Mask(mask) => {
468                        // Boolean mask indexing
469                        let mask_data = mask.to_vec()?;
470
471                        // Find the nth True value in the mask
472                        let target_true_idx = out_indices[out_dim];
473                        let mut true_count = 0;
474                        let mut found_idx = None;
475                        for (i, &mask_val) in mask_data.iter().enumerate() {
476                            if mask_val {
477                                if true_count == target_true_idx {
478                                    found_idx = Some(i);
479                                    break;
480                                }
481                                true_count += 1;
482                            }
483                        }
484
485                        match found_idx {
486                            Some(idx) => idx,
487                            None => {
488                                return Err(TorshError::IndexOutOfBounds {
489                                    index: target_true_idx,
490                                    size: true_count,
491                                });
492                            }
493                        }
494                    }
495                    TensorIndex::NewAxis => {
496                        // NewAxis doesn't consume input dimensions
497                        continue;
498                    }
499                    TensorIndex::Ellipsis => {
500                        // Ellipsis should be handled in shape computation
501                        out_indices[out_dim]
502                    }
503                };
504
505                input_flat_idx += input_idx * input_strides[dim_idx];
506
507                // Only advance output dimension for non-index operations
508                if !matches!(index, TensorIndex::Index(_) | TensorIndex::NewAxis) {
509                    out_dim += 1;
510                }
511            }
512
513            // Handle remaining dimensions
514            for stride in input_strides
515                .iter()
516                .skip(indices.len())
517                .take(self.ndim() - indices.len())
518            {
519                if out_dim < out_indices.len() {
520                    input_flat_idx += out_indices[out_dim] * stride;
521                    out_dim += 1;
522                }
523            }
524
525            if input_flat_idx >= input_data.len() {
526                return Err(TorshError::IndexOutOfBounds {
527                    index: input_flat_idx,
528                    size: input_data.len(),
529                });
530            }
531
532            output_data.push(input_data[input_flat_idx]);
533            if record {
534                index_map.push(input_flat_idx);
535            }
536        }
537
538        let result = Self::from_data(output_data, output_shape.to_vec(), self.device)?;
539        Ok((result, index_map))
540    }
541
542    /// Expand ellipsis into explicit All indices
543    fn expand_ellipsis(&self, indices: &[TensorIndex]) -> Result<Vec<TensorIndex>> {
544        let mut expanded = Vec::new();
545        let mut found_ellipsis = false;
546
547        // Count non-ellipsis, non-newaxis indices to determine how many dimensions ellipsis should expand to
548        let non_expanding_indices = indices
549            .iter()
550            .filter(|idx| !matches!(idx, TensorIndex::Ellipsis | TensorIndex::NewAxis))
551            .count();
552
553        for index in indices {
554            match index {
555                TensorIndex::Ellipsis => {
556                    if found_ellipsis {
557                        return Err(TorshError::InvalidArgument(
558                            "Only one ellipsis (...) is allowed per indexing operation".to_string(),
559                        ));
560                    }
561                    found_ellipsis = true;
562
563                    // Calculate how many dimensions the ellipsis should expand to
564                    let ellipsis_dims = if self.ndim() >= non_expanding_indices {
565                        self.ndim() - non_expanding_indices
566                    } else {
567                        0
568                    };
569
570                    // Expand ellipsis to All indices
571                    for _ in 0..ellipsis_dims {
572                        expanded.push(TensorIndex::All);
573                    }
574                }
575                _ => {
576                    expanded.push(index.clone());
577                }
578            }
579        }
580
581        // If no ellipsis was found, add implicit trailing All indices for remaining dimensions
582        if !found_ellipsis {
583            let current_dims = expanded
584                .iter()
585                .filter(|idx| !matches!(idx, TensorIndex::NewAxis))
586                .count();
587
588            for _ in current_dims..self.ndim() {
589                expanded.push(TensorIndex::All);
590            }
591        }
592
593        Ok(expanded)
594    }
595
596    /// Get a single element (1D indexing)
597    pub fn get_1d(&self, index: usize) -> Result<T> {
598        if self.ndim() != 1 {
599            return Err(TorshError::InvalidShape(
600                "get_1d() can only be used on 1D tensors".to_string(),
601            ));
602        }
603
604        if index >= self.shape().dims()[0] {
605            return Err(TorshError::IndexOutOfBounds {
606                index,
607                size: self.shape().dims()[0],
608            });
609        }
610
611        let data = self.data()?;
612        Ok(data[index])
613    }
614
615    /// Get a single element (2D indexing)
616    pub fn get_2d(&self, row: usize, col: usize) -> Result<T> {
617        if self.ndim() != 2 {
618            return Err(TorshError::InvalidShape(
619                "get_2d() can only be used on 2D tensors".to_string(),
620            ));
621        }
622
623        let shape = self.shape();
624        if row >= shape.dims()[0] || col >= shape.dims()[1] {
625            return Err(TorshError::IndexOutOfBounds {
626                index: row * shape.dims()[1] + col,
627                size: shape.numel(),
628            });
629        }
630
631        let data = self.to_vec()?;
632
633        let index = row * shape.dims()[1] + col;
634        Ok(data[index])
635    }
636
637    /// Get a single element (3D indexing)
638    pub fn get_3d(&self, x: usize, y: usize, z: usize) -> Result<T> {
639        if self.ndim() != 3 {
640            return Err(TorshError::InvalidShape(
641                "get_3d() can only be used on 3D tensors".to_string(),
642            ));
643        }
644
645        let shape = self.shape();
646        if x >= shape.dims()[0] || y >= shape.dims()[1] || z >= shape.dims()[2] {
647            return Err(TorshError::IndexOutOfBounds {
648                index: x * shape.dims()[1] * shape.dims()[2] + y * shape.dims()[2] + z,
649                size: shape.numel(),
650            });
651        }
652
653        let data = self.to_vec()?;
654
655        let index = x * shape.dims()[1] * shape.dims()[2] + y * shape.dims()[2] + z;
656        Ok(data[index])
657    }
658
659    /// Set a single element (1D indexing)
660    pub fn set_1d(&mut self, index: usize, value: T) -> Result<()> {
661        if self.ndim() != 1 {
662            return Err(TorshError::InvalidShape(
663                "set_1d() can only be used on 1D tensors".to_string(),
664            ));
665        }
666
667        if index >= self.shape().dims()[0] {
668            return Err(TorshError::IndexOutOfBounds {
669                index,
670                size: self.shape().dims()[0],
671            });
672        }
673
674        let mut data = self.to_vec()?;
675        data[index] = value;
676        *self = Self::from_data(data, self.shape().dims().to_vec(), self.device())?;
677        Ok(())
678    }
679
680    /// Set a single element (2D indexing)
681    pub fn set_2d(&mut self, row: usize, col: usize, value: T) -> Result<()> {
682        if self.ndim() != 2 {
683            return Err(TorshError::InvalidShape(
684                "set_2d() can only be used on 2D tensors".to_string(),
685            ));
686        }
687
688        let shape = self.shape();
689        if row >= shape.dims()[0] || col >= shape.dims()[1] {
690            return Err(TorshError::IndexOutOfBounds {
691                index: row * shape.dims()[1] + col,
692                size: shape.numel(),
693            });
694        }
695
696        let mut data = self.to_vec()?;
697        let index = row * shape.dims()[1] + col;
698        data[index] = value;
699        *self = Self::from_data(data, self.shape().dims().to_vec(), self.device())?;
700        Ok(())
701    }
702
703    /// Set a single element (3D indexing)
704    pub fn set_3d(&mut self, x: usize, y: usize, z: usize, value: T) -> Result<()> {
705        if self.ndim() != 3 {
706            return Err(TorshError::InvalidShape(
707                "set_3d() can only be used on 3D tensors".to_string(),
708            ));
709        }
710
711        let shape = self.shape();
712        if x >= shape.dims()[0] || y >= shape.dims()[1] || z >= shape.dims()[2] {
713            return Err(TorshError::IndexOutOfBounds {
714                index: x * shape.dims()[1] * shape.dims()[2] + y * shape.dims()[2] + z,
715                size: shape.numel(),
716            });
717        }
718
719        let mut data = self.to_vec()?;
720        let index = x * shape.dims()[1] * shape.dims()[2] + y * shape.dims()[2] + z;
721        data[index] = value;
722        *self = Self::from_data(data, self.shape().dims().to_vec(), self.device())?;
723        Ok(())
724    }
725
726    /// Select along a dimension
727    pub fn select(&self, dim: i32, index: i64) -> Result<Self> {
728        let ndim = self.ndim() as i32;
729        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
730
731        if dim >= self.ndim() {
732            return Err(TorshError::InvalidArgument(format!(
733                "Dimension {} out of range for tensor with {} dimensions",
734                dim,
735                self.ndim()
736            )));
737        }
738
739        let dim_size = self.shape().dims()[dim] as i64;
740        let index = if index < 0 { dim_size + index } else { index };
741
742        if index < 0 || index >= dim_size {
743            return Err(TorshError::IndexOutOfBounds {
744                index: index as usize,
745                size: dim_size as usize,
746            });
747        }
748
749        // Create index array for slicing
750        let mut indices = Vec::new();
751        for d in 0..self.ndim() {
752            if d == dim {
753                indices.push(TensorIndex::Index(index));
754            } else {
755                indices.push(TensorIndex::All);
756            }
757        }
758
759        // Use the existing index function
760        self.index(&indices)
761    }
762
763    /// Slice along a dimension with PyTorch-style parameters
764    pub fn slice_with_step(
765        &self,
766        dim: i32,
767        start: Option<i64>,
768        end: Option<i64>,
769        step: Option<i64>,
770    ) -> Result<Self> {
771        let ndim = self.ndim() as i32;
772        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
773
774        if dim >= self.ndim() {
775            return Err(TorshError::InvalidArgument(format!(
776                "Dimension {} out of range for tensor with {} dimensions",
777                dim,
778                self.ndim()
779            )));
780        }
781
782        // Create index array for slicing
783        let mut indices = Vec::new();
784        for d in 0..self.ndim() {
785            if d == dim {
786                indices.push(TensorIndex::Range(start, end, step));
787            } else {
788                indices.push(TensorIndex::All);
789            }
790        }
791
792        // Use the existing index function
793        self.index(&indices)
794    }
795
796    /// Narrow `dim` to the `length` elements starting at `start`.
797    ///
798    /// The result is a **view**, exactly as in PyTorch: it shares storage with
799    /// the source, so writing through it is visible in the source and no data
800    /// is copied (which is what makes per-timestep gate splitting in the RNN
801    /// layers affordable). It is the same view [`Tensor::slice_tensor`] builds
802    /// for the same range — only the arguments differ: `dim` and `start` may be
803    /// negative (counted from the end), the window is given as a `length`
804    /// rather than an `end`, and a `length` of 0 yields an empty view.
805    ///
806    /// Under `requires_grad` the window's geometry is recorded
807    /// ([`ViewKind::Narrow`]), so the gradient scatters back into the source as
808    /// one zero-padded slab.
809    ///
810    /// # Arguments
811    ///
812    /// * `dim` - Axis to narrow. Negative values count from the end.
813    /// * `start` - First index on `dim`. Negative values count from the end.
814    /// * `length` - Number of elements to keep on `dim`.
815    ///
816    /// # Errors
817    ///
818    /// Returns an error if `dim` is out of range for the tensor's rank, if
819    /// `start` is not a valid index on `dim` (note that `start == dim_size` is
820    /// rejected even for a zero `length`), or if `start + length` exceeds the
821    /// extent of `dim`.
822    ///
823    /// # Examples
824    ///
825    /// ```
826    /// use torsh_core::device::DeviceType;
827    /// use torsh_tensor::Tensor;
828    ///
829    /// let base = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
830    ///     .expect("tensor creation should succeed");
831    /// let mut row = base.narrow(0, 1, 1).expect("narrow should succeed");
832    /// assert_eq!(row.to_vec().expect("to_vec"), vec![3.0, 4.0]);
833    ///
834    /// // The view aliases its source: the write lands in `base`.
835    /// row.set_item_flat(0, -1.0).expect("write through the view");
836    /// assert_eq!(base.to_vec().expect("to_vec"), vec![1.0, 2.0, -1.0, 4.0]);
837    /// ```
838    ///
839    /// # See Also
840    ///
841    /// * [`Tensor::slice_tensor`] - The same view, with unsigned `start`/`end`
842    /// * [`Tensor::slice_with_step`] - Strided slicing (copies)
843    pub fn narrow(&self, dim: i32, start: i64, length: usize) -> Result<Self> {
844        let ndim = self.ndim() as i32;
845        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
846
847        if dim >= self.ndim() {
848            return Err(TorshError::InvalidArgument(format!(
849                "Dimension {} out of range for tensor with {} dimensions",
850                dim,
851                self.ndim()
852            )));
853        }
854
855        let dim_size = self.shape().dims()[dim] as i64;
856        let start = if start < 0 { dim_size + start } else { start };
857
858        if start < 0 || start >= dim_size {
859            return Err(TorshError::InvalidArgument(format!(
860                "Start index {start} out of range for dimension {dim} with size {dim_size}"
861            )));
862        }
863
864        let end = start + length as i64;
865        if end > dim_size {
866            return Err(TorshError::InvalidArgument(format!(
867                "End index {end} out of range for dimension {dim} with size {dim_size}"
868            )));
869        }
870
871        // The window is a single-axis contiguous range, i.e. exactly the view
872        // `slice_tensor` builds — so build it directly instead of routing
873        // through `index()`, which *gathers* the elements into a fresh buffer
874        // (a full slab copy per call, and a result that no longer aliases its
875        // source). `start` is a valid index on `dim` and `start + length` is
876        // within its extent, which is all `narrow_view` needs.
877        Ok(self.narrow_view(dim, start as usize, length))
878    }
879
880    /// Boolean indexing (masking)
881    pub fn masked_select(&self, mask: &Tensor<bool>) -> Result<Self> {
882        if self.shape() != mask.shape() {
883            return Err(TorshError::ShapeMismatch {
884                expected: self.shape().dims().to_vec(),
885                got: mask.shape().dims().to_vec(),
886            });
887        }
888
889        let self_data = self.data()?;
890        let mask_data = mask.data()?;
891
892        // Collect all elements where mask is true
893        let mut selected_data = Vec::new();
894        for (i, &mask_val) in mask_data.iter().enumerate() {
895            if mask_val {
896                selected_data.push(self_data[i]);
897            }
898        }
899
900        // Return 1D tensor with selected elements
901        Self::from_data(
902            selected_data.clone(),
903            vec![selected_data.len()],
904            self.device,
905        )
906    }
907
908    pub fn take(&self, indices: &Tensor<i64>) -> Result<Self> {
909        let self_data = self.data()?;
910
911        let indices_data = indices.data()?;
912
913        let self_size = self.shape().numel();
914        let output_shape = indices.shape().dims().to_vec();
915        let output_size = indices.shape().numel();
916        let mut output_data = Vec::with_capacity(output_size);
917
918        // Take elements at the given flat indices
919        for &idx in indices_data.iter() {
920            let idx = if idx < 0 {
921                (self_size as i64 + idx) as usize
922            } else {
923                idx as usize
924            };
925
926            if idx >= self_size {
927                return Err(TorshError::IndexOutOfBounds {
928                    index: idx,
929                    size: self_size,
930                });
931            }
932
933            output_data.push(self_data[idx]);
934        }
935
936        Self::from_data(output_data, output_shape, self.device)
937    }
938
939    /// Put values at indices
940    pub fn put(&self, indices: &Tensor<i64>, values: &Self) -> Result<Self> {
941        let self_data = self.data()?;
942
943        let indices_data = indices.data()?;
944        let values_data = values.data()?;
945
946        // Check that indices and values have the same shape
947        if indices.shape() != values.shape() {
948            return Err(TorshError::ShapeMismatch {
949                expected: indices.shape().dims().to_vec(),
950                got: values.shape().dims().to_vec(),
951            });
952        }
953
954        let self_size = self.shape().numel();
955        let mut output_data = self_data.clone();
956
957        // Put values at the given flat indices
958        for (i, &idx) in indices_data.iter().enumerate() {
959            let idx = if idx < 0 {
960                (self_size as i64 + idx) as usize
961            } else {
962                idx as usize
963            };
964
965            if idx >= self_size {
966                return Err(TorshError::IndexOutOfBounds {
967                    index: idx,
968                    size: self_size,
969                });
970            }
971
972            output_data[idx] = values_data[i];
973        }
974
975        Self::from_data(output_data, self.shape().dims().to_vec(), self.device)
976    }
977
978    /// Select indices along a dimension
979    ///
980    /// The result joins the autograd graph as an [`Operation::Gather`] (same
981    /// machinery as fancy indexing): each output element records the logical
982    /// index of the input element it came from, so the backward pass scatters
983    /// the gradient back and **accumulates** on any index selected more than
984    /// once. The index map is only built when gradients are being recorded.
985    pub fn index_select(&self, dim: i32, index: &Tensor<i64>) -> Result<Self> {
986        let ndim = self.ndim() as i32;
987        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
988
989        if dim >= self.ndim() {
990            return Err(TorshError::InvalidArgument(format!(
991                "Dimension {} out of range for tensor with {} dimensions",
992                dim,
993                self.ndim()
994            )));
995        }
996
997        // Index must be 1D
998        if index.ndim() != 1 {
999            return Err(TorshError::InvalidShape(
1000                "index_select expects a 1D index tensor".to_string(),
1001            ));
1002        }
1003
1004        // Calculate output shape
1005        let mut output_shape = self.shape().dims().to_vec();
1006        output_shape[dim] = index.shape().dims()[0];
1007
1008        let output_size: usize = output_shape.iter().product();
1009        let mut output_data = Vec::with_capacity(output_size);
1010        let record = crate::should_record_grad(self.requires_grad);
1011        let mut index_map = Vec::with_capacity(if record { output_size } else { 0 });
1012
1013        let self_data = self.data()?;
1014
1015        let index_data = index.data()?;
1016
1017        // Compute strides
1018        let self_strides = self.compute_strides();
1019        let _output_strides = Self::compute_strides_for_shape(&output_shape);
1020
1021        // Select elements
1022        for out_idx in 0..output_size {
1023            // Convert flat index to multi-dimensional index
1024            let mut indices = vec![0; self.ndim()];
1025            let mut remaining = out_idx;
1026            for i in (0..self.ndim()).rev() {
1027                indices[i] = remaining % output_shape[i];
1028                remaining /= output_shape[i];
1029            }
1030
1031            // For the selected dimension, use the index from the index tensor
1032            let select_idx = indices[dim];
1033            let selected_value = index_data[select_idx] as usize;
1034
1035            if selected_value >= self.shape().dims()[dim] {
1036                return Err(TorshError::IndexOutOfBounds {
1037                    index: selected_value,
1038                    size: self.shape().dims()[dim],
1039                });
1040            }
1041
1042            indices[dim] = selected_value;
1043
1044            // Compute flat index in source tensor
1045            let src_flat_idx = indices
1046                .iter()
1047                .zip(&self_strides)
1048                .map(|(idx, stride)| idx * stride)
1049                .sum::<usize>();
1050
1051            output_data.push(self_data[src_flat_idx]);
1052            if record {
1053                // `self_strides` are the default row-major strides of `self`'s
1054                // shape, so this is a logical index into `self` — exactly what
1055                // `Operation::Gather`'s backward scatters through.
1056                index_map.push(src_flat_idx);
1057            }
1058        }
1059
1060        let mut result = Self::from_data(output_data, output_shape, self.device)?;
1061        // See `gather`: guarded on the same `record` that decided whether to
1062        // build the map, so a mid-call grad-mode flip cannot record a node whose
1063        // index map is empty.
1064        if record {
1065            self.record_gather(&mut result, index_map);
1066        }
1067        Ok(result)
1068    }
1069
1070    /// Compute strides for the tensor's shape
1071    pub(crate) fn compute_strides(&self) -> Vec<usize> {
1072        Self::compute_strides_for_shape(self.shape().dims())
1073    }
1074
1075    /// Compute strides for a given shape
1076    pub(crate) fn compute_strides_for_shape(shape: &[usize]) -> Vec<usize> {
1077        let mut strides = vec![1; shape.len()];
1078        for i in (0..shape.len() - 1).rev() {
1079            strides[i] = strides[i + 1] * shape[i + 1];
1080        }
1081        strides
1082    }
1083}
1084
1085/// Helper function to compute strides from shape
1086fn compute_strides_from_shape(shape: &[usize]) -> Vec<usize> {
1087    let mut strides = vec![1; shape.len()];
1088    for i in (0..shape.len() - 1).rev() {
1089        strides[i] = strides[i + 1] * shape[i + 1];
1090    }
1091    strides
1092}
1093
1094/// Convenience macros for indexing
1095#[macro_export]
1096macro_rules! idx {
1097    // Single index: idx![5]
1098    ($idx:expr) => {
1099        vec![TensorIndex::Index($idx)]
1100    };
1101
1102    // Multiple indices: idx![1, 2, 3]
1103    ($($idx:expr),+ $(,)?) => {
1104        vec![$(TensorIndex::Index($idx)),+]
1105    };
1106}
1107
1108#[macro_export]
1109macro_rules! s {
1110    // Full slice: s![..]
1111    (..) => {
1112        TensorIndex::All
1113    };
1114
1115    // To end: s![..5]
1116    (.. $stop:expr) => {
1117        TensorIndex::range(None, Some($stop))
1118    };
1119
1120    // Range (comma syntax): s![1, 5]
1121    ($start:expr, $stop:expr) => {
1122        TensorIndex::range(Some($start), Some($stop))
1123    };
1124
1125    // Range with step (comma syntax): s![1, 5, 2]
1126    ($start:expr, $stop:expr, $step:expr) => {
1127        TensorIndex::range_step(Some($start), Some($stop), $step)
1128    };
1129
1130    // Ellipsis: s![ellipsis]
1131    (ellipsis) => {
1132        TensorIndex::Ellipsis
1133    };
1134
1135    // NewAxis: s![None]
1136    (None) => {
1137        TensorIndex::NewAxis
1138    };
1139}
1140
1141/// Advanced indexing macros
1142#[macro_export]
1143macro_rules! fancy_idx {
1144    // List indexing: fancy_idx![0, 2, 1]
1145    [$($idx:expr),+ $(,)?] => {
1146        TensorIndex::List(vec![$($idx),+])
1147    };
1148}
1149
1150#[macro_export]
1151macro_rules! mask_idx {
1152    // Boolean mask indexing: mask_idx![mask_tensor]
1153    [$mask:expr] => {
1154        TensorIndex::Mask($mask)
1155    };
1156}
1157
1158/// Convenient indexing syntax
1159impl<T: TensorElement> Tensor<T> {
1160    /// Advanced indexing with list of indices (fancy indexing)
1161    pub fn index_with_list(&self, dim: i32, indices: &[i64]) -> Result<Self> {
1162        let ndim = self.ndim() as i32;
1163        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
1164
1165        if dim >= self.ndim() {
1166            return Err(TorshError::InvalidArgument(format!(
1167                "Dimension {} out of range for tensor with {} dimensions",
1168                dim,
1169                self.ndim()
1170            )));
1171        }
1172
1173        let mut index_spec = vec![TensorIndex::All; self.ndim()];
1174        index_spec[dim] = TensorIndex::List(indices.to_vec());
1175
1176        self.index(&index_spec)
1177    }
1178
1179    /// Boolean mask indexing for a specific dimension
1180    pub fn index_with_mask(&self, dim: i32, mask: &Tensor<bool>) -> Result<Self> {
1181        let ndim = self.ndim() as i32;
1182        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
1183
1184        if dim >= self.ndim() {
1185            return Err(TorshError::InvalidArgument(format!(
1186                "Dimension {} out of range for tensor with {} dimensions",
1187                dim,
1188                self.ndim()
1189            )));
1190        }
1191
1192        let mut index_spec = vec![TensorIndex::All; self.ndim()];
1193        index_spec[dim] = TensorIndex::Mask(mask.clone());
1194
1195        self.index(&index_spec)
1196    }
1197
1198    /// Global boolean mask indexing (flattens to 1D result)
1199    pub fn mask_select(&self, mask: &Tensor<bool>) -> Result<Self> {
1200        if self.shape() != mask.shape() {
1201            return Err(TorshError::ShapeMismatch {
1202                expected: self.shape().dims().to_vec(),
1203                got: mask.shape().dims().to_vec(),
1204            });
1205        }
1206
1207        let self_data = self.data()?;
1208
1209        let mask_data = mask.data()?;
1210
1211        // Collect all elements where mask is true
1212        let mut selected_data = Vec::new();
1213        for (i, &mask_val) in mask_data.iter().enumerate() {
1214            if mask_val {
1215                selected_data.push(self_data[i]);
1216            }
1217        }
1218
1219        // Return 1D tensor with selected elements
1220        Self::from_data(
1221            selected_data.clone(),
1222            vec![selected_data.len()],
1223            self.device,
1224        )
1225    }
1226
1227    /// Create boolean mask from condition
1228    pub fn where_condition<F>(&self, condition: F) -> Result<Tensor<bool>>
1229    where
1230        F: Fn(&T) -> bool,
1231        T: Clone,
1232    {
1233        let data = self.data()?;
1234
1235        let mask_data: Vec<bool> = data.iter().map(condition).collect();
1236
1237        Tensor::from_data(mask_data, self.shape().dims().to_vec(), self.device)
1238    }
1239
1240    /// Scatter values along an axis using indices (indexing version)
1241    pub fn scatter_indexed(&self, dim: i32, index: &Tensor<i64>, src: &Self) -> Result<Self> {
1242        let ndim = self.ndim() as i32;
1243        let dim = if dim < 0 { ndim + dim } else { dim } as usize;
1244
1245        if dim >= self.ndim() {
1246            return Err(TorshError::InvalidArgument(format!(
1247                "Dimension {} out of range for tensor with {} dimensions",
1248                dim,
1249                self.ndim()
1250            )));
1251        }
1252
1253        let self_shape_binding = self.shape();
1254        let self_shape = self_shape_binding.dims();
1255        let index_shape_binding = index.shape();
1256        let index_shape = index_shape_binding.dims();
1257        let src_shape_binding = src.shape();
1258        let src_shape = src_shape_binding.dims();
1259
1260        // Validate shapes
1261        if index_shape != src_shape {
1262            return Err(TorshError::ShapeMismatch {
1263                expected: index_shape.to_vec(),
1264                got: src_shape.to_vec(),
1265            });
1266        }
1267
1268        if index_shape.len() != self_shape.len() {
1269            return Err(TorshError::InvalidArgument(
1270                "Index tensor must have same number of dimensions as input tensor".to_string(),
1271            ));
1272        }
1273
1274        // Start with a copy of self
1275        let mut result_data = self.data()?.clone();
1276        let index_data = index.data()?;
1277        let src_data = src.data()?;
1278        let self_strides = self.compute_strides();
1279
1280        let index_size = index_shape.iter().product();
1281
1282        // Process each element in the index tensor
1283        for flat_idx in 0..index_size {
1284            // Convert flat index to multi-dimensional coordinates
1285            let mut coords = Vec::new();
1286            let mut temp_idx = flat_idx;
1287
1288            for &dim_size in index_shape.iter().rev() {
1289                coords.push(temp_idx % dim_size);
1290                temp_idx /= dim_size;
1291            }
1292            coords.reverse();
1293
1294            // Get the index value for the scatter dimension
1295            let scatter_idx = index_data[flat_idx];
1296            let dim_size = self_shape[dim] as i64;
1297            let scatter_idx = if scatter_idx < 0 {
1298                dim_size + scatter_idx
1299            } else {
1300                scatter_idx
1301            };
1302
1303            if scatter_idx < 0 || scatter_idx >= dim_size {
1304                return Err(TorshError::IndexOutOfBounds {
1305                    index: scatter_idx as usize,
1306                    size: dim_size as usize,
1307                });
1308            }
1309
1310            // Calculate destination index in result tensor
1311            coords[dim] = scatter_idx as usize;
1312            let mut dest_idx = 0;
1313            for (coord, &stride) in coords.iter().zip(self_strides.iter()) {
1314                dest_idx += coord * stride;
1315            }
1316
1317            result_data[dest_idx] = src_data[flat_idx];
1318        }
1319
1320        Self::from_data(result_data, self_shape.to_vec(), self.device)
1321    }
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326    use super::*;
1327    use crate::creation::{tensor_2d, zeros};
1328
1329    #[test]
1330    fn test_index_macros() {
1331        // Test single index
1332        let indices = idx![5];
1333        assert_eq!(indices.len(), 1);
1334
1335        // Test multiple indices
1336        let indices = idx![1, 2, 3];
1337        assert_eq!(indices.len(), 3);
1338
1339        // Test slice macros
1340        let _all = s![..];
1341        let _range = s![1, 5];
1342        let _range_step = s![1, 10, 2];
1343        let _to = s![..7];
1344
1345        // Test advanced indexing macros
1346        let _fancy = fancy_idx![0, 2, 1];
1347        let _ellipsis = s![ellipsis];
1348        let _newaxis = s![None];
1349    }
1350
1351    #[test]
1352    fn test_get_set() {
1353        let tensor = tensor_2d(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]])
1354            .expect("tensor creation should succeed");
1355
1356        // Test get
1357        assert_eq!(
1358            tensor.get(&[0, 0]).expect("data access should succeed"),
1359            1.0
1360        );
1361        assert_eq!(
1362            tensor.get(&[0, 1]).expect("data access should succeed"),
1363            2.0
1364        );
1365        assert_eq!(
1366            tensor.get(&[1, 2]).expect("data access should succeed"),
1367            6.0
1368        );
1369
1370        // Test set
1371        tensor
1372            .set(&[1, 1], 10.0)
1373            .expect("data access should succeed");
1374        assert_eq!(
1375            tensor.get(&[1, 1]).expect("data access should succeed"),
1376            10.0
1377        );
1378
1379        // Test out of bounds
1380        assert!(tensor.get(&[2, 0]).is_err());
1381        assert!(tensor.set(&[0, 3], 0.0).is_err());
1382    }
1383
1384    #[test]
1385    fn test_gather() {
1386        // Create a 3x3 tensor
1387        let tensor = tensor_2d(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0]])
1388            .expect("tensor creation should succeed");
1389
1390        // Create indices for gathering along dim=1
1391        let indices = tensor_2d(&[&[0i64, 2, 1], &[1, 0, 2], &[2, 1, 0]])
1392            .expect("tensor creation should succeed");
1393
1394        let result = tensor.gather(1, &indices).expect("gather should succeed");
1395
1396        // Expected: [[1, 3, 2], [5, 4, 6], [9, 8, 7]]
1397        assert_eq!(
1398            result.get(&[0, 0]).expect("data access should succeed"),
1399            1.0
1400        );
1401        assert_eq!(
1402            result.get(&[0, 1]).expect("data access should succeed"),
1403            3.0
1404        );
1405        assert_eq!(
1406            result.get(&[0, 2]).expect("data access should succeed"),
1407            2.0
1408        );
1409        assert_eq!(
1410            result.get(&[1, 0]).expect("data access should succeed"),
1411            5.0
1412        );
1413        assert_eq!(
1414            result.get(&[1, 1]).expect("data access should succeed"),
1415            4.0
1416        );
1417        assert_eq!(
1418            result.get(&[1, 2]).expect("data access should succeed"),
1419            6.0
1420        );
1421        assert_eq!(
1422            result.get(&[2, 0]).expect("data access should succeed"),
1423            9.0
1424        );
1425        assert_eq!(
1426            result.get(&[2, 1]).expect("data access should succeed"),
1427            8.0
1428        );
1429        assert_eq!(
1430            result.get(&[2, 2]).expect("data access should succeed"),
1431            7.0
1432        );
1433    }
1434
1435    #[test]
1436    fn test_scatter() {
1437        // Create a 3x3 tensor of zeros
1438        let tensor = zeros::<f32>(&[3, 3]).expect("tensor creation should succeed");
1439
1440        // Create indices for scattering along dim=1
1441        let indices = tensor_2d(&[&[0i64, 2, 1], &[1, 0, 2], &[2, 1, 0]])
1442            .expect("tensor creation should succeed");
1443
1444        // Source values
1445        let src = tensor_2d(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0]])
1446            .expect("tensor creation should succeed");
1447
1448        let result = tensor
1449            .scatter(1, &indices, &src)
1450            .expect("scatter should succeed");
1451
1452        // Expected: [[1, 3, 2], [5, 4, 6], [9, 8, 7]]
1453        assert_eq!(
1454            result.get(&[0, 0]).expect("data access should succeed"),
1455            1.0
1456        );
1457        assert_eq!(
1458            result.get(&[0, 1]).expect("data access should succeed"),
1459            3.0
1460        );
1461        assert_eq!(
1462            result.get(&[0, 2]).expect("data access should succeed"),
1463            2.0
1464        );
1465        assert_eq!(
1466            result.get(&[1, 0]).expect("data access should succeed"),
1467            5.0
1468        );
1469        assert_eq!(
1470            result.get(&[1, 1]).expect("data access should succeed"),
1471            4.0
1472        );
1473        assert_eq!(
1474            result.get(&[1, 2]).expect("data access should succeed"),
1475            6.0
1476        );
1477        assert_eq!(
1478            result.get(&[2, 0]).expect("data access should succeed"),
1479            9.0
1480        );
1481        assert_eq!(
1482            result.get(&[2, 1]).expect("data access should succeed"),
1483            8.0
1484        );
1485        assert_eq!(
1486            result.get(&[2, 2]).expect("data access should succeed"),
1487            7.0
1488        );
1489    }
1490
1491    #[test]
1492    fn test_index_select() {
1493        // Create a 3x4 tensor
1494        let tensor = tensor_2d(&[
1495            &[1.0, 2.0, 3.0, 4.0],
1496            &[5.0, 6.0, 7.0, 8.0],
1497            &[9.0, 10.0, 11.0, 12.0],
1498        ])
1499        .expect("tensor creation should succeed");
1500
1501        // Select rows 0 and 2
1502        let row_indices =
1503            crate::creation::tensor_1d(&[0i64, 2]).expect("tensor creation should succeed");
1504        let result = tensor
1505            .index_select(0, &row_indices)
1506            .expect("index_select should succeed");
1507
1508        assert_eq!(result.shape().dims(), &[2, 4]);
1509        assert_eq!(
1510            result.get(&[0, 0]).expect("data access should succeed"),
1511            1.0
1512        );
1513        assert_eq!(
1514            result.get(&[0, 3]).expect("data access should succeed"),
1515            4.0
1516        );
1517        assert_eq!(
1518            result.get(&[1, 0]).expect("data access should succeed"),
1519            9.0
1520        );
1521        assert_eq!(
1522            result.get(&[1, 3]).expect("data access should succeed"),
1523            12.0
1524        );
1525
1526        // Select columns 1 and 3
1527        let col_indices =
1528            crate::creation::tensor_1d(&[1i64, 3]).expect("tensor creation should succeed");
1529        let result = tensor
1530            .index_select(1, &col_indices)
1531            .expect("index_select should succeed");
1532
1533        assert_eq!(result.shape().dims(), &[3, 2]);
1534        assert_eq!(
1535            result.get(&[0, 0]).expect("data access should succeed"),
1536            2.0
1537        );
1538        assert_eq!(
1539            result.get(&[0, 1]).expect("data access should succeed"),
1540            4.0
1541        );
1542        assert_eq!(
1543            result.get(&[2, 0]).expect("data access should succeed"),
1544            10.0
1545        );
1546        assert_eq!(
1547            result.get(&[2, 1]).expect("data access should succeed"),
1548            12.0
1549        );
1550    }
1551
1552    #[test]
1553    fn test_list_indexing() {
1554        // Test fancy indexing with list of indices
1555        let tensor = tensor_2d(&[
1556            &[1.0, 2.0, 3.0, 4.0],
1557            &[5.0, 6.0, 7.0, 8.0],
1558            &[9.0, 10.0, 11.0, 12.0],
1559        ])
1560        .expect("tensor creation should succeed");
1561
1562        // Select rows 0 and 2 using list indexing
1563        let indices = vec![TensorIndex::List(vec![0, 2]), TensorIndex::All];
1564        let result = tensor.index(&indices).expect("indexing should succeed");
1565
1566        assert_eq!(result.shape().dims(), &[2, 4]);
1567        assert_eq!(
1568            result.get(&[0, 0]).expect("data access should succeed"),
1569            1.0
1570        );
1571        assert_eq!(
1572            result.get(&[0, 3]).expect("data access should succeed"),
1573            4.0
1574        );
1575        assert_eq!(
1576            result.get(&[1, 0]).expect("data access should succeed"),
1577            9.0
1578        );
1579        assert_eq!(
1580            result.get(&[1, 3]).expect("data access should succeed"),
1581            12.0
1582        );
1583
1584        // Test index_with_list convenience method
1585        let result2 = tensor
1586            .index_with_list(0, &[0, 2])
1587            .expect("index_with_list should succeed");
1588        assert_eq!(result.shape(), result2.shape());
1589        assert_eq!(
1590            result.get(&[0, 0]).expect("data access should succeed"),
1591            result2.get(&[0, 0]).expect("data access should succeed")
1592        );
1593    }
1594
1595    #[test]
1596    fn test_boolean_mask_indexing() {
1597        use crate::creation::tensor_1d;
1598
1599        // Create test tensor
1600        let tensor =
1601            tensor_1d(&[10.0, 20.0, 30.0, 40.0, 50.0]).expect("tensor creation should succeed");
1602
1603        // Create boolean mask
1604        let mask = Tensor::from_data(
1605            vec![true, false, true, false, true],
1606            vec![5],
1607            crate::DeviceType::Cpu,
1608        )
1609        .expect("tensor creation should succeed");
1610
1611        // Test mask_select (global mask)
1612        let result = tensor
1613            .mask_select(&mask)
1614            .expect("mask_select should succeed");
1615        assert_eq!(result.shape().dims(), &[3]);
1616        assert_eq!(result.get(&[0]).expect("data access should succeed"), 10.0);
1617        assert_eq!(result.get(&[1]).expect("data access should succeed"), 30.0);
1618        assert_eq!(result.get(&[2]).expect("data access should succeed"), 50.0);
1619
1620        // Test dimensional mask indexing
1621        let result2 = tensor
1622            .index_with_mask(0, &mask)
1623            .expect("index_with_mask should succeed");
1624        assert_eq!(result2.shape().dims(), &[3]);
1625        assert_eq!(result2.get(&[0]).expect("data access should succeed"), 10.0);
1626        assert_eq!(result2.get(&[1]).expect("data access should succeed"), 30.0);
1627        assert_eq!(result2.get(&[2]).expect("data access should succeed"), 50.0);
1628    }
1629
1630    #[test]
1631    fn test_where_condition() {
1632        use crate::creation::tensor_1d;
1633
1634        let tensor = tensor_1d(&[1.0, 2.0, 3.0, 4.0, 5.0]).expect("tensor creation should succeed");
1635
1636        // Create mask for values > 3.0
1637        let mask = tensor
1638            .where_condition(|&x| x > 3.0)
1639            .expect("where_condition should succeed");
1640
1641        {
1642            let mask_data = mask.data().expect("data access should succeed");
1643            assert!(!mask_data[0]); // 1.0 <= 3.0
1644            assert!(!mask_data[1]); // 2.0 <= 3.0
1645            assert!(!mask_data[2]); // 3.0 <= 3.0
1646            assert!(mask_data[3]); // 4.0 > 3.0
1647            assert!(mask_data[4]); // 5.0 > 3.0
1648        } // Explicitly drop the lock
1649
1650        // Use the mask to select elements
1651        let selected = tensor
1652            .mask_select(&mask)
1653            .expect("mask_select should succeed");
1654        assert_eq!(selected.shape().dims(), &[2]);
1655        assert_eq!(selected.get(&[0]).expect("data access should succeed"), 4.0);
1656        assert_eq!(selected.get(&[1]).expect("data access should succeed"), 5.0);
1657    }
1658
1659    #[test]
1660    fn test_newaxis_indexing() {
1661        use crate::creation::tensor_1d;
1662
1663        let tensor = tensor_1d(&[1.0, 2.0, 3.0]).expect("tensor creation should succeed");
1664
1665        // Add new axis at beginning
1666        let indices = vec![TensorIndex::NewAxis, TensorIndex::All];
1667        let result = tensor.index(&indices).expect("indexing should succeed");
1668        assert_eq!(result.shape().dims(), &[1, 3]);
1669
1670        // Add new axis at end
1671        let indices = vec![TensorIndex::All, TensorIndex::NewAxis];
1672        let result = tensor.index(&indices).expect("indexing should succeed");
1673        assert_eq!(result.shape().dims(), &[3, 1]);
1674
1675        // Add multiple new axes
1676        let indices = vec![
1677            TensorIndex::NewAxis,
1678            TensorIndex::All,
1679            TensorIndex::NewAxis,
1680            TensorIndex::NewAxis,
1681        ];
1682        let result = tensor.index(&indices).expect("indexing should succeed");
1683        assert_eq!(result.shape().dims(), &[1, 3, 1, 1]);
1684    }
1685
1686    #[test]
1687    fn test_ellipsis_indexing() {
1688        // Create 3D tensor
1689        let tensor =
1690            crate::creation::zeros::<f32>(&[2, 3, 4]).expect("tensor creation should succeed");
1691
1692        // Test ellipsis in middle
1693        let indices = vec![TensorIndex::Index(0), TensorIndex::Ellipsis];
1694        let result = tensor.index(&indices).expect("indexing should succeed");
1695        assert_eq!(result.shape().dims(), &[3, 4]);
1696
1697        // Test ellipsis at end
1698        let indices = vec![TensorIndex::Index(1), TensorIndex::Ellipsis];
1699        let result = tensor.index(&indices).expect("indexing should succeed");
1700        assert_eq!(result.shape().dims(), &[3, 4]);
1701    }
1702
1703    #[test]
1704    fn test_complex_indexing() {
1705        // Test combination of different indexing types
1706        let tensor = tensor_2d(&[
1707            &[1.0, 2.0, 3.0, 4.0],
1708            &[5.0, 6.0, 7.0, 8.0],
1709            &[9.0, 10.0, 11.0, 12.0],
1710            &[13.0, 14.0, 15.0, 16.0],
1711        ])
1712        .expect("operation should succeed");
1713
1714        // Combine list indexing with range indexing
1715        let indices = vec![
1716            TensorIndex::List(vec![0, 2, 3]),
1717            TensorIndex::Range(Some(1), Some(4), None),
1718        ];
1719        let result = tensor.index(&indices).expect("indexing should succeed");
1720
1721        assert_eq!(result.shape().dims(), &[3, 3]);
1722        assert_eq!(
1723            result.get(&[0, 0]).expect("data access should succeed"),
1724            2.0
1725        ); // tensor[0, 1]
1726        assert_eq!(
1727            result.get(&[1, 0]).expect("data access should succeed"),
1728            10.0
1729        ); // tensor[2, 1]
1730        assert_eq!(
1731            result.get(&[2, 2]).expect("data access should succeed"),
1732            16.0
1733        ); // tensor[3, 3]
1734    }
1735
1736    #[test]
1737    fn test_negative_indexing() {
1738        use crate::creation::tensor_1d;
1739
1740        let tensor = tensor_1d(&[1.0, 2.0, 3.0, 4.0, 5.0]).expect("tensor creation should succeed");
1741
1742        // Test negative single index
1743        let indices = vec![TensorIndex::Index(-1)];
1744        let result = tensor.index(&indices).expect("indexing should succeed");
1745        assert_eq!(result.numel(), 1);
1746        assert_eq!(result.item().expect("item extraction should succeed"), 5.0);
1747
1748        // Test negative range
1749        let indices = vec![TensorIndex::Range(Some(-3), Some(-1), None)];
1750        let result = tensor.index(&indices).expect("indexing should succeed");
1751        assert_eq!(result.shape().dims(), &[2]);
1752        assert_eq!(result.get(&[0]).expect("data access should succeed"), 3.0);
1753        assert_eq!(result.get(&[1]).expect("data access should succeed"), 4.0);
1754
1755        // Test negative list indexing
1756        let indices = vec![TensorIndex::List(vec![-1, -2, 0])];
1757        let result = tensor.index(&indices).expect("indexing should succeed");
1758        assert_eq!(result.shape().dims(), &[3]);
1759        assert_eq!(result.get(&[0]).expect("data access should succeed"), 5.0); // -1 -> index 4
1760        assert_eq!(result.get(&[1]).expect("data access should succeed"), 4.0); // -2 -> index 3
1761        assert_eq!(result.get(&[2]).expect("data access should succeed"), 1.0); // 0 -> index 0
1762    }
1763
1764    // -----------------------------------------------------------------------
1765    // ITEM 3 / T3 - routing: a contiguous single-axis slice must record its
1766    // geometry, everything else must keep the element-wise gather.
1767    // -----------------------------------------------------------------------
1768
1769    #[test]
1770    fn narrow_records_a_geometric_view() {
1771        let source = tensor_2d(&[
1772            &[0.0f32, 1.0, 2.0, 3.0],
1773            &[4.0, 5.0, 6.0, 7.0],
1774            &[8.0, 9.0, 10.0, 11.0],
1775        ])
1776        .expect("tensor creation should succeed")
1777        .requires_grad_(true);
1778
1779        let narrowed = source.narrow(1, 1, 2).expect("narrow should succeed");
1780        assert!(matches!(
1781            narrowed.operation,
1782            Operation::View {
1783                kind: ViewKind::Narrow { dim: 1, start: 1 },
1784                ..
1785            }
1786        ));
1787
1788        // Every axis taken whole is still one slab.
1789        let whole = source
1790            .index(&[TensorIndex::All, TensorIndex::All])
1791            .expect("indexing should succeed");
1792        assert!(matches!(
1793            whole.operation,
1794            Operation::View {
1795                kind: ViewKind::Narrow { dim: 0, start: 0 },
1796                ..
1797            }
1798        ));
1799    }
1800
1801    #[test]
1802    fn non_geometric_indexing_keeps_the_gather() {
1803        let source = tensor_2d(&[
1804            &[0.0f32, 1.0, 2.0, 3.0],
1805            &[4.0, 5.0, 6.0, 7.0],
1806            &[8.0, 9.0, 10.0, 11.0],
1807        ])
1808        .expect("tensor creation should succeed")
1809        .requires_grad_(true);
1810
1811        // A stride skips elements, so the backward is not one slab.
1812        let stepped = source
1813            .slice_with_step(1, Some(0), Some(4), Some(2))
1814            .expect("stepped slice should succeed");
1815        assert!(matches!(stepped.operation, Operation::Gather { .. }));
1816
1817        // Two narrowed axes are not a contiguous slab either.
1818        let corner = source
1819            .index(&[
1820                TensorIndex::Range(Some(1), Some(3), None),
1821                TensorIndex::Range(Some(1), Some(3), None),
1822            ])
1823            .expect("indexing should succeed");
1824        assert!(matches!(corner.operation, Operation::Gather { .. }));
1825
1826        // Fancy indexing may read one element several times.
1827        let listed = source
1828            .index(&[TensorIndex::All, TensorIndex::List(vec![0, 0, 3])])
1829            .expect("indexing should succeed");
1830        assert!(matches!(listed.operation, Operation::Gather { .. }));
1831
1832        // A single index drops an axis, so the output rank differs.
1833        let row = source
1834            .index(&[TensorIndex::Index(1), TensorIndex::All])
1835            .expect("indexing should succeed");
1836        assert!(matches!(row.operation, Operation::Gather { .. }));
1837    }
1838
1839    #[test]
1840    fn geometric_slices_of_a_detached_tensor_stay_leaves() {
1841        let source = tensor_2d(&[&[0.0f32, 1.0, 2.0], &[3.0, 4.0, 5.0]])
1842            .expect("tensor creation should succeed");
1843        let narrowed = source.narrow(1, 1, 2).expect("narrow should succeed");
1844        assert!(!narrowed.requires_grad());
1845        assert!(matches!(narrowed.operation, Operation::Leaf));
1846    }
1847}