Skip to main content

rssn_advanced/tensor/
shape.rs

1//! Shape and stride metadata for multi-dimensional tensors.
2//!
3//! A `Shape` is an ordered list of dimension extents (e.g. `[3, 4]` for a
4//! 3×4 matrix). A `Strides` is the corresponding list of byte-offsets (in
5//! *elements*, not bytes) that map a multi-index to a flat memory offset.
6//!
7//! ## Broadcasting
8//!
9//! Following `NumPy` semantics, a dimension of size 1 can be broadcast against
10//! any larger dimension. The stride for that dimension is set to `0`, so the
11//! same element is reused for every index along that axis without any copying.
12//!
13//! ## Row-major (C-order) layout
14//!
15//! By default `Shape::row_major_strides()` computes C-order strides so that
16//! the last axis is contiguous. Column-major (Fortran/BLAS) strides can be
17//! constructed manually for interop with BLAS calls.
18
19use smallvec::SmallVec;
20
21/// The rank (number of dimensions) limit stored inline before heap-spilling.
22const INLINE_RANK: usize = 8;
23
24/// Dimension extents for a tensor (e.g. `[batch, rows, cols]`).
25///
26/// Stored inline (stack-allocated) for rank ≤ 8, heap-allocated otherwise.
27///
28/// This struct is `Send` and `Sync` because all its components (`usize` and `smallvec::SmallVec`) are `Send` and `Sync`.
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub struct Shape {
31    /// Inline storage for up to `INLINE_RANK` dimensions.
32    dims: smallvec::SmallVec<[usize; INLINE_RANK]>,
33}
34
35impl Shape {
36    /// Constructs a `Shape` from a slice of dimension extents.
37    #[must_use]
38    pub fn from_dims(dims: &[usize]) -> Self {
39        Self {
40            dims: dims.iter().copied().collect(),
41        }
42    }
43
44    /// Returns the number of dimensions (rank).
45    #[must_use]
46    pub fn rank(&self) -> usize {
47        self.dims.len()
48    }
49
50    /// Returns the total number of elements (product of all dimensions).
51    ///
52    /// Returns `1` for a scalar (rank-0) shape.
53    #[must_use]
54    pub fn numel(&self) -> usize {
55        self.dims.iter().product()
56    }
57
58    /// Returns a slice of dimension extents.
59    #[must_use]
60    pub fn dims(&self) -> &[usize] {
61        &self.dims
62    }
63
64    /// Computes row-major (C-order) strides for this shape.
65    ///
66    /// For a shape `[d0, d1, d2, …, dn]` the strides are
67    /// `[d1*d2*…*dn, d2*…*dn, …, dn, 1]`.
68    ///
69    /// Broadcasting dimensions (size == 1) receive stride `0` so that their
70    /// element is reused without copying.
71    #[must_use]
72    #[allow(clippy::unnecessary_cast)]
73    pub fn row_major_strides(&self) -> Strides {
74        let rank = self.dims.len();
75        if rank == 0 {
76            return Strides {
77                strides: smallvec::SmallVec::new(),
78            };
79        }
80
81        // First, compute standard row-major strides without considering broadcasting
82        let mut actual_strides = smallvec::smallvec![0usize; rank];
83        actual_strides[rank - 1] = 1; // Last dimension has stride 1
84        for i in (0..rank - 1).rev() {
85            // Stride for current dimension is (stride of next dimension) * (size of next dimension)
86            let (result, overflow) =
87                (actual_strides[i + 1] as usize).overflowing_mul(self.dims[i + 1]);
88            actual_strides[i] = if overflow { usize::MAX } else { result };
89        }
90
91        // Then, apply broadcast zeroing for dimensions of size 1
92        let mut final_strides = actual_strides;
93        for (i, &d) in self.dims.iter().enumerate() {
94            if d == 1 {
95                final_strides[i] = 0;
96            }
97        }
98        Strides {
99            strides: final_strides,
100        }
101    }
102
103    /// Returns `true` if the two shapes are broadcast-compatible.
104    ///
105    /// Shapes are broadcast-compatible if, after right-aligning them, every
106    /// pair of dimensions is either equal or one of them is 1.
107    #[must_use]
108    pub fn broadcast_compatible(&self, other: &Self) -> bool {
109        let a = self.dims();
110        let b = other.dims();
111        let len = a.len().max(b.len());
112        for i in 0..len {
113            let dim_idx_from_right = len - 1 - i;
114            let da = if dim_idx_from_right < a.len() {
115                a[a.len() - 1 - dim_idx_from_right]
116            } else {
117                1
118            };
119            let db = if dim_idx_from_right < b.len() {
120                b[b.len() - 1 - dim_idx_from_right]
121            } else {
122                1
123            };
124            if da != db && da != 1 && db != 1 {
125                return false;
126            }
127        }
128        true
129    }
130
131    /// Returns the output shape after broadcasting two shapes together.
132    ///
133    /// # Errors
134    /// Returns `None` if the shapes are not broadcast-compatible.
135    #[must_use]
136    pub fn broadcast_output(&self, other: &Self) -> Option<Self> {
137        let a = self.dims();
138        let b = other.dims();
139        let len = a.len().max(b.len());
140        let mut out = smallvec::SmallVec::<[usize; INLINE_RANK]>::with_capacity(len);
141
142        for i in 0..len {
143            // Calculate the corresponding index for a and b by left-padding with 1s
144            let da = if i + a.len() < len {
145                1
146            } else {
147                a[i + a.len() - len]
148            };
149            let db = if i + b.len() < len {
150                1
151            } else {
152                b[i + b.len() - len]
153            };
154
155            if da != db && da != 1 && db != 1 {
156                return None; // Merged compatibility check here
157            }
158            out.push(da.max(db));
159        }
160        Some(Self { dims: out })
161    }
162
163    /// Returns a scalar (rank-0) shape.
164    #[must_use]
165    pub fn scalar() -> Self {
166        Self {
167            dims: smallvec::SmallVec::new(),
168        }
169    }
170
171    /// Returns a 1-D shape with `n` elements.
172    #[must_use]
173    pub fn vector(n: usize) -> Self {
174        Self::from_dims(&[n])
175    }
176
177    /// Returns a 2-D matrix shape.
178    #[must_use]
179    pub fn matrix(rows: usize, cols: usize) -> Self {
180        Self::from_dims(&[rows, cols])
181    }
182}
183
184impl core::fmt::Display for Shape {
185    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
186        write!(f, "[")?;
187        for (i, d) in self.dims.iter().enumerate() {
188            if i > 0 {
189                write!(f, ", ")?;
190            }
191            write!(f, "{d}")?;
192        }
193        write!(f, "]")
194    }
195}
196
197/// Stride values (element-offset per index step) for each dimension.
198///
199/// A stride of `0` means broadcast: the same element is reused across that axis.
200///
201/// This struct is `Send` and `Sync` because all its components (`usize` and `smallvec::SmallVec`) are `Send` and `Sync`.
202#[derive(Debug, Clone, PartialEq, Eq, Hash)]
203pub struct Strides {
204    strides: smallvec::SmallVec<[usize; INLINE_RANK]>,
205}
206
207impl Strides {
208    /// Returns a slice of stride values.
209    #[must_use]
210    pub fn as_slice(&self) -> &[usize] {
211        &self.strides
212    }
213
214    /// Returns the stride for the given dimension index.
215    #[must_use]
216    pub fn get(&self, dim: usize) -> Option<usize> {
217        self.strides.get(dim).copied()
218    }
219
220    /// Computes a flat element index from a multi-dimensional index.
221    /// Returns `Some(flat_index)` if the calculation does not overflow `usize`,
222    /// `None` otherwise.
223    #[must_use]
224    pub fn checked_flat_index(&self, idx: &[usize]) -> Option<usize> {
225        debug_assert_eq!(
226            idx.len(),
227            self.strides.len(),
228            "Rank mismatch for checked_flat_index"
229        );
230        let mut flat = 0usize;
231        for (&i, &s) in idx.iter().zip(self.strides.iter()) {
232            let term = i.checked_mul(s)?; // Check for overflow on multiplication
233            flat = flat.checked_add(term)?; // Check for overflow on summation
234        }
235        Some(flat)
236    }
237
238    /// Computes a flat element index from a multi-dimensional index.
239    ///
240    /// # Panics
241    /// Panics if `idx.len() != strides.len()` (rank mismatch) or if the flat index calculation overflows `usize`.
242    #[must_use]
243    pub fn flat_index(&self, idx: &[usize]) -> usize {
244        assert_eq!(idx.len(), self.strides.len(), "Index rank mismatch");
245        self.checked_flat_index(idx)
246            .expect("Flat index calculation overflowed. This indicates an invalid tensor configuration or an out-of-bounds access that would result in an extremely large flat index.")
247    }
248
249    /// Calculates the maximum possible flat index for these strides given a shape.
250    /// Returns the maximum flat index. Panics if the calculation overflows `usize`.
251    ///
252    /// # Panics
253    /// Panics if the rank of `self` and `shape` do not match.
254    /// Panics if the maximum flat index calculation overflows `usize`.
255    #[must_use]
256    pub fn max_flat_index(&self, shape: &Shape) -> usize {
257        assert_eq!(
258            self.strides.len(),
259            shape.rank(),
260            "Rank mismatch for Strides::max_flat_index"
261        );
262        let max_idx_components: SmallVec<[usize; INLINE_RANK]> =
263            shape.dims().iter().map(|&d| d.saturating_sub(1)).collect();
264        self.checked_flat_index(&max_idx_components)
265            .expect("Maximum flat index calculation overflowed. This indicates an invalid strides/shape configuration that could lead to out-of-bounds access or silent data corruption.")
266    }
267} // Closes impl Strides
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_row_major_strides_2d() {
274        let s = Shape::matrix(3, 4);
275        let strides = s.row_major_strides();
276        assert_eq!(strides.as_slice(), &[4, 1]);
277    }
278
279    #[test]
280    fn test_broadcast_strides() {
281        let s = Shape::from_dims(&[1, 4]);
282        let strides = s.row_major_strides();
283        // Dimension 0 is broadcast (size=1), so stride[0] == 0
284        assert_eq!(strides.as_slice()[0], 0);
285        assert_eq!(strides.as_slice()[1], 1);
286    }
287
288    #[test]
289    fn test_broadcast_output() {
290        let a = Shape::from_dims(&[3, 1]);
291        let b = Shape::from_dims(&[1, 4]);
292        let out = a.broadcast_output(&b).unwrap();
293        assert_eq!(out.dims(), &[3, 4]);
294    }
295
296    #[test]
297    fn test_numel() {
298        let s = Shape::matrix(3, 4);
299        assert_eq!(s.numel(), 12);
300    }
301
302    #[test]
303    fn test_flat_index() {
304        let s = Shape::matrix(3, 4);
305        let strides = s.row_major_strides();
306        // Element [1, 2] in a 3×4 matrix is at flat offset 1*4 + 2*1 = 6
307        assert_eq!(strides.flat_index(&[1, 2]), 6);
308    }
309}