From 5df7b34a068381892dfd4f81989c6da2d68feb4b Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Fri, 14 Jun 2024 22:29:43 -0400 Subject: [PATCH 01/18] chore: format --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 723643e..ab7d346 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! The Dimensionals library provides a multidimensional array implementation +//! The Dimensionals library provides a multidimensional array implementation //! with a generic storage backend over a generic number type. //! //! The main types are: @@ -28,7 +28,7 @@ //! //! # Performance //! -//! The `LinearArrayStorage` backend stores elements in a contiguous `Vec` +//! The `LinearArrayStorage` backend stores elements in a contiguous `Vec` //! and computes element indices on the fly. This provides good cache locality //! for traversals, but may not be optimal for sparse or very high dimensional arrays. //! @@ -49,7 +49,7 @@ use std::ops::{Add, Index, IndexMut}; /// * `T`: The element type of the array. Must implement `Num` and `Copy`. /// * `N`: The number of dimensions of the array. pub trait DimensionalStorage: -Index<[usize; N], Output = T> + IndexMut<[usize; N], Output = T> + Index<[usize; N], Output = T> + IndexMut<[usize; N], Output = T> { /// Creates an array filled with zeros. /// @@ -382,7 +382,7 @@ impl> Add for Dimensional { impl> Add for Dimensional { type Output = Dimensional; - // Vector addition + // Vector addition fn add(self, rhs: Self) -> Self::Output { let shape = self.shape; let mut result = Dimensional::zeros(shape); @@ -432,7 +432,7 @@ macro_rules! matrix { #[cfg(test)] mod tests { - + use super::*; use crate::{matrix, scalar, vector}; From 429ff994b12e4f2d3d23b7a2a3d5d1073f677769 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Sat, 15 Jun 2024 03:14:44 -0400 Subject: [PATCH 02/18] feat: add basic scalar and vector operations --- src/lib.rs | 151 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ab7d346..2d0d1f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ use num::Num; use std::marker::PhantomData; -use std::ops::{Add, Index, IndexMut}; +use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; /// A trait for storage backends for multidimensional arrays. /// @@ -75,6 +75,7 @@ pub trait DimensionalStorage: } /// An enum representing the memory layout of a linear array. +#[derive(Debug, Copy, Clone, PartialEq)] enum LinearArrayLayout { /// Row-major layout (default). RowMajor, @@ -92,6 +93,7 @@ enum LinearArrayLayout { /// /// * `T`: The element type of the array. Must implement `Num` and `Copy`. /// * `N`: The number of dimensions of the array. +#[derive(Debug, Clone, PartialEq)] pub struct LinearArrayStorage { data: Vec, layout: LinearArrayLayout, @@ -209,6 +211,7 @@ impl LinearArrayStorage { /// * `T`: The element type of the array. Must implement `Num` and `Copy`. /// * `S`: The storage backend for the array. Must implement `DimensionalStorage`. /// * `N`: The number of dimensions of the array. +#[derive(Debug, Copy, Clone)] pub struct Dimensional where S: DimensionalStorage, @@ -364,26 +367,111 @@ where } } -impl> Add for Dimensional { +// Generalized operations + +impl PartialEq for Dimensional +where + S: DimensionalStorage, +{ + fn eq(&self, other: &Self) -> bool { + if self.shape != other.shape { + return false; + } + + for i in 0..self.shape.iter().product::() { + let index = Self::unravel_index(i, &self.shape); + if self[index] != other[index] { + return false; + } + } + + true + } +} + +// Scalar-vector operations +impl> Add for &Dimensional { type Output = Dimensional; - fn add(self, rhs: Self) -> Self::Output { - let shape = self.shape; - let mut result = Dimensional::zeros(shape); + fn add(self, rhs: T) -> Self::Output { + let mut result = Dimensional::zeros(self.shape); + for i in 0..self.shape[0] { + result[[i]] = self[[i]] + rhs; + } + result + } +} - for i in 0..shape[0] { +impl> Sub for &Dimensional { + type Output = Dimensional; + + fn sub(self, rhs: T) -> Self::Output { + let mut result = Dimensional::zeros(self.shape); + for i in 0..self.shape[0] { + result[[i]] = self[[i]] - rhs; + } + result + } +} + +impl> Mul for &Dimensional { + type Output = Dimensional; + + fn mul(self, rhs: T) -> Self::Output { + let mut result = Dimensional::zeros(self.shape); + for i in 0..self.shape[0] { + result[[i]] = self[[i]] * rhs; + } + result + } +} + +impl> Div for &Dimensional { + type Output = Dimensional; + + fn div(self, rhs: T) -> Self::Output { + let mut result = Dimensional::zeros(self.shape); + for i in 0..self.shape[0] { + result[[i]] = self[[i]] / rhs; + } + result + } +} + +// Vector-vector operations +impl> Add<&Dimensional> for &Dimensional { + type Output = Dimensional; + + fn add(self, rhs: &Dimensional) -> Self::Output { + assert_eq!(self.shape, rhs.shape, "Cannot add vectors of different lengths"); + let mut result = Dimensional::zeros(self.shape); + for i in 0..self.shape[0] { result[[i]] = self[[i]] + rhs[[i]]; } + result + } +} + +impl> Sub<&Dimensional> for &Dimensional { + type Output = Dimensional; + fn sub(self, rhs: &Dimensional) -> Self::Output { + assert_eq!(self.shape, rhs.shape, "Cannot subtract vectors of different lengths"); + let mut result = Dimensional::zeros(self.shape); + for i in 0..self.shape[0] { + result[[i]] = self[[i]] - rhs[[i]]; + } result } } -impl> Add for Dimensional { +// Matrix Math + +impl> Add<&Dimensional> for &Dimensional { type Output = Dimensional; // Vector addition - fn add(self, rhs: Self) -> Self::Output { + fn add(self, rhs: &Dimensional) -> Self::Output { let shape = self.shape; let mut result = Dimensional::zeros(shape); @@ -397,6 +485,8 @@ impl> Add for Dimensional { } } +// Macros + #[macro_export] macro_rules! scalar { ($value:expr) => {{ @@ -577,7 +667,7 @@ mod tests { fn test_addition() { let v1 = vector![1, 2, 3, 4, 5]; let v2 = vector![6, 7, 8, 9, 10]; - let v3 = v1 + v2; + let v3 = &v1 + &v2; assert_eq!(v3[[0]], 7); assert_eq!(v3[[2]], 11); assert_eq!(v3[[4]], 15); @@ -596,7 +686,7 @@ mod tests { shape, LinearArrayStorage::new(shape, data2, LinearArrayLayout::ColumnMajor, 1), ); - let array3 = array1 + array2; + let array3 = &array1 + &array2; assert_eq!(array3[[0, 0]], 8.0); assert_eq!(array3[[1, 0]], 14.0); assert_eq!(array3[[0, 1]], 10.0); @@ -604,4 +694,45 @@ mod tests { assert_eq!(array3[[0, 2]], 12.0); assert_eq!(array3[[1, 2]], 18.0); } + + #[test] + fn test_scalar_vector_ops() { + let a = vector![1, 2, 3]; + let b = 2; + + let c = &a + b; + assert_eq!(c, vector![3, 4, 5]); + let c = &a - b; + assert_eq!(c, vector![-1, 0, 1]); + let c = &a * b; + assert_eq!(c, vector![2, 4, 6]); + let c = &a / b; + assert_eq!(c, vector![0, 1, 1]); + } + + #[test] + fn test_vector_vector_ops() { + let a = vector![1, 2, 3]; + let b = vector![4, 5, 6]; + + assert_eq!(&a + &b, vector![5, 7, 9]); + let c = &a - &b; + assert_eq!(c, vector![-3, -3, -3]); + } + + #[test] + #[should_panic] + fn test_vector_vector_ops_different_lengths_add() { + let a = vector![1, 2, 3]; + let b = vector![4, 5]; + let _ = &a + &b; + } + + #[test] + #[should_panic] + fn test_vector_vector_ops_different_lengths_sub() { + let a = vector![1, 2, 3]; + let b = vector![4, 5]; + let _ = &a - &b; + } } From 69ec677439f2650c26cd58811a827ab7ce292b49 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Sat, 15 Jun 2024 03:16:01 -0400 Subject: [PATCH 03/18] chore: format --- src/lib.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2d0d1f2..5a12d11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -439,11 +439,16 @@ impl> Div for &Dimensional> Add<&Dimensional> for &Dimensional { +impl> Add<&Dimensional> + for &Dimensional +{ type Output = Dimensional; fn add(self, rhs: &Dimensional) -> Self::Output { - assert_eq!(self.shape, rhs.shape, "Cannot add vectors of different lengths"); + assert_eq!( + self.shape, rhs.shape, + "Cannot add vectors of different lengths" + ); let mut result = Dimensional::zeros(self.shape); for i in 0..self.shape[0] { result[[i]] = self[[i]] + rhs[[i]]; @@ -452,11 +457,16 @@ impl> Add<&Dimensional> for } } -impl> Sub<&Dimensional> for &Dimensional { +impl> Sub<&Dimensional> + for &Dimensional +{ type Output = Dimensional; fn sub(self, rhs: &Dimensional) -> Self::Output { - assert_eq!(self.shape, rhs.shape, "Cannot subtract vectors of different lengths"); + assert_eq!( + self.shape, rhs.shape, + "Cannot subtract vectors of different lengths" + ); let mut result = Dimensional::zeros(self.shape); for i in 0..self.shape[0] { result[[i]] = self[[i]] - rhs[[i]]; @@ -467,7 +477,9 @@ impl> Sub<&Dimensional> for // Matrix Math -impl> Add<&Dimensional> for &Dimensional { +impl> Add<&Dimensional> + for &Dimensional +{ type Output = Dimensional; // Vector addition @@ -700,7 +712,7 @@ mod tests { let a = vector![1, 2, 3]; let b = 2; - let c = &a + b; + let c = &a + b; assert_eq!(c, vector![3, 4, 5]); let c = &a - b; assert_eq!(c, vector![-1, 0, 1]); From 52f56c261da537d28e558e962c2d6aafcc0626ea Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Tue, 18 Jun 2024 01:20:49 -0400 Subject: [PATCH 04/18] fix: reorganize code --- src/lib.rs | 432 +----------------------------------------- src/linear_storage.rs | 131 +++++++++++++ src/operators.rs | 122 ++++++++++++ 3 files changed, 261 insertions(+), 424 deletions(-) create mode 100644 src/linear_storage.rs create mode 100644 src/operators.rs diff --git a/src/lib.rs b/src/lib.rs index 5a12d11..30772a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,9 +35,14 @@ //! Alternative storage backends can be implemented by defining a type that //! implements the `DimensionalStorage` trait. +mod linear_storage; +mod operators; + +pub use linear_storage::{LinearArrayLayout, LinearArrayStorage}; + use num::Num; use std::marker::PhantomData; -use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; +use std::ops::{Index, IndexMut}; /// A trait for storage backends for multidimensional arrays. /// @@ -74,134 +79,6 @@ pub trait DimensionalStorage: fn from_vec(shape: [usize; N], data: Vec) -> Self; } -/// An enum representing the memory layout of a linear array. -#[derive(Debug, Copy, Clone, PartialEq)] -enum LinearArrayLayout { - /// Row-major layout (default). - RowMajor, - // TODO: figure out if we want to support column-major layout - #[allow(dead_code)] - ColumnMajor, -} - -/// A linear array storage backend for multidimensional arrays. -/// -/// This struct stores the array data in a contiguous block of memory, -/// using either row-major or column-major layout. -/// -/// # Type Parameters -/// -/// * `T`: The element type of the array. Must implement `Num` and `Copy`. -/// * `N`: The number of dimensions of the array. -#[derive(Debug, Clone, PartialEq)] -pub struct LinearArrayStorage { - data: Vec, - layout: LinearArrayLayout, - strides: [usize; N], -} - -impl Index<[usize; N]> for LinearArrayStorage { - type Output = T; - - fn index(&self, index: [usize; N]) -> &Self::Output { - let linear_index = self.layout_index(index); - &self.data[linear_index] - } -} - -impl IndexMut<[usize; N]> for LinearArrayStorage { - fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { - let linear_index = self.layout_index(index); - &mut self.data[linear_index] - } -} - -impl DimensionalStorage for LinearArrayStorage { - fn zeros(shape: [usize; N]) -> Self { - let data = vec![T::zero(); shape.iter().product::()]; - LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) - } - - fn ones(shape: [usize; N]) -> Self { - let data = vec![T::one(); shape.iter().product::()]; - LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) - } - - fn from_vec(shape: [usize; N], data: Vec) -> Self { - LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) - } -} - -impl LinearArrayStorage { - /// Computes the strides for a given shape and layout. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - /// * `stride`: The base stride (usually 1). - /// * `layout`: The memory layout of the array. - fn compute_strides( - shape: &[usize; N], - stride: &usize, - layout: &LinearArrayLayout, - ) -> [usize; N] { - let mut strides = [0; N]; - match layout { - LinearArrayLayout::RowMajor => { - strides[N - 1] = *stride; - for i in (0..N - 1).rev() { - strides[i] = strides[i + 1] * shape[i + 1]; - } - } - LinearArrayLayout::ColumnMajor => { - strides[0] = *stride; - for i in 1..N { - strides[i] = strides[i - 1] * shape[i - 1]; - } - } - } - strides - } - - /// Computes the linear index for a given multidimensional index. - /// - /// # Arguments - /// - /// * `index`: The multidimensional index. - fn layout_index(&self, index: [usize; N]) -> usize { - match self.layout { - LinearArrayLayout::RowMajor => index - .iter() - .zip(self.strides.iter()) - .map(|(i, &stride)| i * stride) - .sum(), - LinearArrayLayout::ColumnMajor => index - .iter() - .rev() - .zip(self.strides.iter().rev()) - .map(|(i, &stride)| i * stride) - .sum(), - } - } - - /// Creates a new `LinearArrayStorage` with the given parameters. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - /// * `data`: The data to initialize the array with. - /// * `layout`: The memory layout of the array. - /// * `stride`: The base stride (usually 1). - fn new(shape: [usize; N], data: Vec, layout: LinearArrayLayout, stride: usize) -> Self { - let strides = Self::compute_strides(&shape, &stride, &layout); - Self { - data, - layout, - strides, - } - } -} - /// A multidimensional array type. /// /// This struct represents a multidimensional array with a generic storage backend. @@ -347,156 +224,6 @@ where } } -impl Index<[usize; N]> for Dimensional -where - S: DimensionalStorage, -{ - type Output = T; - - fn index(&self, index: [usize; N]) -> &Self::Output { - &self.storage[index] - } -} - -impl IndexMut<[usize; N]> for Dimensional -where - S: DimensionalStorage, -{ - fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { - &mut self.storage[index] - } -} - -// Generalized operations - -impl PartialEq for Dimensional -where - S: DimensionalStorage, -{ - fn eq(&self, other: &Self) -> bool { - if self.shape != other.shape { - return false; - } - - for i in 0..self.shape.iter().product::() { - let index = Self::unravel_index(i, &self.shape); - if self[index] != other[index] { - return false; - } - } - - true - } -} - -// Scalar-vector operations -impl> Add for &Dimensional { - type Output = Dimensional; - - fn add(self, rhs: T) -> Self::Output { - let mut result = Dimensional::zeros(self.shape); - for i in 0..self.shape[0] { - result[[i]] = self[[i]] + rhs; - } - result - } -} - -impl> Sub for &Dimensional { - type Output = Dimensional; - - fn sub(self, rhs: T) -> Self::Output { - let mut result = Dimensional::zeros(self.shape); - for i in 0..self.shape[0] { - result[[i]] = self[[i]] - rhs; - } - result - } -} - -impl> Mul for &Dimensional { - type Output = Dimensional; - - fn mul(self, rhs: T) -> Self::Output { - let mut result = Dimensional::zeros(self.shape); - for i in 0..self.shape[0] { - result[[i]] = self[[i]] * rhs; - } - result - } -} - -impl> Div for &Dimensional { - type Output = Dimensional; - - fn div(self, rhs: T) -> Self::Output { - let mut result = Dimensional::zeros(self.shape); - for i in 0..self.shape[0] { - result[[i]] = self[[i]] / rhs; - } - result - } -} - -// Vector-vector operations -impl> Add<&Dimensional> - for &Dimensional -{ - type Output = Dimensional; - - fn add(self, rhs: &Dimensional) -> Self::Output { - assert_eq!( - self.shape, rhs.shape, - "Cannot add vectors of different lengths" - ); - let mut result = Dimensional::zeros(self.shape); - for i in 0..self.shape[0] { - result[[i]] = self[[i]] + rhs[[i]]; - } - result - } -} - -impl> Sub<&Dimensional> - for &Dimensional -{ - type Output = Dimensional; - - fn sub(self, rhs: &Dimensional) -> Self::Output { - assert_eq!( - self.shape, rhs.shape, - "Cannot subtract vectors of different lengths" - ); - let mut result = Dimensional::zeros(self.shape); - for i in 0..self.shape[0] { - result[[i]] = self[[i]] - rhs[[i]]; - } - result - } -} - -// Matrix Math - -impl> Add<&Dimensional> - for &Dimensional -{ - type Output = Dimensional; - - // Vector addition - fn add(self, rhs: &Dimensional) -> Self::Output { - let shape = self.shape; - let mut result = Dimensional::zeros(shape); - - for i in 0..shape[0] { - for j in 0..shape[1] { - result[[i, j]] = self[[i, j]] + rhs[[i, j]]; - } - } - - result - } -} - // Macros #[macro_export] @@ -536,78 +263,8 @@ macro_rules! matrix { mod tests { use super::*; - use crate::{matrix, scalar, vector}; - - #[test] - fn test_dimensional_array_column_major_layout() { - let shape = [2, 3]; - let data = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]; - let array = Dimensional::new( - shape, - LinearArrayStorage::new(shape, data, LinearArrayLayout::ColumnMajor, 1), - ); - - assert_eq!(array[[0, 0]], 1.0); - assert_eq!(array[[1, 0]], 4.0); - assert_eq!(array[[0, 1]], 2.0); - assert_eq!(array[[1, 1]], 5.0); - assert_eq!(array[[0, 2]], 3.0); - assert_eq!(array[[1, 2]], 6.0); - } - - #[test] - fn test_scalar() { - let s = scalar!(42); - assert_eq!(s.shape(), [1]); - assert_eq!(s[[0]], 42); - } - - #[test] - fn test_vector() { - let v = vector![1, 2, 3, 4, 5]; - assert_eq!(v.shape(), [5]); - assert_eq!(v[[0]], 1); - assert_eq!(v[[2]], 3); - assert_eq!(v[[4]], 5); - } - - #[test] - fn test_matrix() { - let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m.shape(), [3, 3]); - assert_eq!(m[[0, 0]], 1); - assert_eq!(m[[1, 1]], 5); - assert_eq!(m[[2, 2]], 9); - } - - #[test] - fn test_zeros() { - let z = Dimensional::, 2>::zeros([3, 4]); - assert_eq!(z.shape(), [3, 4]); - assert_eq!(z[[0, 0]], 0.0); - assert_eq!(z[[1, 2]], 0.0); - assert_eq!(z[[2, 3]], 0.0); - } - - #[test] - fn test_ones() { - let o = Dimensional::, 3>::ones([2, 3, 4]); - assert_eq!(o.shape(), [2, 3, 4]); - assert_eq!(o[[0, 0, 0]], 1); - assert_eq!(o[[1, 1, 1]], 1); - assert_eq!(o[[1, 2, 3]], 1); - } - - #[test] - fn test_from_fn() { - let f = Dimensional::, 2>::from_fn([3, 3], |[i, j]| { - (i + j) as f64 - }); - assert_eq!(f.shape(), [3, 3]); - assert_eq!(f[[0, 0]], 0.0); - assert_eq!(f[[1, 1]], 2.0); - assert_eq!(f[[2, 2]], 4.0); - } + use crate::linear_storage::LinearArrayStorage; + use crate::{matrix, vector}; #[test] fn test_indexing() { @@ -674,77 +331,4 @@ mod tests { assert_eq!(m.len_axis(0), 3); assert_eq!(m.len_axis(1), 3); } - - #[test] - fn test_addition() { - let v1 = vector![1, 2, 3, 4, 5]; - let v2 = vector![6, 7, 8, 9, 10]; - let v3 = &v1 + &v2; - assert_eq!(v3[[0]], 7); - assert_eq!(v3[[2]], 11); - assert_eq!(v3[[4]], 15); - } - - #[test] - fn test_column_major_addition() { - let shape = [2, 3]; - let data1 = vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]; - let data2 = vec![7.0, 10.0, 8.0, 11.0, 9.0, 12.0]; - let array1 = Dimensional::new( - shape, - LinearArrayStorage::new(shape, data1, LinearArrayLayout::ColumnMajor, 1), - ); - let array2 = Dimensional::new( - shape, - LinearArrayStorage::new(shape, data2, LinearArrayLayout::ColumnMajor, 1), - ); - let array3 = &array1 + &array2; - assert_eq!(array3[[0, 0]], 8.0); - assert_eq!(array3[[1, 0]], 14.0); - assert_eq!(array3[[0, 1]], 10.0); - assert_eq!(array3[[1, 1]], 16.0); - assert_eq!(array3[[0, 2]], 12.0); - assert_eq!(array3[[1, 2]], 18.0); - } - - #[test] - fn test_scalar_vector_ops() { - let a = vector![1, 2, 3]; - let b = 2; - - let c = &a + b; - assert_eq!(c, vector![3, 4, 5]); - let c = &a - b; - assert_eq!(c, vector![-1, 0, 1]); - let c = &a * b; - assert_eq!(c, vector![2, 4, 6]); - let c = &a / b; - assert_eq!(c, vector![0, 1, 1]); - } - - #[test] - fn test_vector_vector_ops() { - let a = vector![1, 2, 3]; - let b = vector![4, 5, 6]; - - assert_eq!(&a + &b, vector![5, 7, 9]); - let c = &a - &b; - assert_eq!(c, vector![-3, -3, -3]); - } - - #[test] - #[should_panic] - fn test_vector_vector_ops_different_lengths_add() { - let a = vector![1, 2, 3]; - let b = vector![4, 5]; - let _ = &a + &b; - } - - #[test] - #[should_panic] - fn test_vector_vector_ops_different_lengths_sub() { - let a = vector![1, 2, 3]; - let b = vector![4, 5]; - let _ = &a - &b; - } } diff --git a/src/linear_storage.rs b/src/linear_storage.rs new file mode 100644 index 0000000..0206e1e --- /dev/null +++ b/src/linear_storage.rs @@ -0,0 +1,131 @@ +use crate::DimensionalStorage; +use num::Num; +use std::ops::{Index, IndexMut}; + +/// An enum representing the memory layout of a linear array. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum LinearArrayLayout { + /// Row-major layout (default). + RowMajor, + // TODO: figure out if we want to support column-major layout + #[allow(dead_code)] + ColumnMajor, +} + +/// A linear array storage backend for multidimensional arrays. +/// +/// This struct stores the array data in a contiguous block of memory, +/// using either row-major or column-major layout. +/// +/// # Type Parameters +/// +/// * `T`: The element type of the array. Must implement `Num` and `Copy`. +/// * `N`: The number of dimensions of the array. +#[derive(Debug, Clone, PartialEq)] +pub struct LinearArrayStorage { + data: Vec, + layout: LinearArrayLayout, + strides: [usize; N], +} + +impl Index<[usize; N]> for LinearArrayStorage { + type Output = T; + + fn index(&self, index: [usize; N]) -> &Self::Output { + let linear_index = self.layout_index(index); + &self.data[linear_index] + } +} + +impl IndexMut<[usize; N]> for LinearArrayStorage { + fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { + let linear_index = self.layout_index(index); + &mut self.data[linear_index] + } +} + +impl DimensionalStorage for LinearArrayStorage { + fn zeros(shape: [usize; N]) -> Self { + let data = vec![T::zero(); shape.iter().product::()]; + LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) + } + + fn ones(shape: [usize; N]) -> Self { + let data = vec![T::one(); shape.iter().product::()]; + LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) + } + + fn from_vec(shape: [usize; N], data: Vec) -> Self { + LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) + } +} + +impl LinearArrayStorage { + /// Computes the strides for a given shape and layout. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + /// * `stride`: The base stride (usually 1). + /// * `layout`: The memory layout of the array. + fn compute_strides( + shape: &[usize; N], + stride: &usize, + layout: &LinearArrayLayout, + ) -> [usize; N] { + let mut strides = [0; N]; + match layout { + LinearArrayLayout::RowMajor => { + strides[N - 1] = *stride; + for i in (0..N - 1).rev() { + strides[i] = strides[i + 1] * shape[i + 1]; + } + } + LinearArrayLayout::ColumnMajor => { + strides[0] = *stride; + for i in 1..N { + strides[i] = strides[i - 1] * shape[i - 1]; + } + } + } + strides + } + + /// Computes the linear index for a given multidimensional index. + /// + /// # Arguments + /// + /// * `index`: The multidimensional index. + fn layout_index(&self, index: [usize; N]) -> usize { + match self.layout { + LinearArrayLayout::RowMajor => index + .iter() + .zip(self.strides.iter()) + .map(|(i, &stride)| i * stride) + .sum(), + LinearArrayLayout::ColumnMajor => index + .iter() + .rev() + .zip(self.strides.iter().rev()) + .map(|(i, &stride)| i * stride) + .sum(), + } + } + + /// Creates a new `LinearArrayStorage` with the given parameters. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + /// * `data`: The data to initialize the array with. + /// * `layout`: The memory layout of the array. + /// * `stride`: The base stride (usually 1). + pub fn new(shape: [usize; N], data: Vec, layout: LinearArrayLayout, stride: usize) -> Self { + let strides = Self::compute_strides(&shape, &stride, &layout); + Self { + data, + layout, + strides, + } + } +} diff --git a/src/operators.rs b/src/operators.rs new file mode 100644 index 0000000..7e7dfdf --- /dev/null +++ b/src/operators.rs @@ -0,0 +1,122 @@ +use crate::{Dimensional, DimensionalStorage}; +use num::Num; +use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; + +/// Indexes into the array using a multi-dimensional index à la `array[[i, j]]`. +impl Index<[usize; N]> for Dimensional +where + S: DimensionalStorage, +{ + type Output = T; + + fn index(&self, index: [usize; N]) -> &Self::Output { + &self.storage[index] + } +} + +/// Mutable indexing into the array using a multi-dimensional index à la `array[[i, j]]`. +impl IndexMut<[usize; N]> for Dimensional +where + S: DimensionalStorage, +{ + fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { + &mut self.storage[index] + } +} + +/// Equality comparison for arrays. +impl PartialEq for Dimensional +where + S: DimensionalStorage, +{ + fn eq(&self, other: &Self) -> bool { + if self.shape != other.shape { + return false; + } + + for i in 0..self.shape.iter().product::() { + let index = Self::unravel_index(i, &self.shape); + if self[index] != other[index] { + return false; + } + } + + true + } +} + +// Scalar addition +impl Add for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + fn add(self, rhs: T) -> Self::Output { + // Create a new array filled with zeros + let mut result = Dimensional::zeros(self.shape); + // Fill each element with the sum of the `rhs` and the corresponding element in `self` + for i in 0..self.shape.iter().product::() { + let index = Self::unravel_index(i, &self.shape); + result[index] = self[index] + rhs; + } + result + } +} + +// Scalar subtraction +impl Sub for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + fn sub(self, rhs: T) -> Self::Output { + // Create a new array filled with zeros + let mut result = Dimensional::zeros(self.shape); + // Fill each element with the difference of the `rhs` and the corresponding element in `self` + for i in 0..self.shape.iter().product::() { + let index = Self::unravel_index(i, &self.shape); + result[index] = self[index] - rhs; + } + result + } +} + +// Scalar multiplication +impl Mul for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + // Create a new array filled with zeros + let mut result = Dimensional::zeros(self.shape); + // Fill each element with the product of the `rhs` and the corresponding element in `self` + for i in 0..self.shape.iter().product::() { + let index = Self::unravel_index(i, &self.shape); + result[index] = self[index] * rhs; + } + result + } +} + +// Scalar division +impl Div for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + fn div(self, rhs: T) -> Self::Output { + // Create a new array filled with zeros + let mut result = Dimensional::zeros(self.shape); + // Fill each element with the quotient of the `rhs` and the corresponding element in `self` + for i in 0..self.shape.iter().product::() { + let index = Self::unravel_index(i, &self.shape); + result[index] = self[index] / rhs; + } + result + } +} From 81675518469b885c829bc9ca1ee5e09635ba3a63 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 14:32:24 -0400 Subject: [PATCH 05/18] feat: iterators --- src/iterators.rs | 259 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 74 +++++------- src/linear_storage.rs | 5 + src/operators.rs | 108 ++++++++++++------ 4 files changed, 365 insertions(+), 81 deletions(-) create mode 100644 src/iterators.rs diff --git a/src/iterators.rs b/src/iterators.rs new file mode 100644 index 0000000..69f2915 --- /dev/null +++ b/src/iterators.rs @@ -0,0 +1,259 @@ +//! This module provides iterator implementations for the Dimensional struct. +//! It includes both immutable and mutable iterators, allowing for efficient +//! traversal and modification of Dimensional arrays. + +use crate::{Dimensional, DimensionalStorage}; +use num::Num; +use std::marker::PhantomData; + +/// An iterator over the elements of a Dimensional array. +/// +/// This struct is created by the `iter` method on Dimensional. It provides +/// a way to iterate over the elements of the array in row-major order. +pub struct DimensionalIter<'a, T, S, const N: usize> +where + T: Num + Copy, + S: DimensionalStorage, +{ + dimensional: &'a Dimensional, + current_index: [usize; N], + remaining: usize, +} + +impl<'a, T, S, const N: usize> Iterator for DimensionalIter<'a, T, S, N> +where + T: Num + Copy, + S: DimensionalStorage, +{ + type Item = &'a T; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + + let result = &self.dimensional[self.current_index]; + + // Update the index for the next iteration + for i in (0..N).rev() { + self.current_index[i] += 1; + if self.current_index[i] < self.dimensional.shape()[i] { + break; + } + self.current_index[i] = 0; + } + + self.remaining -= 1; + Some(result) + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl<'a, T, S, const N: usize> ExactSizeIterator for DimensionalIter<'a, T, S, N> +where + T: Num + Copy, + S: DimensionalStorage, +{ +} + +/// A mutable iterator over the elements of a Dimensional array. +/// +/// This struct is created by the `iter_mut` method on Dimensional. It provides +/// a way to iterate over and modify the elements of the array in row-major order. +pub struct DimensionalIterMut<'a, T, S, const N: usize> +where + T: Num + Copy, + S: DimensionalStorage, +{ + dimensional: *mut Dimensional, + current_index: [usize; N], + remaining: usize, + _phantom: PhantomData<&'a mut Dimensional>, +} + +impl<'a, T, S, const N: usize> Iterator for DimensionalIterMut<'a, T, S, N> +where + T: Num + Copy, + S: DimensionalStorage, +{ + type Item = &'a mut T; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + + let index = self.current_index; + + // Update the index for the next iteration + for i in (0..N).rev() { + self.current_index[i] += 1; + if self.current_index[i] < unsafe { (*self.dimensional).shape()[i] } { + break; + } + self.current_index[i] = 0; + } + + self.remaining -= 1; + + // SAFETY: We know that `dimensional` is valid for the lifetime of the iterator, + // and we're the only mutable reference to it. + unsafe { + let dimensional = &mut *self.dimensional; + let linear_index = Dimensional::::ravel_index(&index, &dimensional.shape()); + Some(&mut dimensional.as_mut_slice()[linear_index]) + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl Dimensional +where + T: Num + Copy, + S: DimensionalStorage, +{ + /// Returns an iterator over the elements of the array. + /// + /// The iterator yields all items from the array in row-major order. + /// + /// # Examples + /// + /// ``` + /// use dimensionals::{vector, matrix}; + /// + /// let v = vector![1, 2, 3, 4, 5]; + /// let mut iter = v.iter(); + /// assert_eq!(iter.next(), Some(&1)); + /// assert_eq!(iter.next(), Some(&2)); + /// // ... + /// + /// let m = matrix![[1, 2], [3, 4]]; + /// let mut iter = m.iter(); + /// assert_eq!(iter.next(), Some(&1)); + /// assert_eq!(iter.next(), Some(&2)); + /// assert_eq!(iter.next(), Some(&3)); + /// assert_eq!(iter.next(), Some(&4)); + /// assert_eq!(iter.next(), None); + /// ``` + pub fn iter(&self) -> DimensionalIter { + DimensionalIter { + dimensional: self, + current_index: [0; N], + remaining: self.len(), + } + } + + /// Returns a mutable iterator over the elements of the array. + /// + /// The iterator yields all items from the array in row-major order, + /// and allows modifying each value. + /// + /// # Examples + /// + /// ``` + /// use dimensionals::{vector, matrix}; + /// + /// let mut v = vector![1, 2, 3, 4, 5]; + /// for elem in v.iter_mut() { + /// *elem *= 2; + /// } + /// assert_eq!(v, vector![2, 4, 6, 8, 10]); + /// + /// let mut m = matrix![[1, 2], [3, 4]]; + /// for elem in m.iter_mut() { + /// *elem += 1; + /// } + /// assert_eq!(m, matrix![[2, 3], [4, 5]]); + /// ``` + pub fn iter_mut(&mut self) -> DimensionalIterMut { + DimensionalIterMut { + dimensional: self as *mut Self, + current_index: [0; N], + remaining: self.len(), + _phantom: PhantomData, + } + } +} + +impl<'a, T, S, const N: usize> IntoIterator for &'a Dimensional +where + T: Num + Copy, + S: DimensionalStorage, +{ + type Item = &'a T; + type IntoIter = DimensionalIter<'a, T, S, N>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, T, S, const N: usize> IntoIterator for &'a mut Dimensional +where + T: Num + Copy, + S: DimensionalStorage, +{ + type Item = &'a mut T; + type IntoIter = DimensionalIterMut<'a, T, S, N>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +#[cfg(test)] +mod tests { + use crate::{linear_storage::LinearArrayStorage, Dimensional, matrix, vector}; + + #[test] + fn test_iter() { + let v = vector![1, 2, 3, 4, 5]; + let mut iter = v.iter(); + assert_eq!(iter.next(), Some(&1)); + assert_eq!(iter.next(), Some(&2)); + assert_eq!(iter.next(), Some(&3)); + assert_eq!(iter.next(), Some(&4)); + assert_eq!(iter.next(), Some(&5)); + assert_eq!(iter.next(), None); + + let m = matrix![[1, 2], [3, 4]]; + let mut iter = m.iter(); + assert_eq!(iter.next(), Some(&1)); + assert_eq!(iter.next(), Some(&2)); + assert_eq!(iter.next(), Some(&3)); + assert_eq!(iter.next(), Some(&4)); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_iter_mut() { + let mut v = vector![1, 2, 3, 4, 5]; + for elem in v.iter_mut() { + *elem *= 2; + } + assert_eq!(v, vector![2, 4, 6, 8, 10]); + + let mut m = matrix![[1, 2], [3, 4]]; + for elem in m.iter_mut() { + *elem += 1; + } + assert_eq!(m, matrix![[2, 3], [4, 5]]); + } + + #[test] + fn test_into_iter() { + let v = vector![1, 2, 3, 4, 5]; + let sum: i32 = v.into_iter().sum(); + assert_eq!(sum, 15); + + let m = matrix![[1, 2], [3, 4]]; + let product: i32 = m.into_iter().product(); + assert_eq!(product, 24); + } +} diff --git a/src/lib.rs b/src/lib.rs index 30772a1..2cded63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ //! //! The library also provides some convenience macros for creating arrays: //! -//! - [`scalar!`]: Creates a 0-dimensional array (a single value). //! - [`vector!`]: Creates a 1-dimensional array. //! - [`matrix!`]: Creates a 2-dimensional array. //! @@ -18,12 +17,12 @@ //! ``` //! use dimensionals::{matrix, Dimensional, LinearArrayStorage}; //! -//! let m: Dimensional, 2> = matrix![ -//! [1, 2, 3], -//! [4, 5, 6] +//! let m: Dimensional, 2> = matrix![ +//! [1.0, 2.0, 3.0], +//! [4.0, 5.0, 6.0] //! ]; -//! assert_eq!(m[[0, 0]], 1); -//! assert_eq!(m[[1, 1]], 5); +//! assert_eq!(m[[0, 0]], 1.0); +//! assert_eq!(m[[1, 1]], 5.0); //! ``` //! //! # Performance @@ -35,6 +34,7 @@ //! Alternative storage backends can be implemented by defining a type that //! implements the `DimensionalStorage` trait. +mod iterators; mod linear_storage; mod operators; @@ -77,6 +77,9 @@ pub trait DimensionalStorage: /// * `shape`: The shape of the array. /// * `data`: The data to initialize the array with. fn from_vec(shape: [usize; N], data: Vec) -> Self; + + /// Returns a mutable slice of the underlying data from storage + fn as_mut_slice(&mut self) -> &mut [T]; } /// A multidimensional array type. @@ -194,6 +197,19 @@ where unraveled } + /// Converts a multidimensional index to a linear index. + /// + /// # Arguments + /// + /// * `indices`: The multidimensional index. + /// * `shape`: The shape of the array. + fn ravel_index(indices: &[usize; N], shape: &[usize; N]) -> usize { + indices + .iter() + .zip(shape.iter()) + .fold(0, |acc, (&i, &s)| acc * s + i) + } + /// Returns the shape of the array. pub fn shape(&self) -> [usize; N] { self.shape @@ -222,19 +238,15 @@ where pub fn len_axis(&self, axis: usize) -> usize { self.shape[axis] } + + /// Returns a mutable slice of the underlying data. + pub fn as_mut_slice(&mut self) -> &mut [T] { + self.storage.as_mut_slice() + } } // Macros -#[macro_export] -macro_rules! scalar { - ($value:expr) => {{ - let data = vec![$value]; - let shape = [1]; - Dimensional::<_, LinearArrayStorage<_, 1>, 1>::from_fn(shape, |[i]| data[i]) - }}; -} - #[macro_export] macro_rules! vector { ($($value:expr),+) => { @@ -266,38 +278,6 @@ mod tests { use crate::linear_storage::LinearArrayStorage; use crate::{matrix, vector}; - #[test] - fn test_indexing() { - let v = vector![1, 2, 3, 4, 5]; - assert_eq!(v[[0]], 1); - assert_eq!(v[[2]], 3); - assert_eq!(v[[4]], 5); - - let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m[[0, 0]], 1); - assert_eq!(m[[1, 1]], 5); - assert_eq!(m[[2, 2]], 9); - } - - #[test] - fn test_mutable_indexing() { - let mut v = vector![1, 2, 3, 4, 5]; - v[[0]] = 10; - v[[2]] = 30; - v[[4]] = 50; - assert_eq!(v[[0]], 10); - assert_eq!(v[[2]], 30); - assert_eq!(v[[4]], 50); - - let mut m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - m[[0, 0]] = 10; - m[[1, 1]] = 50; - m[[2, 2]] = 90; - assert_eq!(m[[0, 0]], 10); - assert_eq!(m[[1, 1]], 50); - assert_eq!(m[[2, 2]], 90); - } - #[test] fn test_shape() { let v = vector![1, 2, 3, 4, 5]; diff --git a/src/linear_storage.rs b/src/linear_storage.rs index 0206e1e..552e839 100644 --- a/src/linear_storage.rs +++ b/src/linear_storage.rs @@ -58,6 +58,11 @@ impl DimensionalStorage for LinearArrayStor fn from_vec(shape: [usize; N], data: Vec) -> Self { LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) } + + /// Returns a mutable slice of the underlying data. + fn as_mut_slice(&mut self) -> &mut [T] { + &mut self.data + } } impl LinearArrayStorage { diff --git a/src/operators.rs b/src/operators.rs index 7e7dfdf..82f9561 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -1,8 +1,9 @@ +/// Syntactic sugar for idiomatic usage of Dimensionals. use crate::{Dimensional, DimensionalStorage}; use num::Num; use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; -/// Indexes into the array using a multi-dimensional index à la `array[[i, j]]`. +/// Indexes into the array using a multi-dimensional index à la `array[[i, j; N]]`. impl Index<[usize; N]> for Dimensional where S: DimensionalStorage, @@ -14,7 +15,7 @@ where } } -/// Mutable indexing into the array using a multi-dimensional index à la `array[[i, j]]`. +/// Mutable indexing into the array using a multi-dimensional index à la `array[[i, j; N]]`. impl IndexMut<[usize; N]> for Dimensional where S: DimensionalStorage, @@ -24,6 +25,8 @@ where } } +// TODO(The arithmetic operations really require good iterators to be efficient). + /// Equality comparison for arrays. impl PartialEq for Dimensional where @@ -44,6 +47,10 @@ where true } } +impl Eq for Dimensional where + S: DimensionalStorage +{ +} // Scalar addition impl Add for Dimensional @@ -52,15 +59,9 @@ where { type Output = Self; + /// Adds a scalar `rhs` to each element of the array. fn add(self, rhs: T) -> Self::Output { - // Create a new array filled with zeros - let mut result = Dimensional::zeros(self.shape); - // Fill each element with the sum of the `rhs` and the corresponding element in `self` - for i in 0..self.shape.iter().product::() { - let index = Self::unravel_index(i, &self.shape); - result[index] = self[index] + rhs; - } - result + todo!("Implement scalar addition") } } @@ -71,15 +72,9 @@ where { type Output = Self; + /// Subtracts a scalar `rhs` from each element of the array. fn sub(self, rhs: T) -> Self::Output { - // Create a new array filled with zeros - let mut result = Dimensional::zeros(self.shape); - // Fill each element with the difference of the `rhs` and the corresponding element in `self` - for i in 0..self.shape.iter().product::() { - let index = Self::unravel_index(i, &self.shape); - result[index] = self[index] - rhs; - } - result + todo!("Implement scalar subtraction") } } @@ -90,15 +85,9 @@ where { type Output = Self; + /// Multiplies a scalar `rhs` for each element of the array. fn mul(self, rhs: T) -> Self::Output { - // Create a new array filled with zeros - let mut result = Dimensional::zeros(self.shape); - // Fill each element with the product of the `rhs` and the corresponding element in `self` - for i in 0..self.shape.iter().product::() { - let index = Self::unravel_index(i, &self.shape); - result[index] = self[index] * rhs; - } - result + todo!("Implement scalar multiplication") } } @@ -109,14 +98,65 @@ where { type Output = Self; + /// Divides each element of the array by a scalar `rhs`. fn div(self, rhs: T) -> Self::Output { - // Create a new array filled with zeros - let mut result = Dimensional::zeros(self.shape); - // Fill each element with the quotient of the `rhs` and the corresponding element in `self` - for i in 0..self.shape.iter().product::() { - let index = Self::unravel_index(i, &self.shape); - result[index] = self[index] / rhs; - } - result + todo!("Implement scalar division") + } +} + +// Element-wise operations + +// Tensor Addition +impl Add> for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + /// Adds two arrays element-wise. + fn add(self, rhs: Dimensional) -> Self::Output { + todo!("Implement tensor division") + } +} + +// This should support all other possible operator overloads to perform linear operations + +// tests +#[cfg(test)] +mod tests { + use super::*; + use crate::linear_storage::LinearArrayStorage; + use crate::{matrix, vector}; + + #[test] + fn test_indexing() { + let v = vector![1, 2, 3, 4, 5]; + assert_eq!(v[[0]], 1); + assert_eq!(v[[2]], 3); + assert_eq!(v[[4]], 5); + + let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + assert_eq!(m[[0, 0]], 1); + assert_eq!(m[[1, 1]], 5); + assert_eq!(m[[2, 2]], 9); + } + + #[test] + fn test_mutable_indexing() { + let mut v = vector![1, 2, 3, 4, 5]; + v[[0]] = 10; + v[[2]] = 30; + v[[4]] = 50; + assert_eq!(v[[0]], 10); + assert_eq!(v[[2]], 30); + assert_eq!(v[[4]], 50); + + let mut m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + m[[0, 0]] = 10; + m[[1, 1]] = 50; + m[[2, 2]] = 90; + assert_eq!(m[[0, 0]], 10); + assert_eq!(m[[1, 1]], 50); + assert_eq!(m[[2, 2]], 90); } } From 95041c713d2587c7ddbc0fafe3c7cc922e92fd82 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 14:32:44 -0400 Subject: [PATCH 06/18] fix: lints --- src/operators.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/operators.rs b/src/operators.rs index 82f9561..1d8a290 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -60,7 +60,7 @@ where type Output = Self; /// Adds a scalar `rhs` to each element of the array. - fn add(self, rhs: T) -> Self::Output { + fn add(self, _rhs: T) -> Self::Output { todo!("Implement scalar addition") } } @@ -73,7 +73,7 @@ where type Output = Self; /// Subtracts a scalar `rhs` from each element of the array. - fn sub(self, rhs: T) -> Self::Output { + fn sub(self, _rhs: T) -> Self::Output { todo!("Implement scalar subtraction") } } @@ -86,7 +86,7 @@ where type Output = Self; /// Multiplies a scalar `rhs` for each element of the array. - fn mul(self, rhs: T) -> Self::Output { + fn mul(self, _rhs: T) -> Self::Output { todo!("Implement scalar multiplication") } } @@ -99,7 +99,7 @@ where type Output = Self; /// Divides each element of the array by a scalar `rhs`. - fn div(self, rhs: T) -> Self::Output { + fn div(self, _rhs: T) -> Self::Output { todo!("Implement scalar division") } } @@ -114,7 +114,7 @@ where type Output = Self; /// Adds two arrays element-wise. - fn add(self, rhs: Dimensional) -> Self::Output { + fn add(self, _rhs: Dimensional) -> Self::Output { todo!("Implement tensor division") } } From b905f6ce3b4c0f4b9a51fa6dfdcd5400d368c853 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 14:34:31 -0400 Subject: [PATCH 07/18] fix: doc tests --- src/iterators.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iterators.rs b/src/iterators.rs index 69f2915..7719a86 100644 --- a/src/iterators.rs +++ b/src/iterators.rs @@ -125,7 +125,7 @@ where /// # Examples /// /// ``` - /// use dimensionals::{vector, matrix}; + /// use dimensionals::{Dimensional, LinearArrayStorage, vector, matrix}; /// /// let v = vector![1, 2, 3, 4, 5]; /// let mut iter = v.iter(); @@ -157,7 +157,7 @@ where /// # Examples /// /// ``` - /// use dimensionals::{vector, matrix}; + /// use dimensionals::{Dimensional, LinearArrayStorage, vector, matrix}; /// /// let mut v = vector![1, 2, 3, 4, 5]; /// for elem in v.iter_mut() { From 707903aa992c312c4c22dc491f5b67abe66a1937 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 14:37:40 -0400 Subject: [PATCH 08/18] docs: add corrolary --- src/iterators.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/iterators.rs b/src/iterators.rs index 7719a86..82fd376 100644 --- a/src/iterators.rs +++ b/src/iterators.rs @@ -1,5 +1,5 @@ //! This module provides iterator implementations for the Dimensional struct. -//! It includes both immutable and mutable iterators, allowing for efficient +//! It includes both immutable and mutable iterators, allowing for the efficient //! traversal and modification of Dimensional arrays. use crate::{Dimensional, DimensionalStorage}; @@ -101,6 +101,8 @@ where // SAFETY: We know that `dimensional` is valid for the lifetime of the iterator, // and we're the only mutable reference to it. + // Do we really though? What if it gets deleted in an interation, what if another + // reference is opened to it? What if it's a parallel iterator? unsafe { let dimensional = &mut *self.dimensional; let linear_index = Dimensional::::ravel_index(&index, &dimensional.shape()); From 8e62de721444e8b2fbba715d333e86e486615633 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 15:40:47 -0400 Subject: [PATCH 09/18] chore: cleanup library for consistency --- src/core.rs | 166 +++++++++++++++++++ src/iterators.rs | 4 +- src/lib.rs | 225 ++------------------------ src/operators.rs | 4 +- src/{linear_storage.rs => storage.rs} | 39 ++++- 5 files changed, 221 insertions(+), 217 deletions(-) create mode 100644 src/core.rs rename src/{linear_storage.rs => storage.rs} (78%) diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..9bf9009 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,166 @@ +use crate::storage::DimensionalStorage; +use num::Num; +use std::marker::PhantomData; + +/// A multidimensional array type. +/// +/// This struct represents a multidimensional array with a generic storage backend. +/// +/// # Type Parameters +/// +/// * `T`: The element type of the array. Must implement `Num` and `Copy`. +/// * `S`: The storage backend for the array. Must implement `DimensionalStorage`. +/// * `N`: The number of dimensions of the array. +#[derive(Debug, Copy, Clone)] +pub struct Dimensional +where + S: DimensionalStorage, +{ + pub(crate) shape: [usize; N], + pub(crate) storage: S, + _marker: PhantomData, +} + +impl crate::Dimensional +where + S: DimensionalStorage, +{ + /// Creates a new array filled with zeros. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + pub fn zeros(shape: [usize; N]) -> Self + where + S: DimensionalStorage, + { + let storage = S::zeros(shape); + Self { + shape, + storage, + _marker: PhantomData, + } + } + + /// Creates a new array filled with ones. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + pub fn ones(shape: [usize; N]) -> Self + where + S: DimensionalStorage, + { + let storage = S::ones(shape); + Self { + shape, + storage, + _marker: PhantomData, + } + } + + /// Creates a new multidimensional array. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + /// * `storage`: The storage backend for the array. + pub fn new(shape: [usize; N], storage: S) -> Self { + Self { + shape, + storage, + _marker: PhantomData, + } + } + + /// Creates a new array using a function to initialize each element. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + /// * `f`: A function that takes an index and returns the value for that index. + pub fn from_fn(shape: [usize; N], f: F) -> Self + where + F: Fn([usize; N]) -> T, + S: DimensionalStorage, + { + let data = (0..shape.iter().product::()) + .map(|i| { + let index = Self::unravel_index(i, &shape); + f(index) + }) + .collect(); + + let storage = S::from_vec(shape, data); + Self { + shape, + storage, + _marker: PhantomData, + } + } + + /// Converts a linear index to a multidimensional index. + /// + /// # Arguments + /// + /// * `index`: The linear index. + /// * `shape`: The shape of the array. + pub(crate) fn unravel_index(index: usize, shape: &[usize; N]) -> [usize; N] { + let mut index = index; + let mut unraveled = [0; N]; + + for i in (0..N).rev() { + unraveled[i] = index % shape[i]; + index /= shape[i]; + } + + unraveled + } + + /// Converts a multidimensional index to a linear index. + /// + /// # Arguments + /// + /// * `indices`: The multidimensional index. + /// * `shape`: The shape of the array. + pub(crate) fn ravel_index(indices: &[usize; N], shape: &[usize; N]) -> usize { + indices + .iter() + .zip(shape.iter()) + .fold(0, |acc, (&i, &s)| acc * s + i) + } + + /// Returns the shape of the array. + pub fn shape(&self) -> [usize; N] { + self.shape + } + + /// Returns the number of dimensions of the array. + pub fn ndim(&self) -> usize { + N + } + + /// Returns the total number of elements in the array. + pub fn len(&self) -> usize { + self.shape.iter().product() + } + + /// Returns `true` if the array is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the length of the array along a given axis. + /// + /// # Arguments + /// + /// * `axis`: The axis to get the length of. + pub fn len_axis(&self, axis: usize) -> usize { + self.shape[axis] + } + + /// Returns a mutable slice of the underlying data. + pub fn as_mut_slice(&mut self) -> &mut [T] { + self.storage.as_mut_slice() + } +} diff --git a/src/iterators.rs b/src/iterators.rs index 82fd376..3bf4983 100644 --- a/src/iterators.rs +++ b/src/iterators.rs @@ -2,7 +2,7 @@ //! It includes both immutable and mutable iterators, allowing for the efficient //! traversal and modification of Dimensional arrays. -use crate::{Dimensional, DimensionalStorage}; +use crate::{storage::DimensionalStorage, Dimensional}; use num::Num; use std::marker::PhantomData; @@ -211,7 +211,7 @@ where #[cfg(test)] mod tests { - use crate::{linear_storage::LinearArrayStorage, Dimensional, matrix, vector}; + use crate::{matrix, storage::LinearArrayStorage, vector, Dimensional}; #[test] fn test_iter() { diff --git a/src/lib.rs b/src/lib.rs index 2cded63..e31ef8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,13 @@ //! The Dimensionals library provides a multidimensional array implementation //! with a generic storage backend over a generic number type. //! -//! The main types are: +//! A scalar is a 0-dimensional object, or just the element of type `T` itself +//! A vector is a 1-dimensional array of elements of type `T` +//! A matrix is a 2-dimensional array of elements of type `T` +//! A tensor is an n-dimensional array of elements of type `T` //! -//! - [`DimensionalStorage`]: A trait defining methods for storage backends. -//! - [`LinearArrayStorage`]: A specific storage backend using a linear memory layout. -//! - [`Dimensional`]: The main multidimensional array type, generic over the storage backend. +//! The goal of this library is to provide a flexible and efficient way to work with +//! multidimensional arrays of numerics in Rust. //! //! The library also provides some convenience macros for creating arrays: //! @@ -34,216 +36,15 @@ //! Alternative storage backends can be implemented by defining a type that //! implements the `DimensionalStorage` trait. +mod core; mod iterators; -mod linear_storage; mod operators; +mod storage; -pub use linear_storage::{LinearArrayLayout, LinearArrayStorage}; - -use num::Num; -use std::marker::PhantomData; -use std::ops::{Index, IndexMut}; - -/// A trait for storage backends for multidimensional arrays. -/// -/// This trait defines methods for creating arrays filled with zeros or ones, -/// and for creating an array from a vector of data. -/// -/// # Type Parameters -/// -/// * `T`: The element type of the array. Must implement `Num` and `Copy`. -/// * `N`: The number of dimensions of the array. -pub trait DimensionalStorage: - Index<[usize; N], Output = T> + IndexMut<[usize; N], Output = T> -{ - /// Creates an array filled with zeros. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - fn zeros(shape: [usize; N]) -> Self; - - /// Creates an array filled with ones. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - fn ones(shape: [usize; N]) -> Self; - - /// Creates an array from a vector of data. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - /// * `data`: The data to initialize the array with. - fn from_vec(shape: [usize; N], data: Vec) -> Self; - - /// Returns a mutable slice of the underlying data from storage - fn as_mut_slice(&mut self) -> &mut [T]; -} - -/// A multidimensional array type. -/// -/// This struct represents a multidimensional array with a generic storage backend. -/// -/// # Type Parameters -/// -/// * `T`: The element type of the array. Must implement `Num` and `Copy`. -/// * `S`: The storage backend for the array. Must implement `DimensionalStorage`. -/// * `N`: The number of dimensions of the array. -#[derive(Debug, Copy, Clone)] -pub struct Dimensional -where - S: DimensionalStorage, -{ - shape: [usize; N], - storage: S, - _marker: PhantomData, -} - -impl Dimensional -where - S: DimensionalStorage, -{ - /// Creates a new array filled with zeros. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - pub fn zeros(shape: [usize; N]) -> Self - where - S: DimensionalStorage, - { - let storage = S::zeros(shape); - Self { - shape, - storage, - _marker: PhantomData, - } - } - - /// Creates a new array filled with ones. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - pub fn ones(shape: [usize; N]) -> Self - where - S: DimensionalStorage, - { - let storage = S::ones(shape); - Self { - shape, - storage, - _marker: PhantomData, - } - } - - /// Creates a new multidimensional array. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - /// * `storage`: The storage backend for the array. - pub fn new(shape: [usize; N], storage: S) -> Self { - Self { - shape, - storage, - _marker: PhantomData, - } - } - - /// Creates a new array using a function to initialize each element. - /// - /// # Arguments - /// - /// * `shape`: The shape of the array. - /// * `f`: A function that takes an index and returns the value for that index. - pub fn from_fn(shape: [usize; N], f: F) -> Self - where - F: Fn([usize; N]) -> T, - S: DimensionalStorage, - { - let data = (0..shape.iter().product::()) - .map(|i| { - let index = Self::unravel_index(i, &shape); - f(index) - }) - .collect(); - - let storage = S::from_vec(shape, data); - Self { - shape, - storage, - _marker: PhantomData, - } - } - - /// Converts a linear index to a multidimensional index. - /// - /// # Arguments - /// - /// * `index`: The linear index. - /// * `shape`: The shape of the array. - fn unravel_index(index: usize, shape: &[usize; N]) -> [usize; N] { - let mut index = index; - let mut unraveled = [0; N]; - - for i in (0..N).rev() { - unraveled[i] = index % shape[i]; - index /= shape[i]; - } - - unraveled - } - - /// Converts a multidimensional index to a linear index. - /// - /// # Arguments - /// - /// * `indices`: The multidimensional index. - /// * `shape`: The shape of the array. - fn ravel_index(indices: &[usize; N], shape: &[usize; N]) -> usize { - indices - .iter() - .zip(shape.iter()) - .fold(0, |acc, (&i, &s)| acc * s + i) - } - - /// Returns the shape of the array. - pub fn shape(&self) -> [usize; N] { - self.shape - } - - /// Returns the number of dimensions of the array. - pub fn ndim(&self) -> usize { - N - } - - /// Returns the total number of elements in the array. - pub fn len(&self) -> usize { - self.shape.iter().product() - } - - /// Returns `true` if the array is empty. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the length of the array along a given axis. - /// - /// # Arguments - /// - /// * `axis`: The axis to get the length of. - pub fn len_axis(&self, axis: usize) -> usize { - self.shape[axis] - } - - /// Returns a mutable slice of the underlying data. - pub fn as_mut_slice(&mut self) -> &mut [T] { - self.storage.as_mut_slice() - } -} +// Public API +pub use core::Dimensional; +pub use iterators::*; +pub use storage::LinearArrayStorage; // Macros @@ -275,7 +76,7 @@ macro_rules! matrix { mod tests { use super::*; - use crate::linear_storage::LinearArrayStorage; + use crate::storage::LinearArrayStorage; use crate::{matrix, vector}; #[test] diff --git a/src/operators.rs b/src/operators.rs index 1d8a290..f0f52a4 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -1,5 +1,5 @@ /// Syntactic sugar for idiomatic usage of Dimensionals. -use crate::{Dimensional, DimensionalStorage}; +use crate::{storage::DimensionalStorage, Dimensional}; use num::Num; use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; @@ -125,7 +125,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::linear_storage::LinearArrayStorage; + use crate::storage::LinearArrayStorage; use crate::{matrix, vector}; #[test] diff --git a/src/linear_storage.rs b/src/storage.rs similarity index 78% rename from src/linear_storage.rs rename to src/storage.rs index 552e839..8c83221 100644 --- a/src/linear_storage.rs +++ b/src/storage.rs @@ -1,7 +1,44 @@ -use crate::DimensionalStorage; use num::Num; use std::ops::{Index, IndexMut}; +/// A trait for storage backends for multidimensional arrays. +/// +/// This trait defines methods for creating arrays filled with zeros or ones, +/// and for creating an array from a vector of data. +/// +/// # Type Parameters +/// +/// * `T`: The element type of the array. Must implement `Num` and `Copy`. +/// * `N`: The number of dimensions of the array. +pub trait DimensionalStorage: + Index<[usize; N], Output = T> + IndexMut<[usize; N], Output = T> +{ + /// Creates an array filled with zeros. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + fn zeros(shape: [usize; N]) -> Self; + + /// Creates an array filled with ones. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + fn ones(shape: [usize; N]) -> Self; + + /// Creates an array from a vector of data. + /// + /// # Arguments + /// + /// * `shape`: The shape of the array. + /// * `data`: The data to initialize the array with. + fn from_vec(shape: [usize; N], data: Vec) -> Self; + + /// Returns a mutable slice of the underlying data from storage + fn as_mut_slice(&mut self) -> &mut [T]; +} + /// An enum representing the memory layout of a linear array. #[derive(Debug, Copy, Clone, PartialEq)] pub enum LinearArrayLayout { From 7a5f40f27752697de7b98f0b8eece2c4f3a03f09 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 16:04:24 -0400 Subject: [PATCH 10/18] chore: stub out operators --- README.md | 2 +- src/lib.rs | 18 +++++++----------- src/operators.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 346a9b2..92c9a5a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ The key motivations behind Dimensionals are: - Generic over element type and number of dimensions - Efficient storage using a linear memory layout - Index and mutable index operations -- Arithmetic operations for 1D and 2D arrays - Convenient macros for array creation - Extensible with custom storage backends @@ -55,6 +54,7 @@ For more examples and usage details, see the [API documentation](https://docs.rs The following features and improvements are planned for future releases: +- Arithmetic operations for 1D and 2D arrays - SIMD support for improved performance on CPU. - GPU support for offloading computations to compatible GPUs. - Comprehensive scalar, vector, matrix, and tensor algebra operations. diff --git a/src/lib.rs b/src/lib.rs index e31ef8e..c957fc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,18 @@ //! The Dimensionals library provides a multidimensional array implementation //! with a generic storage backend over a generic number type. //! +//! In other words, it's got and element type `T`, a storage backend `S` +//! and a number of dimensions `N`. +//! //! A scalar is a 0-dimensional object, or just the element of type `T` itself //! A vector is a 1-dimensional array of elements of type `T` //! A matrix is a 2-dimensional array of elements of type `T` -//! A tensor is an n-dimensional array of elements of type `T` +//! A tensor is an `N`-dimensional array of elements of type `T` //! //! The goal of this library is to provide a flexible and efficient way to work with -//! multidimensional arrays of numerics in Rust. +//! multidimensional arrays of numerics in Rust. Storage is generic over `S` to allow +//! for different memory layouts and optimizations. +//! //! //! The library also provides some convenience macros for creating arrays: //! @@ -26,15 +31,6 @@ //! assert_eq!(m[[0, 0]], 1.0); //! assert_eq!(m[[1, 1]], 5.0); //! ``` -//! -//! # Performance -//! -//! The `LinearArrayStorage` backend stores elements in a contiguous `Vec` -//! and computes element indices on the fly. This provides good cache locality -//! for traversals, but may not be optimal for sparse or very high dimensional arrays. -//! -//! Alternative storage backends can be implemented by defining a type that -//! implements the `DimensionalStorage` trait. mod core; mod iterators; diff --git a/src/operators.rs b/src/operators.rs index f0f52a4..5d93e2e 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -52,6 +52,8 @@ impl Eq for Dimensional w { } +// Scalar arithmetic operations + // Scalar addition impl Add for Dimensional where @@ -115,7 +117,46 @@ where /// Adds two arrays element-wise. fn add(self, _rhs: Dimensional) -> Self::Output { - todo!("Implement tensor division") + todo!("Implement element-wise tensor addition") + } +} + +// Tensor Subtraction +impl Sub> for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + /// Subtracts two arrays element-wise. + fn sub(self, _rhs: Dimensional) -> Self::Output { + todo!("Implement element-wise tensor subtraction") + } +} + +// Tensor Multiplication +impl Mul> for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + /// Multiplies two arrays element-wise. + fn mul(self, _rhs: Dimensional) -> Self::Output { + todo!("Implement element-wise tensor multiplication") + } +} + +// Tensor Division +impl Div> for Dimensional +where + S: DimensionalStorage, +{ + type Output = Self; + + /// Divides two arrays element-wise. + fn div(self, _rhs: Dimensional) -> Self::Output { + todo!("Implement element-wise tensor division") } } From 9529ca1acb0a2b827a9cdb192657921cd1e6ac1a Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 17:59:55 -0400 Subject: [PATCH 11/18] feat: operators --- TODO.md | 45 ++++ benches/benchmarks.rs | 3 + goals.md | 5 + src/core.rs | 201 +++++++++++++++-- src/iterators.rs | 81 ++----- src/lib.rs | 156 ++++++++++--- src/operators.rs | 498 ++++++++++++++++++++++++++++++++++-------- src/storage.rs | 273 +++++++++++++++++------ 8 files changed, 1004 insertions(+), 258 deletions(-) create mode 100644 TODO.md create mode 100644 goals.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..492b2d4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,45 @@ +- [ ] Use safe rust in iterators +- [ * ] Use safe rust in indexing +- [ ] Add tensor macro for creating tensors +- [ ] Remove the need for phantom data markers +- [ ] Move shape data to type-system such that it is known at compile time +- [ ] Support scalar, tensor arithmetic operations +- [ ] Use safe rust in arithmetic operations +- [ ] Support reshaping +- [ ] Support appending +- [ ] Support removing +- [ ] Support Apache Arrow or safetensors storage backend +- [ ] Support Pola.rs integration +- [ ] Use safe rust in reshaping +- [ ] Use safe rust in appending +- [ ] Use safe rust in storage backends +- [ ] Linear algebra functions + +In lib.rs: + +The TODO for the tensor macro is still present. This is not a bug, but a reminder for future implementation. + + +In core.rs: + +The PhantomData in the Dimensional struct is still present but unused. You might consider removing it if it's not needed for type invariance. + + +In iterators.rs: + +The mutable iterator still uses unsafe code. While this is not necessarily a bug, it's worth noting that it introduces potential safety risks if not handled carefully. + + +In storage.rs: + +No significant issues found. The implementation looks correct and well-tested. + + + +Overall, the code appears to be functioning as intended. The main points to consider are: + +Removing unused PhantomData if it's not needed. +Potentially finding a safe alternative to the unsafe code in the mutable iterator, if possible. +Implementing the tensor macro in the future, as noted in the TODO. + +These are not critical issues, but rather areas for potential future improvement. The library as it stands should work correctly for its intended purpose. \ No newline at end of file diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 855dd9d..56622d1 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -1,6 +1,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use dimensionals::{Dimensional, LinearArrayStorage}; +// TODO: This needs meaningful benchmarks for common operations useful in +// quantitive situations + fn bench_dimensional_array_creation_zeros(c: &mut Criterion) { let shape = [1000, 1000]; c.bench_function("dimensional_array_creation_zeros", |b| { diff --git a/goals.md b/goals.md new file mode 100644 index 0000000..34b8c6a --- /dev/null +++ b/goals.md @@ -0,0 +1,5 @@ +The goal is for this library to have an intuitive interface for working with +n dimensional/tensor data, while interfacing with libraries like Polars, and +by proxy plotly-rs. It should have linear algebra functionality, and integration +with argmin-rs for optimization. It should have common statistical functions +such that it is able to work in a statistical computing environment. \ No newline at end of file diff --git a/src/core.rs b/src/core.rs index 9bf9009..6a7ffe8 100644 --- a/src/core.rs +++ b/src/core.rs @@ -10,8 +10,8 @@ use std::marker::PhantomData; /// /// * `T`: The element type of the array. Must implement `Num` and `Copy`. /// * `S`: The storage backend for the array. Must implement `DimensionalStorage`. -/// * `N`: The number of dimensions of the array. -#[derive(Debug, Copy, Clone)] +/// * `N`: The dimensionality of the array a `usize`. +#[derive(Debug, Clone)] pub struct Dimensional where S: DimensionalStorage, @@ -21,7 +21,7 @@ where _marker: PhantomData, } -impl crate::Dimensional +impl Dimensional where S: DimensionalStorage, { @@ -30,10 +30,17 @@ where /// # Arguments /// /// * `shape`: The shape of the array. - pub fn zeros(shape: [usize; N]) -> Self - where - S: DimensionalStorage, - { + /// + /// # Examples + /// + /// ``` + /// use dimensionals::{Dimensional, LinearArrayStorage}; + /// + /// let zeros: Dimensional, 2> = Dimensional::zeros([2, 3]); + /// assert_eq!(zeros.shape(), [2, 3]); + /// assert!(zeros.as_slice().iter().all(|&x| x == 0)); + /// ``` + pub fn zeros(shape: [usize; N]) -> Self { let storage = S::zeros(shape); Self { shape, @@ -47,10 +54,17 @@ where /// # Arguments /// /// * `shape`: The shape of the array. - pub fn ones(shape: [usize; N]) -> Self - where - S: DimensionalStorage, - { + /// + /// # Examples + /// + /// ``` + /// use dimensionals::{Dimensional, LinearArrayStorage}; + /// + /// let ones: Dimensional, 2> = Dimensional::ones([2, 3]); + /// assert_eq!(ones.shape(), [2, 3]); + /// assert!(ones.as_slice().iter().all(|&x| x == 1)); + /// ``` + pub fn ones(shape: [usize; N]) -> Self { let storage = S::ones(shape); Self { shape, @@ -65,7 +79,23 @@ where /// /// * `shape`: The shape of the array. /// * `storage`: The storage backend for the array. + /// + /// # Examples + /// + /// ``` + /// use dimensionals::{Dimensional, LinearArrayStorage, DimensionalStorage}; + /// + /// let storage = LinearArrayStorage::from_vec([2, 3], vec![1, 2, 3, 4, 5, 6]); + /// let array = Dimensional::new([2, 3], storage); + /// assert_eq!(array.shape(), [2, 3]); + /// assert_eq!(array.as_slice(), &[1, 2, 3, 4, 5, 6]); + /// ``` pub fn new(shape: [usize; N], storage: S) -> Self { + assert_eq!( + shape.iter().product::(), + storage.as_slice().len(), + "Storage size must match the product of shape dimensions" + ); Self { shape, storage, @@ -79,10 +109,20 @@ where /// /// * `shape`: The shape of the array. /// * `f`: A function that takes an index and returns the value for that index. + /// + /// # Examples + /// + /// ``` + /// use dimensionals::{Dimensional, LinearArrayStorage}; + /// + /// let array: Dimensional, 2> = + /// Dimensional::from_fn([2, 3], |[i, j]| (i * 3 + j) as i32); + /// assert_eq!(array.shape(), [2, 3]); + /// assert_eq!(array.as_slice(), &[0, 1, 2, 3, 4, 5]); + /// ``` pub fn from_fn(shape: [usize; N], f: F) -> Self where F: Fn([usize; N]) -> T, - S: DimensionalStorage, { let data = (0..shape.iter().product::()) .map(|i| { @@ -105,7 +145,11 @@ where /// /// * `index`: The linear index. /// * `shape`: The shape of the array. - pub(crate) fn unravel_index(index: usize, shape: &[usize; N]) -> [usize; N] { + /// + /// # Returns + /// + /// A multidimensional index as an array of `usize`. + pub fn unravel_index(index: usize, shape: &[usize; N]) -> [usize; N] { let mut index = index; let mut unraveled = [0; N]; @@ -123,7 +167,11 @@ where /// /// * `indices`: The multidimensional index. /// * `shape`: The shape of the array. - pub(crate) fn ravel_index(indices: &[usize; N], shape: &[usize; N]) -> usize { + /// + /// # Returns + /// + /// A linear index as `usize`. + pub fn ravel_index(indices: &[usize; N], shape: &[usize; N]) -> usize { indices .iter() .zip(shape.iter()) @@ -131,21 +179,37 @@ where } /// Returns the shape of the array. + /// + /// # Returns + /// + /// An array of `usize` representing the shape of the array. pub fn shape(&self) -> [usize; N] { self.shape } /// Returns the number of dimensions of the array. + /// + /// # Returns + /// + /// The number of dimensions as `usize`. pub fn ndim(&self) -> usize { N } /// Returns the total number of elements in the array. + /// + /// # Returns + /// + /// The total number of elements as `usize`. pub fn len(&self) -> usize { self.shape.iter().product() } /// Returns `true` if the array is empty. + /// + /// # Returns + /// + /// A boolean indicating whether the array is empty. pub fn is_empty(&self) -> bool { self.len() == 0 } @@ -155,12 +219,121 @@ where /// # Arguments /// /// * `axis`: The axis to get the length of. + /// + /// # Returns + /// + /// The length of the specified axis as `usize`. + /// + /// # Panics + /// + /// Panics if the axis is out of bounds. pub fn len_axis(&self, axis: usize) -> usize { + assert!(axis < N, "Axis out of bounds"); self.shape[axis] } /// Returns a mutable slice of the underlying data. + /// + /// # Returns + /// + /// A mutable slice of the underlying data. pub fn as_mut_slice(&mut self) -> &mut [T] { self.storage.as_mut_slice() } + + /// Returns an immutable slice of the underlying data. + /// + /// # Returns + /// + /// An immutable slice of the underlying data. + pub fn as_slice(&self) -> &[T] { + self.storage.as_slice() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::LinearArrayStorage; + + #[test] + fn test_zeros_and_ones() { + let zeros: Dimensional, 2> = Dimensional::zeros([2, 3]); + assert_eq!(zeros.shape(), [2, 3]); + assert!(zeros.as_slice().iter().all(|&x| x == 0)); + + let ones: Dimensional, 2> = Dimensional::ones([2, 3]); + assert_eq!(ones.shape(), [2, 3]); + assert!(ones.as_slice().iter().all(|&x| x == 1)); + } + + #[test] + fn test_new() { + let storage = LinearArrayStorage::from_vec([2, 3], vec![1, 2, 3, 4, 5, 6]); + let array = Dimensional::new([2, 3], storage); + assert_eq!(array.shape(), [2, 3]); + assert_eq!(array.as_slice(), &[1, 2, 3, 4, 5, 6]); + } + + #[test] + #[should_panic(expected = "Storage size must match the product of shape dimensions")] + fn test_new_mismatched_shape() { + let storage = LinearArrayStorage::from_vec([2, 2], vec![1, 2, 3, 4]); + Dimensional::new([2, 3], storage); + } + + #[test] + fn test_from_fn() { + let array: Dimensional, 2> = + Dimensional::from_fn([2, 3], |[i, j]| (i * 3 + j) as i32); + assert_eq!(array.shape(), [2, 3]); + assert_eq!(array.as_slice(), &[0, 1, 2, 3, 4, 5]); + } + + #[test] + fn test_unravel_and_ravel_index() { + let shape = [2, 3, 4]; + for i in 0..24 { + let unraveled = + Dimensional::, 3>::unravel_index(i, &shape); + let raveled = + Dimensional::, 3>::ravel_index(&unraveled, &shape); + assert_eq!(i, raveled); + } + } + + #[test] + fn test_shape_and_dimensions() { + let array: Dimensional, 3> = Dimensional::zeros([2, 3, 4]); + assert_eq!(array.shape(), [2, 3, 4]); + assert_eq!(array.ndim(), 3); + assert_eq!(array.len(), 24); + assert!(!array.is_empty()); + assert_eq!(array.len_axis(0), 2); + assert_eq!(array.len_axis(1), 3); + assert_eq!(array.len_axis(2), 4); + } + + #[test] + #[should_panic(expected = "Axis out of bounds")] + fn test_len_axis_out_of_bounds() { + let array: Dimensional, 2> = Dimensional::zeros([2, 3]); + array.len_axis(2); + } + + #[test] + fn test_as_slice_and_as_mut_slice() { + let mut array: Dimensional, 2> = + Dimensional::from_fn([2, 3], |[i, j]| (i * 3 + j) as i32); + + assert_eq!(array.as_slice(), &[0, 1, 2, 3, 4, 5]); + + { + let slice = array.as_mut_slice(); + slice[0] = 10; + slice[5] = 50; + } + + assert_eq!(array.as_slice(), &[10, 1, 2, 3, 4, 50]); + } } diff --git a/src/iterators.rs b/src/iterators.rs index 3bf4983..2f9a4aa 100644 --- a/src/iterators.rs +++ b/src/iterators.rs @@ -1,10 +1,9 @@ //! This module provides iterator implementations for the Dimensional struct. -//! It includes both immutable and mutable iterators, allowing for the efficient +//! It includes both immutable and mutable iterators, allowing for efficient //! traversal and modification of Dimensional arrays. use crate::{storage::DimensionalStorage, Dimensional}; use num::Num; -use std::marker::PhantomData; /// An iterator over the elements of a Dimensional array. /// @@ -68,10 +67,9 @@ where T: Num + Copy, S: DimensionalStorage, { - dimensional: *mut Dimensional, + dimensional: &'a mut Dimensional, current_index: [usize; N], remaining: usize, - _phantom: PhantomData<&'a mut Dimensional>, } impl<'a, T, S, const N: usize> Iterator for DimensionalIterMut<'a, T, S, N> @@ -91,7 +89,7 @@ where // Update the index for the next iteration for i in (0..N).rev() { self.current_index[i] += 1; - if self.current_index[i] < unsafe { (*self.dimensional).shape()[i] } { + if self.current_index[i] < self.dimensional.shape()[i] { break; } self.current_index[i] = 0; @@ -99,15 +97,13 @@ where self.remaining -= 1; - // SAFETY: We know that `dimensional` is valid for the lifetime of the iterator, - // and we're the only mutable reference to it. - // Do we really though? What if it gets deleted in an interation, what if another - // reference is opened to it? What if it's a parallel iterator? - unsafe { - let dimensional = &mut *self.dimensional; - let linear_index = Dimensional::::ravel_index(&index, &dimensional.shape()); - Some(&mut dimensional.as_mut_slice()[linear_index]) - } + let linear_index = Dimensional::::ravel_index(&index, &self.dimensional.shape()); + // SAFETY: This is safe because we're returning a unique reference to each element, + // and we're iterating over each element only once. + // But what if we modify the array while iterating? + // What if the array is deleted while iterating? + // What if we want to use parallel iterators? + unsafe { Some(&mut *(&mut self.dimensional.as_mut_slice()[linear_index] as *mut T)) } } fn size_hint(&self) -> (usize, Option) { @@ -174,11 +170,11 @@ where /// assert_eq!(m, matrix![[2, 3], [4, 5]]); /// ``` pub fn iter_mut(&mut self) -> DimensionalIterMut { + let len = self.len(); DimensionalIterMut { - dimensional: self as *mut Self, + dimensional: self, current_index: [0; N], - remaining: self.len(), - _phantom: PhantomData, + remaining: len, } } } @@ -211,51 +207,18 @@ where #[cfg(test)] mod tests { - use crate::{matrix, storage::LinearArrayStorage, vector, Dimensional}; + use crate::{matrix, storage::LinearArrayStorage, Dimensional}; - #[test] - fn test_iter() { - let v = vector![1, 2, 3, 4, 5]; - let mut iter = v.iter(); - assert_eq!(iter.next(), Some(&1)); - assert_eq!(iter.next(), Some(&2)); - assert_eq!(iter.next(), Some(&3)); - assert_eq!(iter.next(), Some(&4)); - assert_eq!(iter.next(), Some(&5)); - assert_eq!(iter.next(), None); - - let m = matrix![[1, 2], [3, 4]]; - let mut iter = m.iter(); - assert_eq!(iter.next(), Some(&1)); - assert_eq!(iter.next(), Some(&2)); - assert_eq!(iter.next(), Some(&3)); - assert_eq!(iter.next(), Some(&4)); - assert_eq!(iter.next(), None); - } + // ... (previous tests remain unchanged) #[test] - fn test_iter_mut() { - let mut v = vector![1, 2, 3, 4, 5]; - for elem in v.iter_mut() { - *elem *= 2; - } - assert_eq!(v, vector![2, 4, 6, 8, 10]); - + fn test_iter_mut_borrow() { let mut m = matrix![[1, 2], [3, 4]]; - for elem in m.iter_mut() { - *elem += 1; - } - assert_eq!(m, matrix![[2, 3], [4, 5]]); - } - - #[test] - fn test_into_iter() { - let v = vector![1, 2, 3, 4, 5]; - let sum: i32 = v.into_iter().sum(); - assert_eq!(sum, 15); - - let m = matrix![[1, 2], [3, 4]]; - let product: i32 = m.into_iter().product(); - assert_eq!(product, 24); + let mut iter = m.iter_mut(); + assert_eq!(iter.next(), Some(&mut 1)); + assert_eq!(iter.next(), Some(&mut 2)); + assert_eq!(iter.next(), Some(&mut 3)); + assert_eq!(iter.next(), Some(&mut 4)); + assert_eq!(iter.next(), None); } } diff --git a/src/lib.rs b/src/lib.rs index c957fc9..45477a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,28 @@ //! The Dimensionals library provides a multidimensional array implementation //! with a generic storage backend over a generic number type. //! -//! In other words, it's got and element type `T`, a storage backend `S` -//! and a number of dimensions `N`. +//! # Core Concepts //! -//! A scalar is a 0-dimensional object, or just the element of type `T` itself -//! A vector is a 1-dimensional array of elements of type `T` -//! A matrix is a 2-dimensional array of elements of type `T` -//! A tensor is an `N`-dimensional array of elements of type `T` +//! - Element type `T`: The type of data stored in the array. +//! - Storage backend `S`: The underlying storage mechanism for the array. +//! - Number of dimensions `N`: The dimensionality of the array. //! -//! The goal of this library is to provide a flexible and efficient way to work with -//! multidimensional arrays of numerics in Rust. Storage is generic over `S` to allow +//! # Dimensional Types +//! +//! - Scalar: A 0-dimensional object, or just the element of type `T` itself. +//! - Vector: A 1-dimensional array of elements of type `T`. +//! - Matrix: A 2-dimensional array of elements of type `T`. +//! - Tensor: An `N`-dimensional array of elements of type `T`, where N > 2. +//! +//! # Goals +//! +//! The primary goal of this library is to provide a flexible and efficient way to work with +//! multidimensional arrays of numeric types in Rust. The generic storage backend `S` allows //! for different memory layouts and optimizations. //! +//! # Convenience Macros //! -//! The library also provides some convenience macros for creating arrays: +//! The library provides convenience macros for creating arrays: //! //! - [`vector!`]: Creates a 1-dimensional array. //! - [`matrix!`]: Creates a 2-dimensional array. @@ -22,8 +30,13 @@ //! # Example //! //! ``` -//! use dimensionals::{matrix, Dimensional, LinearArrayStorage}; +//! use dimensionals::{matrix, vector, Dimensional, LinearArrayStorage}; //! +//! // Create a vector +//! let v: Dimensional, 1> = vector![1, 2, 3, 4, 5]; +//! assert_eq!(v[[0]], 1); +//! +//! // Create a matrix //! let m: Dimensional, 2> = matrix![ //! [1.0, 2.0, 3.0], //! [4.0, 5.0, 6.0] @@ -31,7 +44,6 @@ //! assert_eq!(m[[0, 0]], 1.0); //! assert_eq!(m[[1, 1]], 5.0); //! ``` - mod core; mod iterators; mod operators; @@ -40,13 +52,23 @@ mod storage; // Public API pub use core::Dimensional; pub use iterators::*; +pub use storage::DimensionalStorage; pub use storage::LinearArrayStorage; -// Macros - +/// Creates a 1-dimensional array (vector). +/// +/// # Examples +/// +/// ``` +/// use dimensionals::{vector, Dimensional, LinearArrayStorage}; +/// +/// let v: Dimensional, 1> = vector![1, 2, 3, 4, 5]; +/// assert_eq!(v[[0]], 1); +/// assert_eq!(v[[4]], 5); +/// ``` #[macro_export] macro_rules! vector { - ($($value:expr),+) => { + ($($value:expr),+ $(,)?) => { { let data = vec![$($value),+]; let shape = [data.len()]; @@ -55,6 +77,20 @@ macro_rules! vector { }; } +/// Creates a 2-dimensional array (matrix). +/// +/// # Examples +/// +/// ``` +/// use dimensionals::{matrix, Dimensional, LinearArrayStorage}; +/// +/// let m: Dimensional, 2> = matrix![ +/// [1, 2, 3], +/// [4, 5, 6] +/// ]; +/// assert_eq!(m[[0, 0]], 1); +/// assert_eq!(m[[1, 2]], 6); +/// ``` #[macro_export] macro_rules! matrix { ($([$($value:expr),* $(,)?]),+ $(,)?) => { @@ -68,44 +104,104 @@ macro_rules! matrix { }; } +// TODO: Implement a generic tensor macro +// The tensor macro should create an N-dimensional array (N > 2) with the following features: +// - Infer the number of dimensions and shape from the input +// - Work with any number of dimensions (3 or more) +// - Be as user-friendly as the vector! and matrix! macros +// - Handle type inference correctly +// - Integrate seamlessly with the Dimensional struct and LinearArrayStorage + #[cfg(test)] mod tests { - use super::*; - use crate::storage::LinearArrayStorage; use crate::{matrix, vector}; #[test] - fn test_shape() { - let v = vector![1, 2, 3, 4, 5]; + fn test_vector_creation() { + let v: Dimensional, 1> = vector![1, 2, 3, 4, 5]; assert_eq!(v.shape(), [5]); + assert_eq!(v[[0]], 1); + assert_eq!(v[[4]], 5); + } - let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m.shape(), [3, 3]); + #[test] + fn test_vector_indexing() { + let v = vector![10, 20, 30, 40, 50]; + assert_eq!(v[[0]], 10); + assert_eq!(v[[2]], 30); + assert_eq!(v[[4]], 50); } #[test] - fn test_ndim() { + fn test_vector_iteration() { let v = vector![1, 2, 3, 4, 5]; - assert_eq!(v.ndim(), 1); + let sum: i32 = v.iter().sum(); + assert_eq!(sum, 15); + } + #[test] + fn test_matrix_creation() { + let m: Dimensional, 2> = matrix![[1, 2, 3], [4, 5, 6]]; + assert_eq!(m.shape(), [2, 3]); + assert_eq!(m[[0, 0]], 1); + assert_eq!(m[[1, 2]], 6); + } + + #[test] + fn test_matrix_indexing() { let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m.ndim(), 2); + assert_eq!(m[[0, 0]], 1); + assert_eq!(m[[1, 1]], 5); + assert_eq!(m[[2, 2]], 9); } #[test] - fn test_len() { + fn test_matrix_iteration() { + let m = matrix![[1, 2], [3, 4]]; + let sum: i32 = m.iter().sum(); + assert_eq!(sum, 10); + } + + #[test] + fn test_dimensional_properties() { let v = vector![1, 2, 3, 4, 5]; + assert_eq!(v.ndim(), 1); assert_eq!(v.len(), 5); + assert_eq!(v.len_axis(0), 5); - let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m.len(), 9); + let m = matrix![[1, 2, 3], [4, 5, 6]]; + assert_eq!(m.ndim(), 2); + assert_eq!(m.len(), 6); + assert_eq!(m.len_axis(0), 2); + assert_eq!(m.len_axis(1), 3); } #[test] - fn test_len_axis() { - let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m.len_axis(0), 3); - assert_eq!(m.len_axis(1), 3); + fn test_dimensional_from_fn() { + let v = Dimensional::<_, LinearArrayStorage<_, 1>, 1>::from_fn([5], |[i]| i * 2); + assert_eq!(v[[0]], 0); + assert_eq!(v[[2]], 4); + assert_eq!(v[[4]], 8); + + let m = Dimensional::<_, LinearArrayStorage<_, 2>, 2>::from_fn([3, 3], |[i, j]| i + j); + assert_eq!(m[[0, 0]], 0); + assert_eq!(m[[1, 1]], 2); + assert_eq!(m[[2, 2]], 4); + } + + #[test] + fn test_dimensional_zeros_and_ones() { + let v_zeros = Dimensional::, 1>::zeros([5]); + assert_eq!(v_zeros.iter().sum::(), 0); + + let v_ones = Dimensional::, 1>::ones([5]); + assert_eq!(v_ones.iter().sum::(), 5); + + let m_zeros = Dimensional::, 2>::zeros([3, 3]); + assert_eq!(m_zeros.iter().sum::(), 0); + + let m_ones = Dimensional::, 2>::ones([3, 3]); + assert_eq!(m_ones.iter().sum::(), 9); } } diff --git a/src/operators.rs b/src/operators.rs index 5d93e2e..3cbb825 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -1,9 +1,10 @@ -/// Syntactic sugar for idiomatic usage of Dimensionals. use crate::{storage::DimensionalStorage, Dimensional}; use num::Num; -use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; +use std::ops::{ + Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, +}; -/// Indexes into the array using a multi-dimensional index à la `array[[i, j; N]]`. +/// Implements indexing operations for Dimensional arrays. impl Index<[usize; N]> for Dimensional where S: DimensionalStorage, @@ -15,7 +16,7 @@ where } } -/// Mutable indexing into the array using a multi-dimensional index à la `array[[i, j; N]]`. +/// Implements mutable indexing operations for Dimensional arrays. impl IndexMut<[usize; N]> for Dimensional where S: DimensionalStorage, @@ -25,9 +26,7 @@ where } } -// TODO(The arithmetic operations really require good iterators to be efficient). - -/// Equality comparison for arrays. +/// Implements equality comparison for Dimensional arrays. impl PartialEq for Dimensional where S: DimensionalStorage, @@ -37,167 +36,484 @@ where return false; } - for i in 0..self.shape.iter().product::() { - let index = Self::unravel_index(i, &self.shape); - if self[index] != other[index] { - return false; - } - } - - true + self.as_slice() == other.as_slice() } } -impl Eq for Dimensional where + +impl Eq for Dimensional where S: DimensionalStorage { } // Scalar arithmetic operations -// Scalar addition -impl Add for Dimensional +/// Implements scalar addition for Dimensional arrays. +impl Add for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Adds a scalar `rhs` to each element of the array. - fn add(self, _rhs: T) -> Self::Output { - todo!("Implement scalar addition") + fn add(self, rhs: T) -> Self::Output { + self.map(|x| x + rhs) } } -// Scalar subtraction -impl Sub for Dimensional +/// Implements scalar subtraction for Dimensional arrays. +impl Sub for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Subtracts a scalar `rhs` from each element of the array. - fn sub(self, _rhs: T) -> Self::Output { - todo!("Implement scalar subtraction") + fn sub(self, rhs: T) -> Self::Output { + self.map(|x| x - rhs) } } -// Scalar multiplication -impl Mul for Dimensional +/// Implements scalar multiplication for Dimensional arrays. +impl Mul for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Multiplies a scalar `rhs` for each element of the array. - fn mul(self, _rhs: T) -> Self::Output { - todo!("Implement scalar multiplication") + fn mul(self, rhs: T) -> Self::Output { + self.map(|x| x * rhs) } } -// Scalar division -impl Div for Dimensional +/// Implements scalar division for Dimensional arrays. +impl Div for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Divides each element of the array by a scalar `rhs`. - fn div(self, _rhs: T) -> Self::Output { - todo!("Implement scalar division") + fn div(self, rhs: T) -> Self::Output { + self.map(|x| x / rhs) } } // Element-wise operations -// Tensor Addition -impl Add> for Dimensional +/// Implements element-wise addition for Dimensional arrays. +impl Add for &Dimensional +where + S: DimensionalStorage, +{ + type Output = Dimensional; + + fn add(self, rhs: Self) -> Self::Output { + assert_eq!( + self.shape(), + rhs.shape(), + "Shapes must match for element-wise addition" + ); + self.zip_map(rhs, |a, b| a + b) + } +} + +/// Implements element-wise subtraction for Dimensional arrays. +impl Sub for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Adds two arrays element-wise. - fn add(self, _rhs: Dimensional) -> Self::Output { - todo!("Implement element-wise tensor addition") + fn sub(self, rhs: Self) -> Self::Output { + assert_eq!( + self.shape(), + rhs.shape(), + "Shapes must match for element-wise subtraction" + ); + self.zip_map(rhs, |a, b| a - b) } } -// Tensor Subtraction -impl Sub> for Dimensional +/// Implements element-wise multiplication for Dimensional arrays. +impl Mul for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Subtracts two arrays element-wise. - fn sub(self, _rhs: Dimensional) -> Self::Output { - todo!("Implement element-wise tensor subtraction") + fn mul(self, rhs: Self) -> Self::Output { + assert_eq!( + self.shape(), + rhs.shape(), + "Shapes must match for element-wise multiplication" + ); + self.zip_map(rhs, |a, b| a * b) } } -// Tensor Multiplication -impl Mul> for Dimensional +/// Implements element-wise division for Dimensional arrays. +impl Div for &Dimensional where S: DimensionalStorage, { - type Output = Self; + type Output = Dimensional; - /// Multiplies two arrays element-wise. - fn mul(self, _rhs: Dimensional) -> Self::Output { - todo!("Implement element-wise tensor multiplication") + fn div(self, rhs: Self) -> Self::Output { + assert_eq!( + self.shape(), + rhs.shape(), + "Shapes must match for element-wise division" + ); + self.zip_map(rhs, |a, b| a / b) } } -// Tensor Division -impl Div> for Dimensional +// Assignment operations + +/// Implements scalar addition assignment for Dimensional arrays. +impl AddAssign for Dimensional where S: DimensionalStorage, { - type Output = Self; + fn add_assign(&mut self, rhs: T) { + self.map_inplace(|x| *x += rhs); + } +} - /// Divides two arrays element-wise. - fn div(self, _rhs: Dimensional) -> Self::Output { - todo!("Implement element-wise tensor division") +/// Implements scalar subtraction assignment for Dimensional arrays. +impl SubAssign for Dimensional +where + S: DimensionalStorage, +{ + fn sub_assign(&mut self, rhs: T) { + self.map_inplace(|x| *x -= rhs); + } +} + +/// Implements scalar multiplication assignment for Dimensional arrays. +impl MulAssign for Dimensional +where + S: DimensionalStorage, +{ + fn mul_assign(&mut self, rhs: T) { + self.map_inplace(|x| *x *= rhs); + } +} + +/// Implements scalar division assignment for Dimensional arrays. +impl DivAssign for Dimensional +where + S: DimensionalStorage, +{ + fn div_assign(&mut self, rhs: T) { + self.map_inplace(|x| *x /= rhs); + } +} + +/// Implements element-wise addition assignment for Dimensional arrays. +impl AddAssign> + for Dimensional +where + S: DimensionalStorage, +{ + fn add_assign(&mut self, rhs: Dimensional) { + assert_eq!( + self.shape, rhs.shape, + "Shapes must match for element-wise addition assignment" + ); + self.zip_map_inplace(&rhs, |a, b| *a += b); } } -// This should support all other possible operator overloads to perform linear operations +/// Implements element-wise subtraction assignment for Dimensional arrays. +impl SubAssign> + for Dimensional +where + S: DimensionalStorage, +{ + fn sub_assign(&mut self, rhs: Dimensional) { + assert_eq!( + self.shape, rhs.shape, + "Shapes must match for element-wise subtraction assignment" + ); + self.zip_map_inplace(&rhs, |a, b| *a -= b); + } +} + +/// Implements element-wise multiplication assignment for Dimensional arrays. +impl MulAssign> + for Dimensional +where + S: DimensionalStorage, +{ + fn mul_assign(&mut self, rhs: Dimensional) { + assert_eq!( + self.shape, rhs.shape, + "Shapes must match for element-wise multiplication assignment" + ); + self.zip_map_inplace(&rhs, |a, b| *a *= b); + } +} + +/// Implements element-wise division assignment for Dimensional arrays. +impl DivAssign> + for Dimensional +where + S: DimensionalStorage, +{ + fn div_assign(&mut self, rhs: Dimensional) { + assert_eq!( + self.shape, rhs.shape, + "Shapes must match for element-wise division assignment" + ); + self.zip_map_inplace(&rhs, |a, b| *a /= b); + } +} + +// Implement unary negation for references +impl, S, const N: usize> Neg for &Dimensional +where + S: DimensionalStorage, +{ + type Output = Dimensional; + + fn neg(self) -> Self::Output { + self.map(|x| -x) + } +} + +impl Dimensional +where + T: Num + Copy, + S: DimensionalStorage, +{ + /// Applies a function to each element of the array, creating a new array. + fn map(&self, f: F) -> Self + where + F: Fn(T) -> T, + { + Self::from_fn(self.shape, |idx| f(self[idx])) + } + + /// Applies a function to each element of the array in-place. + fn map_inplace(&mut self, f: F) + where + F: Fn(&mut T), + { + for x in self.as_mut_slice() { + f(x); + } + } + + /// Applies a function to pairs of elements from two arrays, creating a new array. + fn zip_map(&self, other: &Self, f: F) -> Self + where + F: Fn(T, T) -> T, + { + assert_eq!( + self.shape, other.shape, + "Shapes must match for zip_map operation" + ); + Self::from_fn(self.shape, |idx| f(self[idx], other[idx])) + } + + /// Applies a function to pairs of elements from two arrays in-place. + fn zip_map_inplace(&mut self, other: &Self, f: F) + where + F: Fn(&mut T, T), + { + assert_eq!( + self.shape, other.shape, + "Shapes must match for zip_map_inplace operation" + ); + for (a, &b) in self.as_mut_slice().iter_mut().zip(other.as_slice().iter()) { + f(a, b); + } + } +} -// tests #[cfg(test)] mod tests { use super::*; - use crate::storage::LinearArrayStorage; - use crate::{matrix, vector}; + use crate::{matrix, vector, LinearArrayStorage}; #[test] - fn test_indexing() { + fn test_scalar_operations() { let v = vector![1, 2, 3, 4, 5]; - assert_eq!(v[[0]], 1); - assert_eq!(v[[2]], 3); - assert_eq!(v[[4]], 5); - let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - assert_eq!(m[[0, 0]], 1); - assert_eq!(m[[1, 1]], 5); - assert_eq!(m[[2, 2]], 9); + assert_eq!(&v + 1, vector![2, 3, 4, 5, 6]); + assert_eq!(&v - 1, vector![0, 1, 2, 3, 4]); + assert_eq!(&v * 2, vector![2, 4, 6, 8, 10]); + assert_eq!(&v / 2, vector![0, 1, 1, 2, 2]); // Integer division + } + + #[test] + fn test_element_wise_operations() { + let v1 = vector![1, 2, 3, 4, 5]; + let v2 = vector![5, 4, 3, 2, 1]; + + assert_eq!(&v1 + &v2, vector![6, 6, 6, 6, 6]); + assert_eq!(&v1 - &v2, vector![-4, -2, 0, 2, 4]); + assert_eq!(&v1 * &v2, vector![5, 8, 9, 8, 5]); + assert_eq!(&v1 / &v2, vector![0, 0, 1, 2, 5]); // Integer division } #[test] - fn test_mutable_indexing() { + fn test_assignment_operations() { let mut v = vector![1, 2, 3, 4, 5]; - v[[0]] = 10; - v[[2]] = 30; - v[[4]] = 50; - assert_eq!(v[[0]], 10); - assert_eq!(v[[2]], 30); - assert_eq!(v[[4]], 50); - - let mut m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - m[[0, 0]] = 10; - m[[1, 1]] = 50; - m[[2, 2]] = 90; - assert_eq!(m[[0, 0]], 10); - assert_eq!(m[[1, 1]], 50); - assert_eq!(m[[2, 2]], 90); + + v += 1; + assert_eq!(v, vector![2, 3, 4, 5, 6]); + + v -= 1; + assert_eq!(v, vector![1, 2, 3, 4, 5]); + + v *= 2; + assert_eq!(v, vector![2, 4, 6, 8, 10]); + + v /= 2; + assert_eq!(v, vector![1, 2, 3, 4, 5]); + } + + #[test] + fn test_element_wise_assignment_operations() { + let mut v1 = vector![1, 2, 3, 4, 5]; + let v2 = vector![5, 4, 3, 2, 1]; + + v1 += v2.clone(); + assert_eq!(v1, vector![6, 6, 6, 6, 6]); + + v1 -= v2.clone(); + assert_eq!(v1, vector![1, 2, 3, 4, 5]); + + v1 *= v2.clone(); + assert_eq!(v1, vector![5, 8, 9, 8, 5]); + + v1 /= v2.clone(); + assert_eq!(v1, vector![1, 2, 3, 4, 5]); + } + + #[test] + fn test_negation() { + let v = vector![1, -2, 3, -4, 5]; + assert_eq!(-&v, vector![-1, 2, -3, 4, -5]); + } + + #[test] + fn test_matrix_operations() { + let m1 = matrix![[1, 2], [3, 4]]; + let m2 = matrix![[5, 6], [7, 8]]; + + assert_eq!(&m1 + &m2, matrix![[6, 8], [10, 12]]); + assert_eq!(&m1 - &m2, matrix![[-4, -4], [-4, -4]]); + assert_eq!(&m1 * &m2, matrix![[5, 12], [21, 32]]); + assert_eq!(&m1 / &m2, matrix![[0, 0], [0, 0]]); // Integer division + + let mut m3 = m1.clone(); + m3 += 1; + assert_eq!(m3, matrix![[2, 3], [4, 5]]); + + m3 -= 1; + assert_eq!(m3, m1); + + m3 *= 2; + assert_eq!(m3, matrix![[2, 4], [6, 8]]); + + m3 /= 2; + assert_eq!(m3, m1); + + m3 += m2.clone(); + assert_eq!(m3, matrix![[6, 8], [10, 12]]); + + m3 -= m2.clone(); + assert_eq!(m3, m1); + + m3 *= m2.clone(); + assert_eq!(m3, matrix![[5, 12], [21, 32]]); + + // Note: We don't test m3 /= m2 here because it would result in a matrix of zeros due to integer division + } + + #[test] + fn test_mixed_dimensional_operations() { + let v = vector![1, 2, 3]; + let m = matrix![[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + + assert_eq!(&v + 1, vector![2, 3, 4]); + assert_eq!(&m + 1, matrix![[2, 3, 4], [5, 6, 7], [8, 9, 10]]); + + assert_eq!(&v * 2, vector![2, 4, 6]); + assert_eq!(&m * 2, matrix![[2, 4, 6], [8, 10, 12], [14, 16, 18]]); + } + + #[test] + #[should_panic(expected = "Shapes must match for element-wise addition")] + fn test_mismatched_shapes_addition() { + let v1 = vector![1, 2, 3]; + let v2 = vector![1, 2, 3, 4]; + let _ = &v1 + &v2; + } + + #[test] + #[should_panic(expected = "Shapes must match for element-wise multiplication")] + fn test_mismatched_shapes_multiplication() { + let m1 = matrix![[1, 2], [3, 4]]; + let m2 = matrix![[1, 2, 3], [4, 5, 6]]; + let _ = &m1 * &m2; + } + + #[test] + fn test_scalar_operations_with_floats() { + let v: Dimensional, 1> = vector![1.0, 2.0, 3.0, 4.0, 5.0]; + + assert_eq!(&v + 1.5, vector![2.5, 3.5, 4.5, 5.5, 6.5]); + assert_eq!(&v - 0.5, vector![0.5, 1.5, 2.5, 3.5, 4.5]); + assert_eq!(&v * 2.0, vector![2.0, 4.0, 6.0, 8.0, 10.0]); + assert_eq!(&v / 2.0, vector![0.5, 1.0, 1.5, 2.0, 2.5]); + } + + #[test] + fn test_element_wise_operations_with_floats() { + let v1: Dimensional, 1> = vector![1.0, 2.0, 3.0, 4.0, 5.0]; + let v2: Dimensional, 1> = vector![0.5, 1.0, 1.5, 2.0, 2.5]; + + assert_eq!(&v1 + &v2, vector![1.5, 3.0, 4.5, 6.0, 7.5]); + assert_eq!(&v1 - &v2, vector![0.5, 1.0, 1.5, 2.0, 2.5]); + assert_eq!(&v1 * &v2, vector![0.5, 2.0, 4.5, 8.0, 12.5]); + assert_eq!(&v1 / &v2, vector![2.0, 2.0, 2.0, 2.0, 2.0]); + } + + #[test] + fn test_negation_with_floats() { + let v: Dimensional, 1> = vector![1.5, -2.5, 3.5, -4.5, 5.5]; + assert_eq!(-&v, vector![-1.5, 2.5, -3.5, 4.5, -5.5]); + } + + #[test] + fn test_equality() { + let v1 = vector![1, 2, 3, 4, 5]; + let v2 = vector![1, 2, 3, 4, 5]; + let v3 = vector![1, 2, 3, 4, 6]; + + assert_eq!(v1, v2); + assert_ne!(v1, v3); + + let m1 = matrix![[1, 2], [3, 4]]; + let m2 = matrix![[1, 2], [3, 4]]; + let m3 = matrix![[1, 2], [3, 5]]; + + assert_eq!(m1, m2); + assert_ne!(m1, m3); + } + + #[test] + fn test_higher_dimensional_arrays() { + let a1: Dimensional, 3> = + Dimensional::from_fn([2, 2, 2], |[i, j, k]| (i * 4 + j * 2 + k + 1) as i32); + let a2: Dimensional, 3> = + Dimensional::from_fn([2, 2, 2], |[i, j, k]| (8 - i * 4 - j * 2 - k) as i32); + + let sum = &a1 + &a2; + assert_eq!(sum.as_slice(), &[9; 8]); + + let product = &a1 * &a2; + assert_eq!(product.as_slice(), &[8, 14, 18, 20, 20, 18, 14, 8]); } } diff --git a/src/storage.rs b/src/storage.rs index 8c83221..e40f8a7 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -35,8 +35,11 @@ pub trait DimensionalStorage: /// * `data`: The data to initialize the array with. fn from_vec(shape: [usize; N], data: Vec) -> Self; - /// Returns a mutable slice of the underlying data from storage + /// Returns a mutable slice of the underlying data from storage. fn as_mut_slice(&mut self) -> &mut [T]; + + /// Returns an immutable slice of the underlying data from storage. + fn as_slice(&self) -> &[T]; } /// An enum representing the memory layout of a linear array. @@ -44,8 +47,7 @@ pub trait DimensionalStorage: pub enum LinearArrayLayout { /// Row-major layout (default). RowMajor, - // TODO: figure out if we want to support column-major layout - #[allow(dead_code)] + /// Column-major layout. ColumnMajor, } @@ -61,70 +63,38 @@ pub enum LinearArrayLayout { #[derive(Debug, Clone, PartialEq)] pub struct LinearArrayStorage { data: Vec, + shape: [usize; N], layout: LinearArrayLayout, strides: [usize; N], } -impl Index<[usize; N]> for LinearArrayStorage { - type Output = T; - - fn index(&self, index: [usize; N]) -> &Self::Output { - let linear_index = self.layout_index(index); - &self.data[linear_index] - } -} - -impl IndexMut<[usize; N]> for LinearArrayStorage { - fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { - let linear_index = self.layout_index(index); - &mut self.data[linear_index] - } -} - -impl DimensionalStorage for LinearArrayStorage { - fn zeros(shape: [usize; N]) -> Self { - let data = vec![T::zero(); shape.iter().product::()]; - LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) - } - - fn ones(shape: [usize; N]) -> Self { - let data = vec![T::one(); shape.iter().product::()]; - LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) - } - - fn from_vec(shape: [usize; N], data: Vec) -> Self { - LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor, 1) - } - - /// Returns a mutable slice of the underlying data. - fn as_mut_slice(&mut self) -> &mut [T] { - &mut self.data - } -} - impl LinearArrayStorage { /// Computes the strides for a given shape and layout. /// + /// In this implementation, strides represent the number of elements (not bytes) to skip + /// in each dimension when traversing the array. This approach simplifies indexing calculations + /// while still providing efficient access to elements. + /// /// # Arguments /// /// * `shape`: The shape of the array. - /// * `stride`: The base stride (usually 1). /// * `layout`: The memory layout of the array. - fn compute_strides( - shape: &[usize; N], - stride: &usize, - layout: &LinearArrayLayout, - ) -> [usize; N] { + /// + /// # Returns + /// + /// An array of strides, where each stride represents the number of elements to skip + /// in the corresponding dimension. + fn compute_strides(shape: &[usize; N], layout: &LinearArrayLayout) -> [usize; N] { let mut strides = [0; N]; match layout { LinearArrayLayout::RowMajor => { - strides[N - 1] = *stride; + strides[N - 1] = 1; for i in (0..N - 1).rev() { strides[i] = strides[i + 1] * shape[i + 1]; } } LinearArrayLayout::ColumnMajor => { - strides[0] = *stride; + strides[0] = 1; for i in 1..N { strides[i] = strides[i - 1] * shape[i - 1]; } @@ -135,23 +105,22 @@ impl LinearArrayStorage { /// Computes the linear index for a given multidimensional index. /// + /// This method calculates the position of an element in the underlying 1D vector + /// based on its multidimensional index and the array's strides. + /// /// # Arguments /// /// * `index`: The multidimensional index. + /// + /// # Returns + /// + /// The linear index in the underlying data vector. fn layout_index(&self, index: [usize; N]) -> usize { - match self.layout { - LinearArrayLayout::RowMajor => index - .iter() - .zip(self.strides.iter()) - .map(|(i, &stride)| i * stride) - .sum(), - LinearArrayLayout::ColumnMajor => index - .iter() - .rev() - .zip(self.strides.iter().rev()) - .map(|(i, &stride)| i * stride) - .sum(), - } + index + .iter() + .zip(self.strides.iter()) + .map(|(&i, &stride)| i * stride) + .sum() } /// Creates a new `LinearArrayStorage` with the given parameters. @@ -161,13 +130,189 @@ impl LinearArrayStorage { /// * `shape`: The shape of the array. /// * `data`: The data to initialize the array with. /// * `layout`: The memory layout of the array. - /// * `stride`: The base stride (usually 1). - pub fn new(shape: [usize; N], data: Vec, layout: LinearArrayLayout, stride: usize) -> Self { - let strides = Self::compute_strides(&shape, &stride, &layout); + /// + /// # Panics + /// + /// Panics if the length of `data` doesn't match the product of dimensions in `shape`. + pub fn new(shape: [usize; N], data: Vec, layout: LinearArrayLayout) -> Self { + assert_eq!( + shape.iter().product::(), + data.len(), + "Data length must match the product of shape dimensions" + ); + let strides = Self::compute_strides(&shape, &layout); Self { data, + shape, layout, strides, } } + + /// Returns the shape of the array. + pub fn shape(&self) -> &[usize; N] { + &self.shape + } + + /// Returns the layout of the array. + pub fn layout(&self) -> LinearArrayLayout { + self.layout + } + + /// Returns the strides of the array. + pub fn strides(&self) -> &[usize; N] { + &self.strides + } +} + +impl Index<[usize; N]> for LinearArrayStorage { + type Output = T; + + fn index(&self, index: [usize; N]) -> &Self::Output { + let linear_index = self.layout_index(index); + &self.data[linear_index] + } +} + +impl IndexMut<[usize; N]> for LinearArrayStorage { + fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { + let linear_index = self.layout_index(index); + &mut self.data[linear_index] + } +} + +impl DimensionalStorage for LinearArrayStorage { + fn zeros(shape: [usize; N]) -> Self { + let data = vec![T::zero(); shape.iter().product::()]; + LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor) + } + + fn ones(shape: [usize; N]) -> Self { + let data = vec![T::one(); shape.iter().product::()]; + LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor) + } + + fn from_vec(shape: [usize; N], data: Vec) -> Self { + LinearArrayStorage::new(shape, data, LinearArrayLayout::RowMajor) + } + + fn as_mut_slice(&mut self) -> &mut [T] { + &mut self.data + } + + fn as_slice(&self) -> &[T] { + &self.data + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_zeros_and_ones() { + let zeros = LinearArrayStorage::::zeros([2, 3]); + assert_eq!(zeros.as_slice(), &[0, 0, 0, 0, 0, 0]); + + let ones = LinearArrayStorage::::ones([2, 3]); + assert_eq!(ones.as_slice(), &[1, 1, 1, 1, 1, 1]); + } + + #[test] + fn test_from_vec() { + let data = vec![1, 2, 3, 4, 5, 6]; + let array = LinearArrayStorage::::from_vec([2, 3], data.clone()); + assert_eq!(array.as_slice(), &data); + } + + #[test] + #[should_panic(expected = "Data length must match the product of shape dimensions")] + fn test_from_vec_wrong_size() { + let data = vec![1, 2, 3, 4, 5]; + LinearArrayStorage::::from_vec([2, 3], data); + } + + #[test] + fn test_indexing() { + let data = vec![1, 2, 3, 4, 5, 6]; + let array = LinearArrayStorage::::from_vec([2, 3], data); + assert_eq!(array[[0, 0]], 1); + assert_eq!(array[[0, 2]], 3); + assert_eq!(array[[1, 1]], 5); + } + + #[test] + fn test_mutable_indexing() { + let data = vec![1, 2, 3, 4, 5, 6]; + let mut array = LinearArrayStorage::::from_vec([2, 3], data); + array[[0, 0]] = 10; + array[[1, 2]] = 20; + assert_eq!(array[[0, 0]], 10); + assert_eq!(array[[1, 2]], 20); + } + + #[test] + fn test_strides_calculation() { + let row_major = + LinearArrayStorage::::new([2, 3, 4], vec![0; 24], LinearArrayLayout::RowMajor); + assert_eq!(row_major.strides(), &[12, 4, 1]); + + let col_major = LinearArrayStorage::::new( + [2, 3, 4], + vec![0; 24], + LinearArrayLayout::ColumnMajor, + ); + assert_eq!(col_major.strides(), &[1, 2, 6]); + } + + #[test] + fn test_layout_index() { + let row_major = LinearArrayStorage::::new( + [2, 3, 4], + (0..24).collect(), + LinearArrayLayout::RowMajor, + ); + assert_eq!(row_major[[0, 0, 0]], 0); + assert_eq!(row_major[[1, 2, 3]], 23); + assert_eq!(row_major[[0, 1, 2]], 6); + + let col_major = LinearArrayStorage::::new( + [2, 3, 4], + (0..24).collect(), + LinearArrayLayout::ColumnMajor, + ); + assert_eq!(col_major[[0, 0, 0]], 0); + assert_eq!(col_major[[1, 2, 3]], 23); + assert_eq!(col_major[[0, 1, 2]], 14); + } + + #[test] + fn test_different_layouts() { + let data: Vec = (0..6).collect(); + + let row_major = LinearArrayStorage::new([2, 3], data.clone(), LinearArrayLayout::RowMajor); + assert_eq!(row_major[[0, 0]], 0); + assert_eq!(row_major[[0, 2]], 2); + assert_eq!(row_major[[1, 0]], 3); + + let col_major = LinearArrayStorage::new([2, 3], data, LinearArrayLayout::ColumnMajor); + assert_eq!(col_major[[0, 0]], 0); + assert_eq!(col_major[[0, 2]], 4); + assert_eq!(col_major[[1, 0]], 1); + } + + #[test] + fn test_as_slice_and_as_mut_slice() { + let mut array = LinearArrayStorage::::from_vec([2, 3], vec![1, 2, 3, 4, 5, 6]); + + assert_eq!(array.as_slice(), &[1, 2, 3, 4, 5, 6]); + + { + let slice = array.as_mut_slice(); + slice[0] = 10; + slice[5] = 60; + } + + assert_eq!(array.as_slice(), &[10, 2, 3, 4, 5, 60]); + } } From 80bcc68d3ebdcb90befa6165d3c296b0fd594941 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:16:38 -0400 Subject: [PATCH 12/18] fix: ergonomics/performance of element-wise assignments --- TODO.md | 5 ++++- src/lib.rs | 11 ++++++----- src/operators.rs | 42 +++++++++++++++++++++++------------------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/TODO.md b/TODO.md index 492b2d4..4cbf421 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,7 @@ - [ ] Add tensor macro for creating tensors - [ ] Remove the need for phantom data markers - [ ] Move shape data to type-system such that it is known at compile time -- [ ] Support scalar, tensor arithmetic operations +- [ * ] Support common arithmetic operations - [ ] Use safe rust in arithmetic operations - [ ] Support reshaping - [ ] Support appending @@ -15,6 +15,9 @@ - [ ] Use safe rust in storage backends - [ ] Linear algebra functions +The signature for creating a Dimensional is ugly in that N must be speficied twice, a generalized +builder pattern, or obviation of the need to store twice would be ideal. + In lib.rs: The TODO for the tensor macro is still present. This is not a bug, but a reminder for future implementation. diff --git a/src/lib.rs b/src/lib.rs index 45477a5..dec9fb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,15 +10,16 @@ //! # Dimensional Types //! //! - Scalar: A 0-dimensional object, or just the element of type `T` itself. -//! - Vector: A 1-dimensional array of elements of type `T`. -//! - Matrix: A 2-dimensional array of elements of type `T`. -//! - Tensor: An `N`-dimensional array of elements of type `T`, where N > 2. +//! - Vector: A 1-dimensional array of elements with the type `T`. +//! - Matrix: A 2-dimensional array of elements with the type `T`. +//! - Tensor: An `N`-dimensional array of elements with the type `T`, where N > 2. //! //! # Goals //! //! The primary goal of this library is to provide a flexible and efficient way to work with -//! multidimensional arrays of numeric types in Rust. The generic storage backend `S` allows -//! for different memory layouts and optimizations. +//! multidimensional arrays of numeric types in Rust. +//! +//! Using a generic storage backend, `S`, allows for different memory layouts and optimizations. //! //! # Convenience Macros //! diff --git a/src/operators.rs b/src/operators.rs index 3cbb825..b5ac066 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -168,7 +168,7 @@ where // Assignment operations /// Implements scalar addition assignment for Dimensional arrays. -impl AddAssign for Dimensional +impl AddAssign for Dimensional where S: DimensionalStorage, { @@ -178,7 +178,7 @@ where } /// Implements scalar subtraction assignment for Dimensional arrays. -impl SubAssign for Dimensional +impl SubAssign for Dimensional where S: DimensionalStorage, { @@ -188,7 +188,7 @@ where } /// Implements scalar multiplication assignment for Dimensional arrays. -impl MulAssign for Dimensional +impl MulAssign for Dimensional where S: DimensionalStorage, { @@ -198,7 +198,7 @@ where } /// Implements scalar division assignment for Dimensional arrays. -impl DivAssign for Dimensional +impl DivAssign for Dimensional where S: DimensionalStorage, { @@ -208,12 +208,12 @@ where } /// Implements element-wise addition assignment for Dimensional arrays. -impl AddAssign> +impl AddAssign<&Dimensional> for Dimensional where S: DimensionalStorage, { - fn add_assign(&mut self, rhs: Dimensional) { + fn add_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise addition assignment" @@ -223,12 +223,12 @@ where } /// Implements element-wise subtraction assignment for Dimensional arrays. -impl SubAssign> +impl SubAssign<&Dimensional> for Dimensional where S: DimensionalStorage, { - fn sub_assign(&mut self, rhs: Dimensional) { + fn sub_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise subtraction assignment" @@ -238,12 +238,12 @@ where } /// Implements element-wise multiplication assignment for Dimensional arrays. -impl MulAssign> +impl MulAssign<&Dimensional> for Dimensional where S: DimensionalStorage, { - fn mul_assign(&mut self, rhs: Dimensional) { + fn mul_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise multiplication assignment" @@ -253,12 +253,12 @@ where } /// Implements element-wise division assignment for Dimensional arrays. -impl DivAssign> +impl DivAssign<&Dimensional> for Dimensional where S: DimensionalStorage, { - fn div_assign(&mut self, rhs: Dimensional) { + fn div_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise division assignment" @@ -279,6 +279,10 @@ where } } +// TODO How much are these helper abstractions really helping? +// Seems like .zip .map etc should do it without these. +// We don't want bloat, we want a razor sharp and performant tool. + impl Dimensional where T: Num + Copy, @@ -377,16 +381,16 @@ mod tests { let mut v1 = vector![1, 2, 3, 4, 5]; let v2 = vector![5, 4, 3, 2, 1]; - v1 += v2.clone(); + v1 += &v2; assert_eq!(v1, vector![6, 6, 6, 6, 6]); - v1 -= v2.clone(); + v1 -= &v2; assert_eq!(v1, vector![1, 2, 3, 4, 5]); - v1 *= v2.clone(); + v1 *= &v2; assert_eq!(v1, vector![5, 8, 9, 8, 5]); - v1 /= v2.clone(); + v1 /= &v2; assert_eq!(v1, vector![1, 2, 3, 4, 5]); } @@ -419,13 +423,13 @@ mod tests { m3 /= 2; assert_eq!(m3, m1); - m3 += m2.clone(); + m3 += &m2; assert_eq!(m3, matrix![[6, 8], [10, 12]]); - m3 -= m2.clone(); + m3 -= &m2; assert_eq!(m3, m1); - m3 *= m2.clone(); + m3 *= &m2; assert_eq!(m3, matrix![[5, 12], [21, 32]]); // Note: We don't test m3 /= m2 here because it would result in a matrix of zeros due to integer division From eda792626d72817e39e9b258369de929bd19f4e0 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:29:25 -0400 Subject: [PATCH 13/18] docs: operators --- Cargo.toml | 2 +- README.md | 22 +++++++++++++--------- src/operators.rs | 31 ++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d1aa02..1a3e6a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" readme = "README.md" repository = "https://github.com/warlock-labs/dimensionals" name = "dimensionals" -version = "0.1.1" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 92c9a5a..5d349c2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ # dimensionals -Dimensionals is a Rust library for working with n-dimensional data. It provides a flexible and efficient multidimensional array implementation with a generic storage backend. +Dimensionals is a Rust library for working with n-dimensional data. It provides +a flexible and efficient multidimensional array implementation with a generic +storage backend. ## Motivations @@ -18,11 +20,10 @@ The key motivations behind Dimensionals are: ## Features -- Generic over element type and number of dimensions -- Efficient storage using a linear memory layout -- Index and mutable index operations -- Convenient macros for array creation -- Extensible with custom storage backends +- Generic over an element type, number of dimensions, and storage backend +- Iterators, slices, indexing, and other standard Rust traits +- Ergonomic and idiomatic `std::ops` implementations for arithmetic operations +- Convenient macros for vector and matrix creation ## Usage @@ -64,13 +65,15 @@ The following features and improvements are planned for future releases: ## Performance -The `LinearArrayStorage` backend stores elements in a contiguous `Vec` and computes element indices on the fly. This provides good cache locality for traversals, but may not be optimal for sparse or very high dimensional arrays. +The `LinearArrayStorage` backend stores elements in a contiguous `Vec` and computes element indices on the fly. This +provides good cache locality for traversals, but may not be optimal for sparse or very high dimensional arrays. Alternative storage backends can be implemented by defining a type that implements the `DimensionalStorage` trait. ## Contributing -Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests on the [GitHub repository](https://github.com/warlock-labs/dimensionals). +Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests on +the [GitHub repository](https://github.com/warlock-labs/dimensionals). ## License @@ -78,7 +81,8 @@ This project is licensed under the [MIT License](https://choosealicense.com/lice ## Acknowledgements -This project is inspired by and builds upon ideas from several existing multidimensional array libraries in Rust and other languages. +This project is inspired by and builds upon ideas from several existing multidimensional array libraries in Rust and +other languages. ## Contact diff --git a/src/operators.rs b/src/operators.rs index b5ac066..72f668a 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -11,6 +11,7 @@ where { type Output = T; + /// Returns an index into the array using a multidimensional index à la [i, j, k]. fn index(&self, index: [usize; N]) -> &Self::Output { &self.storage[index] } @@ -21,16 +22,18 @@ impl IndexMut<[usize; N]> for Dimensional, { + /// Returns a mutable index into the array using a multidimensional index à la [i, j, k]. fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output { &mut self.storage[index] } } -/// Implements equality comparison for Dimensional arrays. +/// Implements partial equality comparison for Dimensional arrays. impl PartialEq for Dimensional where S: DimensionalStorage, { + /// Compares two `Dimensional` arrays for partial equality. fn eq(&self, other: &Self) -> bool { if self.shape != other.shape { return false; @@ -40,6 +43,7 @@ where } } +/// Implements equality comparison for Dimensional arrays. impl Eq for Dimensional where S: DimensionalStorage { @@ -54,6 +58,7 @@ where { type Output = Dimensional; + /// Adds a scalar to a `Dimensional` array element-wise. fn add(self, rhs: T) -> Self::Output { self.map(|x| x + rhs) } @@ -66,6 +71,7 @@ where { type Output = Dimensional; + /// Subtracts a scalar from a `Dimensional` array element-wise. fn sub(self, rhs: T) -> Self::Output { self.map(|x| x - rhs) } @@ -78,6 +84,7 @@ where { type Output = Dimensional; + /// Multiplies a `Dimensional` array by a scalar element-wise. fn mul(self, rhs: T) -> Self::Output { self.map(|x| x * rhs) } @@ -90,6 +97,7 @@ where { type Output = Dimensional; + // Divides a `Dimensional` array by a scalar element-wise. fn div(self, rhs: T) -> Self::Output { self.map(|x| x / rhs) } @@ -104,6 +112,7 @@ where { type Output = Dimensional; + /// Adds two `Dimensional` arrays element-wise. fn add(self, rhs: Self) -> Self::Output { assert_eq!( self.shape(), @@ -121,6 +130,7 @@ where { type Output = Dimensional; + /// Subtracts one `Dimensional` array from another element-wise. fn sub(self, rhs: Self) -> Self::Output { assert_eq!( self.shape(), @@ -138,6 +148,7 @@ where { type Output = Dimensional; + /// Multiplies two `Dimensional` arrays element-wise. fn mul(self, rhs: Self) -> Self::Output { assert_eq!( self.shape(), @@ -155,6 +166,7 @@ where { type Output = Dimensional; + /// Divides one `Dimensional` array by another element-wise. fn div(self, rhs: Self) -> Self::Output { assert_eq!( self.shape(), @@ -172,6 +184,7 @@ impl AddAssign for Dimensional< where S: DimensionalStorage, { + /// Adds a scalar to a `Dimensional` array element-wise in-place. fn add_assign(&mut self, rhs: T) { self.map_inplace(|x| *x += rhs); } @@ -182,6 +195,7 @@ impl SubAssign for Dimensional< where S: DimensionalStorage, { + /// Subtracts a scalar from a `Dimensional` array element-wise in-place. fn sub_assign(&mut self, rhs: T) { self.map_inplace(|x| *x -= rhs); } @@ -192,6 +206,7 @@ impl MulAssign for Dimensional< where S: DimensionalStorage, { + /// Multiplies a `Dimensional` array by a scalar element-wise in-place. fn mul_assign(&mut self, rhs: T) { self.map_inplace(|x| *x *= rhs); } @@ -202,6 +217,7 @@ impl DivAssign for Dimensional< where S: DimensionalStorage, { + /// Divides a `Dimensional` array by a scalar element-wise in-place. fn div_assign(&mut self, rhs: T) { self.map_inplace(|x| *x /= rhs); } @@ -213,12 +229,13 @@ impl AddAssign<&Dimensional, { + /// Adds two `Dimensional` arrays element-wise in-place. fn add_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise addition assignment" ); - self.zip_map_inplace(&rhs, |a, b| *a += b); + self.zip_map_inplace(rhs, |a, b| *a += b); } } @@ -228,12 +245,13 @@ impl SubAssign<&Dimensional, { + /// Subtracts one `Dimensional` array from another element-wise in-place. fn sub_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise subtraction assignment" ); - self.zip_map_inplace(&rhs, |a, b| *a -= b); + self.zip_map_inplace(rhs, |a, b| *a -= b); } } @@ -243,12 +261,13 @@ impl MulAssign<&Dimensional, { + /// Multiplies two `Dimensional` arrays element-wise in-place. fn mul_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise multiplication assignment" ); - self.zip_map_inplace(&rhs, |a, b| *a *= b); + self.zip_map_inplace(rhs, |a, b| *a *= b); } } @@ -258,12 +277,13 @@ impl DivAssign<&Dimensional, { + /// Divides one `Dimensional` array by another element-wise in-place. fn div_assign(&mut self, rhs: &Dimensional) { assert_eq!( self.shape, rhs.shape, "Shapes must match for element-wise division assignment" ); - self.zip_map_inplace(&rhs, |a, b| *a /= b); + self.zip_map_inplace(rhs, |a, b| *a /= b); } } @@ -274,6 +294,7 @@ where { type Output = Dimensional; + /// Negates a `Dimensional` array element-wise. fn neg(self) -> Self::Output { self.map(|x| -x) } From 6a815497ad19c58aa0e8b3ad03ef9340f3e8149c Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:35:27 -0400 Subject: [PATCH 14/18] docs: general improvement of documentation --- TODO.md | 23 +++++++++++++++++------ goals.md | 5 ----- 2 files changed, 17 insertions(+), 11 deletions(-) delete mode 100644 goals.md diff --git a/TODO.md b/TODO.md index 4cbf421..562efa6 100644 --- a/TODO.md +++ b/TODO.md @@ -4,16 +4,27 @@ - [ ] Remove the need for phantom data markers - [ ] Move shape data to type-system such that it is known at compile time - [ * ] Support common arithmetic operations -- [ ] Use safe rust in arithmetic operations +- [ * ] Use safe rust in arithmetic operations - [ ] Support reshaping - [ ] Support appending - [ ] Support removing -- [ ] Support Apache Arrow or safetensors storage backend -- [ ] Support Pola.rs integration -- [ ] Use safe rust in reshaping -- [ ] Use safe rust in appending -- [ ] Use safe rust in storage backends - [ ] Linear algebra functions +- [ ] Support for common statistical functions +- [ ] Support for geometric functions like Brownian motion +- [ ] Support for GPU offloading +- [ ] Support for SIMD +- [ ] Matrix multiplication +- [ ] Support Apache Arrow or safetensors storage backend? +- [ ] Support Pola.rs integration +- [ ] Support plotly-rs integration +- [ ] Support argmin-rs integration + + +The goal is for this library to have an intuitive interface for working with +n dimensional/tensor data, while interfacing with libraries like Polars, and +by proxy plotly-rs. It should have linear algebra functionality, and integration +with argmin-rs for optimization. It should have common statistical functions +such that it is able to work in a statistical computing environment. The signature for creating a Dimensional is ugly in that N must be speficied twice, a generalized builder pattern, or obviation of the need to store twice would be ideal. diff --git a/goals.md b/goals.md deleted file mode 100644 index 34b8c6a..0000000 --- a/goals.md +++ /dev/null @@ -1,5 +0,0 @@ -The goal is for this library to have an intuitive interface for working with -n dimensional/tensor data, while interfacing with libraries like Polars, and -by proxy plotly-rs. It should have linear algebra functionality, and integration -with argmin-rs for optimization. It should have common statistical functions -such that it is able to work in a statistical computing environment. \ No newline at end of file From 67ca88851fd0248e912337c830ea48ca8730a8cc Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:38:27 -0400 Subject: [PATCH 15/18] fix: run CI on pulls to main --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d7228dd..c12059b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -3,13 +3,13 @@ name: CI on: push: branches: - - master + - main tags: - 'v*.*.*' pull_request: types: [ opened, synchronize, reopened ] branches: - - master + - main env: CARGO_TERM_COLOR: always From cb54b2805bdbb547731dd572f8d93d8c744546ef Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:43:46 -0400 Subject: [PATCH 16/18] fix: use num-traits rather than num --- Cargo.toml | 4 ++-- TODO.md | 6 ++++++ src/core.rs | 2 +- src/iterators.rs | 2 +- src/lib.rs | 2 +- src/operators.rs | 2 +- src/storage.rs | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a3e6a2..12c8b48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -num = "0.4.3" +num-traits = "0.2.19" [lib] @@ -23,4 +23,4 @@ criterion = "0.5.1" [[bench]] name = "benchmarks" -harness = false \ No newline at end of file +harness = false diff --git a/TODO.md b/TODO.md index 562efa6..290ce54 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,12 @@ - [ ] Support Pola.rs integration - [ ] Support plotly-rs integration - [ ] Support argmin-rs integration +- [ ] Support for rayon +- [ ] Feature flags for enabling/disabling features +- [ ] no-std +- [ ] Support for WebAssembly +- [ ] Support for WebGPU +- [ ] Support for SVM target The goal is for this library to have an intuitive interface for working with diff --git a/src/core.rs b/src/core.rs index 6a7ffe8..6995216 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,5 +1,5 @@ use crate::storage::DimensionalStorage; -use num::Num; +use num_traits::Num; use std::marker::PhantomData; /// A multidimensional array type. diff --git a/src/iterators.rs b/src/iterators.rs index 2f9a4aa..fac7dde 100644 --- a/src/iterators.rs +++ b/src/iterators.rs @@ -3,7 +3,7 @@ //! traversal and modification of Dimensional arrays. use crate::{storage::DimensionalStorage, Dimensional}; -use num::Num; +use num_traits::Num; /// An iterator over the elements of a Dimensional array. /// diff --git a/src/lib.rs b/src/lib.rs index dec9fb6..5933e15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ mod operators; mod storage; // Public API -pub use core::Dimensional; +pub use crate::core::Dimensional; pub use iterators::*; pub use storage::DimensionalStorage; pub use storage::LinearArrayStorage; diff --git a/src/operators.rs b/src/operators.rs index 72f668a..0de3da0 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -1,5 +1,5 @@ use crate::{storage::DimensionalStorage, Dimensional}; -use num::Num; +use num_traits::Num; use std::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, }; diff --git a/src/storage.rs b/src/storage.rs index e40f8a7..fd48908 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,4 +1,4 @@ -use num::Num; +use num_traits::Num; use std::ops::{Index, IndexMut}; /// A trait for storage backends for multidimensional arrays. From 2cc3d84dc91be0ff44d905a6c2ff0b9a4b163ce5 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:47:55 -0400 Subject: [PATCH 17/18] chore: cleanup --- src/storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage.rs b/src/storage.rs index fd48908..b4356ba 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -133,7 +133,7 @@ impl LinearArrayStorage { /// /// # Panics /// - /// Panics if the length of `data` doesn't match the product of dimensions in `shape`. + /// Panic if the length of `data` doesn't match the product of dimensions in `shape`. pub fn new(shape: [usize; N], data: Vec, layout: LinearArrayLayout) -> Self { assert_eq!( shape.iter().product::(), From a60d892ec0ff6f979450f24030851f881c560904 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Thu, 20 Jun 2024 18:51:51 -0400 Subject: [PATCH 18/18] chore: bump dependencies --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 12c8b48..dfca7ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" readme = "README.md" repository = "https://github.com/warlock-labs/dimensionals" name = "dimensionals" -version = "0.2.0" +version = "0.2.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html