Tensor

hex.pm versionBuild Status

The Tensor library adds support for Vectors, Matrixes and higher-dimension Tensors to Elixir. These data structures allow easier creation and manipulation of multi-dimensional collections of things. One could use them for math, but also to build e.g. board game representations.

The Tensor library builds them in a sparse way.

Vector

A Vector is a one-dimensional collection of elements. It can be viewed as a list with a known length.

iex> use Tensor
iex> vec = Vector.new([1,2,3,4,5])
#Vector-(5)[1, 2, 3, 4, 5]
iex> vec2 = Vector.new(~w{foo bar baz qux})
#Vector-(4)["foo", "bar", "baz", "qux"]
iex> vec2[2]
"baz"
iex> Vector.add(vec, 3)
#Vector-(5)[4, 5, 6, 7, 8]
iex> Vector.add(vec, vec)
#Vector-(5)[2, 4, 6, 8, 10]

It is nicer than a list because:

Vectors are very cool, so the following things have been defined to make working with them a bliss:

When working with numerical vectors, you might also like to:

Matrix

A Matrix is a two-dimensional collection of elements, with known width and height.

These are highly useful for certain mathematical calculations, but also for e.g. board games.

Matrices are super useful, so there are many helper methods defined to work with them.


iex> use Tensor
iex> mat = Matrix.new([[1,2,3],[4,5,6],[7,8,9]],3,3)
#Matrix-(3×3)
                          
       1,       2,       3
       4,       5,       6
       7,       8,       9
                          
iex> Matrix.rotate_clockwise(mat)
#Matrix-(3×3)
                          
       7,       4,       1
       8,       5,       2
       9,       6,       3
                          
iex> mat[0]
#Vector-(3)[1, 2, 3]
iex> mat[2][2]
9
iex> Matrix.diag([1,2,3])
#Matrix-(3×3)
                          
       1,       0,       0
       0,       2,       0
       0,       0,       3
                          

iex> Matrix.add(mat, 2)
#Matrix-(3×3)
                          
       3,       4,       5
       6,       7,       8
       9,      10,      11
                          
iex> Matrix.add(mat, mat)
Matrix.add(mat, mat)
#Matrix-(3×3)
                          
       2,       4,       6
       8,      10,      12
      14,      16,      18
                          

The Matrix module lets you:

As well as some common math operations

Higher-Dimension Tensor

Tensors are implemented using maps internally. This means that read and write access to elements in them is O(log n).

  iex> use Tensor
  iex> tensor = Tensor.new([[[1,2],[3,4],[5,6]],[[7,8],[9,10],[11,12]]], [3,3,2])
  #Tensor(3×3×2)
        1,       2
          3,       4
            5,       6
        7,       8
          9,      10
            11,      12
        0,       0
          0,       0
            0,       0
  iex> tensor[1]
  #Matrix-(3×2)
                   
         7,       8
         9,      10
        11,      12
                   

Vector and Matrices are also Tensors. There exist some functions that only make sense when used on these one- or two-dimensional structures. Therefore, the extra Vector and Matrix modules exist.

Sparcity

The Vectors/Matrices/Tensors are stored in a sparse way. Only the values that differ from the identity (defaults to nil) are actually stored in the Vector/Matrix/Tensor.

This allows for smaller data sizes, as well as faster operations when peforming on, for instance, diagonal matrices.

Numbers

Tensor uses the Numbers library for the implementations of elementwise addition/subtraction/multiplication etc. This means that you can fill a Tensor with e.g. Decimals or Rationals, and it will Just Work!

It even is the case that Tensor itself implements the Numeric behaviour, which means that you can nest Vectors/Matrices/Tensors in your Vectors/Matrices/Tensors, and doing math with them will still work!! (as long as the elements inside the innermost Vector/Matrix/Tensor follow the Numeric behaviour as well, of course.)

Syntactic Sugar

For Tensors, many sugary protocols and behaviours have been implemented to let them play nicely with other parts of your applications:

Access Behaviour

Tensors have implementations of the Access Behaviour, which let you do:

  iex> use Tensor
  iex> mat = Matrix.new([[1,2],[3,4]], 2,2)
  iex> mat[0]
  #Vector-(2)[1, 2]
  iex> mat[1][1]
  4
  iex> put_in mat[1][0], 100
  #Matrix-(2×2)
                   
         1,       2
       100,       4
                   

It is even possible to use negative indices to look from the end of the Vector/Matrix/Tensor!

Enumerable Protocol

Tensors allow you to enumerate over the values inside, using the Enumerable protocol. Note that:

As there are many other ways to iterate over values inside tensors, functions like Tensor.to_list , Matrix.columns also exist.

There are also functions like Tensor.map, which returns a new Tensor containg the results of this mapping operation. Tensor.map is nice in the way that it will only iterate over the actual values that have a value other than the default, which makes it fast.

If you can think of other nice ways to enumerate over Tensors, please let me know, as these would make great additions to the library!

Collectable Protocol

If you want to build up a Vector from a collection of values, or a Matrix from a collection of Vectors, (or an order-3 tensor from a collection of Matrices, etc), you can do so by harnessing the power of the Collectable protocol.

  iex> use Tensor
  iex> mat = Matrix.new(0,3)
  iex> v = Vector.new([1,2,3])
  iex> Enum.into([v,v,v], mat)
  #Matrix-(3×3)
                            
         1,       2,       3
         1,       2,       3
         1,       2,       3
                            

Inspect Protocol

The Inspect protocol has been overridden for all Tensors.

FunLand.Reducable Semiprotocol

This is a lightweight version of the Enumerable protocol, with a simple implementation.

    iex> use Tensor
    iex> x = Vector.new([1,2,3,4])
    iex> FunLand.Reducable.reduce(x, 0, fn x, acc -> acc + x end)
    10

Extractable Protocol

This allows you to extract a single element per time from the Vector/Tensor/Matrix. Because it is fastest to extract the elements with the highest index, these are returned first.


    iex> use Tensor
    iex> x = Matrix.new([[1,2],[3,4]], 2, 2)
    iex> {:ok, {item, x}} = Extractable.extract(x)
    iex> item
    #Vector<(2)[3, 4]>
    iex> {:ok, {item, x}} = Extractable.extract(x)
    iex> item
    #Vector<(2)[1, 2]>
    iex> Extractable.extract(x)
    {:error, :empty}

Insertable Protocol

This allows you ti insert a single element per time into the Vector/Tensor/Matrix. Insertion always happens at the highest index location. (The size of the highest dimension of the Tensor is increased by one)

    iex> use Tensor
    iex> x = Matrix.new(0, 2)
    iex> {:ok, x} = Insertable.insert(x, Vector.new([1, 2]))
    iex> {:ok, x} = Insertable.insert(x, Vector.new([3, 4]))
    #Matrix<(2×2)
                     
           1,       2
           3,       4
                     
    >

Efficiency

The Tensor package is completely built in Elixir. It is not a wrapper for any functionality written in other languages.

This does mean that if you want to do heavy number crunching, you might want to look for something else.

However, as Tensor uses as sparse tensor implementation, many calculations can be much faster than you might expect from a terse tensor implementation, depending on your input data.

Installation

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

  def deps do
    [
      {:tensor, "~> 2.1"}
    ]
  end

Changelog

Roadmap