viva_tensor/core/tensor

Core Tensor module - the heart of viva_tensor.

Why opaque? Learned the hard way that letting users construct Tensor(data: [1,2,3], shape: [2,2]) leads to 3am debugging sessions. Algebraic data types are great until someone violates your invariants.

The strided representation comes straight from how NumPy does it internally (see: https://numpy.org/doc/stable/reference/arrays.ndarray.html#internal-memory-layout) Basically: instead of copying data for transpose, just swap the strides. O(1) vs O(n). The kind of trick that makes you feel smart.

Fun fact: Erlang’s :array module uses a tree structure (not contiguous memory), so our “O(1)” access is actually O(log32 n). Close enough for jazz.

let a = tensor.zeros([2, 3])
let b = tensor.ones([2, 3])
use c <- result.try(tensor.add(a, b))

Types

The tensor itself. Opaque so nobody can break invariants.

Two flavors:

  • Dense: backed by List, simple but O(n) access
  • Strided: backed by Erlang :array, O(1) access + zero-copy views
pub opaque type Tensor

Values

pub fn arange(start: Float, end: Float, step: Float) -> Tensor

Create tensor with values from start to end (exclusive)

pub fn cols(t: Tensor) -> Int

Number of columns (for 2D tensors)

pub fn dim(
  t: Tensor,
  axis: Int,
) -> Result(Int, error.TensorError)

Get specific dimension size

pub fn eye(n: Int) -> Tensor

Identity matrix. The multiplicative identity of matrix algebra. IA = AI = A. One of the few things in linear algebra that’s intuitive.

pub fn fill(shape: List(Int), value: Float) -> Tensor

Create tensor filled with a value

pub fn from_list(data: List(Float)) -> Tensor

Create 1D tensor (vector) from list

pub fn from_list2d(
  rows: List(List(Float)),
) -> Result(Tensor, error.TensorError)

Create 2D tensor (matrix) from list of lists

pub fn from_native_ref(
  ref: ffi.NativeTensorRef,
  shape: List(Int),
) -> Tensor

Wrap a NIF resource ref as a Tensor

pub fn get(
  t: Tensor,
  index: Int,
) -> Result(Float, error.TensorError)

Get element by flat index. For strided tensors, computes the real offset.

pub fn get2d(
  t: Tensor,
  row: Int,
  col: Int,
) -> Result(Float, error.TensorError)

Get element by 2D coordinates

pub fn get_col(
  t: Tensor,
  col_idx: Int,
) -> Result(Tensor, error.TensorError)

Get matrix column as vector

pub fn get_row(
  t: Tensor,
  row_idx: Int,
) -> Result(Tensor, error.TensorError)

Get matrix row as vector

pub fn he_init(
  fan_in fan_in: Int,
  fan_out fan_out: Int,
) -> Tensor

He init (2015 paper: “Delving Deep into Rectifiers”) std = sqrt(2/fan_in) accounts for ReLU killing half the activations. The “2” is not arbitrary - it comes from E[ReLU(x)²] = Var(x)/2 for x~N(0,σ²)

pub fn is_contiguous(t: Tensor) -> Bool

Check if tensor has contiguous memory layout

pub fn is_native(t: Tensor) -> Bool

Check if tensor is backed by native C memory

pub fn linspace(start: Float, end: Float, num: Int) -> Tensor

Create linearly spaced tensor

pub fn matrix(
  rows rows: Int,
  cols cols: Int,
  data data: List(Float),
) -> Result(Tensor, error.TensorError)

Create matrix with explicit dimensions

pub fn native_fill(
  shape: List(Int),
  value: Float,
) -> Result(Tensor, error.TensorError)

Create native tensor filled with value

pub fn native_from_list(
  data: List(Float),
  shape: List(Int),
) -> Result(Tensor, error.TensorError)

Create native tensor from list data

pub fn native_ones(
  shape: List(Int),
) -> Result(Tensor, error.TensorError)

Create native tensor of ones

pub fn native_ref(t: Tensor) -> Result(ffi.NativeTensorRef, Nil)

Extract native ref (for passing to NIF resource ops)

pub fn native_zeros(
  shape: List(Int),
) -> Result(Tensor, error.TensorError)

Create native tensor of zeros (data in C memory)

pub fn new(
  data: List(Float),
  shape: List(Int),
) -> Result(Tensor, error.TensorError)

Create tensor with validation. This is the “safe” constructor.

pub fn ones(shape: List(Int)) -> Tensor

Create tensor of ones

pub fn random_normal(
  shape shape: List(Int),
  mean mean: Float,
  std std: Float,
) -> Tensor

Normal distribution via Box-Muller transform (1958). Could use Ziggurat for ~3x speedup but Box-Muller is elegant and “premature optimization is the root of all evil” - Knuth

pub fn random_uniform(shape: List(Int)) -> Tensor

Uniform random in [0, 1). Seeds from system entropy on first call.

pub fn rank(t: Tensor) -> Int

Number of dimensions (rank)

pub fn rows(t: Tensor) -> Int

Number of rows (for 2D tensors)

pub fn shape(t: Tensor) -> List(Int)

Get tensor shape

pub fn size(t: Tensor) -> Int

Total number of elements

pub fn to_contiguous(t: Tensor) -> Tensor

Alias for to_dense - ensure contiguous memory layout

pub fn to_dense(t: Tensor) -> Tensor

Convert strided tensor back to dense (materializes the view)

pub fn to_list(t: Tensor) -> List(Float)

Get tensor data as list

pub fn to_strided(t: Tensor) -> Tensor

Convert to strided (backed by Erlang :array for O(1) random access)

pub fn transpose_strided(
  t: Tensor,
) -> Result(Tensor, error.TensorError)

Zero-copy transpose (just swap strides and shape)

pub fn vector(data: List(Float)) -> Tensor

Create vector (alias for from_list)

pub fn xavier_init(
  fan_in fan_in: Int,
  fan_out fan_out: Int,
) -> Tensor

Xavier/Glorot init (2010 paper: “Understanding the difficulty of training deep FFNs”) The limit = sqrt(6 / (fan_in + fan_out)) keeps variance stable across layers. Use this for tanh/sigmoid. For ReLU, use he_init instead.

pub fn zeros(shape: List(Int)) -> Tensor

Create tensor of zeros

Search Document