Repository: OpenMined/PySyft (python)

Issue:
Implement Base Tensor Object
In this ticket, we want to create a new basic type called a "Tensor". A Tensor is a nested list of numbers with an arbitrary number of dimensions. With one dimension, it's a list of numbers known as a "vector". With two dimensions, it's a list of lists of numbers known as a "matrix". In this ticket, we want to build our implementation of Tensor with inspiration from PyTorch's  [Tensor](https://github.com/pytorch/pytorch/blob/master/torch/tensor.py) object ([Tensor Docs](http://pytorch.org/docs/master/tensors.html)). 

So, in this ticket, you should build a basic tensor class. You should be able to pass in as an argument an arbitrary shape (number of rows, columns, etc.) when you create it. Furthermore, we want this basic class to support elementwise addition, subtraction, multiplication, and division.

Acceptance Criteria:
- __init__ supports initializing a tensor with an arbitrary number of dimensions and arbitrary scalar type (test with float and int)
- elementwise operators for +-/* and their inline operators should be overridden
- elementwise operations should check to make sure both tensors are of identical dimension
- elementwise operators should also support (and automatically detect!) the input of a single number, which should be applied to the entire tensor (aka... multiply every number by "5").


Pull request description:
Implemented a base tensor class that can perform basic math operations
Fixes #19

Patch:
diff --git a/.gitignore b/.gitignore
index b7fe12aa16..92979d232e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@ syft.egg-info/
 */**/__pycache__
 .ipynb_checkpoints/
 *.pickle
+lastfailed
+pytest_runner*
+.eggs/
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000000..9af7e6f11b
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[aliases]
+test=pytest
\ No newline at end of file
diff --git a/setup.py b/setup.py
index d899539242..0a3aff4fae 100755
--- a/setup.py
+++ b/setup.py
@@ -10,8 +10,6 @@ def read(fname):
 
 requirements = read('requirements.txt').split()
 
-
-
 setup(
     name = "syft",
     version = "0.1.0",
@@ -29,4 +27,6 @@ setup(
     ],
     scripts=['bin/syft_cmd'],
     install_requires=requirements,
+    setup_requires=['pytest-runner'],
+    tests_require=['pytest']
 )
diff --git a/syft/__init__.py b/syft/__init__.py
index 47275a6e5b..67a265d83b 100644
--- a/syft/__init__.py
+++ b/syft/__init__.py
@@ -1,26 +1,1 @@
-import importlib
-import pkgutil
-
-ignore_packages = set(['test'])
-
-def import_submodules(package, recursive=True):
-    """ Import all submodules of a module, recursively, including subpackages
-
-    :param package: package (name or actual module)
-    :type package: str | module
-    :rtype: dict[str, types.ModuleType]
-    """
-    if isinstance(package, str):
-        package = importlib.import_module(package)
-    results = {}
-    for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
-        # test submodule names are 'syft.test.*', so this matches the 'ignore_packages' above
-        if 'test' not in str(name):
-            full_name = package.__name__ + '.' + name
-            results[full_name] = importlib.import_module(full_name)
-            if recursive and is_pkg:
-                results.update(import_submodules(full_name))
-    return results
-
-# import submodules recursively
-import_submodules(__name__)
+from .tensor import TensorBase
diff --git a/syft/tensor.py b/syft/tensor.py
new file mode 100644
index 0000000000..6d1ce8dc30
--- /dev/null
+++ b/syft/tensor.py
@@ -0,0 +1,85 @@
+import numpy as np
+
+def _ensure_ndarray(arr):
+    if not isinstance(arr, np.ndarray):
+        arr = np.array(arr)
+
+    return arr
+
+class TensorBase(object):
+    """
+    A base tensor class that perform basic element-wise operation such as 
+    addition, subtraction, multiplication and division
+    """
+
+    def __init__(self, arr_like, encrypted=False):
+        self.data = _ensure_ndarray(arr_like)
+        self.encrypted = encrypted
+
+    def __add__(self, arr_like):
+        """Performs element-wise addition between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        return self.data + arr_like
+
+    def __iadd__(self, arr_like):
+        """Performs in place element-wise addition between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        self.data = self.data + arr_like
+        return self.data
+
+    def __sub__(self, arr_like):
+        """Performs element-wise subtraction between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        return self.data - arr_like
+
+    def __isub__(self, arr_like):
+        """Performs in place element-wise subtraction between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        self.data = self.data - arr_like
+        return self.data
+
+    def __mul__(self, arr_like):
+        """Performs element-wise multiplication between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        return self.data * arr_like
+
+    def __imul__(self, arr_like):
+        """Performs in place element-wise multiplication between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        self.data = self.data * arr_like
+        return self.data
+
+    def __truediv__(self, arr_like):
+        """Performs element-wise division between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        return self.data / arr_like
+
+    def __itruediv__(self, arr_like):
+        """Performs in place element-wise subtraction between two array like objects"""
+        if self.encrypted:
+            return NotImplemented
+
+        arr_like = _ensure_ndarray(arr_like)
+        self.data = self.data / arr_like
+        return self.data


Test patch:
diff --git a/syft/test/__init__.py b/syft/test/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
new file mode 100644
index 0000000000..ae9cb22b63
--- /dev/null
+++ b/tests/test_tensor.py
@@ -0,0 +1,66 @@
+from syft import TensorBase
+import unittest
+import numpy as np
+
+# Here's our "unit tests".
+class AddTests(unittest.TestCase):
+    def testSimple(self):
+        t = TensorBase(np.array([1,2,3]))
+        self.assertTrue(np.array_equal(t + np.array([1,2,3]), [2,4,6]))
+    
+    def testInplace(self):
+        t = TensorBase(np.array([1,2,3]))
+        t += np.array([1,2,3])
+        self.assertTrue(np.array_equal(t.data, [2,4,6]))
+
+    def testScalar(self):
+        t = TensorBase(np.array([1,2,3]))
+        self.assertTrue(np.array_equal(t + 2, [3, 4, 5]))
+
+class SubTests(unittest.TestCase):
+    def testSimple(self):
+        t = TensorBase(np.array([1,2,3]))
+        self.assertTrue(np.array_equal(t - np.array([1,2,3]), [0,0,0]))
+    
+    def testInplace(self):
+        t = TensorBase(np.array([1,2,3]))
+        t -= np.array([1,2,3])
+        self.assertTrue(np.array_equal(t.data, [0,0,0]))
+
+    def testScalar(self):
+        t = TensorBase(np.array([1,2,3]))
+        self.assertTrue(np.array_equal(t - 1, [0, 1, 2]))
+
+class MultTests(unittest.TestCase):
+    def testSimple(self):
+        t = TensorBase(np.array([1,2,3]))
+        self.assertTrue(np.array_equal(t * np.array([1,2,3]), [1,4,9]))
+
+    def testInplace(self):
+        t = TensorBase(np.array([1,2,3]))
+        t *= np.array([1,2,3])
+        self.assertTrue(np.array_equal(t.data, [1,4,9]))
+
+    def testScalar(self):
+        t = TensorBase(np.array([1,2,3]))
+        self.assertTrue(np.array_equal(t * 2, [2, 4, 6]))
+
+class DivTests(unittest.TestCase):
+    def testSimple(self):
+        t = TensorBase(np.array([2,4,8]))
+        self.assertTrue(np.array_equal(t / np.array([2,2,2]), [1,2,4]))
+
+    def testInplace(self):
+        t = TensorBase(np.array([1,2,3]))
+        t *= np.array([1,2,3])
+        self.assertTrue(np.array_equal(t.data, [1,4,9]))
+
+    def testScalar(self):
+        t = TensorBase(np.array([2,4,6]))
+        self.assertTrue(np.array_equal(t / 2, [1, 2, 3]))
+    
+def main():
+    unittest.main()
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file