nicowagner.me

Linear Algebra with Rust (1) - Vectors

Recently, I've started reading the book Mathematics for Machine Learning (M. Deisenroth, A. Faisal, and C. Ong) to learn more about the underlying mathematics of modern data science and machine learning techniques. The first three chapters are about linear algebra, analytics geometry and matrix decompositions. At this point, I stopped reading on and wanted to consolidate what I had learned using the Rust programming language and the ndarray crate.

Vectors

A vector is a finite list of numbers, whereby the order matters. Typically, a vector is written as a vertical array in square or curved brackets. For example, a vector $\mathbf{x}$ with the three elements (components) $1$, $2$ and $3$ is written as

$$ \mathbf{x} = \begin{bmatrix} 1\\ 2\\ 3\\ \end{bmatrix}. $$

The size or dimension of the vector is the number of elements of the vector; the size of $\mathbf{x}$ is $3$. The $i$th element of a vector $\mathbf{x}$ is denoted with a subscript $i$ that indicates the position of that element within the vector ($\mathbf{x}_2$ is the second element of $\mathbf{x}$). Two vectors $\mathbf{x}$ and $\mathbf{y}$ are equal, if both have the same size and the $i$th element of $\mathbf{x}$ is equal to the $i$th element of $\mathbf{y}$ for all $i$ from $1$ to $n$.

ndarray

That's enough theory for now, let's write some Rust code using the ndarray crate that is widely used alongside with the nalgebra crate.

The crate ndarray provides the Array data type to build $n$-dimensional arrays. This type is a type alias, which makes the elements of the underlying ArrayBase type owned by that array. The term dimension has a slightly different meaning in ndarray. It stands for the number of axes of the array. An array that represents a vector has the dimension 1 (1 axis), an array that represents a matrix is 2-dimensional (2 axes), and so on. Thus, we need a 1-dimensional array to create a vector. Fortunately, there is already a type alias Array1 for the frequent case of one dimension. This type is only generic over underlying data type.

Creating an Array

After finding the right data type, it is quite easy to create the corresponding vector. The following code creates a vector (1-dimensional array) from the Rust built-in type Vec and displays it in the debug output on the console.

use ndarray::Array1;

fn main() {
    let x = Array1::<u64>::from_vec(vec![1, 2, 3]);
    println!("x = {x:?}");
}

The debug output looks like this:

$ cargo run -q
x = [1, 2, 3], shape=[3], strides=[1], layout=CFcf (0xf), const ndim=1

This output tells us, that the one-dimensional (const ndim=1) array $\mathbf{x}$ contains/owns the elements $1$, $2$ and $3$ (x = [1, 2, 3]) which are grouped in a contiguous block of three elements (shape=[3])1.

Size of an Array

To len function returns the size or the mathematical definition of dimension:

use ndarray::Array1;

fn main() {
    let x = Array1::<u64>::from_vec(vec![1, 2, 3]);
    println!("The size of x = {x} is {}.", x.len());
}

It outputs

$ cargo run -q
The size of x = [1, 2, 3] is 3.

Elements of an Array

The get function provides access to the elements of an array. In the one-dimensional case it's enough to pass the position of the element, whereby the index runs from $0$ to $n-1$. This function acts the same as the most built-in collection types. It returns a reference to the corresponding element wrapped in an Option. If the element doesn't exist None is returned.

use ndarray::Array1;

fn main() {
    let x = Array1::<u64>::from_vec(vec![1, 2, 3]);
    println!("x1 = {:?}", x.get(0));
    println!("x2 = {:?}", x.get(1));
    println!("x3 = {:?}", x.get(2));
    println!("x4 = {:?}", x.get(3));
}

The program prints the elements as expected:

$ cargo run -q
x1 = Some(1)
x2 = Some(2)
x3 = Some(3)
x4 = None

Of course, ndarray provides even better options for accessing the element, performing calculations or manipulations. This will be the subject of another post.

1

I'll explain the meaning of strides and layout in a later post.