Skip to main content

tenferro_ad/
shape_packing.rs

1use std::ops::Range;
2
3use tenferro_tensor::{GatherConfig, SliceConfig, Tensor, TensorDeviceTransfer, TypedTensor};
4
5use crate::eager::EagerTensor;
6use crate::error::{Error, Result};
7
8fn normalize_existing_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
9    let normalized = if axis >= 0 {
10        axis as usize
11    } else {
12        rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
13            tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), rank)
14        })?
15    };
16    if normalized >= rank {
17        return Err(
18            tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), rank).into(),
19        );
20    }
21    Ok(normalized)
22}
23
24fn normalize_insert_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
25    let insert_rank = rank
26        .checked_add(1)
27        .ok_or_else(|| tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), rank))?;
28    let normalized = if axis >= 0 {
29        axis as usize
30    } else {
31        insert_rank
32            .checked_sub(axis.unsigned_abs())
33            .ok_or_else(|| {
34                tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), insert_rank)
35            })?
36    };
37    if normalized > rank {
38        return Err(tenferro_tensor::Error::axis_out_of_bounds(
39            op,
40            axis.unsigned_abs(),
41            insert_rank,
42        )
43        .into());
44    }
45    Ok(normalized)
46}
47
48fn index_select_config(
49    shape: &[usize],
50    axis: isize,
51    positions: &[usize],
52) -> Result<(Tensor, GatherConfig)> {
53    let axis = normalize_existing_axis("index_select", axis, shape.len())?;
54    let axis_extent = shape[axis];
55    for &position in positions {
56        if position >= axis_extent {
57            return Err(tenferro_tensor::Error::invalid_argument(
58                "index_select",
59                "position",
60                format!(
61                    "position {position} out of bounds for axis {axis} with extent {axis_extent}"
62                ),
63            )
64            .into());
65        }
66    }
67
68    let mut slice_sizes = shape.to_vec();
69    slice_sizes[axis] = 1;
70
71    let offset_dims = (0..shape.len()).filter(|&dim| dim != axis).collect();
72    let index_data = positions
73        .iter()
74        .map(|&position| {
75            i64::try_from(position).map_err(|_| {
76                tenferro_tensor::Error::invalid_argument(
77                    "index_select",
78                    "position",
79                    format!("position {position} cannot be represented as i64"),
80                )
81            })
82        })
83        .collect::<tenferro_tensor::Result<Vec<_>>>()?;
84    let indices = Tensor::I64(TypedTensor::from_vec_col_major(
85        vec![positions.len(), 1],
86        index_data,
87    )?);
88
89    let config = GatherConfig {
90        offset_dims,
91        collapsed_slice_dims: vec![axis],
92        start_index_map: vec![axis],
93        index_vector_dim: 1,
94        slice_sizes,
95    };
96
97    Ok((indices, config))
98}
99
100fn validate_stack_shapes(op: &'static str, shapes: &[&[usize]]) -> Result<()> {
101    let Some(first) = shapes.first() else {
102        return Err(tenferro_tensor::Error::invalid_argument(
103            op,
104            "inputs",
105            "stack requires at least one input",
106        )
107        .into());
108    };
109    for shape in shapes.iter().skip(1) {
110        if *shape != *first {
111            return Err(tenferro_tensor::Error::shape_mismatch(op, *first, *shape).into());
112        }
113    }
114    Ok(())
115}
116
117#[derive(Clone, Debug)]
118enum AxisSelection {
119    Slice {
120        axis: usize,
121        range: Range<usize>,
122        step: usize,
123    },
124    Take {
125        axis: usize,
126        indices: Vec<usize>,
127    },
128}
129
130fn validate_axis_selection(
131    op: &'static str,
132    rank: usize,
133    seen: &mut [bool],
134    axis: usize,
135) -> Result<()> {
136    if axis >= rank {
137        return Err(tenferro_tensor::Error::axis_out_of_bounds(op, axis, rank).into());
138    }
139    if seen[axis] {
140        return Err(tenferro_tensor::Error::duplicate_axis(op, axis, "selection").into());
141    }
142    seen[axis] = true;
143    Ok(())
144}
145
146fn apply_slice_axis_config(
147    op: &'static str,
148    shape: &[usize],
149    selections: &[AxisSelection],
150) -> Result<Option<SliceConfig>> {
151    let mut starts = vec![0; shape.len()];
152    let mut limits = shape.to_vec();
153    let mut strides = vec![1; shape.len()];
154    let mut has_slice = false;
155    for selection in selections {
156        let AxisSelection::Slice { axis, range, step } = selection else {
157            continue;
158        };
159        if *step == 0 {
160            return Err(tenferro_tensor::Error::invalid_argument(
161                op,
162                "step",
163                format!("axis {axis} has zero step"),
164            )
165            .into());
166        }
167        let extent = shape[*axis];
168        if range.start > range.end || range.end > extent {
169            return Err(tenferro_tensor::Error::invalid_argument(
170                op,
171                "range",
172                format!(
173                    "axis {axis} range {}..{} is out of bounds for extent {extent}",
174                    range.start, range.end
175                ),
176            )
177            .into());
178        }
179        starts[*axis] = range.start;
180        limits[*axis] = range.end;
181        strides[*axis] = *step;
182        has_slice = true;
183    }
184    Ok(has_slice.then_some(SliceConfig {
185        starts,
186        limits,
187        strides,
188    }))
189}
190
191/// Rank-preserving eager tensor slicing builder.
192///
193/// Unspecified axes are kept whole. Range selections become one `Slice`
194/// operation; host-known position selections become `Gather`/`index_select`
195/// operations.
196///
197/// # Examples
198///
199/// ```rust
200/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
201///
202/// let ctx = EagerRuntime::new()?;
203/// let x = EagerTensor::from_tensor_in(
204///     Tensor::from_vec_col_major(vec![3, 4], vec![0.0_f64; 12]).unwrap(),
205///     ctx,
206/// ).unwrap();
207/// let y = x.slice_builder().axis(0, 0..2).axis_step(1, 0..4, 2).apply().unwrap();
208/// assert_eq!(y.shape(), &[2, 2]);
209/// # Ok::<(), tenferro_ad::Error>(())
210/// ```
211#[derive(Clone, Debug)]
212pub struct EagerSliceBuilder<'a> {
213    tensor: &'a EagerTensor,
214    selections: Vec<AxisSelection>,
215}
216
217impl<'a> EagerSliceBuilder<'a> {
218    fn new(tensor: &'a EagerTensor) -> Self {
219        Self {
220            tensor,
221            selections: Vec::new(),
222        }
223    }
224
225    /// Add an exclusive-end range selection for one axis.
226    ///
227    /// # Examples
228    ///
229    /// ```rust
230    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
231    ///
232    /// let ctx = EagerRuntime::new()?;
233    /// let x = EagerTensor::from_tensor_in(
234    ///     Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
235    ///     ctx,
236    /// ).unwrap();
237    /// let y = x.slice_builder().axis(0, 1..3).apply().unwrap();
238    /// assert_eq!(y.shape(), &[2]);
239    /// # Ok::<(), tenferro_ad::Error>(())
240    /// ```
241    pub fn axis(mut self, axis: usize, range: Range<usize>) -> Self {
242        self.selections.push(AxisSelection::Slice {
243            axis,
244            range,
245            step: 1,
246        });
247        self
248    }
249
250    /// Add an exclusive-end strided range selection for one axis.
251    ///
252    /// # Examples
253    ///
254    /// ```rust
255    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
256    ///
257    /// let ctx = EagerRuntime::new()?;
258    /// let x = EagerTensor::from_tensor_in(
259    ///     Tensor::from_vec_col_major(vec![5], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]).unwrap(),
260    ///     ctx,
261    /// ).unwrap();
262    /// let y = x.slice_builder().axis_step(0, 0..5, 2).apply().unwrap();
263    /// assert_eq!(y.shape(), &[3]);
264    /// # Ok::<(), tenferro_ad::Error>(())
265    /// ```
266    pub fn axis_step(mut self, axis: usize, range: Range<usize>, step: usize) -> Self {
267        self.selections
268            .push(AxisSelection::Slice { axis, range, step });
269        self
270    }
271
272    /// Add a host-known position selection for one axis.
273    ///
274    /// # Examples
275    ///
276    /// ```rust
277    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
278    ///
279    /// let ctx = EagerRuntime::new()?;
280    /// let x = EagerTensor::from_tensor_in(
281    ///     Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(),
282    ///     ctx,
283    /// ).unwrap();
284    /// let y = x.slice_builder().take_axis(0, &[2, 0]).apply().unwrap();
285    /// assert_eq!(y.shape(), &[2]);
286    /// # Ok::<(), tenferro_ad::Error>(())
287    /// ```
288    pub fn take_axis(mut self, axis: usize, indices: &[usize]) -> Self {
289        self.selections.push(AxisSelection::Take {
290            axis,
291            indices: indices.to_vec(),
292        });
293        self
294    }
295
296    /// Build and apply the requested slice/take operations.
297    ///
298    /// # Examples
299    ///
300    /// ```rust
301    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
302    ///
303    /// let ctx = EagerRuntime::new()?;
304    /// let x = EagerTensor::from_tensor_in(
305    ///     Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
306    ///     ctx,
307    /// ).unwrap();
308    /// let y = x.slice_builder().axis(0, 1..4).apply().unwrap();
309    /// assert_eq!(y.shape(), &[3]);
310    /// # Ok::<(), tenferro_ad::Error>(())
311    /// ```
312    /// # Errors
313    ///
314    /// Returns [`tenferro_tensor::ValidationError::AxisOutOfBounds`] or
315    /// `DuplicateAxis` when selections address an invalid/repeated axis,
316    /// `InvalidArgument` for zero steps or out-of-bounds ranges, or a typed
317    /// backend/runtime-state error while applying the selections.
318    pub fn apply(self) -> Result<EagerTensor> {
319        let shape = self.tensor.shape().to_vec();
320        let mut seen = vec![false; shape.len()];
321        for selection in &self.selections {
322            let axis = match selection {
323                AxisSelection::Slice { axis, .. } | AxisSelection::Take { axis, .. } => *axis,
324            };
325            validate_axis_selection("slice_builder", shape.len(), &mut seen, axis)?;
326        }
327
328        let mut output = self.tensor.clone();
329        if let Some(config) = apply_slice_axis_config("slice_builder", &shape, &self.selections)? {
330            output = output.slice(config)?;
331        }
332        for selection in self.selections {
333            if let AxisSelection::Take { axis, indices } = selection {
334                output = output.take_axis(axis, &indices)?;
335            }
336        }
337        Ok(output)
338    }
339}
340
341impl EagerTensor {
342    /// Slice one axis with an exclusive-end range, keeping all other axes.
343    ///
344    /// # Examples
345    ///
346    /// ```rust
347    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
348    ///
349    /// let ctx = EagerRuntime::new()?;
350    /// let x = EagerTensor::from_tensor_in(
351    ///     Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
352    ///     ctx,
353    /// ).unwrap();
354    /// let y = x.slice_axis(0, 1..3).unwrap();
355    /// assert_eq!(y.shape(), &[2]);
356    /// # Ok::<(), tenferro_ad::Error>(())
357    /// ```
358    /// # Errors
359    ///
360    /// Returns [`tenferro_tensor::ValidationError::AxisOutOfBounds`] when `axis` is not
361    /// present, `InvalidArgument` when `range` exceeds the axis extent, or a
362    /// typed backend/runtime-state error.
363    pub fn slice_axis(&self, axis: usize, range: Range<usize>) -> Result<Self> {
364        self.slice_builder().axis(axis, range).apply()
365    }
366
367    /// Start a rank-preserving slicing builder for this tensor.
368    ///
369    /// # Examples
370    ///
371    /// ```rust
372    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
373    ///
374    /// let ctx = EagerRuntime::new()?;
375    /// let x = EagerTensor::from_tensor_in(
376    ///     Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(),
377    ///     ctx,
378    /// ).unwrap();
379    /// let y = x.slice_builder().axis(0, 0..2).apply().unwrap();
380    /// assert_eq!(y.shape(), &[2]);
381    /// # Ok::<(), tenferro_ad::Error>(())
382    /// ```
383    pub fn slice_builder(&self) -> EagerSliceBuilder<'_> {
384        EagerSliceBuilder::new(self)
385    }
386
387    /// Select entries from one axis using host-known indices.
388    ///
389    /// The index list is primal metadata: gradients flow to `self`, including
390    /// accumulation for repeated indices, but not to the selected positions.
391    ///
392    /// # Examples
393    ///
394    /// ```
395    /// use tenferro_cpu::CpuBackend;
396    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
397    ///
398    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
399    /// let x = EagerTensor::from_tensor_in(
400    ///     Tensor::from_vec_col_major(vec![3], vec![10.0_f64, 20.0, 30.0]).unwrap(),
401    ///     ctx,
402    /// ).unwrap();
403    /// let y = x.take_axis(0, &[2, 0]).unwrap();
404    ///
405    /// assert_eq!(y.materialized().unwrap().as_slice::<f64>().unwrap(), &[30.0, 10.0]);
406    /// # Ok::<(), tenferro_ad::Error>(())
407    /// ```
408    /// # Errors
409    ///
410    /// Returns [`tenferro_tensor::ValidationError::AxisOutOfBounds`] for an invalid axis,
411    /// `InvalidArgument` when an index is outside the axis extent or cannot fit
412    /// in the backend index dtype, or a typed backend/runtime-state error.
413    pub fn take_axis(&self, axis: usize, indices: &[usize]) -> Result<Self> {
414        let axis = isize::try_from(axis).map_err(|_| {
415            Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
416                "take_axis",
417                "axis",
418                format!("{axis} cannot be represented as isize"),
419            ))
420        })?;
421        self.index_select(axis, indices)
422    }
423
424    /// Select matrix rows using host-known row indices.
425    ///
426    /// # Examples
427    ///
428    /// ```
429    /// use tenferro_cpu::CpuBackend;
430    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
431    ///
432    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
433    /// let x = EagerTensor::from_tensor_in(
434    ///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
435    ///     ctx,
436    /// ).unwrap();
437    /// let y = x.take_rows(&[1]).unwrap();
438    ///
439    /// assert_eq!(y.shape(), &[1, 2]);
440    /// assert_eq!(y.materialized().unwrap().as_slice::<f64>().unwrap(), &[2.0, 4.0]);
441    /// # Ok::<(), tenferro_ad::Error>(())
442    /// ```
443    /// # Errors
444    ///
445    /// Returns [`tenferro_tensor::ValidationError::InvalidArgument`] for a row index
446    /// outside the matrix, or [`Error::Validation`] for a non-matrix input;
447    /// backend/runtime-state failures retain their typed source.
448    pub fn take_rows(&self, rows: &[usize]) -> Result<Self> {
449        self.take_axis(0, rows)
450    }
451
452    /// Select matrix columns using host-known column indices.
453    ///
454    /// # Examples
455    ///
456    /// ```
457    /// use tenferro_cpu::CpuBackend;
458    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
459    ///
460    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
461    /// let x = EagerTensor::from_tensor_in(
462    ///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
463    ///     ctx,
464    /// ).unwrap();
465    /// let y = x.take_cols(&[1]).unwrap();
466    ///
467    /// assert_eq!(y.shape(), &[2, 1]);
468    /// assert_eq!(y.materialized().unwrap().as_slice::<f64>().unwrap(), &[3.0, 4.0]);
469    /// # Ok::<(), tenferro_ad::Error>(())
470    /// ```
471    /// # Errors
472    ///
473    /// Returns [`tenferro_tensor::ValidationError::InvalidArgument`] for a column index
474    /// outside the matrix, or [`Error::Validation`] for a non-matrix input;
475    /// backend/runtime-state failures retain their typed source.
476    pub fn take_cols(&self, cols: &[usize]) -> Result<Self> {
477        self.take_axis(1, cols)
478    }
479
480    /// Select a matrix block using host-known row and column indices.
481    ///
482    /// This is a convenience wrapper over row selection followed by column
483    /// selection. The row and column lists, plus the approximation rank implied
484    /// by their lengths, are fixed primal metadata.
485    ///
486    /// # Examples
487    ///
488    /// ```
489    /// use tenferro_cpu::CpuBackend;
490    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
491    ///
492    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
493    /// let x = EagerTensor::from_tensor_in(
494    ///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
495    ///     ctx,
496    /// ).unwrap();
497    /// let y = x.take_block(&[1], &[0]).unwrap();
498    ///
499    /// assert_eq!(y.shape(), &[1, 1]);
500    /// assert_eq!(y.materialized().unwrap().as_slice::<f64>().unwrap(), &[2.0]);
501    /// # Ok::<(), tenferro_ad::Error>(())
502    /// ```
503    /// # Errors
504    ///
505    /// Propagates [`tenferro_tensor::ValidationError::InvalidArgument`] for an out of
506    /// bounds row or column and [`Error::Validation`] for a non-matrix input;
507    /// backend/runtime-state failures retain their typed source.
508    pub fn take_block(&self, rows: &[usize], cols: &[usize]) -> Result<Self> {
509        self.take_rows(rows)?.take_cols(cols)
510    }
511
512    /// Select entries from one axis using host-known positions.
513    ///
514    /// # Examples
515    ///
516    /// ```
517    /// use tenferro_cpu::CpuBackend;
518    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
519    ///
520    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
521    /// let x = EagerTensor::from_tensor_in(
522    ///     Tensor::from_vec_col_major(vec![3], vec![10.0_f64, 20.0, 30.0]).unwrap(),
523    ///     ctx,
524    /// ).unwrap();
525    /// let y = x.index_select(-1, &[2, 0]).unwrap();
526    ///
527    /// assert_eq!(y.materialized().unwrap().as_slice::<f64>().unwrap(), &[30.0, 10.0]);
528    /// # Ok::<(), tenferro_ad::Error>(())
529    /// ```
530    /// # Errors
531    ///
532    /// Returns [`tenferro_tensor::ValidationError::AxisOutOfBounds`] for an invalid
533    /// signed axis, `InvalidArgument` for an out-of-range position or integer
534    /// conversion overflow, or a typed backend/runtime-state error.
535    pub fn index_select(&self, axis: isize, positions: &[usize]) -> Result<Self> {
536        let (indices, config) = index_select_config(self.shape(), axis, positions)?;
537        let indices = {
538            let mut backend = self.ctx.lock_backend()?;
539            backend.upload_host_tensor(&indices)?
540        };
541        let indices = self.ctx.constant_from(indices)?;
542        self.gather(&indices, config)
543    }
544
545    /// Stack tensors along a newly inserted axis.
546    ///
547    /// The returned tensor uses the context of the first input, matching
548    /// [`Self::concatenate`]. All inputs must belong to that same context.
549    ///
550    /// # Examples
551    ///
552    /// ```
553    /// use tenferro_cpu::CpuBackend;
554    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
555    ///
556    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
557    /// let a = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![], vec![1.0_f64]).unwrap(), ctx.clone()).unwrap();
558    /// let b = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![], vec![2.0_f64]).unwrap(), ctx).unwrap();
559    /// let out = EagerTensor::stack(&[&a, &b], -1).unwrap();
560    ///
561    /// assert_eq!(out.shape(), &[2]);
562    /// assert_eq!(out.materialized().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0]);
563    /// # Ok::<(), tenferro_ad::Error>(())
564    /// ```
565    /// # Errors
566    ///
567    /// Returns [`tenferro_tensor::ValidationError::InvalidArgument`] when `tensors` is
568    /// empty or `dim` is outside the insertion rank, `ShapeMismatch` when
569    /// inputs differ in shape, or a typed context/backend/runtime-state error.
570    pub fn stack(tensors: &[&Self], dim: isize) -> Result<Self> {
571        let first = tensors.first().copied().ok_or_else(|| {
572            Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
573                "stack",
574                "inputs",
575                "stack requires at least one input",
576            ))
577        })?;
578        let shapes = tensors
579            .iter()
580            .map(|tensor| tensor.shape())
581            .collect::<Vec<_>>();
582        validate_stack_shapes("stack", &shapes)?;
583
584        let axis = normalize_insert_axis("stack", dim, first.shape().len())?;
585        let mut expanded_shape = first.shape().to_vec();
586        expanded_shape.insert(axis, 1);
587
588        let expanded = tensors
589            .iter()
590            .map(|tensor| tensor.reshape(&expanded_shape))
591            .collect::<Result<Vec<_>>>()?;
592        let refs = expanded.iter().collect::<Vec<_>>();
593        Self::concatenate(&refs, axis)
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::{normalize_existing_axis, normalize_insert_axis};
600
601    #[test]
602    fn axis_normalization_handles_ranks_larger_than_isize_max() {
603        assert_eq!(normalize_existing_axis("test", 0, usize::MAX).unwrap(), 0);
604        assert_eq!(
605            normalize_existing_axis("test", -1, usize::MAX).unwrap(),
606            usize::MAX - 1
607        );
608        assert_eq!(
609            normalize_insert_axis("test", -1, usize::MAX - 1).unwrap(),
610            usize::MAX - 1
611        );
612        assert!(normalize_insert_axis("test", -1, usize::MAX).is_err());
613    }
614}