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 tensor
1.
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] }
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 TensorView
2.
#![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] }
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] }
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); }