matten: The core `Tensor`

@nabbisen

Series


This is the second of four short posts about matten. The first post explained the motivation. This one shows what the library looks like in practice.


Getting started

# Cargo.toml
[dependencies]
matten = "0.28"

The default feature set includes serde, json, and csv. If you want the smallest possible dependency footprint, you can turn them off:

matten = { version = "0.28", default-features = false }

Creating tensors

The whole import is use matten::Tensor;. No generic parameters, no lifetime annotations.

use matten::Tensor;

// From data and an explicit shape
let a = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
assert_eq!(a.shape(), &[2, 2]);
assert_eq!(a.ndim(), 2);

// Convenience constructors
let z = Tensor::zeros(&[3, 3]);
let o = Tensor::ones(&[3, 3]);
let f = Tensor::full(&[2, 4], 5.0);

Shape mismatches produce an actionable error rather than a panic when you use the boundary-style constructor:

use matten::{MattenError, Tensor};

let result = Tensor::try_new(vec![1.0, 2.0, 3.0], &[2, 2]);
assert!(matches!(result, Err(MattenError::Shape { .. })));

Arithmetic and broadcasting

The operators work on references, so you keep ownership of the originals. Shape broadcasting follows NumPy-style right-alignment rules.

use matten::Tensor;

let a = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
let b = Tensor::ones(&[2, 2]);

let c = &a + &b;          // [2.0, 3.0, 4.0, 5.0]
let d = &a * 2.0;         // scalar broadcast: [2.0, 4.0, 6.0, 8.0]

// Broadcasting a row across a matrix
let row = Tensor::new(vec![1.0, 2.0], &[1, 2]);
let mat = Tensor::ones(&[3, 2]);
let result = &mat + &row; // shape [3, 2]

Shape operations

use matten::Tensor;

let t = Tensor::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);

let flat = t.flatten();           // shape [6]
let reshaped = t.reshape(&[3, 2])?;
let transposed = t.transpose()?;  // shape [3, 2]

// Reductions
let s = t.sum();          // scalar
let m = t.mean()?;
let col_sums = t.sum_axis(0)?;    // shape [3]

JSON and CSV

Both are on by default. The API returns Result at the boundary, so a malformed input gives you an error rather than a panic.

use matten::Tensor;

// JSON — two accepted forms
let t = Tensor::from_json(r#"{"shape":[2,2],"data":[1.0,2.0,3.0,4.0]}"#)?;
let t = Tensor::from_json("[[1.0, 2.0], [3.0, 4.0]]")?;

// From a file
let t = Tensor::load_json("data/tensor.json")?;

// CSV
let t = Tensor::from_csv("1.0,2.0,3.0\n4.0,5.0,6.0\n")?;
let t = Tensor::load_csv("data/matrix.csv")?;

Serialisation goes through serde, so serde_json::to_string(&t) and serde_json::from_str(&json_str) round-trip correctly when the json or serde feature is active.

Error handling

matten has two deliberate error zones.

  • Internal shape operations (constructing from new, reshaping, slicing) panic with an actionable message — useful during fast prototyping because you see the problem immediately.
  • External boundary operations (from_json, from_csv, load_*) always return Result<Tensor, MattenError>, because real input data is not always clean.
    • MattenError is #[non_exhaustive], so match on the variant you care about and use a wildcard for the rest:
use matten::{MattenError, Tensor};

match Tensor::from_csv("1.0,not_a_number\n") {
    Ok(t) => println!("got shape {:?}", t.shape()),
    Err(MattenError::Parse { .. }) => println!("bad input"),
    Err(e) => println!("other error: {e:?}"),
}

That covers the everyday numeric core. The next post covers something different: what happens when the input data is not a clean f64 matrix — when it has mixed types, missing values, or integers alongside floats.

Links: crates.io · docs.rs · mdBook · repository

Series

matten
  1. matten Introduction: A family-car tensor library for Rust
  2. matten: The core `Tensor`

Comments or feedbacks are welcomed and appreciated.