easy_ml/tensors/views/
indexes.rs

1use crate::tensors::Dimension;
2use crate::tensors::views::{DataLayout, TensorMut, TensorRef};
3use std::marker::PhantomData;
4
5/**
6* A combination of pre provided indexes and a tensor. The provided indexes reduce the
7* dimensionality of the TensorRef exposed to less than the dimensionality of the TensorRef
8* this is created from.
9*
10* ```
11* use easy_ml::tensors::Tensor;
12* use easy_ml::tensors::views::{TensorView, TensorIndex};
13* let vector = Tensor::from([("a", 2)], vec![ 16, 8 ]);
14* let scalar = vector.select([("a", 0)]);
15* let also_scalar = TensorView::from(TensorIndex::from(&vector, [("a", 0)]));
16* assert_eq!(scalar.index_by([]).get([]), also_scalar.index_by([]).get([]));
17* assert_eq!(scalar.index_by([]).get([]), 16);
18* ```
19*
20* Note: due to limitations in Rust's const generics support, TensorIndex only implements TensorRef
21* for D from `1` to `6`.
22
23*/
24#[derive(Clone, Debug)]
25pub struct TensorIndex<T, S, const D: usize, const I: usize> {
26    source: S,
27    provided: [Option<usize>; D],
28    _type: PhantomData<T>,
29}
30
31impl<T, S, const D: usize, const I: usize> TensorIndex<T, S, D, I>
32where
33    S: TensorRef<T, D>,
34{
35    /**
36     * Creates a TensorIndex from a source and a list of provided dimension name/index pairs.
37     *
38     * The corresponding dimensions in the source will be masked to always return the provided
39     * index. Henece, a matrix can be viewed as a vector if you provide one of the row/column
40     * index to use. More generally, the tensor the TensorIndex exposes will have a dimensionality
41     * of D - I, where D is the dimensionality of the source, and I is the dimensionality of the
42     * provided indexes.
43     *
44     * # Panics
45     *
46     * - If any provided index is for a dimension that does not exist in the source's shape.
47     * - If any provided index is not within range for the length of the dimension.
48     * - If multiple indexes are provided for the same dimension.
49     */
50    #[track_caller]
51    pub fn from(source: S, provided_indexes: [(Dimension, usize); I]) -> TensorIndex<T, S, D, I> {
52        let shape = source.view_shape();
53        if I > D {
54            panic!("D - I must be >= 0, D: {:?}, I: {:?}", D, I);
55        }
56        if crate::tensors::dimensions::has_duplicates(&provided_indexes) {
57            panic!(
58                "Multiple indexes cannot be provided for the same dimension name, provided: {:?}",
59                provided_indexes,
60            );
61        }
62        let mut provided = [None; D];
63        for (name, index) in &provided_indexes {
64            // Every provided index must match a dimension name in the source and be a valid
65            // index within the length
66            match shape
67                .iter()
68                .enumerate()
69                .find(|(_i, (n, length))| n == name && index < length)
70            {
71                None => panic!(
72                    "Provided indexes must all correspond to valid indexes into the source shape, source shape: {:?}, provided: {:?}",
73                    shape, provided_indexes,
74                ),
75                // Assign the provided index to the matching position of the source
76                Some((i, (_n, _length))) => provided[i] = Some(*index),
77            }
78        }
79        TensorIndex {
80            source,
81            provided,
82            _type: PhantomData,
83        }
84    }
85
86    /**
87     * Consumes the TensorIndex, yielding the source it was created from.
88     */
89    pub fn source(self) -> S {
90        self.source
91    }
92
93    /**
94     * Gives a reference to the TensorIndex's source (in which the data is not reduced in
95     * dimensionality).
96     */
97    // # Safety
98    //
99    // Giving out a mutable reference to our source could allow it to be changed out from under us
100    // and make our provided indexes invalid. However, since the source implements TensorRef
101    // interior mutability is not allowed, so we can give out shared references without breaking
102    // our own integrity.
103    pub fn source_ref(&self) -> &S {
104        &self.source
105    }
106}
107
108macro_rules! tensor_index_ref_impl {
109    (unsafe impl TensorRef for TensorIndex $d:literal $i:literal $helper_name:ident) => {
110        impl<T, S> TensorIndex<T, S, $d, $i>
111        where
112            S: TensorRef<T, $d>,
113        {
114            fn $helper_name(&self, indexes: [usize; $d - $i]) -> Option<[usize; $d]> {
115                let mut supplied = indexes.iter();
116                // Indexes have to be in the order of our shape, so they must fill in the None
117                // slots of our provided array since we created that in the same order as our
118                // view_shape
119                let mut combined = [0; $d];
120                let mut d = 0;
121                for provided in self.provided.iter() {
122                    combined[d] = match provided {
123                        // This error case should never happen but depending on if we're using
124                        // this method in an unsafe function or not we may want to handle it
125                        // differently
126                        None => *supplied.next()?,
127                        Some(i) => *i,
128                    };
129                    d += 1;
130                }
131                Some(combined)
132            }
133        }
134
135        // # Safety
136        // The source we index into implements TensorRef, and we do not give out any mutable
137        // references to it. Since it may not implement interior mutability due to implementing
138        // TensorRef, we know that it won't change under us. Since we know it won't change under
139        // us, we can rely on the invariants when we created the provided array. The provided
140        // array therefore will be in the same order as the source's view_shape. Hence we can
141        // index correctly by filling in the None slots of provided with the supplied indexes,
142        // which also have to be in order.
143        unsafe impl<T, S> TensorRef<T, { $d - $i }> for TensorIndex<T, S, $d, $i>
144        where
145            S: TensorRef<T, $d>,
146        {
147            fn get_reference(&self, indexes: [usize; $d - $i]) -> Option<&T> {
148                // unwrap because None returns from the helper method are not input error, they
149                // should never happen for any input
150                self.source
151                    .get_reference(self.$helper_name(indexes).unwrap())
152            }
153
154            fn view_shape(&self) -> [(Dimension, usize); $d - $i] {
155                let shape = self.source.view_shape();
156                let mut unprovided = shape
157                    .iter()
158                    .enumerate()
159                    .filter(|(i, _)| self.provided[*i].is_none())
160                    .map(|(_, (name, length))| (*name, *length));
161                std::array::from_fn(|_| unprovided.next().unwrap())
162            }
163
164            unsafe fn get_reference_unchecked(&self, indexes: [usize; $d - $i]) -> &T {
165                unsafe {
166                    // The caller is responsible for providing valid indexes so our
167                    // unwrap should never happen
168                    self.source
169                        .get_reference_unchecked(self.$helper_name(indexes).unwrap_unchecked())
170                }
171            }
172
173            fn data_layout(&self) -> DataLayout<{ $d - $i }> {
174                // Our pre provided index means the view shape no longer matches up to a single
175                // line of data in memory.
176                DataLayout::NonLinear
177            }
178        }
179
180        unsafe impl<T, S> TensorMut<T, { $d - $i }> for TensorIndex<T, S, $d, $i>
181        where
182            S: TensorMut<T, $d>,
183        {
184            fn get_reference_mut(&mut self, indexes: [usize; $d - $i]) -> Option<&mut T> {
185                self.source
186                    .get_reference_mut(self.$helper_name(indexes).unwrap())
187            }
188
189            unsafe fn get_reference_unchecked_mut(&mut self, indexes: [usize; $d - $i]) -> &mut T {
190                unsafe {
191                    // The caller is responsible for providing valid indexes so our
192                    // unwrap should never happen
193                    self.source
194                        .get_reference_unchecked_mut(self.$helper_name(indexes).unwrap_unchecked())
195                }
196            }
197        }
198    };
199}
200
201tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 6 1 compute_select_indexes_6_1);
202tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 6 2 compute_select_indexes_6_2);
203tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 6 3 compute_select_indexes_6_3);
204tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 6 4 compute_select_indexes_6_4);
205tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 6 5 compute_select_indexes_6_5);
206tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 6 6 compute_select_indexes_6_6);
207tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 5 1 compute_select_indexes_5_1);
208tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 5 2 compute_select_indexes_5_2);
209tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 5 3 compute_select_indexes_5_3);
210tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 5 4 compute_select_indexes_5_4);
211tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 5 5 compute_select_indexes_5_5);
212tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 4 1 compute_select_indexes_4_1);
213tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 4 2 compute_select_indexes_4_2);
214tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 4 3 compute_select_indexes_4_3);
215tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 4 4 compute_select_indexes_4_4);
216tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 3 1 compute_select_indexes_3_1);
217tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 3 2 compute_select_indexes_3_2);
218tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 3 3 compute_select_indexes_3_3);
219tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 2 1 compute_select_indexes_2_1);
220tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 2 2 compute_select_indexes_2_2);
221tensor_index_ref_impl!(unsafe impl TensorRef for TensorIndex 1 1 compute_select_indexes_1_1);
222
223#[test]
224fn dimensionality_reduction() {
225    use crate::tensors::Tensor;
226    use crate::tensors::views::TensorView;
227    #[rustfmt::skip]
228    let tensor = Tensor::from([("batch", 2), ("row", 2), ("column", 2)], vec![
229        0, 1,
230        2, 3,
231
232        4, 5,
233        6, 7
234    ]);
235    // selects second 2x2
236    let matrix = TensorView::from(TensorIndex::from(&tensor, [("batch", 1)]));
237    assert_eq!(matrix.shape(), [("row", 2), ("column", 2)]);
238    assert_eq!(
239        matrix,
240        Tensor::from([("row", 2), ("column", 2)], vec![4, 5, 6, 7])
241    );
242    // selects first column
243    let vector = TensorView::from(TensorIndex::from(matrix.source(), [("column", 0)]));
244    assert_eq!(vector.shape(), [("row", 2)]);
245    assert_eq!(vector, Tensor::from([("row", 2)], vec![4, 6]));
246    // equivalent to selecting both together
247    let vector = TensorView::from(TensorIndex::from(&tensor, [("batch", 1), ("column", 0)]));
248    assert_eq!(vector.shape(), [("row", 2)]);
249    assert_eq!(vector, Tensor::from([("row", 2)], vec![4, 6]));
250
251    // selects second row of data
252    let matrix = TensorView::from(TensorIndex::from(&tensor, [("row", 1)]));
253    assert_eq!(matrix.shape(), [("batch", 2), ("column", 2)]);
254    assert_eq!(
255        matrix,
256        Tensor::from([("batch", 2), ("column", 2)], vec![2, 3, 6, 7])
257    );
258
259    // selects second column of data
260    let matrix = TensorView::from(TensorIndex::from(&tensor, [("column", 1)]));
261    assert_eq!(matrix.shape(), [("batch", 2), ("row", 2)]);
262    assert_eq!(
263        matrix,
264        Tensor::from([("batch", 2), ("row", 2)], vec![1, 3, 5, 7])
265    );
266    // selects first batch
267    let vector = TensorView::from(TensorIndex::from(matrix.source(), [("batch", 0)]));
268    assert_eq!(vector.shape(), [("row", 2)]);
269    assert_eq!(vector, Tensor::from([("row", 2)], vec![1, 3,]));
270    // equivalent to selecting both together
271    let vector = TensorView::from(TensorIndex::from(&tensor, [("batch", 0), ("column", 1)]));
272    assert_eq!(vector.shape(), [("row", 2)]);
273    assert_eq!(vector, Tensor::from([("row", 2)], vec![1, 3,]));
274}
275
276/**
277 * A combination of dimension names and a tensor. The provided dimensions increase
278 * the dimensionality of the TensorRef exposed to more than the dimensionality of the TensorRef
279 * this is created from, by adding additional dimensions with a length of one.
280 *
281 * ```
282 * use easy_ml::tensors::Tensor;
283 * use easy_ml::tensors::views::{TensorView, TensorExpansion};
284 * let vector = Tensor::from([("a", 2)], vec![ 16, 8 ]);
285 * let matrix = vector.expand([(1, "b")]);
286 * let also_matrix = TensorView::from(TensorExpansion::from(&vector, [(1, "b")]));
287 * assert_eq!(matrix, also_matrix);
288 * assert_eq!(matrix, Tensor::from([("a", 2), ("b", 1)], vec![ 16, 8 ]));
289 * ```
290 *
291 * Note: due to limitations in Rust's const generics support, TensorExpansion only implements
292 * TensorRef for D from `1` to `6`.
293 */
294#[derive(Clone, Debug)]
295pub struct TensorExpansion<T, S, const D: usize, const I: usize> {
296    source: S,
297    extra: [(usize, Dimension); I],
298    _type: PhantomData<T>,
299}
300
301impl<T, S, const D: usize, const I: usize> TensorExpansion<T, S, D, I>
302where
303    S: TensorRef<T, D>,
304{
305    /**
306     * Creates a TensorExpansion from a source and extra dimension names inserted into the shape
307     * at the provided indexes.
308     *
309     * Each extra dimension name adds a dimension to the tensor with a length of 1 so they
310     * do not change the total number of elements. Hence, a vector can be viewed as a matrix
311     * if you provide an extra row/column dimension. More generally, the tensor the TensorExpansion
312     * exposes will have a dimensionality of D + I, where D is the dimensionality of the source,
313     * and I is the dimensionality of the extra dimensions.
314     *
315     * The extra dimension names can be added before any dimensions in the source's shape, in
316     * the range 0 inclusive to D inclusive. It is possible to add multiple dimension names before
317     * an existing dimension.
318     *
319     * # Panics
320     *
321     * - If any extra dimension name is already in use
322     * - If any dimension number `d` to insert an extra dimension name into is not 0 <= `d` <= D
323     * - If the extra dimension names are not unique
324     */
325    #[track_caller]
326    pub fn from(
327        source: S,
328        extra_dimension_names: [(usize, Dimension); I],
329    ) -> TensorExpansion<T, S, D, I> {
330        let mut dimensions = extra_dimension_names;
331        if crate::tensors::dimensions::has_duplicates_extra_names(&extra_dimension_names) {
332            panic!("All extra dimension names {:?} must be unique", dimensions,);
333        }
334        let shape = source.view_shape();
335        for &(d, name) in &dimensions {
336            if d > D {
337                panic!(
338                    "All extra dimensions {:?} must be inserted in the range 0 <= d <= D of the source shape {:?}",
339                    dimensions, shape
340                );
341            }
342            for &(n, _) in &shape {
343                if name == n {
344                    panic!(
345                        "All extra dimension names {:?} must not be already present in the source shape {:?}",
346                        dimensions, shape
347                    );
348                }
349            }
350        }
351
352        // Sort by ascending insertion positions
353        dimensions.sort_by(|a, b| a.0.cmp(&b.0));
354
355        TensorExpansion {
356            source,
357            extra: dimensions,
358            _type: PhantomData,
359        }
360    }
361
362    /**
363     * Consumes the TensorExpansion, yielding the source it was created from.
364     */
365    pub fn source(self) -> S {
366        self.source
367    }
368
369    /**
370     * Gives a reference to the TensorExpansion's source (in which the data is not increased in
371     * dimensionality).
372     */
373    // # Safety
374    //
375    // Giving out a mutable reference to our source could allow it to be changed out from under us
376    // and make our extra dimensions invalid. However, since the source implements TensorRef
377    // interior mutability is not allowed, so we can give out shared references without breaking
378    // our own integrity.
379    pub fn source_ref(&self) -> &S {
380        &self.source
381    }
382}
383
384macro_rules! tensor_expansion_ref_impl {
385    (unsafe impl TensorRef for TensorExpansion $d:literal $i:literal $helper_name:ident) => {
386        impl<T, S> TensorExpansion<T, S, $d, $i>
387        where
388            S: TensorRef<T, $d>,
389        {
390            fn $helper_name(&self, indexes: [usize; $d + $i]) -> Option<[usize; $d]> {
391                let mut used = [0; $d];
392                let mut i = 0; // index from 0..D
393                let mut extra = 0; // index from 0..I
394                for &index in indexes.iter() {
395                    match self.extra.get(extra) {
396                        // Simple case is when we've already matched against each index for the
397                        // extra dimensions, in which case the rest of the provided indexes will
398                        // be used on the source.
399                        None => {
400                            used[i] = index;
401                            i += 1;
402                        }
403                        Some((j, _name)) => {
404                            if *j == i {
405                                // The i'th actual dimension in our source is preceeded by this
406                                // dimension in our extra dimensions.
407                                if index != 0 {
408                                    // Invalid index
409                                    return None;
410                                }
411                                extra += 1;
412                                // Do not increment i, and don't actually index our source with
413                                // this index value as it indexes against the extra dimension
414                            } else {
415                                used[i] = index;
416                                i += 1;
417                            }
418                        }
419                    }
420                }
421                // Now we've filtered out the indexes that were for extra dimensions from the
422                // input, we can index our source with the real indexes.
423                Some(used)
424            }
425        }
426
427        unsafe impl<T, S> TensorRef<T, { $d + $i }> for TensorExpansion<T, S, $d, $i>
428        where
429            S: TensorRef<T, $d>,
430        {
431            fn get_reference(&self, indexes: [usize; $d + $i]) -> Option<&T> {
432                self.source.get_reference(self.$helper_name(indexes)?)
433            }
434
435            fn view_shape(&self) -> [(Dimension, usize); $d + $i] {
436                let shape = self.source.view_shape();
437                let mut extra_shape = [("", 0); $d + $i];
438                let mut i = 0; // index from 0..D
439                let mut extra = 0; // index from 0..I
440                for dimension in extra_shape.iter_mut() {
441                    match self.extra.get(extra) {
442                        None => {
443                            *dimension = shape[i];
444                            i += 1;
445                        }
446                        Some((j, extra_name)) => {
447                            if *j == i {
448                                *dimension = (extra_name, 1);
449                                extra += 1;
450                                // Do not increment i, this was an extra dimension
451                            } else {
452                                *dimension = shape[i];
453                                i += 1;
454                            }
455                        }
456                    }
457                }
458                extra_shape
459            }
460
461            unsafe fn get_reference_unchecked(&self, indexes: [usize; $d + $i]) -> &T {
462                unsafe {
463                    // The caller is responsible for providing valid indexes so our
464                    // unwrap should never happen
465                    self.source
466                        .get_reference_unchecked(self.$helper_name(indexes).unwrap_unchecked())
467                }
468            }
469
470            fn data_layout(&self) -> DataLayout<{ $d + $i }> {
471                // Our extra dimensions means the view shape no longer matches up to a single
472                // line of data in memory.
473                DataLayout::NonLinear
474            }
475        }
476
477        unsafe impl<T, S> TensorMut<T, { $d + $i }> for TensorExpansion<T, S, $d, $i>
478        where
479            S: TensorMut<T, $d>,
480        {
481            fn get_reference_mut(&mut self, indexes: [usize; $d + $i]) -> Option<&mut T> {
482                self.source.get_reference_mut(self.$helper_name(indexes)?)
483            }
484
485            unsafe fn get_reference_unchecked_mut(&mut self, indexes: [usize; $d + $i]) -> &mut T {
486                unsafe {
487                    // The caller is responsible for providing valid indexes so our
488                    // unwrap should never happen
489                    self.source
490                        .get_reference_unchecked_mut(self.$helper_name(indexes).unwrap_unchecked())
491                }
492            }
493        }
494    };
495}
496
497tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 0 1 compute_expansion_indexes_0_1);
498tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 0 2 compute_expansion_indexes_0_2);
499tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 0 3 compute_expansion_indexes_0_3);
500tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 0 4 compute_expansion_indexes_0_4);
501tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 0 5 compute_expansion_indexes_0_5);
502tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 0 6 compute_expansion_indexes_0_6);
503tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 1 1 compute_expansion_indexes_1_1);
504tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 1 2 compute_expansion_indexes_1_2);
505tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 1 3 compute_expansion_indexes_1_3);
506tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 1 4 compute_expansion_indexes_1_4);
507tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 1 5 compute_expansion_indexes_1_5);
508tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 2 1 compute_expansion_indexes_2_1);
509tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 2 2 compute_expansion_indexes_2_2);
510tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 2 3 compute_expansion_indexes_2_3);
511tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 2 4 compute_expansion_indexes_2_4);
512tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 3 1 compute_expansion_indexes_3_1);
513tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 3 2 compute_expansion_indexes_3_2);
514tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 3 3 compute_expansion_indexes_3_3);
515tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 4 1 compute_expansion_indexes_4_1);
516tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 4 2 compute_expansion_indexes_4_2);
517tensor_expansion_ref_impl!(unsafe impl TensorRef for TensorExpansion 5 1 compute_expansion_indexes_5_1);
518
519#[test]
520fn dimensionality_expansion() {
521    use crate::tensors::Tensor;
522    use crate::tensors::views::TensorView;
523    let tensor = Tensor::from([("row", 2), ("column", 2)], (0..4).collect());
524    let tensor_3 = TensorView::from(TensorExpansion::from(&tensor, [(0, "batch")]));
525    assert_eq!(tensor_3.shape(), [("batch", 1), ("row", 2), ("column", 2)]);
526    assert_eq!(
527        tensor_3,
528        Tensor::from([("batch", 1), ("row", 2), ("column", 2)], vec![0, 1, 2, 3,])
529    );
530    let vector = Tensor::from([("a", 5)], (0..5).collect());
531    let tensor = TensorView::from(TensorExpansion::from(&vector, [(1, "b"), (1, "c")]));
532    assert_eq!(
533        tensor,
534        Tensor::from([("a", 5), ("b", 1), ("c", 1)], (0..5).collect())
535    );
536    let matrix = Tensor::from([("row", 2), ("column", 2)], (0..4).collect());
537    let dataset = TensorView::from(TensorExpansion::from(&matrix, [(2, "color"), (0, "batch")]));
538    assert_eq!(
539        dataset,
540        Tensor::from(
541            [("batch", 1), ("row", 2), ("column", 2), ("color", 1)],
542            (0..4).collect()
543        )
544    );
545}
546
547#[test]
548#[should_panic(
549    expected = "Unable to index with [2, 2, 2, 2], Tensor dimensions are [(\"a\", 2), (\"b\", 2), (\"c\", 1), (\"d\", 2)]."
550)]
551fn dimensionality_reduction_invalid_extra_index() {
552    use crate::tensors::Tensor;
553    use crate::tensors::views::TensorView;
554    let tensor = Tensor::from([("a", 2), ("b", 2), ("d", 2)], (0..8).collect());
555    let tensor = TensorView::from(TensorExpansion::from(&tensor, [(2, "c")]));
556    assert_eq!(tensor.shape(), [("a", 2), ("b", 2), ("c", 1), ("d", 2)]);
557    tensor.index().get_ref([2, 2, 2, 2]);
558}