NDArray Examples
NDArray[A] is a strided N-dimensional array. It generalises Matrix[A] to arbitrary rank while sharing the same column-major memory model and zero-copy view semantics.
Construction
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
// 1D from a flat Array
val v = NDArray.fromArray(Array(1.0, 2.0, 3.0, 4.0, 5.0, 6.0))
// v: NDArray[Double] = vecxt.ndarray$NDArray@1de50f5b
v.ndim
// res0: Int = 1
v.numel
// res1: Int = 6
v.shape.mkString("[", ",", "]")
// res2: String = [6]
v.strides.mkString("[", ",", "]")
// res3: String = [1]
// 2D: shape [rows=2, cols=3], column-major strides [1, 2]
val m = NDArray(Array(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), Array(2, 3))
// m: NDArray[Double] = vecxt.ndarray$NDArray@53646947
m.ndim
// res4: Int = 2
m.shape.mkString("[", ",", "]") // [2,3]
// res5: String = [2,3]
m.strides.mkString("[", ",", "]") // [1,2] — col-major: stride(0)=1, stride(1)=rows
// res6: String = [1,2]
// 3D: shape [2, 3, 4]
val t = NDArray(Array.tabulate(24)(_.toDouble), Array(2, 3, 4))
// t: NDArray[Double] = vecxt.ndarray$NDArray@68aaf3fb
t.ndim
// res7: Int = 3
t.shape.mkString("[", ",", "]") // [2,3,4]
// res8: String = [2,3,4]
t.strides.mkString("[", ",", "]") // [1,2,6] — col-major
// res9: String = [1,2,6]
// Zeros, ones, fill
val z = NDArray.zeros[Double](Array(2, 3))
// z: NDArray[Double] = vecxt.ndarray$NDArray@7a389d91
val o = NDArray.ones[Double](Array(3))
// o: NDArray[Double] = vecxt.ndarray$NDArray@62be920d
val f = NDArray.fill(Array(2, 2), 7.0)
// f: NDArray[Double] = vecxt.ndarray$NDArray@743e70a0
z.layout
// res10: String = ndim: 2, shape: [2,3], strides: [1,2], offset: 0, data length: 6
Element Access
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
// col-major 2×3: data stored column by column
// data = [col0row0, col0row1, col1row0, col1row1, col2row0, col2row1]
// = [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 ]
val m = NDArray(Array(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), Array(2, 3))
// m: NDArray[Double] = vecxt.ndarray$NDArray@281b383a
m(0, 0) // row 0, col 0 → 1.0
// res12: Double = 1.0
m(1, 0) // row 1, col 0 → 2.0 (column-major: next row in same column)
// res13: Double = 2.0
m(0, 1) // row 0, col 1 → 3.0
// res14: Double = 3.0
m(1, 2) // row 1, col 2 → 6.0
// res15: Double = 6.0
// 3D element access: strides [1,2,6], so (1,2,3) → 1*1 + 2*2 + 3*6 = 23
val t = NDArray(Array.tabulate(24)(_.toDouble), Array(2, 3, 4))
// t: NDArray[Double] = vecxt.ndarray$NDArray@261e1ce1
t(1, 2, 3)
// res16: Double = 23.0
// Update
val a = NDArray.fromArray(Array(10.0, 20.0, 30.0))
// a: NDArray[Double] = vecxt.ndarray$NDArray@404f925b
a.update(1, 99.0)
a.toArray.mkString("[", ",", "]") // [10.0, 99.0, 30.0]
// res18: String = [10.0,99.0,30.0]
Slicing and Views
Slices are zero-copy — they share the backing array.
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
val m = NDArray(Array.tabulate(12)(_.toDouble), Array(3, 4))
// m: NDArray[Double] = vecxt.ndarray$NDArray@2788e3c3
m.layout
// res20: String = ndim: 2, shape: [3,4], strides: [1,3], offset: 0, data length: 12
// slice(dim, start, end): keep rows 1 and 2 (indices 1 until 3)
val s = m.slice(0, 1, 3)
// s: NDArray[Double] = vecxt.ndarray$NDArray@555a4ad8
s.shape.mkString("[", ",", "]") // [2,4]
// res21: String = [2,4]
s.data eq m.data // true — same backing array
// res22: Boolean = true
// Multi-dimensional slicing with :: and Range
val subm = m(::, 1 until 3)
// subm: NDArray[Double] = vecxt.ndarray$NDArray@47d01ed0
subm.shape.mkString("[", ",", "]") // [3,2]
// res23: String = [3,2]
// Gather non-contiguous rows via Array[Int]
val gathered = m(Array(0, 2), ::)
// gathered: NDArray[Double] = vecxt.ndarray$NDArray@1239c060
gathered.shape.mkString("[", ",", "]") // [2,4]
// res24: String = [2,4]
gathered(0, 0) == m(0, 0)
// res25: Boolean = true
gathered(1, 0) == m(2, 0)
// res26: Boolean = true
Transpose
Transpose is zero-copy — it permutes strides without touching data.
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
val m = NDArray(Array.tabulate(6)(_.toDouble), Array(2, 3))
// m: NDArray[Double] = vecxt.ndarray$NDArray@5868ea00
m.shape.mkString("[", ",", "]") // [2,3]
// res28: String = [2,3]
m.strides.mkString("[", ",", "]") // [1,2]
// res29: String = [1,2]
val t = m.T
// t: NDArray[Double] = vecxt.ndarray$NDArray@5f3b343b
t.shape.mkString("[", ",", "]") // [3,2]
// res30: String = [3,2]
t.strides.mkString("[", ",", "]") // [2,1]
// res31: String = [2,1]
t.data eq m.data // true — same backing array
// res32: Boolean = true
// Element equivalence: m(i,j) == m.T(j,i)
m(0, 1) == t(1, 0)
// res33: Boolean = true
m(1, 2) == t(2, 1)
// res34: Boolean = true
// N-D permutation: shape [2,3,4] → permute(2,0,1) → shape [4,2,3]
val cube = NDArray(Array.tabulate(24)(_.toDouble), Array(2, 3, 4))
// cube: NDArray[Double] = vecxt.ndarray$NDArray@5475941f
val perm = cube.transpose(Array(2, 0, 1))
// perm: NDArray[Double] = vecxt.ndarray$NDArray@2dbfaf5
perm.shape.mkString("[", ",", "]") // [4,2,3]
// res35: String = [4,2,3]
Reshape, Squeeze, Flatten
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
// reshape: zero-copy for contiguous arrays
val m = NDArray(Array.tabulate(12)(_.toDouble), Array(3, 4))
// m: NDArray[Double] = vecxt.ndarray$NDArray@6b3092d2
val r = m.reshape(Array(4, 3))
// r: NDArray[Double] = vecxt.ndarray$NDArray@5d338002
r.shape.mkString("[", ",", "]") // [4,3]
// res37: String = [4,3]
r.data eq m.data // true — same backing array
// res38: Boolean = true
// flatten: 1D view
val flat = m.flatten
// flat: NDArray[Double] = vecxt.ndarray$NDArray@a18859b
flat.shape.mkString("[", ",", "]") // [12]
// res39: String = [12]
// unsqueeze: add a size-1 dimension
val v = NDArray.fromArray(Array(1.0, 2.0, 3.0))
// v: NDArray[Double] = vecxt.ndarray$NDArray@32a22f09
val row = v.unsqueeze(0) // shape [1, 3] — row vector
// row: NDArray[Double] = vecxt.ndarray$NDArray@3dd1d140
val col = v.unsqueeze(1) // shape [3, 1] — column vector
// col: NDArray[Double] = vecxt.ndarray$NDArray@7f43e6f8
row.shape.mkString("[", ",", "]") // [1,3]
// res40: String = [1,3]
col.shape.mkString("[", ",", "]") // [3,1]
// res41: String = [3,1]
// squeeze: remove all size-1 dimensions
val squeezed = row.squeeze
// squeezed: NDArray[Double] = vecxt.ndarray$NDArray@1c690ee7
squeezed.shape.mkString("[", ",", "]") // [3]
// res42: String = [3]
// squeeze specific dimension
val arr3d = NDArray(Array.tabulate(6)(_.toDouble), Array(1, 2, 3))
// arr3d: NDArray[Double] = vecxt.ndarray$NDArray@295f69b2
arr3d.squeeze.shape.mkString("[", ",", "]") // [2,3]
// res43: String = [2,3]
Element-wise Arithmetic
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
val a = NDArray(Array(1.0, 2.0, 3.0, 4.0), Array(2, 2))
// a: NDArray[Double] = vecxt.ndarray$NDArray@4ecfb8d3
val b = NDArray(Array(10.0, 20.0, 30.0, 40.0), Array(2, 2))
// b: NDArray[Double] = vecxt.ndarray$NDArray@14390236
// Binary ops — same shape required
(a + b).toArray.mkString("[", ",", "]")
// res45: String = [11.0,22.0,33.0,44.0]
(b - a).toArray.mkString("[", ",", "]")
// res46: String = [9.0,18.0,27.0,36.0]
(a * b).toArray.mkString("[", ",", "]")
// res47: String = [10.0,40.0,90.0,160.0]
(b / a).toArray.mkString("[", ",", "]")
// res48: String = [10.0,10.0,10.0,10.0]
// Scalar ops
(a + 100.0).toArray.mkString("[", ",", "]")
// res49: String = [101.0,102.0,103.0,104.0]
(a * 2.0).toArray.mkString("[", ",", "]")
// res50: String = [2.0,4.0,6.0,8.0]
(2.0 * a).toArray.mkString("[", ",", "]")
// res51: String = [2.0,4.0,6.0,8.0]
(10.0 / a).toArray.mkString("[", ",", "]")
// res52: String = [10.0,5.0,3.3333333333333335,2.5]
// Unary ops
a.neg.toArray.mkString("[", ",", "]")
// res53: String = [-1.0,-2.0,-3.0,-4.0]
a.abs.toArray.mkString("[", ",", "]")
// res54: String = [1.0,2.0,3.0,4.0]
a.sqrt.toArray.mkString("[", ",", "]")
// res55: String = [1.0,1.4142135623730951,1.7320508075688772,2.0]
a.exp.toArray.mkString("[", ",", "]")
// res56: String = [2.718281828459045,7.38905609893065,20.085536923187668,54.598150033144236]
NDArray(Array(1.0, Math.E), Array(2)).log.toArray.mkString("[", ",", "]")
// res57: String = [0.0,1.0]
NDArray(Array(0.0, 1.0, -1.0), Array(3)).tanh.toArray.mkString("[", ",", "]")
// res58: String = [0.0,0.7615941559557649,-0.7615941559557649]
NDArray(Array(0.0), Array(1)).sigmoid.toArray.mkString("[", ",", "]") // [0.5]
// res59: String = [0.5]
// In-place ops (array must be contiguous)
val c = NDArray(Array(1.0, 2.0, 3.0), Array(3))
// c: NDArray[Double] = vecxt.ndarray$NDArray@26713044
c += NDArray(Array(9.0, 8.0, 7.0), Array(3))
c.toArray.mkString("[", ",", "]") // [10.0,10.0,10.0]
// res61: String = [10.0,10.0,10.0]
val d = NDArray(Array(10.0, 20.0, 30.0), Array(3))
// d: NDArray[Double] = vecxt.ndarray$NDArray@fa71fbf
d *= 2.0
d.toArray.mkString("[", ",", "]") // [20.0,40.0,60.0]
// res63: String = [20.0,40.0,60.0]
Comparison Operations
Comparison ops return NDArray[Boolean] with the same shape.
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
val a = NDArray(Array(1.0, 5.0, 3.0, 7.0, 2.0, 6.0), Array(2, 3))
// a: NDArray[Double] = vecxt.ndarray$NDArray@44262944
// Scalar comparisons — returns NDArray[Boolean]
(a > 4.0).toArray.mkString("[", ",", "]") // [false,true,false,true,false,true]
// res65: String = [false,true,false,true,false,true]
(a <= 3.0).toArray.mkString("[", ",", "]") // [true,false,true,false,true,false]
// res66: String = [true,false,true,false,true,false]
(a =:= 5.0).toArray.mkString("[", ",", "]") // [false,true,false,false,false,false]
// res67: String = [false,true,false,false,false,false]
// Array comparisons
val b = NDArray(Array(2.0, 4.0, 3.0, 6.0, 2.0, 8.0), Array(2, 3))
// b: NDArray[Double] = vecxt.ndarray$NDArray@63d0f2d8
(a > b).toArray.mkString("[", ",", "]") // [false,true,false,true,false,false]
// res68: String = [false,true,false,true,false,false]
(a =:= b).toArray.mkString("[", ",", "]") // [false,false,true,false,true,false]
// res69: String = [false,false,true,false,true,false]
Broadcasting
Broadcasting in vecxt is explicit: use broadcastTo or broadcastPair before binary ops.
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
// Broadcast a row vector shape [1,3] → [4,3] (zero-copy: stride-0 in dim 0)
val row = NDArray(Array(1.0, 2.0, 3.0), Array(1, 3))
// row: NDArray[Double] = vecxt.ndarray$NDArray@23de2878
val bcast = row.broadcastTo(Array(4, 3))
// bcast: NDArray[Double] = vecxt.ndarray$NDArray@2715ae63
bcast.shape.mkString("[", ",", "]") // [4,3]
// res71: String = [4,3]
bcast.strides.mkString("[", ",", "]") // [0,1] — stride-0 replicates dim 0
// res72: String = [0,1]
// Materialise the broadcast: add zeros to produce a concrete array
val result = bcast + NDArray.zeros[Double](Array(4, 3))
// result: NDArray[Double] = vecxt.ndarray$NDArray@2958d814
result.shape.mkString("[", ",", "]")
// res73: String = [4,3]
result.toArray.mkString("[", ",", "]")
// res74: String = [1.0,1.0,1.0,1.0,2.0,2.0,2.0,2.0,3.0,3.0,3.0,3.0]
// broadcastPair: broadcast two operands to their common shape
val p = NDArray(Array(1.0, 2.0, 3.0), Array(3))
// p: NDArray[Double] = vecxt.ndarray$NDArray@3762063a
val q = NDArray(Array(10.0), Array(1))
// q: NDArray[Double] = vecxt.ndarray$NDArray@10aadd67
val (p2, q2) = broadcastPair(p, q)
// p2: NDArray[Double] = vecxt.ndarray$NDArray@3762063a
// q2: NDArray[Double] = vecxt.ndarray$NDArray@7f24b4b6
(p2 + q2).toArray.mkString("[", ",", "]") // [11.0,12.0,13.0]
// res75: String = [11.0,12.0,13.0]
// broadcastShape: inspect the common shape without creating arrays
broadcastShape(Array(1, 3), Array(4, 1)).mkString("[", ",", "]") // [4,3]
// res76: String = [4,3]
// Typical use: add a bias row to every row of a 2D array
val data = NDArray.fill(Array(4, 3), 0.0)
// data: NDArray[Double] = vecxt.ndarray$NDArray@7f3aab09
val bias = NDArray(Array(10.0, 20.0, 30.0), Array(1, 3))
// bias: NDArray[Double] = vecxt.ndarray$NDArray@797076de
val (data2, bias2) = broadcastPair(data, bias)
// data2: NDArray[Double] = vecxt.ndarray$NDArray@7f3aab09
// bias2: NDArray[Double] = vecxt.ndarray$NDArray@179c8d4f
val biased = data2 + bias2
// biased: NDArray[Double] = vecxt.ndarray$NDArray@36c3ae2
biased.toArray.mkString("[", ",", "]")
// res77: String = [10.0,10.0,10.0,10.0,20.0,20.0,20.0,20.0,30.0,30.0,30.0,30.0]
Views and Mutation Semantics
NDArray views (slice, T, reshape on contiguous, squeeze, unsqueeze) share the backing array. Mutation through any view is visible in the original.
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
val m = NDArray(Array.tabulate(6)(_.toDouble), Array(2, 3))
// m: NDArray[Double] = vecxt.ndarray$NDArray@32f4df50
// Slice is a view
val col1view = m.slice(1, 1, 2) // dim=1 (cols), keep col 1 only
// col1view: NDArray[Double] = vecxt.ndarray$NDArray@b864ea2
col1view.data eq m.data // true — shared backing array
// res79: Boolean = true
// Mutate through the slice — visible in original
col1view.update(0, 0, 999.0) // writes to m(0,1)
m(0, 1) // 999.0
// res81: Double = 999.0
// Transpose is also a view
val t = m.T
// t: NDArray[Double] = vecxt.ndarray$NDArray@6ac1f150
t.data eq m.data // true
// res82: Boolean = true
// Copy explicitly for independence
val indep = NDArray(m.toArray, m.shape.clone())
// indep: NDArray[Double] = vecxt.ndarray$NDArray@345c7c54
indep.data eq m.data // false — fresh backing array
// res83: Boolean = false
Working with Higher Dimensions (3D+)
import vecxt.all.*
import vecxt.BoundsCheck.DoBoundsCheck.yes
// A "batch" of 4 matrices, each 3×5: shape [4, 3, 5]
// col-major strides: [1, 4, 12]
val batch = NDArray(Array.tabulate(60)(_.toDouble), Array(4, 3, 5))
// batch: NDArray[Double] = vecxt.ndarray$NDArray@1f77e533
batch.ndim
// res85: Int = 3
batch.numel
// res86: Int = 60
batch.strides.mkString("[", ",", "]") // [1,4,12]
// res87: String = [1,4,12]
// Extract the second matrix (index 1 along dim 0) as a view
val slice1 = batch.slice(0, 1, 2)
// slice1: NDArray[Double] = vecxt.ndarray$NDArray@4000f3cb
slice1.shape.mkString("[", ",", "]") // [1,3,5]
// res88: String = [1,3,5]
// Remove the leading size-1 dim → shape [3,5]
val mat1 = slice1.squeeze
// mat1: NDArray[Double] = vecxt.ndarray$NDArray@4421c5fe
mat1.shape.mkString("[", ",", "]") // [3,5]
// res89: String = [3,5]
// Element access: batch=1, row=2, col=4
batch(1, 2, 4)
// res90: Double = 57.0