Tensor creation

In many cases, we import RSTSR by following code:

#![allow(unused)]
fn main() {
use rstsr_core::prelude::*;
}

It may be possible that names of RSTSR structs or functions clash with other crates. You may wish to import RSTSR by following code if that happens:

#![allow(unused)]
fn main() {
use rstsr_core::prelude::rstsr as rt;
use rt::rstsr_traits::*;
}

1. Converting Rust Vector to RSTSR Tensor

1.1 1-D tensor from rust vector

RSTSR tensor can be created by (owned) vector object.

In the following case, memory of vector object vec will be transferred to tensor object tensor1. Except for relatively small overhead (generating layout of tensor), no explicit data copy occurs.

#![allow(unused)]
fn main() {
    // move ownership of vec to 1-D tensor (default CPU device)
    let vec = vec![1.0, 2.968, 3.789, 4.35, 5.575];
    let tensor = rt::asarray(vec);

    // only print 2 decimal places
    println!("{:.2}", tensor);
    // output: [ 1.00 2.97 3.79 4.35 5.58]
}
1

This will generate tensor object for default CPU device. Without further configuration, RSTSR chooses DeviceFaer as the default tensor device, with all threads visible to rayon. If other devices are of interest (such as single-threaded DeviceCpuSerial), or you may wish to confine number of threads for DeviceFaer, then you may wish to apply another version of asarray. For example, to limit 4 threads when performing computation, you may initialize tensor by the following code:

#![allow(unused)]
fn main() {
    // move ownership of vec to 1-D tensor
    // custom CPU device that limits threads to 4
    let vec = vec![1, 2, 3, 4, 5];
    let device = DeviceFaer::new(4);
    let tensor = rt::asarray((vec, &device));
    println!("{:?}", tensor);

    // output:
    // === Debug Tensor Print ===
    // [ 1 2 3 4 5]
    // DeviceFaer { base: DeviceCpuRayon { num_threads: 4 } }
    // 1-Dim, contiguous: CcFf
    // shape: [5], stride: [1], offset: 0
    // Type: rstsr_core::tensorbase::TensorBase<rstsr_core::tensor::data::DataOwned<rstsr_core::storage::device::Storage<i32, rstsr_core::device_faer::device::DeviceFaer>>, [usize; 1]>
}

1.2 -D tensor from rust vector

For -D tensor, the recommended way to build from existing vector, without explicit memory copy, is

  • first, build 1-D tensor from contiguous memory;
  • second, reshape to the -D tensor you desire;
#![allow(unused)]
fn main() {
    // generate 2-D tensor from 1-D vec, without explicit data copy
    let vec = vec![1, 2, 3, 4, 5, 6];
    let tensor = rt::asarray(vec).into_shape_assume_contig([2, 3]);
    println!("{:}", tensor);

    // if you feel function `into_shape_assume_contig` ugly, following code also works
    let vec = vec![1, 2, 3, 4, 5, 6];
    let tensor = rt::asarray(vec).into_shape([2, 3]);
    println!("{:}", tensor);

    // and even more concise
    let vec = vec![1, 2, 3, 4, 5, 6];
    let tensor = rt::asarray((vec, [2, 3]));
    println!("{:}", tensor);

    // output:
    // [[ 1 2 3]
    //  [ 4 5 6]]
}

We do not recommend generating -D tensor from nested vectors, i.e. Vec<Vec<T>>. Explicit memory copy will always occur anyway in this case. So for nested vectors, you may wish to first generate a flattened Vec<T>, then perform reshape on this:

#![allow(unused)]
fn main() {
    let vec = vec![vec![1, 2, 3], vec![4, 5, 6]];

    // generate 2-D tensor from nested Vec<T>, WITH EXPLICIT DATA COPY
    // so this is not recommended for large data
    let (nrow, ncol) = (vec.len(), vec[0].len());
    let vec = vec.into_iter().flatten().collect::<Vec<_>>();

    // please also note that nested vec is always row-major, so using `.c()` is more appropriate
    let tensor = rt::asarray((vec, [nrow, ncol].c()));
    println!("{:}", tensor);
    // output:
    // [[ 1 2 3]
    //  [ 4 5 6]]
}

2. Converting Rust Slices to RSTSR TensorView

Rust language is extremely sensitive to ownership of variables, unlike python. For rust, reference of contiguous memory of data is usually represented as slice &[T]. For RSTSR, this is stored by TensorView2.

#![allow(unused)]
fn main() {
    // generate 1-D tensor view from &[T], without data copy
    let vec = vec![1, 2, 3, 4, 5, 6];
    let tensor = rt::asarray(&vec);

    // note `tensor` is TensorView instead of Tensor, so it doesn't own data
    println!("{:?}", tensor);

    // check if pointer of vec and tensor's storage are the same
    assert_eq!(vec.as_ptr(), tensor.storage().rawvec().as_ptr());

    // output:
    // === Debug Tensor Print ===
    // [ 1 2 3 4 5 6]
    // DeviceFaer { base: DeviceCpuRayon { num_threads: 0 } }
    // 1-Dim, contiguous: CcFf
    // shape: [6], stride: [1], offset: 0
    // Type: rstsr_core::tensorbase::TensorBase<rstsr_core::tensor::data::DataRef<rstsr_core::storage::device::Storage<i32, rstsr_core::device_faer::device::DeviceFaer>>, [usize; 1]>
}

You may also convert mutable slice &mut [T] into tensor. For RSTSR, this is stored by TensorMut:

#![allow(unused)]
fn main() {
    // generate 2-D tensor mutable view from &mut [T], without data copy
    let mut vec = vec![1, 2, 3, 4, 5, 6];
    let mut tensor = rt::asarray((&mut vec, [2, 3]));

    // you may perform arithmetic operations on `tensor`
    tensor *= 2;
    println!("{:}", tensor);
    // output:
    // [[ 2 4 6]
    //  [ 8 10 12]]

    // you may also see variable `vec` is also changed
    println!("{:?}", vec);
    // output: [2, 4, 6, 8, 10, 12]
}
2

Initialization of TensorView by rust slices &[T] is performed by ManuallyDrop internally. For the data types T that scientific computation concerns (such as f64, Complex<f64>), it will not cause memory leak. However, if type T has its own deconstructor (drop function), you may wish to double check for memory leak safety. This also applies to TensorMut by mutable rust slices &mut [T].

3. Intrinsic RSTSR Tensor Creation Functions

3.1 1-D tensor creation functions

Most useful 1-D tensor creation functions are arange and linspace.

arange creates tensors with regularly incrementing values. Following code shows multiple ways to generate tensor3.

#![allow(unused)]
fn main() {
    let tensor = rt::arange(10);
    println!("{:}", tensor);
    // output: [ 0 1 2 ... 7 8 9]

    let device = DeviceFaer::new(4);
    let tensor = rt::arange((2.0, 10.0, &device));
    println!("{:}", tensor);
    // output: [ 2 3 4 5 6 7 8 9]

    let tensor = rt::arange((2.0, 3.0, 0.1));
    println!("{:}", tensor);
    // output: [ 2 2.1 2.2 ... 2.7000000000000006 2.8000000000000007 2.900000000000001]
}
3

Many RSTSR functions, especially tensor creation functions, are signature-overloaded. Input should be wrapped by tuple to pass multiple function parameters.

linspace will create tensors with a specified number of elements, and spaced equally between the specified beginning and end values.

#![allow(unused)]
fn main() {
    use num::complex::c64;

    let tensor = rt::linspace((0.0, 10.0, 11));
    println!("{:}", tensor);
    // output: [ 0 1 2 ... 8 9 10]

    let tensor = rt::linspace((c64(1.0, 2.0), c64(-15.0, 10.0), 5, &DeviceFaer::new(4)));
    println!("{:}", tensor);
    // output: [ 1+2i -3+4i -7+6i -11+8i -15+10i]
}

3.2 2-D tensor creation functions

Most useful 2-D tensor creation functions are eye and diag.

eye generates identity matrix. In many cases, you may just provide the number of rows, and eye(n_row) will return a square identity matrix, or eye((n_row, &device)) if device is of concern. If you may wish to generate a rectangular identity matrix with offset, you may call eye((n_row, n_col, offset)).

#![allow(unused)]
fn main() {
    let device = DeviceFaer::new(4);
    let tensor: Tensor<f64, _> = rt::eye((3, &device));
    println!("{:}", tensor);
    // output:
    // [[ 1 0 0]
    //  [ 0 1 0]
    //  [ 0 0 1]]

    let tensor: Tensor<f64, _> = rt::eye((3, 4, -1));
    println!("{:}", tensor);
    // output:
    // [[ 0 0 0 0]
    //  [ 1 0 0 0]
    //  [ 0 1 0 0]]
}

diag generates diagonal 2-D tensor from 1-D tensor, or generate 1-D tensor from diagonal of 2-D tensor. diag is defined as overloaded function; if offset of diagonal is of concern, you may wish to call diag((&tensor, offset)).

#![allow(unused)]
fn main() {
    let vec = rt::arange(3) + 1;
    let tensor = vec.diag();
    println!("{:}", tensor);
    // output:
    // [[ 1 0 0]
    //  [ 0 2 0]
    //  [ 0 0 3]]

    let tensor = rt::arange(9).into_shape([3, 3]);
    let diag = tensor.diag();
    println!("{:}", diag);
    // output: [ 0 4 8]
}

3.3 General -D tensor creation functions

Most useful -D tensor creation functions are zeros, ones, empty. These functions can build tensors with any desired shape (or layout).

  • zeros fill tensor with all zero values;
  • ones fill tensor with all one values;
  • unsafe empty give tensor with uninitialized values;
  • fill fill tensor with the same value provided by user;

We will mostly use zeros as example. For common usages, you may wish to generate a tensor with shape (or additionally device bounded to tensor):

#![allow(unused)]
fn main() {
    // generate tensor with default device
    let tensor: Tensor<f64, _> = rt::zeros([2, 2, 3]); // Tensor<f64, Ix3>
    println!("{:}", tensor);
    // output:
    // [[[ 0 0 0]
    //   [ 0 0 0]]
    //
    //  [[ 0 0 0]
    //   [ 0 0 0]]]

    // generate tensor with custom device
    // note: the third type annotation refers to device type, hence is required if not default device
    // Tensor<f64, Ix2, DeviceCpuSerial>
    let tensor: Tensor<f64, _, _> = rt::zeros(([3, 4], &DeviceCpuSerial));
    println!("{:}", tensor);
    // output:
    // [[ 0 0 0 0]
    //  [ 0 0 0 0]
    //  [ 0 0 0 0]]
}

You may also specify layout: whether it is c-contiguous (row-major) or f-contiguous (column-major)4. In RSTSR, attribute function c and f are used for generating c/f-contiguous layouts:

#![allow(unused)]
fn main() {
    // generate tensor with c-contiguous
    let tensor: Tensor<f64, _> = rt::zeros([2, 2, 3].c());
    println!("shape: {:?}, stride: {:?}", tensor.shape(), tensor.stride());
    // output: shape: [2, 2, 3], stride: [6, 3, 1]

    // generate tensor with f-contiguous
    let tensor: Tensor<f64, _> = rt::zeros([2, 2, 3].f());
    println!("shape: {:?}, stride: {:?}", tensor.shape(), tensor.stride());
    // output: shape: [2, 2, 3], stride: [1, 2, 4]
}

A special -D case is 0-D tensor (scalar). You may also generate 0-D tensor by zeros:

#![allow(unused)]
fn main() {
    // generate 0-D tensor
    let mut a: Tensor<f64, _> = rt::zeros([]);
    println!("{:}", a);
    // output: 0

    // 0-D tensor arithmetics are also valid
    a += 2.0;
    println!("{:}", a);
    // output: 2

    let b = rt::arange(3.0) + 1.0;
    let c = a + b;
    println!("{:}", c);
    // output: [ 3 4 5]
}

You may also initialize a tensor without filling specific values. This is unsafe.

#![allow(unused)]
fn main() {
    // generate empty tensor with default device
    let tensor: Tensor<i32, _> = unsafe { rt::empty([10, 10]) };
    println!("{:?}", tensor);
}

This crate has not implemented API for random initialization. However, you may still able to perform this kind of task by asarray.

#![allow(unused)]
fn main() {
    use rand::rngs::StdRng;
    use rand::{Rng, SeedableRng};

    // generate f-contiguous layout and it's memory buffer size
    let layout = [2, 3].f();
    let size = layout.size();

    // generate random numbers to vector
    let seed: u64 = 42;
    let mut rng = StdRng::seed_from_u64(seed);
    let random_vec: Vec<f64> = (0..size).map(|_| rng.gen()).collect();

    // create a tensor from random vector and f-contiguous layout
    let tensor = rt::asarray((random_vec, layout));

    // print tensor with 3 decimal places with width of 7
    println!("{:7.3}", tensor);
}