#!/usr/bin/env python
# coding: utf-8
# # Introduction to TACO
#
# TACO, which stands for Tensor Algebra Compiler, is a library for performing sparse and dense linear algebra and tensor algebra computations.
#
# The computations can range from relatively simple ones like sparse matrix-vector multiplication to more complex ones like matricized tensor times Khatri-Rao product. All these computations can be performed on any mix of dense and sparse tensors. Under the hood, TACO automatically generates efficient code to perform these computations.
#
# This notebook provides a brief introduction to the TACO Python library. For a more comprehensive overview, please see the documentation linked [here](http://tensor-compiler.org/symphony-docs/index.html). We will also link to relevant pages as we progress.
# ## Table of Contents:
# * [Getting Started](#first-section)
# * [Defining Tensor Formats](#second-section)
# * [NumPy and SciPy I/O](#third-section)
# * [Example Application: SDDMM](#fourth-section)
# # Getting Started
#
# First, let's import TACO. Press `Shift` + `Enter` to run the code below.
# In[ ]:
import pytaco as pt
from pytaco import dense, compressed
# In the above, `dense` and `compressed` are [mode (dimension) formats](http://tensor-compiler.org/symphony-docs/reference/rst_files/mode_format.html). We can think of tensors as multi-dimensional arrays, and the mode formats allow us to specify how we would like to store the data in each dimension:
# * If a dimension is `dense`, then all of the elements in that dimension are stored.
# * And if a dimension is `compressed`, then only nonzeros are stored.
#
# For example, we can declare a $512 \times 64 \times 2048$ [tensor](http://tensor-compiler.org/symphony-docs/reference/rst_files/tensor_class.html) whose first dimension is dense and second and third dimensions are compressed:
# In[ ]:
T = pt.tensor([512, 64, 2048], pt.format([dense, compressed, compressed]))
# We can initialize $T$ by calling its `insert` [method](http://tensor-compiler.org/symphony-docs/reference/rst_files/functions/pytaco.tensor.insert.html) to add a nonzero element to the tensor. The `insert` method takes two arguments: a list of coordinates and the value to be inserted at those coordinates:
# In[ ]:
# Set T(0, 1, 0) = 42.0
T.insert([0, 1, 0], 42.0)
# If multiple elements are inserted at the same coordinates, they are summed together:
# In[ ]:
# Set T(0, 0, 1) = 12.0 + 24.0 = 36.0
T.insert([0, 0, 1], 12.0)
T.insert([0, 0, 1], 24.0)
# We can then iterate over the nonzero elements of the tensor as follows:
# In[ ]:
for coordinates, value in T:
print("Coordinates: {}, Value: {}".format(coordinates, value))
# # Defining Tensor Formats
#
# Consider a matrix $M$ (aka a two-dimensional tensor) containing the following values:
#
# $$M = \begin{bmatrix}
# 6 & \cdot & 9 & 8 \\
# \cdot & \cdot & \cdot & \cdot \\
# 5 & \cdot & \cdot & 7
# \end{bmatrix}$$
#
# Denote the rows and columns as dimensions $d_1$ and $d_2$, respectively.
# We look at how $M$ is represented differently in different formats. For convenience, let's define a helper function to initialize $M$.
# In[ ]:
def make_example_matrix(format):
M = pt.tensor([3, 4], format)
M.insert([0, 0], 6)
M.insert([0, 2], 9)
M.insert([0, 3], 8)
M.insert([2, 0], 5)
M.insert([2, 3], 7)
return M
# ## (dense $d_1$, dense $d_2$)
# Note that passing in `dense` makes all of the dimensions dense. This is equivalent to `pt.format([dense, dense])`.
# In[ ]:
make_example_matrix(dense)
# For this example, we focus on the last line of the output, the `vals` array: since all values are stored, it is a flattened $3 \times 4$ matrix stored in row-major order.
# ## (dense $d_1$, compressed $d_2$)
# This is called compressed sparse row (CSR) format.
# In[ ]:
csr = pt.format([dense, compressed])
# In[ ]:
make_example_matrix(csr)
# Since $d_1$ is dense, we need only store the `size` of the dimension; values, both zero and nonzero, are stored for every coordinate in dimension $d_1$.
#
# Since $d_2$ is compressed, we store a `pos` array (`[0, 3, 3, 5]`) and an `idx` array (`[0, 2, 3, 0, 3]`); these together form a segmented vector with one segment per entry in the previous dimension. The `idx` array stores all the indices with nonzero values in the dimension, while the `pos` array stores the location in the `idx` array where each segment begins. In particular, segment $i$ is stored in locations `pos[i]:pos[i+1]` in the `idx` array.
#
# The below animation visualizes the format. Hover over any non-empty entry of the matrix on the left to see how the value is stored.
# In[ ]:
get_ipython().run_line_magic('run', './animation/animation_2.py')
# ## (dense $d_2$, compressed $d_1$)
#
# We switch the order of the dimensions by passing in `[1, 0]` for the optional parameter `mode_ordering`. This results in a column-major (rather than row-major) format called compressed sparse column (CSC).
# In[ ]:
csc = pt.format([dense, compressed], [1, 0])
# In[ ]:
make_example_matrix(csc)
# In this format, $d_2$ has only `size`, while $d_1$ has a `pos` and `idx` array.
# In[ ]:
get_ipython().run_line_magic('run', './animation/animation_3.py')
# ## (compressed $d_1$, compressed $d_2$, compressed $d_3$)
#
# To more clearly visualize the compressed sparse fiber (CSF) format, where all dimensions are sparse, we move to three dimensions. The tensor $N$ defined below is a $2 \times 3 \times 4$ tensor.
#
# Similarly as above, passing in `compressed` is equivalent to `pt.format([compressed, compressed, compressed])`.
# In[ ]:
N = pt.tensor([2, 3, 4], compressed)
# First layer.
N.insert([0, 0, 0], 6)
N.insert([0, 0, 2], 9)
N.insert([0, 0, 3], 8)
N.insert([0, 2, 0], 5)
N.insert([0, 2, 3], 7)
# Second layer.
N.insert([1, 0, 1], 2)
N.insert([1, 1, 0], 3)
N.insert([1, 2, 2], 6)
N.insert([1, 1, 3], 1)
# In[ ]:
N
# This animation represents $N$ as two $3 \times 4$ matrices.
# In[ ]:
get_ipython().run_line_magic('run', './animation/animation_4.py')
# # NumPy and SciPy I/O
#
# We can also initialize tensors with NumPy arrays or SciPy sparse (CSR or CSC) matrices. Let's start by importing the packages we need.
# In[ ]:
import numpy as np
import scipy as sp
# Given a NumPy array such as the one randomly generated below, we can convert it to a TACO tensor using the `pytaco.from_array` [function](http://tensor-compiler.org/symphony-docs/reference/rst_files/functions/pytaco.from_array.html), which creates a dense tensor.
# In[ ]:
array = np.random.uniform(size=10)
tensor = pt.from_array(array)
# In[ ]:
tensor
# We can also export TACO tensors to NumPy arrays.
# In[ ]:
tensor.to_array()
# Similarly, given a SciPy sparse matrix, we can convert it to a TACO tensor. For a CSR matrix like the one below, we use the `pytaco.from_sp_csr` [function](http://tensor-compiler.org/symphony-docs/reference/rst_files/functions/pytaco.from_sp_csr.html), which creates a CSR tensor.
# In[ ]:
size = 20
density = 0.1
sparse_matrix = sp.sparse.rand(size, size, density, format = 'csr')
# In[ ]:
B = pt.from_sp_csr(sparse_matrix)
# And we can export the tensor to a SciPy sparse matrix.
# In[ ]:
B.to_sp_csr()
# # Example Application: SDDMM
#
# The following example demonstrates how computations on tensors are performed using TACO.
# Sampled dense-dense matrix product (SDDMM) is a bottleneck operation in many factor analysis algorithms used in machine learning, including Alternating Least Squares and Latent Dirichlet Allocation. Mathematically, the operation can be expressed as
#
# $$A = B \circ CD,$$
#
# where $A$ and $B$ are sparse matrices, $C$ and $D$ are dense matrices, and $\circ$ denotes component-wise multiplication. This operation can also be expressed in [index notation](http://tensor-compiler.org/symphony-docs/pycomputations/index.html#specifying-tensor-algebra-computations) as
#
# $$A_{ij} = B_{ij} \cdot C_{ik} \cdot D_{kj}.$$
# Starting with the $B$ generated above, we can view its `shape` attribute, which we expect to be `[size, size]`:
# In[ ]:
B.shape
# Examining the formula, we choose compatible dimensions for $C$ and $D$ and generate them randomly.
# In[ ]:
import random
otherSize = 10
C = pt.tensor([B.shape[0], otherSize], pt.format([dense, dense])) # Row-major dense.
D = pt.tensor([otherSize, B.shape[1]], pt.format([dense, dense], [1, 0])) # Column-major dense.
for tensor in [C, D]:
for i in range(tensor.shape[0]):
for j in range(tensor.shape[1]):
tensor.insert([i, j], random.uniform(-10, 10))
# ## Expressing the Computation
# We can express the result $A$ as a CSR matrix.
# In[ ]:
A = pt.tensor(B.shape, csr)
# The syntax for TACO computations closely mirrors index notation, with the caveat that we also have to explicitly declare the index variables beforehand:
# In[ ]:
i, j, k = pt.get_index_vars(3)
# In[ ]:
A[i, j] = B[i, j] * C[i, k] * D[k, j]
# ## Performing the Computation
#
# Once a tensor algebra computation has been defined, we can simply invoke the result tensor's `evaluate` method to perform the actual computation.
#
# [Under the hood](http://tensor-compiler.org/symphony-docs/pycomputations/index.html#performing-the-computation), TACO will first invoke the result tensor's `compile` method to generate code that performs the computation. TACO will then perform the actual computation by first invoking `assemble` to compute the sparsity structure of the result and subsequently invoking `compute` to compute the values of the result's nonzero elements.
# In[ ]:
A.evaluate()
# If we define a computation and then access the result without first manually invoking `evaluate` or `compile`/`assemble`/`compute`, TACO will automatically invoke the computation immediately before the result is accessed.
# Finally, we can display the result.
# In[ ]:
A