N-Dimensional Arrays
Joule provides first-class multi-dimensional array types for scientific computing, machine learning, and signal processing.
Overview
| Type | Description | Owns Data | Energy Cost |
|---|---|---|---|
NDArray[T; N] | Owned N-dimensional array | Yes | Allocation + compute |
NDView[T; N] | Non-owning view into an NDArray | No | Zero-copy |
CowArray[T; N] | Clone-on-write array | Shared | Free reads, allocation on write |
DynArray[T] | Dynamically-ranked array | Yes | Allocation + 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:
- Dimensions are compared from the right
- Dimensions must be equal, or one of them must be 1
- Missing dimensions on the left are treated as 1
Energy Costs
| Operation | Cost | Notes |
|---|---|---|
| Element access | 0.5 pJ | L1 cache hit |
| Element-wise op | 0.8 pJ/element | Arithmetic + memory |
| Reduction (sum/mean) | 0.8 pJ/element | Sequential scan |
| Matrix multiply | ~2N^3 * 0.8 pJ | Cubic complexity |
| Reshape/transpose | 0 pJ | Metadata-only (no copy) |
| Slice (NDView) | 0 pJ | Zero-copy view |
| Broadcasting | 0 pJ overhead | Applied 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