Numy

hex.pm version Build Status Workflow hex.pm

Numy is LAPACK based scientific computing library. Online API documentation is here.

Table of contents

Example

See this example in LAPACK reference documentation.

iex(1)> a = Numy.Lapack.new_tensor([3,5])
iex(2)> Numy.Tz.assign(a, [
...(2)> [1,1,1],
...(2)> [2,3,4],
...(2)> [3,5,2],
...(2)> [4,2,5],
...(2)> [5,4,3]])
:ok
iex(3)> b = Numy.Lapack.new_tensor([2,5])
iex(4)> Numy.Tz.assign(b, [
...(4)> [-10,-3],
...(4)> [12,14],
...(4)> [14,12],
...(4)> [16,16],
...(4)> [18,16]])
:ok
iex(5)> Numy.Lapack.solve_lls(a,b)
0
iex(6)> solution = Numy.Lapack.data(b,2*3)
[1.9999999999999982, 0.9999999999999983, 0.9999999999999991, 0.9999999999999997,
 1.0000000000000024, 2.0000000000000018]
iex(7)> Numy.Float.equal?(solution, [[2,1], [1,1], [1,2]])
true

Comparison

The closest to Numy project (that I am aware of) is Matrex. Matrex is using immutable binaries and NIF code is calling enif_make_binary to return a result (matrix). enif_make_binary allocates memory space for the new binary. Numy on other hand is using mutable NIF resources and can reuse already allocated memory to store the result inside the context of NIF module.

Installation

Ubuntu 18.04, sudo apt install build-essential liblapacke-dev gfortran.

The package can be installed by adding numy to your list of dependencies in mix.exs:

def deps do
  [
    {:numy, "~> 0.1.5"}
  ]
end

Mutable internal state

For performance reasons, Numy NIF objects are mutable. That is, some API functions change internal state of an object. Two sets of APIs are provided, one has functions that change object's internal state and other that does not change it. In order to maintain that immutability, original input/output object is copied and it is its copy that gets mutated.

Example of immutable addition of two vectors

iex(1)> v = Numy.Lapack.Vector.new([1,2,3])
iex(2)> Numy.Vc.add(v,v) # Vc API functions do not mutate internal state
iex(3)> Numy.Vc.data(v)
[1.0, 2.0, 3.0]

Example of two vector addition when one of the vectors changes its state

iex(1)> v = Numy.Lapack.Vector.new([1,2,3])
iex(4)> Numy.Vcm.add!(v,v) # Vcm is API that mutates internal state, functions have suffix '!'
iex(5)> Numy.Vc.data(v)
[2.0, 4.0, 6.0]

Vector operations

Vector Jupyter tutorials:

FunctionVcVcmDescription
new(nelm)Create new vector of size nelm
new(list)Create new vector from Elixir list
new(v)Create new vector as copy of another vector
clone(v)xMake a clone of original vector
new(v1,v2)Create new vector as concatenation of 2 other vectors
save_to_file(v)Save vectors to a file
load_from_file(fn)Load vecotr from a file
assign_zeros(v)xAssign 0.0 to all elements
assign_ones(v)xAssign 1.0 to all elements
assign_random(v)xAssign random values to the elements
assign_all(v,val)xAssign certain values to all elements
empty?(v)xReturn true if vector is empty
data(v)xGet data as a list
at(v,pos)xGet value of N-th element
set_at!(v,pos,val)xSet value of N-th element
contains?(v,val)xCheck if value exists in the vector
find(v,val)xFind value in vector and return its position, -1 if can't find
equal?(v1,v2)xCompare 2 vectors
add(v1,v2)xAdd 2 vectors, cᵢ ← aᵢ + bᵢ
add!(v1,v2)xaᵢ ← aᵢ + bᵢ
sub(v1,v2)xSubtract one vector from other, cᵢ ← aᵢ - bᵢ
sub!(v1,v2)xaᵢ ← aᵢ - bᵢ
mul(v1,v2)xMultiply 2 vectors, cᵢ ← aᵢ×bᵢ
mul!(v1,v2)xaᵢ ← aᵢ×bᵢ
div(v1,v2)xDivide 2 vectors, cᵢ ← aᵢ÷bᵢ
div!(v1,v2)xaᵢ ← aᵢ÷bᵢ
scale(v,factor)xMultiply each element by a constant, aᵢ ← aᵢ×scale_factor
scale!(v,factor)xaᵢ ← aᵢ×scale_factor
offset(v,off)xAdd a constant to each element, aᵢ ← aᵢ + offset
offset!(c,off)xaᵢ ← aᵢ + offset
negate(v)xChange sign of each element, aᵢ ← -aᵢ
negate!(v)xaᵢ ← -aᵢ
dot(v1,v2)xDot product of 2 vectors, ∑aᵢ×bᵢ
sum(v)xSum of all elements, ∑aᵢ
average(v)xAverage (∑aᵢ)/length
max(v)xGet max value
min(v)xGet min value
max_index(v)xGet index of max value
min_index(v)xGet index of min value
apply_heaviside(v)xStep function, aᵢ ← 0 if aᵢ < 0 else 1
apply_heaviside!(v)x
apply_sigmoid(v)xf(x) = 1/(1 + e⁻ˣ)
apply_sigmoid!(v)x
sort(v)xSort elements of array
sort!(v)xSort elements of array in-place
reverse(v)xReverse order of elements
reverse!(v)xReverse in-place
axpby(v)xcᵢ ← aᵢ×factor_a + bᵢ×factor_b
axpby!(v)xaᵢ ← aᵢ×factor_a + bᵢ×factor_b
swap_ranges(a,b,n)swap values between 2 vectors
abs(v)x
abs!(v)x
pow(v)x
pow!(v)x
pow2(v)xcᵢ ← aᵢ²
pow2!(v)xaᵢ ← aᵢ²
norm2(v)x√x₀² + x₁² + ... + xₙ²

Set operations

Numy.Lapack.Vector implements Numy.Set protocol with base Set operations: union, intersection, diff, symm_diff, jaccard_index.

Note: order of elements of input vector can change (they get sorted) when Numy.Set functions are invoked. In other words, Set functions mutate inputs.

iex(6)> a = Numy.Lapack.Vector.new(1..5)
#Vector<size=5, [1.0, 2.0, 3.0, 4.0, 5.0]>
iex(7)> b = Numy.Lapack.Vector.new(5..10)
#Vector<size=6, [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]>
iex(8)> Numy.Set.union(a,b)
#Vector<size=10, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]>
iex(9)> Numy.Set.intersection(a,b)
#Vector<size=1, [5.0]>
iex(10)> Numy.Set.diff(a,b)
#Vector<size=4, [1.0, 2.0, 3.0, 4.0]>
iex(11)> Numy.Set.symm_diff(a,b)
#Vector<size=9, [1.0, 2.0, 3.0, 4.0, 6.0, 7.0, 8.0, 9.0, 10.0]>

Simple Linear Regression

Basic vector operation like mean, offset, pow2 and sum allow to implement simple Linear Regression in Elixir. Module Numy.Fit.SimpleLinear is Elixir code that finds the line that fits input data by calculating variance and covariance.

iex(34)> x = Numy.Lapack.Vector.new(0..9)
#Vector<size=10, [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]>
iex(35)> y = Vc.scale(x,2) |> Vcm.offset!(-3.0) # make slope=2 and intercept=-3
#Vector<size=10, [-3.0, -1.0, 1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0]>
iex(36)> err = Numy.Lapack.Vector.new(10) |> Vc.assign_random |> Vcm.offset!(-0.5) |> Vcm.scale!(0.1)
iex(37)> Vcm.add!(y,err) # add errors to the ideal line
iex(38)> line = Numy.Fit.SimpleLinear.fit(x,y)
{-2.9939933270609496, 1.9966330251198818} # got intercept=-3 and slope=2 as expected