Repository: OpenMined/PySyft (python)

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.

Pull request:
Implement default set functionality resolves #93
<!--
Thanks for submitting a PR. Please make sure  that you have read the contribution guidelines: https://github.com/OpenMined/Docs/blob/master/contributing/guidelines.md
-->
#### Reference Issue
Fixes #93 

<!-- Example: Fixes #334 -->


#### What does your implementation fix? 

Following methods added on TensorBase :
- `storage_offset`
- `set_` 

#### Explain your changes.

Implements the same API as `torch.Tensor.set_()`, only it uses an `np.ndarray` in place of `torch.Storage`. 

So as a syft user instead of passing a `torch.Storage` to `set_` I need to pass a `np.ndarray`

#### include other comments?(if any)


<!--
Please note that we are a small team of volunteers so patience is
essential; We value all the contributions, no matter how small they are. 
If we are slow to review, either the pull request needs 
some tweaking,convincing, etc. 
or probably the reviewers could be busy. In any
case, we hope for your understanding in the process.

However If you feel that  this needs to be fastened up.
Reachout to us on our slack #pysyft channel.

Thanks for the contribution!
-->

Patch:
diff --git a/syft/tensor.py b/syft/tensor.py
index 3bbdaffd6f..e54015a827 100644
--- a/syft/tensor.py
+++ b/syft/tensor.py
@@ -3095,7 +3095,7 @@ class TensorBase(object):
             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 @@ class TensorBase(object):
 
         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
index e7296b87c8..58848538ab 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -5,6 +5,7 @@ from syft import tensor
 import numpy as np
 import math
 import pytest
+import struct
 
 
 # Here's our "unit tests".
@@ -20,7 +21,6 @@ class DimTests(unittest.TestCase):
     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 @@ class RenormTests(unittest.TestCase):
         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]]))