Skip to main content

tensor4all_core/
col_major_array.rs

1//! N-dimensional column-major array types.
2//!
3//! Column-major layout: the element at multi-index `[i0, i1, i2, ...]` is stored
4//! at flat offset `i0 + shape[0] * (i1 + shape[1] * (i2 + ...))`.
5//!
6//! Three flavors are provided:
7//! - [`ColMajorArrayRef`] — borrowed data and shape (read-only)
8//! - [`ColMajorArrayMut`] — mutably borrowed data, borrowed shape
9//! - [`ColMajorArray`] — fully owned data and shape
10
11/// Errors that can occur when constructing or modifying a column-major array.
12#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
13pub enum ColMajorArrayError {
14    /// The length of the data does not match the product of the shape dimensions.
15    #[error("Shape mismatch: shape {shape:?} requires {expected} elements, but got {actual}")]
16    ShapeMismatch {
17        /// The requested shape.
18        shape: Vec<usize>,
19        /// Number of elements implied by the shape.
20        expected: usize,
21        /// Number of elements actually provided.
22        actual: usize,
23    },
24
25    /// The column length does not match `nrows`.
26    #[error("Column length mismatch: expected {expected} elements, but got {actual}")]
27    ColumnLengthMismatch {
28        /// Expected number of rows.
29        expected: usize,
30        /// Actual number of elements in the column.
31        actual: usize,
32    },
33
34    /// A 2D operation was called on an array that is not 2-dimensional.
35    #[error("Expected a 2D array, but ndim = {ndim}")]
36    Not2D {
37        /// The actual number of dimensions.
38        ndim: usize,
39    },
40
41    /// The product of shape dimensions overflows `usize`.
42    #[error("Shape product overflow: shape {shape:?} overflows usize")]
43    ShapeOverflow {
44        /// The shape that caused the overflow.
45        shape: Vec<usize>,
46    },
47
48    /// Incrementing the column count would overflow `usize`.
49    #[error("Column count overflow")]
50    ColumnCountOverflow,
51}
52
53// ---------------------------------------------------------------------------
54// Helper: compute the total number of elements from a shape
55// ---------------------------------------------------------------------------
56
57fn checked_shape_numel(shape: &[usize]) -> Option<usize> {
58    shape
59        .iter()
60        .copied()
61        .try_fold(1usize, |acc, d| acc.checked_mul(d))
62}
63
64/// Compute the flat offset for a column-major multi-index, using checked
65/// arithmetic. Returns `None` if any index is out of bounds or on overflow.
66fn flat_offset(shape: &[usize], index: &[usize]) -> Option<usize> {
67    if index.len() != shape.len() {
68        return None;
69    }
70    // Traverse from the last axis to the first:
71    //   offset = i_{n-1}
72    //   offset = i_{n-2} + shape[n-2] * offset  -- but we build from the back
73    // Actually, column-major: offset = i0 + s0*(i1 + s1*(i2 + ...))
74    // Evaluate right-to-left (Horner-like):
75    let mut offset: usize = 0;
76    for (idx, dim) in index.iter().zip(shape.iter()).rev() {
77        if *idx >= *dim {
78            return None;
79        }
80        offset = offset.checked_mul(*dim)?.checked_add(*idx)?;
81    }
82    Some(offset)
83}
84
85// ===========================================================================
86// ColMajorArrayRef
87// ===========================================================================
88
89/// A borrowed, read-only view of an N-dimensional column-major array.
90#[derive(Debug, Clone, Copy)]
91pub struct ColMajorArrayRef<'a, T> {
92    data: &'a [T],
93    shape: &'a [usize],
94}
95
96impl<'a, T> ColMajorArrayRef<'a, T> {
97    /// Create a new borrowed array view.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`ColMajorArrayError::ShapeOverflow`] when the product of `shape`
102    /// overflows `usize`, or [`ColMajorArrayError::ShapeMismatch`] when
103    /// `data.len()` does not equal that product.
104    ///
105    pub fn new(data: &'a [T], shape: &'a [usize]) -> Result<Self, ColMajorArrayError> {
106        let expected =
107            checked_shape_numel(shape).ok_or_else(|| ColMajorArrayError::ShapeOverflow {
108                shape: shape.to_vec(),
109            })?;
110        if data.len() != expected {
111            return Err(ColMajorArrayError::ShapeMismatch {
112                shape: shape.to_vec(),
113                expected,
114                actual: data.len(),
115            });
116        }
117        Ok(Self { data, shape })
118    }
119
120    /// Number of dimensions.
121    pub fn ndim(&self) -> usize {
122        self.shape.len()
123    }
124
125    /// Shape of the array.
126    pub fn shape(&self) -> &[usize] {
127        self.shape
128    }
129
130    /// Total number of elements.
131    pub fn len(&self) -> usize {
132        self.data.len()
133    }
134
135    /// Whether the array is empty (zero elements).
136    pub fn is_empty(&self) -> bool {
137        self.data.is_empty()
138    }
139
140    /// Flat (contiguous) data slice.
141    pub fn data(&self) -> &[T] {
142        self.data
143    }
144
145    /// Get a reference to the element at the given multi-index, or `None` if
146    /// out of bounds.
147    pub fn get(&self, index: &[usize]) -> Option<&T> {
148        let off = flat_offset(self.shape, index)?;
149        self.data.get(off)
150    }
151}
152
153// ===========================================================================
154// ColMajorArrayMut
155// ===========================================================================
156
157/// A mutably borrowed view of an N-dimensional column-major array.
158#[derive(Debug)]
159pub struct ColMajorArrayMut<'a, T> {
160    data: &'a mut [T],
161    shape: &'a [usize],
162}
163
164impl<'a, T> ColMajorArrayMut<'a, T> {
165    /// Create a new mutable borrowed array view.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`ColMajorArrayError::ShapeOverflow`] when the product of `shape`
170    /// overflows `usize`, or [`ColMajorArrayError::ShapeMismatch`] when
171    /// `data.len()` does not equal that product.
172    ///
173    pub fn new(data: &'a mut [T], shape: &'a [usize]) -> Result<Self, ColMajorArrayError> {
174        let expected =
175            checked_shape_numel(shape).ok_or_else(|| ColMajorArrayError::ShapeOverflow {
176                shape: shape.to_vec(),
177            })?;
178        if data.len() != expected {
179            return Err(ColMajorArrayError::ShapeMismatch {
180                shape: shape.to_vec(),
181                expected,
182                actual: data.len(),
183            });
184        }
185        Ok(Self { data, shape })
186    }
187
188    /// Number of dimensions.
189    pub fn ndim(&self) -> usize {
190        self.shape.len()
191    }
192
193    /// Shape of the array.
194    pub fn shape(&self) -> &[usize] {
195        self.shape
196    }
197
198    /// Total number of elements.
199    pub fn len(&self) -> usize {
200        self.data.len()
201    }
202
203    /// Whether the array is empty (zero elements).
204    pub fn is_empty(&self) -> bool {
205        self.data.is_empty()
206    }
207
208    /// Flat (contiguous) data slice (read-only).
209    pub fn data(&self) -> &[T] {
210        self.data
211    }
212
213    /// Flat (contiguous) data slice (mutable).
214    pub fn data_mut(&mut self) -> &mut [T] {
215        self.data
216    }
217
218    /// Get a reference to the element at the given multi-index, or `None` if
219    /// out of bounds.
220    pub fn get(&self, index: &[usize]) -> Option<&T> {
221        let off = flat_offset(self.shape, index)?;
222        self.data.get(off)
223    }
224
225    /// Get a mutable reference to the element at the given multi-index, or
226    /// `None` if out of bounds.
227    pub fn get_mut(&mut self, index: &[usize]) -> Option<&mut T> {
228        let off = flat_offset(self.shape, index)?;
229        self.data.get_mut(off)
230    }
231}
232
233// ===========================================================================
234// ColMajorArray (owned)
235// ===========================================================================
236
237/// A fully owned N-dimensional column-major array.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct ColMajorArray<T> {
240    data: Vec<T>,
241    shape: Vec<usize>,
242}
243
244impl<T> ColMajorArray<T> {
245    /// Create a new owned array from data and shape.
246    ///
247    /// Returns an error if `data.len()` does not equal the product of the
248    /// shape dimensions.
249    /// # Errors
250    ///
251    /// Returns [`ColMajorArrayError::ShapeOverflow`] when the product of `shape`
252    /// overflows `usize`, or [`ColMajorArrayError::ShapeMismatch`] when
253    /// `data.len()` does not equal that product.
254    ///
255    pub fn new(data: Vec<T>, shape: Vec<usize>) -> Result<Self, ColMajorArrayError> {
256        let expected =
257            checked_shape_numel(&shape).ok_or_else(|| ColMajorArrayError::ShapeOverflow {
258                shape: shape.clone(),
259            })?;
260        if data.len() != expected {
261            return Err(ColMajorArrayError::ShapeMismatch {
262                shape,
263                expected,
264                actual: data.len(),
265            });
266        }
267        Ok(Self { data, shape })
268    }
269
270    /// Number of dimensions.
271    pub fn ndim(&self) -> usize {
272        self.shape.len()
273    }
274
275    /// Shape of the array.
276    pub fn shape(&self) -> &[usize] {
277        &self.shape
278    }
279
280    /// Total number of elements.
281    pub fn len(&self) -> usize {
282        self.data.len()
283    }
284
285    /// Whether the array is empty (zero elements).
286    pub fn is_empty(&self) -> bool {
287        self.data.is_empty()
288    }
289
290    /// Flat (contiguous) data slice (read-only).
291    pub fn data(&self) -> &[T] {
292        &self.data
293    }
294
295    /// Flat (contiguous) data slice (mutable).
296    pub fn data_mut(&mut self) -> &mut [T] {
297        &mut self.data
298    }
299
300    /// Get a reference to the element at the given multi-index, or `None` if
301    /// out of bounds.
302    pub fn get(&self, index: &[usize]) -> Option<&T> {
303        let off = flat_offset(&self.shape, index)?;
304        self.data.get(off)
305    }
306
307    /// Get a mutable reference to the element at the given multi-index, or
308    /// `None` if out of bounds.
309    pub fn get_mut(&mut self, index: &[usize]) -> Option<&mut T> {
310        let off = flat_offset(&self.shape, index)?;
311        self.data.get_mut(off)
312    }
313
314    /// Consume the array and return the underlying data vector.
315    pub fn into_data(self) -> Vec<T> {
316        self.data
317    }
318
319    /// Borrow as a [`ColMajorArrayRef`].
320    pub fn as_ref(&self) -> ColMajorArrayRef<'_, T> {
321        ColMajorArrayRef {
322            data: &self.data,
323            shape: &self.shape,
324        }
325    }
326
327    /// Borrow as a [`ColMajorArrayMut`].
328    pub fn as_mut(&mut self) -> ColMajorArrayMut<'_, T> {
329        ColMajorArrayMut {
330            data: &mut self.data,
331            shape: &self.shape,
332        }
333    }
334
335    // -- 2D helpers ---------------------------------------------------------
336
337    /// Number of rows, or `None` when the array is not 2D.
338    pub fn nrows(&self) -> Option<usize> {
339        if self.ndim() == 2 {
340            Some(self.shape[0])
341        } else {
342            None
343        }
344    }
345
346    /// Number of columns, or `None` when the array is not 2D.
347    pub fn ncols(&self) -> Option<usize> {
348        if self.ndim() == 2 {
349            Some(self.shape[1])
350        } else {
351            None
352        }
353    }
354
355    /// Return a slice for column `j` of a 2D array, or `None` if `j` is out
356    /// of range or if the array is not 2D.
357    pub fn column(&self, j: usize) -> Option<&[T]> {
358        if self.ndim() != 2 {
359            return None;
360        }
361        let nrows = self.shape[0];
362        if j >= self.shape[1] {
363            return None;
364        }
365        let start = nrows.checked_mul(j)?;
366        let end = start.checked_add(nrows)?;
367        Some(&self.data[start..end])
368    }
369
370    /// Append a column to a 2D array.
371    ///
372    /// The `col` slice must have length equal to `nrows()`. This extends
373    /// the internal data and increments `shape[1]`.
374    ///
375    /// Returns an error if the array is not 2D or if the column length does
376    /// not match `nrows()`.
377    /// # Errors
378    ///
379    /// Returns [`ColMajorArrayError::Not2D`] when the array is not 2D,
380    /// [`ColMajorArrayError::ColumnLengthMismatch`] when `col.len()` differs
381    /// from the row count, and [`ColMajorArrayError::ColumnCountOverflow`] when
382    /// appending the column would overflow the column count.
383    ///
384    pub fn push_column(&mut self, col: &[T]) -> Result<(), ColMajorArrayError>
385    where
386        T: Clone,
387    {
388        if self.ndim() != 2 {
389            return Err(ColMajorArrayError::Not2D { ndim: self.ndim() });
390        }
391        let nrows = self.shape[0];
392        if col.len() != nrows {
393            return Err(ColMajorArrayError::ColumnLengthMismatch {
394                expected: nrows,
395                actual: col.len(),
396            });
397        }
398        self.data.extend_from_slice(col);
399        self.shape[1] = self.shape[1]
400            .checked_add(1)
401            .ok_or(ColMajorArrayError::ColumnCountOverflow)?;
402        Ok(())
403    }
404}
405
406// -- Factories (require trait bounds on T) ----------------------------------
407
408impl<T: Clone> ColMajorArray<T> {
409    /// Create an array filled with a given value.
410    ///
411    /// Returns an error if the product of shape dimensions overflows `usize`.
412    /// # Errors
413    ///
414    /// Returns [`ColMajorArrayError::ShapeOverflow`] when the product of `shape`
415    /// overflows `usize`.
416    ///
417    pub fn filled(shape: Vec<usize>, value: T) -> Result<Self, ColMajorArrayError> {
418        let n = checked_shape_numel(&shape).ok_or_else(|| ColMajorArrayError::ShapeOverflow {
419            shape: shape.clone(),
420        })?;
421        Ok(Self {
422            data: vec![value; n],
423            shape,
424        })
425    }
426}
427
428impl<T: Default + Clone> ColMajorArray<T> {
429    /// Create an array filled with [`Default::default()`] (e.g., zeros for
430    /// numeric types).
431    ///
432    /// Returns an error if the product of shape dimensions overflows `usize`.
433    /// # Errors
434    ///
435    /// Returns [`ColMajorArrayError::ShapeOverflow`] when the product of `shape`
436    /// overflows `usize`.
437    ///
438    pub fn zeros(shape: Vec<usize>) -> Result<Self, ColMajorArrayError> {
439        let n = checked_shape_numel(&shape).ok_or_else(|| ColMajorArrayError::ShapeOverflow {
440            shape: shape.clone(),
441        })?;
442        Ok(Self {
443            data: vec![T::default(); n],
444            shape,
445        })
446    }
447}
448
449// ===========================================================================
450// Tests
451// ===========================================================================
452
453#[cfg(test)]
454mod tests;