Standard library · Numbers
num/grid
Imported as import "num/grid" as grid, its names are then grid.…. Every signature below is the one the checker infers.
grid — arrays of numbers with a shape, the way NumPy has them.
A grid is one flat Buf(Float) and the shape laid over it: shape says how many along each axis, strides how far to step in the buffer for one along each axis, off where element zero sits. transpose, rows, col and slice1 are views — the same buffer, a different shape — and cost nothing; copy and every operation that makes a new grid lay their result out contiguously, which is the shape every fast path here recognises.
The arithmetic (add, mul, gsqrt, sum, matmul …) is written as plain loops over the buffer rather than through a lambda per element, so that the compiler can hoist the bounds checks and vectorize; map and zip take a lambda and are for everything the named operations do not cover. A grid is storage, as a Buf is: put1/put2 write in place, and the views share it.
A comparison makes a Mask — one byte per element, as NumPy's boolean arrays are — and where, mcount, any and all read one.
What is here, by section: shapes and views; making grids; elementwise arithmetic and the rest of the maths; masks and where; reductions, including the nan-skipping ones and the order statistics; joining, sorting and searching; and solving, on an LU factorisation with partial pivoting. What is not here: dtypes other than float, more than two dimensions, broadcasting between different shapes, and the decompositions beyond LU.
Three things keep it fast. An operation writes its result into an operand's own storage when that operand is a temporary nobody else can see (unique), so a * 2.0 + 1.0 makes one grid and not two. The reductions run eight partial sums in four vector lanes, and matmul is a register-blocked kernel written in Float2. And on a --parallel build with RILL_THREADS set, anything over a quarter of a million elements is split across the workers in strands; a single-worker run never spawns.
import "num/grid" as g a = g.arange(6) |> g.reshape2(2, 3) b = g.transpose(a) println(g.matmul(a, b)) # [[5, 14], [14, 50]]
From Python it is import grid as g — bindings/python builds this library into a shared object and gives its handles NumPy's notation.
import "num/grid" as g a = g.arange(6) |> g.reshape2(2, 3) a # => [[0, 1, 2], [3, 4, 5]] b = g.transpose(a) g.matmul(a, b) # => [[5, 14], [14, 50]] g.sum(a) # => 15 g.gtk(a, 2.0) # => [[false, false, false], [true, true, true]] g.where(g.gtk(a, 2.0), a, g.zeros_like(a)) # => [[0, 0, 0], [3, 4, 5]]
Types
Grid
Grid(shape: Buf(Int), strides: Buf(Int), off: Int, data: Buf(Float))
Mask
1 where a comparison held, 0 where it did not; always contiguous.
Mask(shape: Buf(Int), bits: Buf(U8))
Acc
Acc(c00: Float2, c01: Float2, c02: Float2, c03: Float2, c04: Float2, c10: Float2, c11: Float2, c12: Float2, c13: Float2, c14: Float2, c20: Float2, c21: Float2, c22: Float2, c23: Float2, c24: Float2, c30: Float2, c31: Float2, c32: Float2, c33: Float2, c34: Float2)
Lu
The LU factors of a square matrix, worked out in one pass with partial pivoting: a holds L below the diagonal (its own diagonal is all ones, and is not stored) and U on and above it, piv says which row of the original each row came from, sign is 1 or -1 by how many rows were exchanged, and ok is false when the matrix turned out to be singular. Everything below solves through this: one factorisation, then as many right-hand sides as wanted.
Lu(a: Grid, piv: Buf(Int), sign: Float, ok: Bool)
Blk
c -= a * b, where all three sit inside bigger buffers: coff, aoff and boff say where each starts and cld, ald and bld how wide the buffer each lives in is. c is m by nn, a is m by kk, b is kk by nn.
Four rows by eight columns at a time, the sixteen answers kept in Float2 registers while the shared dimension is walked — the same shape of kernel as matmul, and for the same reason: written as one multiply-add per sixteen bytes moved it is the memory bus that answers, not the processor. Both halves of solving go through this: the trailing update of the factorisation, and the substitutions afterwards.
Blk(c00: Float2, c01: Float2, c02: Float2, c03: Float2, c10: Float2, c11: Float2, c12: Float2, c13: Float2, c20: Float2, c21: Float2, c22: Float2, c23: Float2, c30: Float2, c31: Float2, c32: Float2, c33: Float2)
Implementations
impl Show(Grid)impl Show(Mask)
Functions
fn ndim(g: Grid) -> Int
a = g.arange(6) |> g.reshape2(2, 3) g.ndim(a) # => 2 g.ndim(g.arange(3)) # => 1
fn size(g: Grid) -> Int
a = g.arange(6) |> g.reshape2(2, 3) g.size(a) # => 6
fn count(shape: Buf(Int)) -> Int
g.count(g.shape2(2, 3)) # => 6
fn rows_of(g: Grid) -> Int
a = g.arange(6) |> g.reshape2(2, 3) g.rows_of(a) # => 2
fn cols_of(g: Grid) -> Int
a = g.arange(6) |> g.reshape2(2, 3) g.cols_of(a) # => 3
fn shape1(n: Int) -> Buf(Int)
#g.shape1(4) # => 1 g.shape1(4)[0] # => 4
fn shape2(r: Int, c: Int) -> Buf(Int)
s = g.shape2(2, 3) s[0] # => 2 s[1] # => 3
fn same_shape(a: Grid, b: Grid) -> Bool
g.same_shape(g.zeros2(2, 3), g.ones2(2, 3)) # => true g.same_shape(g.zeros2(2, 3), g.ones2(3, 2)) # => false
fn contiguous(g: Grid) -> Bool
Whether the elements sit in the buffer in order, starting at its front.
a = g.arange(6) |> g.reshape2(2, 3) g.contiguous(a) # => true g.contiguous(g.transpose(a)) # => false
fn zeros(shape: Buf(Int)) -> Grid
g.zeros(g.shape2(2, 2)) # => [[0, 0], [0, 0]]
fn zeros1(n: Int) -> Grid
g.zeros1(3) # => [0, 0, 0]
fn zeros2(r: Int, c: Int) -> Grid
g.zeros2(1, 2) # => [[0, 0]]
fn full(shape: Buf(Int), v: Float) -> Grid
g.full(g.shape1(3), 7.5) # => [7.5, 7.5, 7.5]
fn ones(shape: Buf(Int)) -> Grid
g.ones(g.shape2(2, 2)) # => [[1, 1], [1, 1]]
fn ones1(n: Int) -> Grid
g.ones1(2) # => [1, 1]
fn ones2(r: Int, c: Int) -> Grid
g.ones2(2, 1) # => [[1], [1]]
fn full2(r: Int, c: Int, v: Float) -> Grid
g.full2(2, 2, 3.0) # => [[3, 3], [3, 3]]
fn arange(n: Int) -> Grid
0, 1, …, n - 1
g.arange(5) # => [0, 1, 2, 3, 4]
fn linspace(a: Float, b: Float, n: Int) -> Grid
g.linspace(0.0, 1.0, 5) # => [0, 0.25, 0.5, 0.75, 1]
fn of(xs: List(Float)) -> Grid
A 1-D grid from a list of floats.
g.of(Cons(1.5, Cons(2.5, Nil))) # => [1.5, 2.5]
fn from_fn2(r: Int, c: Int, f: (Int, Int) -> Float) -> Grid
A grid whose element at (i, j) is f(i, j).
g.from_fn2(2, 3, \i, j -> to_float(i * 10 + j)) # => [[0, 1, 2], [10, 11, 12]]
fn from_fn1(n: Int, f: (Int) -> Float) -> Grid
g.from_fn1(4, \i -> to_float(i * i)) # => [0, 1, 4, 9]
fn at1(g: Grid, i: Int) -> Float
g.at1(g.arange(5), 3) # => 3
fn at2(g: Grid, i: Int, j: Int) -> Float
a = g.arange(6) |> g.reshape2(2, 3) g.at2(a, 1, 2) # => 5
fn put1(g: Grid, i: Int, v: Float) -> Unit
v = g.zeros1(3) g.put1(v, 1, 9.0) v # => [0, 9, 0]
fn put2(g: Grid, i: Int, j: Int, v: Float) -> Unit
m = g.zeros2(2, 2) g.put2(m, 1, 0, 9.0) m # => [[0, 0], [9, 0]]
fn transpose(g: Grid) -> Grid
Two-dimensional transpose: the same buffer read the other way round.
a = g.arange(6) |> g.reshape2(2, 3) g.transpose(a) # => [[0, 3], [1, 4], [2, 5]]
fn rows(g: Grid, lo: Int, hi: Int) -> Grid
Rows lo up to hi of a 2-D grid, or elements lo up to hi of a 1-D one.
m = g.arange(6) |> g.reshape2(3, 2) g.rows(m, 1, 3) # => [[2, 3], [4, 5]] g.rows(g.arange(5), 1, 3) # => [1, 2]
fn slice1(g: Grid, lo: Int, hi: Int) -> Grid
g.slice1(g.arange(5), 2, 4) # => [2, 3]
fn row(g: Grid, i: Int) -> Grid
a = g.arange(6) |> g.reshape2(2, 3) g.row(a, 1) # => [3, 4, 5]
fn col(g: Grid, j: Int) -> Grid
a = g.arange(6) |> g.reshape2(2, 3) g.col(a, 2) # => [2, 5]
fn copy(g: Grid) -> Grid
A contiguous grid with the same elements.
a = g.arange(6) |> g.reshape2(2, 3) t = g.transpose(a) g.contiguous(g.copy(t)) # => true g.copy(t) # => [[0, 3], [1, 4], [2, 5]]
fn reshape(g: Grid, shape: Buf(Int)) -> Grid
g.reshape(g.arange(4), g.shape2(2, 2)) # => [[0, 1], [2, 3]]
fn reshape2(g: Grid, r: Int, c: Int) -> Grid
g.reshape2(g.arange(6), 3, 2) # => [[0, 1], [2, 3], [4, 5]]
fn flatten(g: Grid) -> Grid
a = g.arange(6) |> g.reshape2(2, 3) g.flatten(a) # => [0, 1, 2, 3, 4, 5]
fn empty_like(g: Grid) -> Grid
A grid of the same shape, filled with nothing in particular, with zeros, with ones, or with one number.
a = g.arange(6) |> g.reshape2(2, 3) g.same_shape(g.empty_like(a), a) # => true
fn zeros_like(g: Grid) -> Grid
a = g.arange(6) |> g.reshape2(2, 3) g.zeros_like(a) # => [[0, 0, 0], [0, 0, 0]]
fn ones_like(g: Grid) -> Grid
g.ones_like(g.arange(3)) # => [1, 1, 1]
fn full_like(g: Grid, v: Float) -> Grid
g.full_like(g.arange(2), 4.0) # => [4, 4]
fn squeeze(g: Grid) -> Grid
A 2-D grid with one row or one column becomes the 1-D grid of its elements; anything else is unchanged.
g.squeeze(g.ones2(1, 3)) # => [1, 1, 1] g.squeeze(g.ones2(2, 2)) # => [[1, 1], [1, 1]]
fn ravel(g: Grid) -> Grid
a = g.arange(6) |> g.reshape2(2, 3) g.ravel(g.transpose(a)) # => [0, 3, 1, 4, 2, 5]
fn logspace(a: Float, b: Float, n: Int, base: Float) -> Grid
n numbers from a to b, evenly spaced in the exponent: logspace takes the ends as powers of the base, geomspace as the values.
g.logspace(0.0, 2.0, 3, 10.0) # => [1, 10, 100]
fn geomspace(a: Float, b: Float, n: Int) -> Grid
g.geomspace(1.0, 8.0, 4) # => [1, 2, 4, 8]
fn concat(a: Grid, b: Grid, axis: Int) -> Grid
Joining: axis 0 adds rows, 1 adds columns. Two 1-D grids join end to end.
g.concat(g.ones2(1, 2), g.zeros2(1, 2), 0) # => [[1, 1], [0, 0]] g.concat(g.ones2(1, 2), g.zeros2(1, 2), 1) # => [[1, 1, 0, 0]] g.concat(g.arange(2), g.arange(2), 0) # => [0, 1, 0, 1]
fn as_rows(g: Grid) -> Grid
g.as_rows(g.arange(3)) # => [[0, 1, 2]]
fn vstack(a: Grid, b: Grid) -> Grid
g.vstack(g.arange(2), g.ones1(2)) # => [[0, 1], [1, 1]]
fn hstack(a: Grid, b: Grid) -> Grid
g.hstack(g.ones2(2, 1), g.zeros2(2, 1)) # => [[1, 0], [1, 0]]
fn stack(a: Grid, b: Grid) -> Grid
Two 1-D grids as the two rows of a 2-D one.
g.stack(g.arange(2), g.ones1(2)) # => [[0, 1], [1, 1]]
fn tile(g: Grid, k: Int) -> Grid
The whole grid laid down k times, and each element k times over.
g.tile(g.arange(2), 2) # => [0, 1, 0, 1]
fn repeat(g: Grid, k: Int) -> Grid
g.repeat(g.arange(2), 2) # => [0, 0, 1, 1]
fn diag(g: Grid) -> Grid
From a 1-D grid, the square matrix with it down the diagonal; from a 2-D one, the diagonal itself.
g.diag(g.arange(2)) # => [[0, 0], [0, 1]] g.diag(g.arange(4) |> g.reshape2(2, 2)) # => [0, 3]
fn diagonal(g: Grid) -> Grid
g.diagonal(g.arange(4) |> g.reshape2(2, 2)) # => [0, 3]
fn tril(g: Grid, k: Int) -> Grid
The lower and upper triangles, keeping everything on or beyond the k-th diagonal and zeroing the rest.
g.tril(g.ones2(2, 2), 0) # => [[1, 0], [1, 1]]
fn triu(g: Grid, k: Int) -> Grid
g.triu(g.ones2(2, 2), 0) # => [[1, 1], [0, 1]]
fn meshgrid(x: Grid, y: Grid) -> (Grid, Grid)
Every pair from x and y, as NumPy lays them out: rows of x repeated down, columns of y repeated across.
(xs, ys) = g.meshgrid(g.arange(2), g.arange(3)) xs # => [[0, 1], [0, 1], [0, 1]] ys # => [[0, 0], [1, 1], [2, 2]]
fn fliplr(g: Grid) -> Grid
Reversed: along the last axis, or both axes of a 2-D grid.
g.fliplr(g.arange(3)) # => [2, 1, 0] g.fliplr(g.arange(4) |> g.reshape2(2, 2)) # => [[1, 0], [3, 2]]
fn flipud(g: Grid) -> Grid
g.flipud(g.arange(4) |> g.reshape2(2, 2)) # => [[2, 3], [0, 1]]
fn flip(g: Grid) -> Grid
g.flip(g.arange(4) |> g.reshape2(2, 2)) # => [[3, 2], [1, 0]]
fn roll(g: Grid, k: Int) -> Grid
Everything moved along by k, what falls off one end coming back at the other.
g.roll(g.arange(4), 1) # => [3, 0, 1, 2] g.roll(g.arange(4), -1) # => [1, 2, 3, 0]
fn take(g: Grid, idx: Grid) -> Grid
The elements at the given positions, which are a grid of numbers.
g.take(g.of(Cons(10.0, Cons(20.0, Cons(30.0, Nil)))), g.of(Cons(2.0, Cons(0.0, Nil)))) # => [30, 10]
fn dense(g: Grid) -> Grid
The contiguous copy every operation works from when a view is handed in.
a = g.arange(6) |> g.reshape2(2, 3) g.dense(g.transpose(a)) # => [[0, 3], [1, 4], [2, 5]]
fn add(a: Grid, b: Grid) -> Grid
g.add(g.arange(3), g.ones1(3)) # => [1, 2, 3]
fn add_into(o: Buf(Float), y: Buf(Float), lo: Int, hi: Int) -> Int
The same, written into the first operand's own storage: one buffer, so the loop has no overlap to worry about and vectorizes as the fresh one.
v = g.arange(3) g.add_into(v.data, g.ones1(3).data, 0, 3) # the buffers, over a range: what the Python binding calls v # => [1, 2, 3]
fn sub(a: Grid, b: Grid) -> Grid
g.sub(g.arange(3), g.ones1(3)) # => [-1, 0, 1]
fn sub_into(o: Buf(Float), y: Buf(Float), lo: Int, hi: Int) -> Int
The same, written into the first operand's own storage: one buffer, so the loop has no overlap to worry about and vectorizes as the fresh one.
v = g.arange(3) g.sub_into(v.data, g.ones1(3).data, 0, 3) v # => [-1, 0, 1]
fn mul(a: Grid, b: Grid) -> Grid
g.mul(g.arange(3), g.arange(3)) # => [0, 1, 4]
fn mul_into(o: Buf(Float), y: Buf(Float), lo: Int, hi: Int) -> Int
The same, written into the first operand's own storage: one buffer, so the loop has no overlap to worry about and vectorizes as the fresh one.
v = g.arange(3) g.mul_into(v.data, g.arange(3).data, 0, 3) v # => [0, 1, 4]
fn div(a: Grid, b: Grid) -> Grid
g.div(g.ones1(2), g.full(g.shape1(2), 4.0)) # => [0.25, 0.25]
fn div_into(o: Buf(Float), y: Buf(Float), lo: Int, hi: Int) -> Int
The same, written into the first operand's own storage: one buffer, so the loop has no overlap to worry about and vectorizes as the fresh one.
v = g.ones1(2) g.div_into(v.data, g.full(g.shape1(2), 4.0).data, 0, 2) v # => [0.25, 0.25]
fn maximum(a: Grid, b: Grid) -> Grid
g.maximum(g.arange(3), g.full(g.shape1(3), 1.0)) # => [1, 1, 2]
fn maximum_into(o: Buf(Float), y: Buf(Float), lo: Int, hi: Int) -> Int
The same, written into the first operand's own storage: one buffer, so the loop has no overlap to worry about and vectorizes as the fresh one.
v = g.arange(3) g.maximum_into(v.data, g.ones1(3).data, 0, 3) v # => [1, 1, 2]
fn minimum(a: Grid, b: Grid) -> Grid
g.minimum(g.arange(3), g.full(g.shape1(3), 1.0)) # => [0, 1, 1]
fn minimum_into(o: Buf(Float), y: Buf(Float), lo: Int, hi: Int) -> Int
The same, written into the first operand's own storage: one buffer, so the loop has no overlap to worry about and vectorizes as the fresh one.
v = g.arange(3) g.minimum_into(v.data, g.ones1(3).data, 0, 3) v # => [0, 1, 1]
fn scale(g: Grid, v: Float) -> Grid
Every element times v, plus v, to the power v.
g.scale(g.arange(3), 2.0) # => [0, 2, 4]
fn scale_into(o: Buf(Float), v: Float, lo: Int, hi: Int) -> Int
v = g.arange(3) g.scale_into(v.data, 2.0, 0, 3) v # => [0, 2, 4]
fn shift(g: Grid, v: Float) -> Grid
g.shift(g.arange(3), 10.0) # => [10, 11, 12]
fn shift_into(o: Buf(Float), v: Float, lo: Int, hi: Int) -> Int
v = g.arange(3) g.shift_into(v.data, 10.0, 0, 3) v # => [10, 11, 12]
fn power(g: Grid, v: Float) -> Grid
g.power(g.arange(4), 2.0) # => [0, 1, 4, 9]
fn power_into(o: Buf(Float), v: Float, lo: Int, hi: Int) -> Int
v = g.arange(4) g.power_into(v.data, 2.0, 0, 4) v # => [0, 1, 4, 9]
fn neg(g: Grid) -> Grid
g.neg(g.arange(3)) # => [-0, -1, -2]
fn gabs(g: Grid) -> Grid
g.gabs(g.of(Cons(-1.5, Cons(2.0, Nil)))) # => [1.5, 2]
fn gabs_into(o: Buf(Float), lo: Int, hi: Int) -> Int
v = g.of(Cons(-1.5, Nil)) g.gabs_into(v.data, 0, 1) v # => [1.5]
fn gsqrt(g: Grid) -> Grid
g.gsqrt(g.of(Cons(4.0, Cons(9.0, Nil)))) # => [2, 3]
fn gsqrt_into(o: Buf(Float), lo: Int, hi: Int) -> Int
v = g.of(Cons(4.0, Nil)) g.gsqrt_into(v.data, 0, 1) v # => [2]
fn gexp(g: Grid) -> Grid
g.gexp(g.zeros1(2)) # => [1, 1]
fn gexp_into(o: Buf(Float), lo: Int, hi: Int) -> Int
v = g.zeros1(1) g.gexp_into(v.data, 0, 1) v # => [1]
fn glog(g: Grid) -> Grid
g.glog(g.of(Cons(1.0, Nil))) # => [0]
fn glog_into(o: Buf(Float), lo: Int, hi: Int) -> Int
v = g.ones1(1) g.glog_into(v.data, 0, 1) v # => [0]
fn pi() -> Float
pi, to the last bit a double holds.
g.pi() > 3.14159 && g.pi() < 3.1416 # => true
fn gsin(g: Grid) -> Grid
g.gsin(g.zeros1(2)) # => [0, 0]
fn gcos(g: Grid) -> Grid
g.gcos(g.zeros1(2)) # => [1, 1]
fn gtan(g: Grid) -> Grid
g.gtan(g.zeros1(1)) # => [0]
fn gasin(g: Grid) -> Grid
g.gasin(g.ones1(1)) |> g.scale(2.0) |> g.allclose(g.of(Cons(g.pi(), Nil)), 0.000001) # => true
fn gacos(g: Grid) -> Grid
g.gacos(g.ones1(2)) # => [0, 0]
fn gatan(g: Grid) -> Grid
g.gatan(g.zeros1(1)) # => [0]
fn gsinh(g: Grid) -> Grid
g.gsinh(g.zeros1(1)) # => [0]
fn gcosh(g: Grid) -> Grid
g.gcosh(g.zeros1(1)) # => [1]
fn gtanh(g: Grid) -> Grid
g.gtanh(g.zeros1(1)) # => [0]
fn gexp2(g: Grid) -> Grid
g.gexp2(g.arange(4)) # => [1, 2, 4, 8]
fn gexpm1(g: Grid) -> Grid
g.gexpm1(g.zeros1(1)) # => [0]
fn glog2(g: Grid) -> Grid
g.glog2(g.of(Cons(8.0, Nil))) # => [3]
fn glog10(g: Grid) -> Grid
g.glog10(g.of(Cons(1000.0, Nil))) # => [3]
fn glog1p(g: Grid) -> Grid
g.glog1p(g.zeros1(1)) # => [0]
fn gcbrt(g: Grid) -> Grid
g.gcbrt(g.of(Cons(27.0, Cons(-8.0, Nil)))) # => [3, -2]
fn gfloor(g: Grid) -> Grid
g.gfloor(g.of(Cons(1.7, Cons(-1.2, Nil)))) # => [1, -2]
fn gceil(g: Grid) -> Grid
g.gceil(g.of(Cons(1.2, Cons(-1.7, Nil)))) # => [2, -1]
fn gtrunc(g: Grid) -> Grid
g.gtrunc(g.of(Cons(1.7, Cons(-1.7, Nil)))) # => [1, -1]
fn ground(g: Grid) -> Grid
To the nearest whole number, a half going to the even one — what rint does and what NumPy's round and rint both do. C's round sends a half away from zero instead, which would put round(2.5) at 3 and round(-2.5) at -3; this puts both at 2 and -2.
g.ground(g.of(Cons(2.5, Cons(3.5, Cons(-2.5, Nil))))) # => [2, 4, -2]
fn grint(g: Grid) -> Grid
g.grint(g.of(Cons(0.5, Cons(1.5, Nil)))) # => [0, 2]
fn gsquare(g: Grid) -> Grid
g.gsquare(g.arange(4)) # => [0, 1, 4, 9]
fn greciprocal(g: Grid) -> Grid
g.greciprocal(g.of(Cons(2.0, Cons(4.0, Nil)))) # => [0.5, 0.25]
fn gsign(g: Grid) -> Grid
1 where positive, -1 where negative, and zero left as it is (sign included).
g.gsign(g.of(Cons(-3.0, Cons(0.0, Cons(2.0, Nil))))) # => [-1, 0, 1]
fn gdegrees(g: Grid) -> Grid
g.gdegrees(g.of(Cons(g.pi(), Nil))) # => [180]
fn gradians(g: Grid) -> Grid
g.gradians(g.of(Cons(180.0, Nil))) |> g.allclose(g.of(Cons(g.pi(), Nil)), 0.000001) # => true
fn gclip(g: Grid, a: Float, b: Float) -> Grid
Held between a and b.
g.gclip(g.arange(5), 1.0, 3.0) # => [1, 1, 2, 3, 3]
fn gmaxk(g: Grid, v: Float) -> Grid
The larger, and the smaller, of each element and one number.
g.gmaxk(g.arange(3), 1.0) # => [1, 1, 2]
fn gmink(g: Grid, v: Float) -> Grid
g.gmink(g.arange(3), 1.0) # => [0, 1, 1]
fn gfmodk(g: Grid, v: Float) -> Grid
g.gfmodk(g.arange(5), 3.0) # => [0, 1, 2, 0, 1]
fn gatan2(a: Grid, b: Grid) -> Grid
The angle of (y, x) — gatan2(y, x), as C and NumPy order it.
g.gatan2(g.ones1(1), g.zeros1(1)) |> g.allclose(g.of(Cons(g.pi() / 2.0, Nil)), 0.000001) # => true
fn ghypot(a: Grid, b: Grid) -> Grid
g.ghypot(g.of(Cons(3.0, Nil)), g.of(Cons(4.0, Nil))) # => [5]
fn gfmod(a: Grid, b: Grid) -> Grid
g.gfmod(g.of(Cons(7.0, Nil)), g.of(Cons(3.0, Nil))) # => [1]
fn gcopysign(a: Grid, b: Grid) -> Grid
g.gcopysign(g.of(Cons(3.0, Nil)), g.of(Cons(-1.0, Nil))) # => [-3]
fn gpowg(a: Grid, b: Grid) -> Grid
Each element to the matching element's power.
g.gpowg(g.of(Cons(2.0, Cons(3.0, Nil))), g.of(Cons(3.0, Cons(2.0, Nil)))) # => [8, 9]
fn map(g: Grid, f: (Float) -> Float) -> Grid
f on every element; zip on every pair. For what the named operations above do not cover — they are the fast ones: a lambda is a call per element.
g.map(g.arange(3), \x -> x * x + 1.0) # => [1, 2, 5]
fn zip(a: Grid, b: Grid, f: (Float, Float) -> Float) -> Grid
g.zip(g.arange(3), g.ones1(3), \x, y -> x - y) # => [-1, 0, 1]
fn mask_of(shape: Buf(Int)) -> Mask
g.mcount(g.mask_of(g.shape1(3))) # => 0 g.any(g.mask_of(g.shape1(3))) # => false
fn gt(a: Grid, b: Grid) -> Mask
gt(a, b) is a > b element by element; gtk(a, v) compares with one number. lt, ge, le, eq, ne and their k forms likewise.
g.gt(g.arange(3), g.ones1(3)) # => [false, false, true]
fn lt(a: Grid, b: Grid) -> Mask
g.lt(g.arange(3), g.ones1(3)) # => [true, false, false]
fn ge(a: Grid, b: Grid) -> Mask
g.ge(g.arange(3), g.ones1(3)) # => [false, true, true]
fn le(a: Grid, b: Grid) -> Mask
g.le(g.arange(3), g.ones1(3)) # => [true, true, false]
fn eq(a: Grid, b: Grid) -> Mask
g.eq(g.arange(3), g.ones1(3)) # => [false, true, false]
fn ne(a: Grid, b: Grid) -> Mask
g.ne(g.arange(3), g.ones1(3)) # => [true, false, true]
fn gtk(g: Grid, v: Float) -> Mask
g.gtk(g.arange(3), 1.0) # => [false, false, true]
fn ltk(g: Grid, v: Float) -> Mask
g.ltk(g.arange(3), 1.0) # => [true, false, false]
fn gek(g: Grid, v: Float) -> Mask
g.gek(g.arange(3), 1.0) # => [false, true, true]
fn lek(g: Grid, v: Float) -> Mask
g.lek(g.arange(3), 1.0) # => [true, true, false]
fn eqk(g: Grid, v: Float) -> Mask
g.eqk(g.arange(3), 1.0) # => [false, true, false]
fn nek(g: Grid, v: Float) -> Mask
g.nek(g.arange(3), 1.0) # => [true, false, true]
fn where(m: Mask, a: Grid, b: Grid) -> Grid
a where the mask holds, b elsewhere.
g.where(g.gtk(g.arange(4), 1.0), g.arange(4), g.zeros1(4)) # => [0, 0, 2, 3]
fn mcount(m: Mask) -> Int
How many elements the mask holds for.
g.mcount(g.gtk(g.arange(5), 1.0)) # => 3
fn any(m: Mask) -> Bool
g.any(g.gtk(g.arange(3), 1.0)) # => true g.any(g.gtk(g.arange(3), 5.0)) # => false
fn all(m: Mask) -> Bool
g.all(g.gek(g.arange(3), 0.0)) # => true
fn mask_not(m: Mask) -> Mask
g.mask_not(g.gtk(g.arange(3), 1.0)) # => [true, true, false]
fn mask_and(a: Mask, b: Mask) -> Mask
g.mask_and(g.gtk(g.arange(4), 0.0), g.ltk(g.arange(4), 3.0)) # => [false, true, true, false]
fn mask_or(a: Mask, b: Mask) -> Mask
g.mask_or(g.ltk(g.arange(4), 1.0), g.gtk(g.arange(4), 2.0)) # => [true, false, false, true]
fn to_grid(m: Mask) -> Grid
The mask as a grid of 1.0 and 0.0.
g.to_grid(g.gtk(g.arange(3), 1.0)) # => [0, 0, 1]
fn isnan(g: Grid) -> Mask
Not a number, endless, and neither: v != v is true of a NaN alone, and v - v is zero for every ordinary number and a NaN for an infinity.
g.isnan(g.of(Cons(1.0, Cons(g.nan(), Nil)))) # => [false, true]
fn isinf(g: Grid) -> Mask
g.isinf(g.of(Cons(1.0, Cons(g.inf(), Nil)))) # => [false, true]
fn isfinite(g: Grid) -> Mask
g.isfinite(g.of(Cons(1.0, Cons(g.inf(), Cons(g.nan(), Nil))))) # => [true, false, false]
fn isclose(a: Grid, b: Grid, eps: Float) -> Mask
Element by element, within eps of each other.
g.isclose(g.of(Cons(1.0, Cons(2.0, Nil))), g.of(Cons(1.0000001, Cons(2.5, Nil))), 0.001) # => [true, false]
fn allclose(a: Grid, b: Grid, eps: Float) -> Bool
g.allclose(g.arange(3), g.arange(3), 0.0) # => true
fn sum(g: Grid) -> Float
Reductions run eight partial sums side by side — a flat tuple, so eight registers, which the compiler pairs into four vector lanes — and add them at the end: one running sum is a chain of adds each waiting for the last, eight are enough chains to keep the memory bus busy rather than the adder. (The order of additions differs from one running sum, as NumPy's pairwise summation differs from both; the answers agree to the last few bits.)
g.sum(g.arange(5)) # => 10
fn mean(g: Grid) -> Float
g.mean(g.arange(5)) # => 2
fn prod(g: Grid) -> Float
g.prod(g.of(Cons(2.0, Cons(3.0, Cons(4.0, Nil))))) # => 24
fn dot(a: Grid, b: Grid) -> Float
g.dot(g.of(Cons(1.0, Cons(2.0, Nil))), g.of(Cons(3.0, Cons(4.0, Nil)))) # => 11
fn norm(g: Grid) -> Float
g.norm(g.of(Cons(3.0, Cons(4.0, Nil)))) # => 5
fn gmax(g: Grid) -> Float
The largest and smallest, eight lanes at a time for the same reason as sum; a comparison is not an addition, so the eight agree exactly with one.
g.gmax(g.of(Cons(2.0, Cons(9.0, Cons(4.0, Nil))))) # => 9
fn gmin(g: Grid) -> Float
g.gmin(g.of(Cons(2.0, Cons(9.0, Cons(4.0, Nil))))) # => 2
fn argmax(g: Grid) -> Int
Where the largest is, the first of them if there are several: four lanes, each keeping the first place it saw its own largest, and the lanes settled by value first and place second.
g.argmax(g.of(Cons(2.0, Cons(9.0, Cons(9.0, Nil))))) # => 1
fn argmin(g: Grid) -> Int
g.argmin(g.of(Cons(2.0, Cons(9.0, Cons(1.0, Nil))))) # => 2
fn var(g: Grid) -> Float
g.var(g.of(Cons(2.0, Cons(4.0, Cons(4.0, Cons(6.0, Nil)))))) # => 2
fn std(g: Grid) -> Float
g.std(g.of(Cons(2.0, Cons(4.0, Cons(4.0, Cons(6.0, Nil)))))) # => 1.41421
fn cumsum(g: Grid) -> Grid
g.cumsum(g.arange(4)) # => [0, 1, 3, 6]
fn sum_axis(g: Grid, axis: Int) -> Grid
Sums of a 2-D grid along an axis: 0 adds the rows together (one number per column), 1 adds each row up (one number per row). No copy for a view: the loops follow the strides. When the axis being summed steps by one, each answer is a contiguous eight-lane sum; when the other axis does, the answers are accumulated a row at a time, which vectorizes across them. Either way a transposed grid is summed as fast as the grid it came from, and the answers are split across the workers.
a = g.arange(6) |> g.reshape2(2, 3) g.sum_axis(a, 0) # => [3, 5, 7] g.sum_axis(a, 1) # => [3, 12]
fn mean_axis(g: Grid, axis: Int) -> Grid
a = g.arange(6) |> g.reshape2(2, 3) g.mean_axis(a, 1) # => [1, 4]
fn sort(g: Grid) -> Grid
Quicksort: a median-of-three pivot, insertion sort under sixteen, and the larger half left to the tail call — so the stack is logarithmic however the input is shaped.
g.sort(g.of(Cons(3.0, Cons(1.0, Cons(2.0, Nil))))) # => [1, 2, 3]
fn argsort(g: Grid) -> Grid
Where the elements would go if they were sorted, ties in the order they were in: a merge sort over the positions, which is what keeps it stable.
g.argsort(g.of(Cons(3.0, Cons(1.0, Cons(2.0, Nil))))) # => [1, 2, 0]
fn distinct(g: Grid) -> Grid
The values that occur, each once, in order.
g.distinct(g.of(Cons(3.0, Cons(1.0, Cons(3.0, Nil))))) # => [1, 3]
fn searchsorted(g: Grid, v: Float) -> Int
Where v would go in a sorted grid, before anything equal to it.
g.searchsorted(g.of(Cons(1.0, Cons(3.0, Cons(5.0, Nil)))), 3.0) # => 1 g.searchsorted(g.of(Cons(1.0, Cons(3.0, Cons(5.0, Nil)))), 4.0) # => 2
fn nonzero(g: Grid) -> Grid
The positions of the elements that are not zero.
g.nonzero(g.of(Cons(0.0, Cons(2.0, Cons(0.0, Cons(5.0, Nil)))))) # => [1, 3]
fn count_nonzero(g: Grid) -> Int
g.count_nonzero(g.of(Cons(0.0, Cons(2.0, Cons(5.0, Nil))))) # => 2
fn median(g: Grid) -> Float
The middle value, and the value a fraction of the way along — both from a sorted copy, and both interpolating between neighbours the way NumPy does.
g.median(g.of(Cons(3.0, Cons(1.0, Cons(2.0, Nil))))) # => 2 g.median(g.of(Cons(4.0, Cons(1.0, Cons(2.0, Cons(3.0, Nil)))))) # => 2.5
fn percentile(g: Grid, q: Float) -> Float
g.percentile(g.arange(5), 50.0) # => 2 g.percentile(g.arange(5), 25.0) # => 1
fn quantile(g: Grid, q: Float) -> Float
g.quantile(g.arange(5), 0.75) # => 3
fn ptp(g: Grid) -> Float
The distance from the smallest to the largest.
g.ptp(g.of(Cons(2.0, Cons(9.0, Cons(4.0, Nil))))) # => 7
fn average(g: Grid) -> Float
g.average(g.arange(4)) # => 1.5
fn diff(g: Grid) -> Grid
The difference between each element and the one before it.
g.diff(g.of(Cons(1.0, Cons(4.0, Cons(9.0, Nil))))) # => [3, 5]
fn cumprod(g: Grid) -> Grid
g.cumprod(g.of(Cons(1.0, Cons(2.0, Cons(3.0, Nil))))) # => [1, 2, 6]
fn nansum(g: Grid) -> Float
The same reductions with the NaNs left out, as NumPy's nan* do.
g.nansum(g.of(Cons(1.0, Cons(g.nan(), Cons(2.0, Nil))))) # => 3
fn nanmean(g: Grid) -> Float
g.nanmean(g.of(Cons(1.0, Cons(g.nan(), Cons(3.0, Nil))))) # => 2
fn nanmax(g: Grid) -> Float
g.nanmax(g.of(Cons(1.0, Cons(g.nan(), Cons(3.0, Nil))))) # => 3
fn nanmin(g: Grid) -> Float
g.nanmin(g.of(Cons(1.0, Cons(g.nan(), Cons(3.0, Nil))))) # => 1
fn nanargmax(g: Grid) -> Int
g.nanargmax(g.of(Cons(1.0, Cons(g.nan(), Cons(3.0, Nil))))) # => 2
fn nanargmin(g: Grid) -> Int
g.nanargmin(g.of(Cons(1.0, Cons(g.nan(), Cons(3.0, Nil))))) # => 0
fn nan() -> Float
The two floats that are not numbers, for a program that needs to say so.
g.nan() == g.nan() # => false
fn inf() -> Float
g.inf() > 1e308 # => true
fn matmul(a: Grid, b: Grid) -> Grid
(r × k) times (k × c), the way a BLAS does it: ten columns of the right operand are packed into a panel that stays in the nearest cache, and a kernel walks four rows of the left operand down that panel keeping the 4 × 10 block of answers in twenty vector registers — Float2 pairs, one fused multiply-add each per step. Column blocks are handed to strands when there are workers to take them. Rows and columns past the last whole block are finished by the plain triple loop.
a = g.arange(6) |> g.reshape2(2, 3) g.matmul(a, g.transpose(a)) # => [[5, 14], [14, 50]]
fn matvec(a: Grid, v: Grid) -> Grid
Matrix times vector, as a 1-D grid.
a = g.arange(6) |> g.reshape2(2, 3) g.matvec(a, g.ones1(3)) # => [3, 12]
fn outer(a: Grid, b: Grid) -> Grid
g.outer(g.arange(2), g.of(Cons(1.0, Cons(2.0, Nil)))) # => [[0, 0], [1, 2]]
fn identity(n: Int) -> Grid
g.identity(2) # => [[1, 0], [0, 1]]
fn trace(g: Grid) -> Float
g.trace(g.arange(4) |> g.reshape2(2, 2)) # => 3
fn lu(a: Grid) -> Lu
m = g.of(Cons(4.0, Cons(3.0, Cons(6.0, Cons(3.0, Nil))))) |> g.reshape2(2, 2) f = g.lu(m) f.ok # => true f.sign # => -1
fn solve(a: Grid, b: Grid) -> Grid
x such that a x = b, for a vector b or a matrix of them.
m = g.of(Cons(2.0, Cons(1.0, Cons(1.0, Cons(3.0, Nil))))) |> g.reshape2(2, 2) g.solve(m, g.of(Cons(3.0, Cons(5.0, Nil)))) # => [0.8, 1.4]
fn try_solve(a: Grid, b: Grid) -> Grid
The same, saying so by handing back a grid of no elements rather than stopping the program — what a host that has to answer for itself calls.
singular = g.ones2(2, 2) g.size(g.try_solve(singular, g.ones1(2))) # => 0
fn det(a: Grid) -> Float
The determinant: the product down U's diagonal, with the sign the row exchanges left. Zero when the matrix is singular.
g.det(g.of(Cons(1.0, Cons(2.0, Cons(3.0, Cons(4.0, Nil))))) |> g.reshape2(2, 2)) # => -2 g.det(g.ones2(2, 2)) # => 0
fn inv(a: Grid) -> Grid
The inverse. Solving against the identity would do it, but a third of that work is on zeros: the forward pass turns the identity into a lower triangle, so a block of rows only has to touch the columns it can have reached. Doing it that way needs the answer's columns put back in the order the row exchanges took them out of, which is the last pass here.
g.inv(g.of(Cons(4.0, Cons(7.0, Cons(2.0, Cons(6.0, Nil))))) |> g.reshape2(2, 2)) # => [[0.6, -0.7], [-0.2, 0.4]]
fn try_inv(a: Grid) -> Grid
g.size(g.try_inv(g.ones2(2, 2))) # => 0
fn matrix_power(a: Grid, k: Int) -> Grid
a multiplied by itself k times, by squaring: a hundredth power is seven multiplications rather than ninety-nine.
g.matrix_power(g.of(Cons(1.0, Cons(1.0, Cons(1.0, Cons(0.0, Nil))))) |> g.reshape2(2, 2), 10) # => [[89, 55], [55, 34]]
fn lstsq(a: Grid, b: Grid) -> Grid
The least-squares solution of an overdetermined system, through the normal equations: x least-squaring a x - b. Forming aᵀa squares the conditioning, so a badly conditioned a wants a decomposition this library does not have yet.
xs = g.of(Cons(1.0, Cons(1.0, Cons(1.0, Cons(2.0, Cons(1.0, Cons(3.0, Nil))))))) |> g.reshape2(3, 2) g.lstsq(xs, g.of(Cons(2.0, Cons(4.0, Cons(6.0, Nil))))) |> g.allclose(g.of(Cons(0.0, Cons(2.0, Nil))), 0.000001) # => true
fn try_lstsq(a: Grid, b: Grid) -> Grid
g.size(g.try_lstsq(g.zeros2(2, 2), g.ones1(2))) # => 0
fn norm1(a: Grid) -> Float
The sizes of a matrix: the largest column sum, the largest row sum, and the square root of the sum of the squares.
g.norm1(g.of(Cons(1.0, Cons(-2.0, Cons(3.0, Cons(4.0, Nil))))) |> g.reshape2(2, 2)) # => 6
fn norm_inf(a: Grid) -> Float
g.norm_inf(g.of(Cons(1.0, Cons(-2.0, Cons(3.0, Cons(4.0, Nil))))) |> g.reshape2(2, 2)) # => 7
fn norm_fro(a: Grid) -> Float
g.norm_fro(g.of(Cons(3.0, Cons(0.0, Cons(0.0, Cons(4.0, Nil))))) |> g.reshape2(2, 2)) # => 5
fn cond(a: Grid) -> Float
How far from singular, measured with the largest column sum.
g.cond(g.identity(3)) # => 1
fn inner(a: Grid, b: Grid) -> Float
The dot product under other names, the outer product of every pair, and the cross product of two three-element vectors.
g.inner(g.of(Cons(1.0, Cons(2.0, Nil))), g.of(Cons(3.0, Cons(4.0, Nil)))) # => 11
fn vdot(a: Grid, b: Grid) -> Float
g.vdot(g.of(Cons(1.0, Cons(2.0, Nil))), g.of(Cons(3.0, Cons(4.0, Nil)))) # => 11
fn kron(a: Grid, b: Grid) -> Grid
g.kron(g.of(Cons(1.0, Cons(2.0, Nil))) |> g.as_rows, g.of(Cons(1.0, Cons(10.0, Nil))) |> g.as_rows) # => [[1, 10, 2, 20]]
fn cross(a: Grid, b: Grid) -> Grid
g.cross(g.of(Cons(1.0, Cons(0.0, Cons(0.0, Nil)))), g.of(Cons(0.0, Cons(1.0, Cons(0.0, Nil))))) # => [0, 0, 1]
fn render(g: Grid) -> Str
g.render(g.arange(3)) # => [0, 1, 2]
fn render_mask(m: Mask) -> Str
g.render_mask(g.gtk(g.arange(3), 1.0)) # => [false, false, true]