# Tensor¶

## Introduction¶

`n2d2.Tensor` is a wrapper of the `Tensor` object available in N2D2 (see Tensor).

The class `n2d2.Tensor` contains a reference to the element which produce it and can be seen as the edge of the computation graph.

## Manipulating tensors¶

For setting and getting value we will be using the following tensor as an example :

```tensor = n2d2.Tensor([2, 3])
```
```0 0 0
0 0 0
```

You can set and get values using :

### Coordinates¶

```tensor[1,0] = 1 # Using coordinates
value = tensor[1,0]
```

If you print the tensor you will see :

```0 0 0
1 0 0
```

### Index¶

You can use an index to get or set elements of a tensor. The index correspond to the flatten representation of your tensor.

```tensor = 2
value = tensor
```

If you print the tensor you will see :

```2 0 0
0 0 0
```

### Slice¶

Note

Slice are supported only for assignment !

```tensor[1:3] = 3
```

If you print the tensor you will see :

```0 3 3
0 0 0
```

### Set values method¶

If you want to set multiple values easily, you can use the method `n2d2.Tensor.set_values()`

```tensor.set_values([[1,2,3], [4,5,6]])
```

If you print the tensor you will see :

```1 2 3
4 5 6
```

## Numpy¶

### To Numpy¶

You can create a `numpy.array` using a `n2d2.Tensor` with the class method : `n2d2.Tensor.to_numpy()`

```tensor = n2d2.Tensor([2, 3])
np_array = tensor.to_numpy()
```

This will create the following tensor :

```0 0 0
0 0 0
```

By default the `numpy.array` doesn’t create a memory copy meaning that if you want to manipulate a `n2d2.Tensor` you can use the numpy library.

```np_array = 1
print(tensor)
```
```1 1 1
0 0 0
```

Note

If you do not want to create a memory copy, you should set the parameter `copy=True`.

```np_array = tensor.to_numpy(copy=True)
```

### From Numpy¶

You can create a `n2d2.Tensor` using a `numpy.array` with the class method : `n2d2.Tensor.from_numpy()`

```np_array = numpy.array([[1,2,3], [4,5,6]])
tensor = n2d2.Tensor.from_numpy(np_array)
```

This will create the following tensor :

```1 2 3
4 5 6
```

Note

You cannot create a `n2d2.Tensor` from a `numpy.array` without a memory copy because Tensor require a contiguous memory space which is not required for an array.

## CUDA Tensor¶

You can store your tensor with CPU or GPU (using `CUDA`). By default, n2d2 creates a CPU tensor.

If you want to create a `CUDA` Tensor you can do so by setting the parameter `cuda` to True in the constructor

```tensor = n2d2.Tensor([2,3], cuda=True)
```

You can switch from CPU to GPU at anytime :

```tensor.cpu()  # Converting to a CPU tensor
tensor.cuda() # Converting to a CUDA tensor
```

When working on a `CUDA` tensor you have to understand that they are stored in two different places.

The host and the device. The device is the GPU. The host correspond to your interface with the tensor that exists in the GPU. You cannot access the device directly, the GPU don’t have input/output functions.

This is why you have two methods to synchronized these two versions (`n2d2.Tensor.htod()` and `n2d2.Tensor.dtoh()`).

Synchronizing the device and the host can be an important overhead, it is recommended to compute everything on the device and to synchronize the host at the end.

### Synchronization example¶

Let’s consider the following `CUDA` Tensor :

```t = n2d2.Tensor([2, 2], cuda=True)
```
```0 0
0 0
```

We set the following values :

```t.set_values([[1, 2], [3, 4]])
```
```1 2
3 4
```

Then we will synchronized the device with the host. this mean that we send the values to the GPU.

```t.htod()
```
```1 2
3 4
```

As you can see, nothing change when printing the tensor. We have updated the GPU with the new values. Now let’s change the values stored in the tensor :

```t.set_values([[2, 3], [4, 5]])
```
```2 3
4 5
```

When printing the tensor we see the new values we just set. Now let’s synchronize the host with the device !

```t.dtoh()
```
```1 2
3 4
```

As you can see when printing the tensor, we now have the old values of the tensor.