N-Dimensional Arrays

Joule provides first-class multi-dimensional array types for scientific computing, machine learning, and signal processing.

Overview

TypeDescriptionOwns DataEnergy Cost
NDArray[T; N]Owned N-dimensional arrayYesAllocation + compute
NDView[T; N]Non-owning view into an NDArrayNoZero-copy
CowArray[T; N]Clone-on-write arraySharedFree reads, allocation on write
DynArray[T]Dynamically-ranked arrayYesAllocation + compute

The rank N is a compile-time constant, enabling the compiler to optimize indexing and verify dimensionality at compile time.

NDArray[T; N]

Owned, contiguous, row-major multi-dimensional array.

// Create a 2D array (matrix)
let mat: NDArray[f64; 2] = NDArray::zeros([3, 4]);     // 3x4 matrix of zeros
let ones: NDArray[f64; 2] = NDArray::ones([2, 2]);     // 2x2 matrix of ones
let filled: NDArray[f64; 2] = NDArray::full([3, 3], 7.0); // 3x3 filled with 7.0

// Create from data
let v: NDArray[f64; 1] = NDArray::from_vec(vec![1.0, 2.0, 3.0]);
let m: NDArray[f64; 2] = NDArray::from_vec_shape(vec![1.0, 2.0, 3.0, 4.0], [2, 2]);

Indexing

// Multi-dimensional indexing
let val = mat[1, 2];           // row 1, column 2
mat[0, 0] = 42.0;             // set element

// Slicing — returns NDView
let row = mat[0, ..];          // first row
let col = mat[.., 1];          // second column
let sub = mat[1..3, 0..2];    // submatrix
let strided = mat[.., ::2];   // every other column

Methods

let a: NDArray[f64; 2] = NDArray::zeros([3, 4]);

// Shape and metadata
a.shape();         // [3, 4]
a.rank();          // 2
a.len();           // 12 (total elements)
a.strides();       // [4, 1] (row-major)

// Element-wise operations
let b = a.add(&other);        // element-wise addition
let c = a.mul(&other);        // element-wise multiplication
let d = a.map(|x: f64| -> f64 { x * 2.0 });

// Reductions
let total = a.sum();           // sum all elements
let mean = a.mean();           // average
let max = a.max();             // maximum element
let min = a.min();             // minimum element

// Shape manipulation
let reshaped = a.reshape([4, 3]);   // reshape (same element count)
let flat = a.flatten();             // flatten to 1D
let transposed = a.transpose();     // transpose axes

// Linear algebra (2D)
let product = a.matmul(&b);        // matrix multiplication
let dot = v1.dot(&v2);             // dot product (1D)

NDView[T; N]

A non-owning view into an NDArray. Views are zero-copy — they reference the original data without allocation.

let arr: NDArray[f64; 2] = NDArray::zeros([4, 4]);

// Create views via slicing
let row: NDView[f64; 1] = arr.row(0);
let col: NDView[f64; 1] = arr.col(2);
let sub: NDView[f64; 2] = arr.slice([1..3, 1..3]);

// Views support the same read operations as NDArray
let sum = row.sum();
let max = sub.max();

CowArray[T; N]

Clone-on-write array. Reading is free (shares data with the source). Writing triggers a copy only if the data is shared.

let original: NDArray[f64; 2] = NDArray::ones([100, 100]);
let cow = CowArray::from(&original);  // no copy yet

// Reading is free
let val = cow[0, 0];   // reads from original's memory

// Writing triggers a copy (if shared)
cow[0, 0] = 42.0;      // now owns its own data

DynArray[T]

Dynamically-ranked array. The rank is determined at runtime, not compile time. Use when the dimensionality isn't known until runtime (e.g., loading arbitrary tensors from files).

let dyn_arr: DynArray[f64] = DynArray::zeros(vec![3, 4, 5]);  // 3D
let rank = dyn_arr.rank();     // 3 (runtime value)
let shape = dyn_arr.shape();   // [3, 4, 5]

Broadcasting

Binary operations between arrays of different shapes follow broadcasting rules:

let mat: NDArray[f64; 2] = NDArray::ones([3, 4]);   // shape [3, 4]
let row: NDArray[f64; 1] = NDArray::from_vec(vec![1.0, 2.0, 3.0, 4.0]); // shape [4]

// row is broadcast to [3, 4] — each row gets the same values added
let result = mat.add(&row);  // shape [3, 4]

Broadcasting rules:

  1. Dimensions are compared from the right
  2. Dimensions must be equal, or one of them must be 1
  3. Missing dimensions on the left are treated as 1

Energy Costs

OperationCostNotes
Element access0.5 pJL1 cache hit
Element-wise op0.8 pJ/elementArithmetic + memory
Reduction (sum/mean)0.8 pJ/elementSequential scan
Matrix multiply~2N^3 * 0.8 pJCubic complexity
Reshape/transpose0 pJMetadata-only (no copy)
Slice (NDView)0 pJZero-copy view
Broadcasting0 pJ overheadApplied during compute

Choosing an Array Type

  • Know the rank at compile time? Use NDArray[T; N] — the compiler verifies dimensions
  • Need a read-only window? Use NDView[T; N] — zero-copy, zero allocation
  • Might or might not modify? Use CowArray[T; N] — defers allocation until write
  • Rank determined at runtime? Use DynArray[T] — flexible but no compile-time dimension checks