NDArray
About This Module
Prework
Prework Reading
Pre-lecture Reflections
Lecture
Learning Objectives
-
An -dimensional (e.g. 1-D, 2-D, 3-D, ...) container for general elements and numerics.
-
Similar to Python's Numpy but with important differences.
-
Rich set of methods for creating views and slices into the array
-
Provides a rich set of methods and mathematical operations
Key similarities with Python Numpy (from link)
- Arrays have single element types
- Arrays can have arbitrary dimensions
- Arrays can have arbitrary strides
- Indexing starts at 0
- Default ordering is row-major (more on that below)
- Arithmetic operators (+, -, *, /) perform element-wise operations
- Arrays that are not views are contiguous in memory
- Many cheap operations that return views into the array instead of copying data
Some important differences from Numpy
- Numpy arrays and views are all mutable and can all change the contents of an array.
- NDarrays have:
- flavors that can change contents,
- flavors that cannot, and
- flavors that make copies when things change.
- Numpy arrays are always dynamic in their number of dimensions. NDarrays can be static or dynamic.
- Slices with negative steps behave differently (more on that later)
To use it
To add latest ndarray crate.
% cargo add ndarray
or manually add
[dependencies]
ndarray="0.15.6"
or later in your Cargo.toml file
Why use NDarray over Vec or array?
It is easier to do a bunch of things like:
- Data Cleaning and Preprocessing offering functions like slicing and fill
- Statistics built in (lots of math functions come with it)
- Machine learning: Used in many of the ML libraries written in Rust
- Linear Algebra: Built in methods like matrix inversion, multiplication and decomposition.
Let's look at some example usage.
// This is required in a Jupyter notebook. // For a cargo project, you would add it to your Cargo.toml file. :dep ndarray = { version = "^0.15.6" } use ndarray::prelude::*; fn main() { let a = array![ // handy macro for creating arrays [1.,2.,3.], [4.,5.,6.], ]; assert_eq!(a.ndim(), 2); // get the number of dimensions of array a assert_eq!(a.len(), 6); // get the number of elements in array a assert_eq!(a.shape(), [2, 3]); // get the shape of array a assert_eq!(a.is_empty(), false); // check if the array has zero elements println!("Print the array with debug formatting:"); println!("{:?}", a); println!("\nPrint the array with display formatting:"); println!("{}", a); } main();
Print the array with debug formatting:
[[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]], shape=[2, 3], strides=[3, 1], layout=Cc (0x5), const ndim=2
Print the array with display formatting:
[[1, 2, 3],
For a side by side comparison NumPy, see https://docs.rs/ndarray/latest/ndarray/doc/ndarray_for_numpy_users/index.html
Array Creation
| NumPy | ndarray | Notes |
|---|---|---|
| np.array([[1.,2.,3.], [4.,5.,6.]]) | array![[1.,2.,3.], [4.,5.,6.]] or arr2(&[[1.,2.,3.], [4.,5.,6.]]) | 2×3 floating-point array literal |
| np.arange(0., 10., 0.5) | Array::range(0., 10., 0.5) | create a 1-D array with values 0., 0.5, …, 9.5 |
| np.linspace(0., 10., 11) | Array::linspace(0., 10., 11) | create a 1-D array with 11 elements with values 0., …, 10. |
| np.logspace(2.0, 3.0, num=4, base=10.0) | Array::logspace(10.0, 2.0, 3.0, 4) | create a 1-D array with 4 logarithmically spaced elements with values 100., 215.4, 464.1, 1000. |
| np.geomspace(1., 1000., num=4) | Array::geomspace(1e0, 1e3, 4) | create a 1-D array with 4 geometrically spaced elements from 1 to 1,000 inclusive: 1., 10., 100., 1000. |
| np.ones((3, 4, 5)) | Array::ones((3, 4, 5)) | create a 3×4×5 array filled with ones (inferring the element type) |
| np.zeros((3, 4, 5)) | Array::zeros((3, 4, 5)) | create a 3×4×5 array filled with zeros (inferring the element type) |
| np.zeros((3, 4, 5), order='F') | Array::zeros((3, 4, 5).f()) | create a 3×4×5 array with Fortran (column-major) memory layout filled with zeros (inferring the element type) |
| np.zeros_like(a, order='C') | Array::zeros(a.raw_dim()) | create an array of zeros of the shape shape as a, with row-major memory layout (unlike NumPy, this infers the element type from context instead of duplicating a’s element type) |
| np.full((3, 4), 7.) | Array::from_elem((3, 4), 7.) | create a 3×4 array filled with the value 7. |
| np.array([1, 2, 3, 4]).reshape((2, 2)) | Array::from_shape_vec((2, 2), vec![1, 2, 3, 4])? or .into_shape((2,2).unwrap() | create a 2×2 array from the elements in the Vec |
#![allow(unused)] fn main() { let a:Array<f64, Ix1> = Array::linspace(0., 4.5, 10); println!("{:?}", a); let b:Array<f64, _> = a.into_shape((2,5)).unwrap(); println!("\n{:?}", b); let c:Array<f64, _> = Array::zeros((3,4).f()); println!("\n{:?}", c); }
[4, 5, 6]]
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5], shape=[10], strides=[1], layout=CFcf (0xf), const ndim=1
[[0.0, 0.5, 1.0, 1.5, 2.0],
[2.5, 3.0, 3.5, 4.0, 4.5]], shape=[2, 5], strides=[5, 1], layout=Cc (0x5), const ndim=2
Good overview at this TDS Post, but subscription required.
#![allow(unused)] fn main() { :dep ndarray = { version = "^0.15.6" } // Not working on Jupyter notebook //:dep ndarray-rand = { version = "^0.15.0" } use ndarray::{Array, ShapeBuilder}; // Not working on Jupyter notebook //use ndarray_rand::RandomExt; //use ndarray_rand::rand_distr::Uniform; // Ones let ones = Array::<f64, _>::ones((1, 4)); println!("{:?}", ones); // Output: // [[1.0, 1.0, 1.0, 1.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2 // Range let range = Array::<f64, _>::range(0., 5., 1.); println!("{:?}", range); // Output: // [0.0, 1.0, 2.0, 3.0, 4.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1 // Linspace let linspace = Array::<f64, _>::linspace(0., 5., 5); println!("{:?}", linspace); // Output: // [0.0, 1.25, 2.5, 3.75, 5.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1 // Fill let mut ones = Array::<f64, _>::ones((1, 4)); ones.fill(0.); println!("{:?}", ones); // Output: // [[0.0, 0.0, 0.0, 0.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2 // Eye -- Identity Matrix let eye = Array::<f64, _>::eye(4); println!("{:?}", eye); // Output: // [[1.0, 0.0, 0.0, 0.0], // [0.0, 1.0, 0.0, 0.0], // [0.0, 0.0, 1.0, 0.0], // [0.0, 0.0, 0.0, 1.0]], shape=[4, 4], strides=[4, 1], layout=Cc (0x5), const ndim=2 // Random // Not working on Jupyter notebook //let random = Array::random((2, 5), Uniform::new(0., 10.)); //println!("{:?}", random); // Output: // [[9.375493735188611, 4.088737328406999, 9.778579742815943, 0.5225866490310649, 1.518053969762827], // [9.860829919571666, 2.9473768443117, 7.768332993584486, 7.163926861520167, 9.814750664983297]], shape=[2, 5], strides=[5, 1], layout=Cc (0x5), const ndim=2 }
[[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0]], shape=[3, 4], strides=[1, 3], layout=Ff (0xa), const ndim=2
[[1.0, 1.0, 1.0, 1.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2
[0.0, 1.0, 2.0, 3.0, 4.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1
[0.0, 1.25, 2.5, 3.75, 5.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1
What does the printout mean?
- The values of the array
- The shape of the array, most important dimension first
- The stride (always 1 for arrays in the last dimension, but can be different for views)
- The layout (storage order, view order)
- The number of dimensions
Indexing and Slicing
| NumPy | ndarray | Notes |
|---|---|---|
| a[-1] | a[a.len() - 1] | access the last element in 1-D array a |
| a[1, 4] | a[[1, 4]] | access the element in row 1, column 4 |
| a[1] or a[1, :, :] | a.slice(s![1, .., ..]) or a.index_axis(Axis(0), 1) | get a 2-D subview of a 3-D array at index 1 of axis 0 |
| a[0:5] or a[:5] or a[0:5, :] | a.slice(s![0..5, ..]) or a.slice(s![..5, ..]) or a.slice_axis(Axis(0), Slice::from(0..5)) | get the first 5 rows of a 2-D array |
| a[-5:] or a[-5:, :] | a.slice(s![-5.., ..]) or a.slice_axis(Axis(0), Slice::from(-5..)) | get the last 5 rows of a 2-D array |
| a[:3, 4:9] | a.slice(s![..3, 4..9]) | columns 4, 5, 6, 7, and 8 of the first 3 rows |
| a[1:4:2, ::-1] | a.slice(s![1..4;2, ..;-1]) | rows 1 and 3 with the columns in reverse order |
The s![] slice macro
-
The
s![]macro in Rust's ndarray crate is a convenient way to create slice specifications for array operations. -
It's used to create a
SliceInfoobject that describes how to slice or view an array.
Here's how it works:
- The
s![]macro is used to create slice specifications that are similar to Python's slice notation - It's commonly used with methods like
slice(),slice_mut(), and other array view operations - Inside the macro, you can specify ranges and steps for each dimension
For example:
#![allow(unused)] fn main() { let mut slice = array.slice_mut(s![1.., 0, ..]); }
This creates a slice that:
- Takes all elements from index 1 onwards in the first dimension (
1..) - Takes only index 0 in the second dimension (
0) - Takes all elements in the third dimension (
..)
The syntax inside s![] supports several patterns:
..- take all elements in that dimensionstart..end- take elements from start (inclusive) to end (exclusive)start..=end- take elements from start (inclusive) to end (inclusive)start..- take elements from start (inclusive) to the end..end- take elements from the beginning to end (exclusive)index- take only that specific index
For example:
#![allow(unused)] fn main() { // Take every other element in the first dimension s![..;2] // Take elements 1 to 3 in the first dimension, all elements in the second s![1..4, ..] // Take elements from index 2 to the end, with step size 2 s![2..;2] // Take specific indices s![1, 2, 3] }
From TDS Post.
#![allow(unused)] fn main() { use ndarray::{s}; { // Create a 3-dimensional array (2x3x4) let mut array = Array::from_shape_fn((2, 3, 4), |(i, j, k)| { (i * 100 + j * 10 + k) as f32 }); // Print the original 3-dimensional array println!("Original 3D array:\n{:?}", array); // Create a 2-dimensional slice (taking the first 2D layer) let mut slice = array.slice_mut(s![1.., 0, ..]); // Print the 2-dimensional slice println!("2D slice:\n{:?}", slice); // Create a 1-dimensional slice (taking the first 2D and 3D layer) let slice2 = array.slice(s![1, 0, ..]); // Print the 1-dimensional slice println!("1D slice:\n{:?}", slice2); } }
[[0.0, 0.0, 0.0, 0.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2
[[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0]], shape=[4, 4], strides=[4, 1], layout=Cc (0x5), const ndim=2
Original 3D array:
[[[0.0, 1.0, 2.0, 3.0],
[10.0, 11.0, 12.0, 13.0],
[20.0, 21.0, 22.0, 23.0]],
[[100.0, 101.0, 102.0, 103.0],
[110.0, 111.0, 112.0, 113.0],
[120.0, 121.0, 122.0, 123.0]]], shape=[2, 3, 4], strides=[12, 4, 1], layout=Cc (0x5), const ndim=3
2D slice:
[[100.0, 101.0, 102.0, 103.0]], shape=[1, 4], strides=[0, 1], layout=CFcf (0xf), const ndim=2
1D slice:
[100.0, 101.0, 102.0, 103.0], shape=[4], strides=[1], layout=CFcf (0xf), const ndim=1
()
2D and 3D datatypes, again from TDS Post.
#![allow(unused)] fn main() { use ndarray::{array, Array, Array2, Array3, ShapeBuilder}; // 1D array let array_d1 = Array::from_vec(vec![1., 2., 3., 4.]); println!("{:?}", array_d1); // Output: // [1.0, 2.0, 3.0, 4.0], shape=[4], strides=[1], layout=CFcf (0xf), const ndim=1 // or let array_d11 = Array::from_shape_vec((1, 4), vec![1., 2., 3., 4.]); println!("{:?}", array_d11.unwrap()); // Output: // [[1.0, 2.0, 3.0, 4.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2 // 2D array let array_d2 = array![ [-1.01, 0.86, -4.60, 3.31, -4.81], [ 3.98, 0.53, -7.04, 5.29, 3.55], [ 3.30, 8.26, -3.89, 8.20, -1.51], [ 4.43, 4.96, -7.66, -7.33, 6.18], [ 7.31, -6.43, -6.16, 2.47, 5.58], ]; // or let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 3., 4.]); println!("{:?}", array_d2.unwrap()); // Output: // [[1.0, 2.0], // [3.0, 4.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2 // or let mut data = vec![1., 2., 3., 4.]; let array_d21 = Array2::from_shape_vec((2, 2), data); // 3D array let mut data = vec![1., 2., 3., 4.]; let array_d3 = Array3::from_shape_vec((2, 2, 1), data); println!("{:?}", array_d3); // Output: // [[[1.0], // [2.0]], // [[3.0], // [4.0]]], shape=[2, 2, 1], strides=[2, 1, 1], layout=Cc (0x5), const ndim=3 }
[1.0, 2.0, 3.0, 4.0], shape=[4], strides=[1], layout=CFcf (0xf), const ndim=1
[[1.0, 2.0, 3.0, 4.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2
[[1.0, 2.0],
[3.0, 4.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2
Reshaping
From TDS Post.
From TDS Post.
Examining the array parameters
| NumPy | ndarray | Notes |
|---|---|---|
| np.ndim(a) or a.ndim | a.ndim() | get the number of dimensions of array a |
| np.size(a) or a.size | a.len() | get the number of elements in array a |
| np.shape(a) or a.shape | a.shape() or a.dim() | get the shape of array a |
| a.shape[axis] | a.len_of(Axis(axis)) | get the length of an axis |
| a.strides | a.strides() | get the strides of array a |
| np.size(a) == 0 or a.size == 0 | a.is_empty() | check if the array has zero elements |
Simple Math
| NumPy | ndarray | Notes |
|---|---|---|
| a.transpose() or a.T | a.t() or a.reversed_axes() | transpose of array a (view for .t() or by-move for .reversed_axes()) |
| mat1.dot(mat2) | mat1.dot(&mat2) | 2-D matrix multiply |
| mat.dot(vec) | mat.dot(&vec) | 2-D matrix dot 1-D column vector |
| vec.dot(mat) | vec.dot(&mat) | 1-D row vector dot 2-D matrix |
| vec1.dot(vec2) | vec1.dot(&vec2) | vector dot product |
| a * b, a + b, etc. | a * b, a + b, etc. | element-wise arithmetic operations |
| a**3 | a.mapv(|a| a.powi(3)) | element-wise power of 3 |
| np.sqrt(a) | a.mapv(f64::sqrt) | element-wise square root for f64 array |
| (a>0.5) | a.mapv(|a| a > 0.5) | array of bools of same shape as a with true where a > 0.5 and false elsewhere |
| np.sum(a) or a.sum() | a.sum() | sum the elements in a |
| np.sum(a, axis=2) or a.sum(axis=2) | a.sum_axis(Axis(2)) | sum the elements in a along axis 2 |
| np.mean(a) or a.mean() | a.mean().unwrap() | calculate the mean of the elements in f64 array a |
| np.mean(a, axis=2) or a.mean(axis=2) | a.mean_axis(Axis(2)) | calculate the mean of the elements in a along axis 2 |
| np.allclose(a, b, atol=1e-8) | a.abs_diff_eq(&b, 1e-8) | check if the arrays’ elementwise differences are within an absolute tolerance (it requires the approx feature-flag) |
| np.diag(a) | a.diag() | view the diagonal of a |
mapv vs map:
mapv iterates over the values of the array, map iterates over mutable references of the array
Array-n
Array0 to Array6 also defined in addition to Array as special cases with fixed dimensions instead dynamically defined dimensions.
#![allow(unused)] fn main() { { // Create a 2-dimensional array (3x4) let mut a = Array::from_shape_fn((3, 4), |(j, k)| { (j * 10 + k) as f32 }); // Print the original 2-dimensional array println!("Original 2D array:\n{:?}", a); let b = a * 2.0; println!("\nb:\n{:?}", b); let c = b.mapv(|v| v>24.8); println!("\nc:\n{:?}", c); } }
Ok([[[1.0],
[2.0]],
[[3.0],
[4.0]]], shape=[2, 2, 1], strides=[2, 1, 1], layout=Cc (0x5), const ndim=3)
Original 2D array:
[[0.0, 1.0, 2.0, 3.0],
[10.0, 11.0, 12.0, 13.0],
[20.0, 21.0, 22.0, 23.0]], shape=[3, 4], strides=[4, 1], layout=Cc (0x5), const ndim=2
b:
[[0.0, 2.0, 4.0, 6.0],
[20.0, 22.0, 24.0, 26.0],
[40.0, 42.0, 44.0, 46.0]], shape=[3, 4], strides=[4, 1], layout=Cc (0x5), const ndim=2
c:
[[false, false, false, false],
[false, false, false, true],
[true, true, true, true]], shape=[3, 4], strides=[4, 1], layout=Cc (0x5), const ndim=2
()
Type Conversions
- std::convert::From ensures lossless, safe conversions at compile-time and is generally recommended.
- std::convert::TryFrom can be used for potentially unsafe conversions. It will return a Result which can be handled or unwrap()ed to panic if any value at runtime cannot be converted losslessly.
| NumPy | ndarray | Notes |
|---|---|---|
| a.astype(np.float32) | a.mapv(|x| f32::from(x)) | convert array to f32. Only use if can't fail |
| a.astype(np.int32) | a.mapv(|x| i32::from(x)) | convert array to i32. Only use if can't fail |
| a.astype(np.uint8) | a.mapv(|x| u8::try_from(x).unwrap()) | try to convert to u8 array, panic if any value cannot be converted lossless at runtime (e.g. negative value) |
| a.astype(np.int32) | a.mapv(|x| x as i32) | convert to i32 array with “saturating” conversion; care needed because it can be a lossy conversion or result in non-finite values! |
Basic Linear Algebra
Provided by NDArray.
Transpose
#![allow(unused)] fn main() { let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 3., 4.]); println!("{:?}", array_d2.unwrap()); // Output // [[1.0, 2.0], // [3.0, 4.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2) let binding = array_d2.expect("Expect 2d matrix"); let array_d2t = binding.t(); println!("{:?}", array_d2t); // Output // [[1.0, 3.0], // [2.0, 4.0]], shape=[2, 2], strides=[1, 2], layout=Ff (0xa), const ndim=2 }
[E0382] Error: use of moved value: `array_d2`
╭─[command_8:1:1]
│
1 │ let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 3., 4.]);
│ ────┬───
│ ╰───── move occurs because `array_d2` has type `Result<ndarray::ArrayBase<OwnedRepr<f64>, ndarray::Dim<[usize; 2]>>, ShapeError>`, which does not implement the `Copy` trait
2 │ println!("{:?}", array_d2.unwrap());
│ ────┬───│────┬───
│ ╰────────────── help: consider calling `.as_ref()` or `.as_mut()` to borrow the type's contents
│ │ │
│ ╰────────── help: you can `clone` the value and consume it, but this might not be your desired behavior: `.clone()`
│ │
│ ╰───── `array_d2` moved due to this method call
│
8 │ let binding = array_d2.expect("Expect 2d matrix");
│ ────┬───
│ ╰───── value used here after move
│
│ Note: note: `Result::<T, E>::unwrap` takes ownership of the receiver `self`, which moves `array_d2`
───╯
[E0597] Error: `binding` does not live long enough
╭─[command_8:1:1]
│
8 │ let binding = array_d2.expect("Expect 2d matrix");
│ ───┬───
│ ╰───── binding `binding` declared here
│
10 │ let array_d2t = binding.t();
│ ───┬─┬─────
│ ╰───────── borrowed value does not live long enough
│ │
│ ╰─────── argument requires that `binding` is borrowed for `'static`
────╯
Matrix Multiplication
#![allow(unused)] fn main() { use ndarray::{array, Array2}; let a: Array2<f64> = array![[3., 2.], [2., -2.]]; let b: Array2<f64> = array![[3., 2.], [2., -2.]]; let c = a.dot(&b); print!("{:?}", c); // Output // [[13.0, 2.0], // [2.0, 8.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2 }
[[13.0, 2.0],
Linear Algebra with ndarray-linalg
-
The crate
ndarray-linalgimplements more advanced lineary algebra operations that comes withNDArray. -
It relies on a native linear algebra library like
OpenBLASand can be tricky to configure. -
We show it here just for reference.
| NumPy | ndarray | Notes |
|---|---|---|
| numpy.linalg.inv(a) | a.inv() | Invert matrix a. Must be square |
| numpy.linalg.eig(a) | a.eig() | Compute eigenvalues and eigenvectors of matrix. Must be square |
| numpy.linalg.svd(a) | a.svd(true, true) | Compute the Singular Value Decomposition of matrix |
| numpy.linalg.det(a) | a.det() | Compute the determinant of a matrix. Must be square |
#![allow(unused)] fn main() { //```rust :dep ndarray = { version = "^0.15" } // This is the MAC version // See ./README.md for setup instructions :dep ndarray-linalg = { version = "^0.16", features = ["openblas-system"] } // This is the linux verison //:dep ndarray-linalg = { version = "^0.15", features = ["openblas"] } use ndarray::array; use ndarray_linalg::*; { // Create a 2D square matrix (3x3) let matrix = array![ [1.0, 2.0, 3.0], [0.0, 1.0, 4.0], [5.0, 6.0, 0.0] ]; // Compute the eigenvalues and eigenvectors match matrix.eig() { Ok((eigenvalues, eigenvectors)) => { println!("Eigenvalues:\n{:?}", eigenvalues); println!("Eigenvectors:\n{:?}", eigenvectors); }, Err(e) => println!("Failed to compute eigenvalues and eigenvectors: {:?}", e), } } //``` }
[2.0, 8.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2EVCXR_VARIABLE_CHANGED_TYPE:b
The type of the variable b was redefined, so was lost.
The type of the variable array_d21 was redefined, so was lost.
The type of the variable array_d3 was redefined, so was lost.
The type of the variable a was redefined, so was lost.
The type of the variable range was redefined, so was lost.
The type of the variable eye was redefined, so was lost.
The type of the variable c was redefined, so was lost.
The type of the variable array_d1 was redefined, so was lost.
The type of the variable linspace was redefined, so was lost.
The type of the variable ones was redefined, so was lost.
The type of the variable b was redefined, so was lost.
Eigenvalues:
[Complex { re: 7.256022422687388, im: 0.0 }, Complex { re: -0.026352822204034426, im: 0.0 }, Complex { re: -5.229669600483354, im: 0.0 }], shape=[3], strides=[1], layout=CFcf (0xf), const ndim=1
Eigenvectors:
[[Complex { re: -0.4992701697014973, im: 0.0 }, Complex { re: -0.7576983872742932, im: 0.0 }, Complex { re: -0.22578016277159085, im: 0.0 }],
[Complex { re: -0.4667420094775666, im: 0.0 }, Complex { re: 0.6321277120502754, im: 0.0 }, Complex { re: -0.526348454767688, im: 0.0 }],
[Complex { re: -0.7299871192254567, im: 0.0 }, Complex { re: -0.162196515314045, im: 0.0 }, Complex { re: 0.8197442419819131, im: 0.0 }]], shape=[3, 3], strides=[1, 3], layout=Ff (0xa), const ndim=2
()
The same code is provided as a cargo project.
Python Numpy Reference
For reference you can look at mnist_fcn.ipynb which implements and trains the network with only numpy matrices, but does use PyTorch dataset loaders for conciseness.