Repository: OpenMined/PySyft

Issue:
Implement Default dim Functionality for Base Tensor Type
**User Story A:** As a Data Scientist using Syft's Base Tensor type, we want to implement a default method for computing operations on a Tensor of arbitrary type. For this ticket to be complete, diag() should return a list of the dimensions of the tensor. 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:

Patch:
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/syft/__init__.py b/syft/__init__.py
index 67a265d83b..432a653b3d 100644
--- a/syft/__init__.py
+++ b/syft/__init__.py
@@ -1,1 +1,2 @@
-from .tensor import TensorBase
+from .tensor import *
+from .math import *
diff --git a/syft/math.py b/syft/math.py
new file mode 100644
index 0000000000..a7070e282a
--- /dev/null
+++ b/syft/math.py
@@ -0,0 +1,47 @@
+import numpy as np
+
+from syft import TensorBase
+from .tensor import _ensure_tensorbase
+
+__all__ = [
+    'dot', 'matmul',
+]
+
+
+def dot(tensor1, tensor2):
+    """Returns inner product of two tensors.
+
+    N-dimensional tensors are flattened into 1-D vectors, therefore this method should only be used on vectors.
+    """
+
+    tensor1 = _ensure_tensorbase(tensor1)
+    tensor2 = _ensure_tensorbase(tensor2)
+
+    if tensor1.encrypted is True or tensor2.encrypted is True:
+        return NotImplemented
+    return np.vdot(tensor1.data, tensor2.data)
+
+
+def matmul(tensor1, tensor2):
+    """Performs matrix multiplication between two tensors.
+
+    Exact behavior depends on the input tensors' dimensionality like so:
+    * If both tensors are 1-dimensional, their dot product is returned.
+    * If both tensors are 2-dimensional, their matrix-matrix product is returned.
+    * If either tensor has dimensionality > 2, the last 2 dimensions are treated as matrices and multiplied.
+    * If tensor1 is 1-dimensional, it is converted to a matrix by prepending a 1 to its dimensions.
+      This prepended dimension is removed after the matrix multiplication.
+    * If tensor2 is 1-dimensional, it is converted to a matrix by prepending a 1 to its dimensions.
+      This prepended dimension is removed after the matrix multiplication.
+    """
+
+    tensor1 = _ensure_tensorbase(tensor1)
+    tensor2 = _ensure_tensorbase(tensor2)
+
+    if tensor1.encrypted is True or tensor2.encrypted is True:
+        return NotImplemented
+
+    if tensor1.dim() == 1 and tensor2.dim() == 1:
+        return dot(tensor1, tensor2)
+    else:
+        return TensorBase(np.matmul(tensor1.data, tensor2.data))
diff --git a/syft/tensor.py b/syft/tensor.py
index 7d549da9bb..d7fb627572 100644
--- a/syft/tensor.py
+++ b/syft/tensor.py
@@ -1,4 +1,10 @@
 import numpy as np
+import syft
+
+__all__ = [
+    'equal', 'TensorBase',
+]
+
 
 def _ensure_ndarray(arr):
     if not isinstance(arr, np.ndarray):
@@ -6,83 +12,131 @@ def _ensure_ndarray(arr):
 
     return arr
 
+
+def _ensure_tensorbase(tensor):
+    if not isinstance(tensor, TensorBase):
+        tensor = TensorBase(tensor)
+
+    return tensor
+
+
+def equal(tensor1, tensor2):
+    """Checks if two tensors are equal.
+
+    Two tensors are considered equal if they are the same size and contain the same elements.
+
+    Assumption:
+    tensor1 and tensor2 are of type TensorBase.
+    Non-TensorBase objects will be converted to TensorBase objects.
+    """
+
+    tensor1 = _ensure_tensorbase(tensor1)
+    tensor2 = _ensure_tensorbase(tensor2)
+
+    if tensor1.encrypted is True or tensor2.encrypted is True:
+        return NotImplemented
+
+    return tensor1.data.shape == tensor2.data.shape and np.allclose(tensor1.data, tensor2.data)
+
+
 class TensorBase(object):
     """
-    A base tensor class that perform basic element-wise operation such as
-    addition, subtraction, multiplication and division
+    A base tensor class that performs basic element-wise operation such as
+    addition, subtraction, multiplication and division, and also dot and matrix products.
     """
 
     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"""
+    def __add__(self, tensor):
+        """Performs element-wise addition between two tensors"""
+        if self.encrypted:
+            return NotImplemented
+
+        tensor = _ensure_tensorbase(tensor)
+        return TensorBase(self.data + tensor.data)
+
+    def __iadd__(self, tensor):
+        """Performs in place element-wise addition between two tensors"""
+        if self.encrypted:
+            return NotImplemented
+
+        tensor = _ensure_tensorbase(tensor)
+        self.data = self.data + tensor.data
+        return self
+
+    def __sub__(self, tensor):
+        """Performs element-wise subtraction between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        return self.data + arr_like
+        tensor = _ensure_tensorbase(tensor)
+        return TensorBase(self.data - tensor.data)
 
-    def __iadd__(self, arr_like):
-        """Performs in place element-wise addition between two array like objects"""
+    def __isub__(self, tensor):
+        """Performs in place element-wise subtraction between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        self.data = self.data + arr_like
-        return self.data
+        tensor = _ensure_tensorbase(tensor)
+        self.data = self.data - tensor.data
+        return self
 
-    def __sub__(self, arr_like):
-        """Performs element-wise subtraction between two array like objects"""
+    def __eq__(self, tensor):
+        """Checks if two tensors are equal"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        return self.data - arr_like
+        return syft.equal(self, tensor)
 
-    def __isub__(self, arr_like):
-        """Performs in place element-wise subtraction between two array like objects"""
+    def dot(self, tensor):
+        """Returns inner product of two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        self.data = self.data - arr_like
-        return self.data
+        return syft.dot(self, tensor)
 
-    def __mul__(self, arr_like):
-        """Performs element-wise multiplication between two array like objects"""
+    def __matmul__(self, tensor):
+        """Performs matrix multiplication between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        return self.data * arr_like
+        return syft.matmul(self, tensor)
 
-    def __imul__(self, arr_like):
-        """Performs in place element-wise multiplication between two array like objects"""
+    def __mul__(self, tensor):
+        """Performs element-wise multiplication between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        self.data = self.data * arr_like
-        return self.data
+        tensor = _ensure_tensorbase(tensor)
+        return TensorBase(self.data * tensor.data)
 
-    def __truediv__(self, arr_like):
-        """Performs element-wise division between two array like objects"""
+    def __imul__(self, tensor):
+        """Performs in place element-wise multiplication between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        return self.data / arr_like
+        tensor = _ensure_tensorbase(tensor)
+        self.data = self.data * tensor.data
+        return self
 
-    def __itruediv__(self, arr_like):
-        """Performs in place element-wise subtraction between two array like objects"""
+    def __truediv__(self, tensor):
+        """Performs element-wise division between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        arr_like = _ensure_ndarray(arr_like)
-        self.data = self.data / arr_like
-        return self.data
+        tensor = _ensure_tensorbase(tensor)
+        return TensorBase(self.data / tensor.data)
+
+    def __itruediv__(self, tensor):
+        """Performs in place element-wise subtraction between two tensors"""
+        if self.encrypted:
+            return NotImplemented
+
+        tensor = _ensure_tensorbase(tensor)
+        self.data = self.data / tensor.data
+        return self
 
     def shape(self):
         """Returns a tuple of input array dimensions."""
@@ -91,6 +145,13 @@ class TensorBase(object):
 
         return self.data.shape
 
+    def dim(self):
+        """Returns an integer of the number of dimensions of this tensor."""
+        if self.encrypted:
+            return NotImplemented
+
+        return self.data.ndim
+
     def sum(self, dim=None):
         """Returns the sum of all elements in the input array."""
         if self.encrypted:


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_math.py b/tests/test_math.py
new file mode 100644
index 0000000000..32ff2cff5f
--- /dev/null
+++ b/tests/test_math.py
@@ -0,0 +1,62 @@
+import unittest
+
+import numpy as np
+
+import syft
+from syft import TensorBase
+
+
+class DotTests(unittest.TestCase):
+    def testDotInt(self):
+        t1 = TensorBase(np.array([1, 2, 3]))
+        t2 = TensorBase(np.array([4, 5, 6]))
+        self.assertEqual(syft.dot(t1, t2), 32)
+
+    def testDotFloat(self):
+        t1 = TensorBase(np.array([1.3, 2.5, 3.7]))
+        t2 = TensorBase(np.array([4.9, 5.8, 6.5]))
+        self.assertEqual(syft.dot(t1, t2), 44.92)
+
+
+class MatmulTests(unittest.TestCase):
+    def testMatmul1DInt(self):
+        t1 = TensorBase(np.array([1, 2, 3]))
+        t2 = TensorBase(np.array([4, 5, 6]))
+        self.assertEqual(syft.matmul(t1, t2), syft.dot(t1, t2))
+
+    def testMatmul1DFloat(self):
+        t1 = TensorBase(np.array([1.3, 2.5, 3.7]))
+        t2 = TensorBase(np.array([4.9, 5.8, 6.5]))
+        self.assertEqual(syft.matmul(t1, t2), syft.dot(t1, t2))
+
+    def testMatmul2DIdentity(self):
+        t1 = TensorBase(np.array([[1, 0],
+                                  [0, 1]]))
+        t2 = TensorBase(np.array([[5.8, 6.5],
+                                  [7.8, 8.9]]))
+        self.assertTrue(syft.equal(syft.matmul(t1, t2), [[5.8, 6.5],
+                                                         [7.8, 8.9]]))
+
+    def testMatmul2DInt(self):
+        t1 = TensorBase(np.array([[1, 2],
+                                  [3, 4]]))
+        t2 = TensorBase(np.array([[5, 6],
+                                  [7, 8]]))
+        self.assertTrue(syft.equal(syft.matmul(t1, t2), [[19, 22],
+                                                         [43, 50]]))
+
+    def testMatmul2DFloat(self):
+        t1 = TensorBase(np.array([[1.3, 2.5],
+                                  [3.4, 4.5]]))
+        t2 = TensorBase(np.array([[5.8, 6.5],
+                                  [7.8, 8.9]]))
+        self.assertTrue(syft.equal(syft.matmul(t1, t2), [[27.04, 30.7],
+                                                         [54.82, 62.15]]))
+
+
+def main():
+    unittest.main()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
index 8222b8250d..cba262e243 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -1,80 +1,117 @@
 from syft import TensorBase
+import syft
 import unittest
 import numpy as np
 
+
 # Here's our "unit tests".
+class DimTests(unittest.TestCase):
+    def testDimOne(self):
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(t.dim() == 1)
+
+
 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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        t += np.array([1, 2, 3])
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        t -= np.array([1, 2, 3])
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        t *= np.array([1, 2, 3])
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([2, 4, 8]))
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([1, 2, 3]))
+        t *= np.array([1, 2, 3])
+        self.assertTrue(syft.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]))
+        t = TensorBase(np.array([2, 4, 6]))
+        self.assertTrue(syft.equal(t / 2, [1, 2, 3]))
+
 
 class ShapeTests(unittest.TestCase):
     def testShape(self):
         t = TensorBase(np.array([[0, 1], [0, 5]]))
-        self.assertTrue(np.array_equal(t.shape(), (2, 2)))
+        self.assertTrue(syft.equal(t.shape(), (2, 2)))
+
 
 class SumTests(unittest.TestCase):
     def testDimNoneInt(self):
-        t = TensorBase(np.array([1,2,3]))
-        self.assertTrue(np.array_equal(t.sum(), 6))
+        t = TensorBase(np.array([1, 2, 3]))
+        self.assertTrue(syft.equal(t.sum(), 6))
 
     def testDimIsNotNoneInt(self):
         t = TensorBase(np.array([[0, 1], [0, 5]]))
-        self.assertTrue(np.array_equal(t.sum(dim=1), [1, 5]))
+        self.assertTrue(syft.equal(t.sum(dim=1), [1, 5]))
+
+
+class EqualTests(unittest.TestCase):
+    def testEqual(self):
+        t1 = TensorBase(np.array([1.2, 2, 3]))
+        t2 = TensorBase(np.array([1.2, 2, 3]))
+        self.assertTrue(syft.equal(t1, t2))
+
+    def testEqOp(self):
+        t1 = TensorBase(np.array([1, 2.4, 3]))
+        t2 = TensorBase(np.array([1, 2.4, 3]))
+        self.assertTrue(t1 == t2)
+
+    def testNotEqual(self):
+        t1 = TensorBase(np.array([1, 2, 3]))
+        t2 = TensorBase(np.array([1, 4, 3]))
+        self.assertFalse(syft.equal(t1, t2))
+
+    def testIneqOp(self):
+        t1 = TensorBase(np.array([1, 2, 3]))
+        t2 = TensorBase(np.array([1, 4, 5]))
+        self.assertTrue(t1 != t2)
+
 
 def main():
     unittest.main()
 
+
 if __name__ == '__main__':
     main()