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;