Rill v0.13 Reference

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]