Repository: OpenMined/PySyft

Issue:
Implement Default set Functionality for Base Tensor Type
**User Story A:** As a Data Scientist using Syft's Base Tensor type, I want to leverage a default method for computing operations on a Tensor of arbitrary type. For this ticket to be complete, set_() should operate inline. For a reference on the operation this performs check out [PyTorch](http://pytorch.org/docs/master/tensors.html)'s documentation.

**Acceptance Criteria:**
- If the Base Tensor type's attribute "encrypted" is set to True, it should return a NotImplemented error.
- a unit test demonstrating the correct operation on the Base Tensor type implemented over int and float Tensors.
- inline documentation in the python code. For inspiration on inline documentation, please check out [PyTorch](http://pytorch.org/docs/master/tensors.html)'s documentation for this operator.
Implement Default set Functionality for Base Tensor Type
**User Story A:** As a Data Scientist using Syft's Base Tensor type, I want to leverage a default method for computing operations on a Tensor of arbitrary type. For this ticket to be complete, set_() should operate inline. For a reference on the operation this performs check out [PyTorch](http://pytorch.org/docs/master/tensors.html)'s documentation.

**Acceptance Criteria:**
- If the Base Tensor type's attribute "encrypted" is set to True, it should return a NotImplemented error.
- a unit test demonstrating the correct operation on the Base Tensor type implemented over int and float Tensors.
- inline documentation in the python code. For inspiration on inline documentation, please check out [PyTorch](http://pytorch.org/docs/master/tensors.html)'s documentation for this operator.


Discussion:
I will work on this one.
I have a few questions before moving forward. 

What does **should operate inline** mean ?

The [pytorch signature](http://pytorch.org/docs/master/tensors.html#torch.Tensor.set_) for this function is `set_(source=None, storage_offset=0, size=None, stride=None)` and the **source** parameter can be either a Tensor, or a Storage which is custom made by an underlying C extension

However since our tensor's internal storage is an ndarray the source is always going to be a Tensor right ? which means size is useless, or am I missing something ?

For reference: [ndarray underlying strucutre](https://docs.scipy.org/doc/numpy/reference/arrays.interface.html#__array_interface__)
@iamtrask can you give a bit more details how this method is supposed to behave ? There are many ways to implement this and I'm not sure if the signature needs to be the same as PyTorch
I will work on this one.
I have a few questions before moving forward. 

What does **should operate inline** mean ?

The [pytorch signature](http://pytorch.org/docs/master/tensors.html#torch.Tensor.set_) for this function is `set_(source=None, storage_offset=0, size=None, stride=None)` and the **source** parameter can be either a Tensor, or a Storage which is custom made by an underlying C extension

However since our tensor's internal storage is an ndarray the source is always going to be a Tensor right ? which means size is useless, or am I missing something ?

For reference: [ndarray underlying strucutre](https://docs.scipy.org/doc/numpy/reference/arrays.interface.html#__array_interface__)
@iamtrask can you give a bit more details how this method is supposed to behave ? There are many ways to implement this and I'm not sure if the signature needs to be the same as PyTorch

Resolving patch:
diff --git a/syft/tensor.py b/syft/tensor.py
--- a/syft/tensor.py
+++ b/syft/tensor.py
@@ -3095,7 +3095,7 @@ def stride(self, dim=None):
             return NotImplemented
 
         out = self.data.strides
-        output = tuple(map(lambda x: x / 8, out))
+        output = tuple(map(lambda x: int(x / 8), out))
 
         if dim is None:
             return output
@@ -3481,6 +3481,114 @@ def unfold(self, dim, size, step):
 
         return TensorBase(np.concatenate(sub_arrays, axis=dim))
 
+    def storage_offset(self):
+        """
+        Returns this tensor’s offset in the underlying
+        storage in terms of number of storage elements (not bytes).
+
+        Parameters
+        ----------
+
+        Returns
+        -------
+        Offset of the underlying storage
+        """
+        if self.encrypted:
+            return NotImplemented
+
+        offset = 0
+
+        # Base ndarray has 0 offset
+        if self.data.base is None:
+            offset = 0
+
+        elif type(self.data.base) is bytes:
+            offset = 0
+
+        else:
+            offset_raw = len(self.data.base.tobytes()) - len(self.data.tobytes())
+            offset = offset_raw / self.data.dtype.itemsize
+
+        return offset
+
+    def set_(self, source=None, offset=0, size=None, stride=None):
+        """
+        Sets the source `numpy.ndarray`, offset, size, and strides.
+
+        If only a tensor is passed in :attr:`source`, this tensor will share
+        the same `numpy.ndarray` and have the same size and strides as the
+        given tensor. Changes to elements in one tensor will be reflected
+        in the other.
+
+        If :attr:`source` is `numpy.ndarray`, the method sets the underlying
+        `numpy.ndarray`, offset, size, and stride.
+
+        Parameters
+        ----------
+
+        source: TensorBase or `numpy.ndarray` or None
+            Input Tensor or ndarray
+
+        offset: int
+            The offset in the underlying `numpy.ndarray` in items not bytes.
+
+        size: Tuple
+            The desired size. Defaults to the size of the source.
+
+        stride: Tuple
+            The desired stride. Defaults to C-contiguous strides.
+        """
+
+        TYPE_ERROR = ''' Received invalid combination of arguments. Expected on of:
+            * no arguments
+            * (syft.TensorBase source)
+            * (np.ndarray storage)
+            * (np.ndarray storage, int offset, tuple size)
+            * (np.ndarray storage, int offset, tuple size, tuple stride)
+        '''
+
+        if self.encrypted or \
+                (source is not None and type(source) is TensorBase and
+                 source.encrypted):
+            return NotImplemented
+
+        # Calling as _set(source=ndarray)
+        if source is not None and \
+            type(source) is np.ndarray and \
+                (offset, size, stride) == (0, None, None):
+            self.data = source
+
+        # Calling as _set(source=ndarray, offset, size)
+        # Calling as _set(source=ndarray, offset, size, stride)
+        elif source is not None and type(source) is np.ndarray \
+                and size is not None:
+
+            _stride = stride
+            if stride is not None:
+                _stride = np.multiply(stride, 8)
+
+            offset_nd = offset * self.data.dtype.itemsize
+            self.data = np.ndarray(shape=size,
+                                   buffer=source.data,
+                                   dtype=self.data.dtype,
+                                   offset=offset_nd,
+                                   strides=_stride)
+
+        # Calling as _set(source=TensorBase)
+        elif source is not None and \
+            type(source) is TensorBase and \
+                (offset, size, stride) == (0, None, None):
+                self.data = source.data
+
+        # Calling as _set()
+        elif (source, offset, size, stride) == (None, 0, None, None):
+            self.data = np.array(None)
+
+        else:
+            raise TypeError(TYPE_ERROR)
+
+        return self
+
     def uniform(self, low=0, high=1):
         """
         Returns a new tensor filled with numbers sampled uniformly


Test patch:
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -5,6 +5,7 @@
 import numpy as np
 import math
 import pytest
+import struct
 
 
 # Here's our "unit tests".
@@ -20,7 +21,6 @@ def test_view(self):
     def test_as_view(self):
         t = TensorBase(np.array([1.0, 2.0, 3.0]))
         t1 = t.view([-1, 1])
-        print(t.data.dtype)
         self.assertTrue(syft.equal(t.view_as(t1), TensorBase(np.array([[1.0], [2.0], [3.0]]))))
 
     def test_resize(self):
@@ -1575,6 +1575,220 @@ def testRenorm_(self):
         self.assertTrue(np.allclose(t, np.array([[1.0, 2.0, 3.0], [2.735054, 3.418817, 4.102581]])))
 
 
+class StorageTests(unittest.TestCase):
+
+    def test_storage_offset(self):
+        # int
+        # create custom buffer
+        data = [7, 4, 5, 5, 8, 2, 1, 1, 4, 3, 9, 4, 9, 9, 3, 1]
+        buf = struct.pack('16i', *data)
+
+        # Use custom buffer and ndarray tensors
+        t1 = TensorBase(np.ndarray(shape=(4, 4), buffer=buf, dtype=np.int32))
+        t2 = TensorBase(np.array([[7, 4, 5, 5],
+                                  [8, 2, 1, 1],
+                                  [4, 3, 9, 4],
+                                  [9, 9, 3, 1]], dtype=np.int32))
+
+        # Test default offset = 0
+        self.assertEqual(t1.storage_offset(), 0)
+        self.assertEqual(t2.storage_offset(), 0)
+
+        # value of first element in first row [7,4,5,5]
+        self.assertEqual(t1[0][0], 7)
+        self.assertEqual(t2[0][0], 7)
+
+        t1.set_(t1.data, offset=8, size=(2, 4))
+        self.assertEqual(t1.storage_offset(), 8)
+        self.assertEqual(t1[0][0], 4)
+
+        t2.set_(t2.data, offset=8, size=(2, 4))
+        self.assertEqual(t2.storage_offset(), 8)
+        self.assertEqual(t2[0][0], 4)
+
+        t1 = TensorBase(np.ndarray(shape=(4, 4), buffer=buf, dtype=np.int32))
+        t1.set_(t1.data, offset=4, size=(3, 4))
+        self.assertEqual(t1.storage_offset(), 4)
+        self.assertEqual(t1[0][0], 8)
+
+        t2 = TensorBase(np.array([[7, 4, 5, 5],
+                                  [8, 2, 1, 1],
+                                  [4, 3, 9, 4],
+                                  [9, 9, 3, 1]], dtype=np.int32))
+        t2.set_(t2.data, offset=4, size=(3, 4))
+        self.assertEqual(t2.storage_offset(), 4)
+        self.assertEqual(t2[0][0], 8)
+
+    def test_storage_offset_encrypted(self):
+        t1 = TensorBase(np.array([1, 2, 4, 4]), encrypted=True)
+        res = t1.storage_offset()
+        self.assertEqual(res, NotImplemented)
+
+    def test_tensor_set_encrypted(self):
+        t1 = TensorBase(np.array([]))
+        t2 = TensorBase(np.array([1, 2, 3, 4]), encrypted=True)
+        res = t1.set_(t2)
+        self.assertEqual(res, NotImplemented)
+
+        res = t2.set_(t1)
+        self.assertEqual(res, NotImplemented)
+
+    def test_tensor_set_source_nd(self):
+        t1 = np.array([1, 2, 3, 4])
+        t2 = np.array([0.1, 0.2, 0.3, 0.4])
+
+        tb = TensorBase([])
+        tb2 = TensorBase([])
+
+        r1 = tb.set_(t1)
+        r2 = tb2.set_(t2)
+
+        self.assertEqual(tb, t1)
+        self.assertEqual(tb2, t2)
+
+        self.assertEqual(r1, t1)
+        self.assertEqual(r2, t2)
+
+    def test_tensor_set_empty(self):
+        t1 = TensorBase([1, 2, 3, 4])
+        t2 = TensorBase([0.1, 0.2, 0.3, 0.4])
+
+        # Tensor with none storage
+        tn = TensorBase([])
+        tn.data = np.array(None)
+
+        # test set_ with no argument
+        result = t1.set_()
+        result_f = t2.set_()
+
+        self.assertEqual(result.data, tn.data)
+        self.assertEqual(result_f.data, tn.data)
+
+    def test_tensor_set_(self):
+        t1 = TensorBase([1, 2, 3, 4])
+        t2 = TensorBase([0.1, 0.2, 0.3, 0.4])
+
+        # test call signature
+        with pytest.raises(TypeError):
+            t1.set_(offset=2)
+            t1.set_(size=2)
+            t1.set_(stride=(1,))
+            t1.set_(offset=2, size=(1,))
+            t1.set_(offset=2, size=(1,), stride=(1,))
+            t1.set_(t2, offset=2, size=(1,), stride=(1,))
+
+        # end test call signature
+
+        # tests on ints
+
+        # begin setting tensor
+
+        t1 = TensorBase([])
+        t2 = TensorBase(np.array([1, 2, 3, 4]))
+        t1.set_(t2)
+
+        # memory location is the same
+        self.assertEqual(t1.data.__array_interface__['data'],
+                         t2.data.__array_interface__['data'])
+
+        # data is the same
+        self.assertTrue((t1.data == t2.data).all())
+        self.assertEqual(t1.data.dtype, np.int)
+
+        # stride and size are the same
+        self.assertEqual(t1.size(), t2.size())
+        self.assertEqual(t1.stride(), t2.stride())
+
+        # end setting tensor
+
+        # begin set offset and size
+
+        t1 = TensorBase([1, 2, 3, 4])
+
+        t1.set_(t1.data, offset=1, size=(3,))
+        self.assertEqual(t1[0], 2)
+        self.assertEqual(t1[2], 4)
+
+        with pytest.raises(IndexError):
+            t1[3]
+
+        # end set offset and size
+
+        # begin set strides
+        t1 = TensorBase([])
+        t2 = TensorBase(np.zeros(shape=(3, 4, 9, 10), dtype=np.int))
+
+        # set strides on new ndarray
+        strides = (10, 360, 90, 1)
+        size = (9, 3, 4, 10)
+        t1.set_(t2.data, 0, size, strides)
+
+        self.assertEqual(t1.data.base.__array_interface__['data'],
+                         t2.data.__array_interface__['data'])
+
+        self.assertEqual(t1.stride(), strides)
+
+        # set strides on underlying ndarray
+        t1 = TensorBase(np.zeros(shape=(3, 4, 9, 10))).uniform_()
+        strides = (10, 360, 90, 1)
+        size = (9, 3, 4, 10)
+
+        t1.set_(t1.data, offset=0, size=size, stride=strides)
+        self.assertEqual(t1.stride(), strides)
+
+        # end set strides
+
+        # tests on floats #######
+
+        # begin setting the underlying ndarray
+
+        t1 = TensorBase([])
+        t2 = TensorBase(np.array([1.0, 2.0, 3.0]))
+        t1.set_(t2)
+        self.assertEqual(t1.data.__array_interface__['data'], t2.data.__array_interface__['data'])
+        self.assertTrue((t1.data == t2.data).all())
+        self.assertEqual(t1.data.dtype, np.float)
+        self.assertEqual(t1.size(), t2.size())
+        self.assertEqual(t1.stride(), t2.stride())
+
+        # end setting the underlying ndarray
+
+        # begin set offset and size
+
+        t2 = TensorBase(np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]))
+
+        t2.set_(t2.data, offset=3, size=(3,))
+        self.assertEqual(t2[0], 4.0)
+        self.assertEqual(t2[2], 6.0)
+        with pytest.raises(IndexError):
+            t2[3]
+
+        # end set offset and size
+
+        # begin set strides
+
+        t1 = TensorBase([])
+        shape = (3, 4, 9, 10)
+        t2 = TensorBase(np.zeros(shape=shape, dtype=np.float))
+
+        # set strides on new ndarray
+        strides = (10, 360, 90, 1)
+        size = (9, 3, 4, 10)
+        t1.set_(t2.data, 0, size, strides)
+        self.assertEqual(t1.data.base.__array_interface__['data'],
+                         t2.data.__array_interface__['data'])
+        self.assertEqual(t1.stride(), strides)
+
+        # set strides on underlying ndarray
+        t1 = TensorBase(np.zeros(shape=shape, dtype=np.float))
+        strides = (10, 360, 90, 1)
+        size = (9, 3, 4, 10)
+
+        t1.set_(t1.data, offset=0, size=size, stride=strides)
+        self.assertEqual(t1.stride(), strides)
+        # end set strides
+
+
 class SparseTests(unittest.TestCase):
     def test_sparse(self):
         matrix = TensorBase(np.array([[1, 0], [0, 0]]))