Repository: OpenMined/PySyft (python)

Issue:
Implement Default cross Functionality for Base Tensor Type.
<!-- Please make sure that you review this: https://github.com/OpenMined/Docs/blob/master/contributing/guidelines.md  -->
<!-- If you are looking to file a bug make sure to look at the .github/BUG_ISSUE_TEMPLATE.md -->


#### User story: 
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 `cross` should return a new tensor. For a reference on the operation these perform check out [PyTorch's](http://pytorch.org/docs/master/torch.html#torch.cross) documentation.

<!-- Provide a detailed explaination about the proposed feature, you can draw inspiration from something like this: 
https://github.com/OpenMined/PySyft/issues/227 or https://github.com/OpenMined/PySyft/issues/12 -->

#### Acceptance Criteria:

<!-- Provide an outline af all the things that needs to be addressed in order to close this Issue,
be as descriptive as possible -->
 - [ ] If the Base Tensor type's attribute "encrypted" is set to True, it should return a NotImplemented error.
 - [ ] corresponding unit tests demonstrating the correct operation on the Base Tensor type implemented over int and float Tensors.
 - [ ] inline documentation as described over [here](https://github.com/OpenMined/PySyft/blob/85bc68e81a2f4bfc0f0bf6c4252b88d6d7b54004/syft/math.py#L5). 

<!-- Thanks for your contributions! -->


Pull request:
Add cross functionality for Base Tensor Type
#### Reference Issue
- Fixes #382

#### What does your implementation fix? 
Adds default cross functionality for base tensor type

#### Explain your changes.
- [X] Implement cross functionality using numpy.cross
- [X] If the Base Tensor type's attribute "encrypted" is set to True, it should return a NotImplemented error.
- [X] corresponding unit tests demonstrating the correct operation on the Base Tensor type implemented over int and float Tensors.
- [X] Inline documentation.


Patch:
diff --git a/syft/__init__.py b/syft/__init__.py
index 43b3fa7b10..cada8b5181 100644
--- a/syft/__init__.py
+++ b/syft/__init__.py
@@ -7,7 +7,8 @@ from syft import nonlin
 from syft.tensor import equal, TensorBase
 from syft.math import cumprod, cumsum, ceil, dot, matmul, addmm, addcmul
 from syft.math import addcdiv, addmv, bmm, addbmm, baddbmm, transpose
-from syft.math import unsqueeze, zeros, ones, rand, randn, mm, fmod, diag, lerp, renorm, numel
+from syft.math import unsqueeze, zeros, ones, rand, randn, mm, fmod, diag
+from syft.math import lerp, renorm, numel, cross
 
 s = str(he)
 s += str(nn)
@@ -26,3 +27,4 @@ s += str(fmod)
 s += str(lerp)
 s += str(numel)
 s += str(renorm)
+s += str(cross)
diff --git a/syft/math.py b/syft/math.py
index 4d395a6a0d..207942c75b 100644
--- a/syft/math.py
+++ b/syft/math.py
@@ -20,7 +20,7 @@ __all__ = [
     'cumprod', 'cumsum', 'ceil', 'dot', 'floor', 'matmul', 'addmm', 'addcmul',
     'addcdiv', 'addmv', 'bmm', 'addbmm', 'baddbmm', 'sigmoid', 'unsqueeze',
     'sin', 'sinh', 'cos', 'cosh', 'tan', 'tanh', 'zeros', 'ones', 'rand',
-    'randn', 'mm', 'fmod', 'diag', 'lerp', 'renorm', 'numel'
+    'randn', 'mm', 'fmod', 'diag', 'lerp', 'renorm', 'numel', 'cross'
 ]
 
 
@@ -1008,3 +1008,49 @@ def split(tensor, split_size, axis=0):
     list_ = np.array_split(tensor.data, split_according, axis)
 
     return list(map(lambda x: TensorBase(x), list_))
+
+
+def cross(input, other, dim=-1):
+    """
+    Computes cross products between two tensors in the given dimension
+    The two vectors must have the same size, and the size of the dim
+    dimension should be 3.
+
+    Parameters
+    ----------
+
+    input: TensorBase
+        the first input tensor
+
+    other: TensorBase
+        the second input tensor
+
+    dim: int, optional
+        the dimension to take the cross-product in. Default is -1
+
+    Returns
+    -------
+    TensorBase: The result Tensor
+    """
+    input = _ensure_tensorbase(input)
+    other = _ensure_tensorbase(other)
+
+    if input.encrypted or other.encrypted:
+        return NotImplemented
+
+    # Verify that the shapes of both vectors are same
+    if input.shape() != other.shape():
+        raise ValueError('inconsistent dimensions {} and {}'.format(
+            input.shape(), other.shape()))
+
+    # verify that the given dim is valid
+    if dim < -len(input.shape()) or dim >= len(input.shape()):
+        raise ValueError('invalid dim. Should be between {} and {}'.format(
+            -len(input.shape()), len(input.shape()) - 1))
+
+    # verify that the size of dimension dim is 3
+    if input.shape()[dim] != 3:
+        raise ValueError('size of dimension {}(dim) is {}. Should be 3'.format(
+            dim, input.shape()[-1]))
+
+    return TensorBase(np.cross(input, other, axis=dim))
diff --git a/syft/tensor.py b/syft/tensor.py
index 89f23c3b05..f5dadf7f7d 100644
--- a/syft/tensor.py
+++ b/syft/tensor.py
@@ -3605,3 +3605,24 @@ class TensorBase(object):
             return NotImplemented
 
         return syft.math.split(self, split_size, axis)
+
+    def cross(self, tensor, dim=-1):
+        """
+        Computes cross products between two tensors in the given dimension
+        The two vectors must have the same size, and the size of the dim
+        dimension should be 3.
+
+        Parameters
+        ----------
+
+        tensor: TensorBase
+            the second input tensor
+
+        dim: int, optional
+            the dimension to take the cross-product in. Default is -1
+
+        Returns
+        -------
+        TensorBase: The result Tensor
+        """
+        return syft.math.cross(self, tensor, dim)


Test patch:
diff --git a/tests/test_math.py b/tests/test_math.py
index 09aad0a9f7..c90a0c3cb9 100644
--- a/tests/test_math.py
+++ b/tests/test_math.py
@@ -441,3 +441,31 @@ class SplitTests(unittest.TestCase):
             self.assertTrue(syft.equal(split.shape(), target_shape))
             self.assertTrue(syft.equal(t.narrow(axis, start, target_shape[axis]), split))
             start += target_shape[axis]
+
+
+class CrossTests(unittest.TestCase):
+    def setUp(self):
+        self.a = np.eye(2, 3)
+        self.b = np.ones((2, 3))
+
+    def test_cross(self):
+        a = TensorBase(self.a)
+        b = TensorBase(self.b)
+
+        # Verify that the expected result is retuned
+        expected_result = np.array([0, -1, 1, 1, 0, -1]).reshape(2, 3)
+        self.assertTrue(np.array_equal(a.cross(b), expected_result))
+
+        # Verify that ValueError is thrown when dimension is out of bounds
+        self.assertRaises(ValueError, a.cross, b, 5)
+
+        # Verify that ValueError is thrown when size dimension dim != 3
+        self.assertRaises(ValueError, a.cross, b, 0)
+
+        # Verify that ValueError is thrown when dimensions don't match
+        a = TensorBase(self.a.reshape(3, 2))
+        self.assertRaises(ValueError, a.cross, b)
+
+        # Verify that NotImplemented is returned if Tensor is encrypted
+        a = TensorBase(self.a, True)
+        self.assertEqual(a.cross(b), NotImplemented)
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
index 3c9052c3bf..9d7143a875 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -1635,6 +1635,20 @@ class SplitTests(unittest.TestCase):
             start += target_shape[axis]
 
 
+class CrossTests(unittest.TestCase):
+    def setUp(self):
+        self.a = np.eye(2, 3)
+        self.b = np.ones((2, 3))
+
+    def test_cross(self):
+        a = TensorBase(self.a)
+        b = TensorBase(self.b)
+        expected_result = np.array([0, -1, 1, 1, 0, -1]).reshape(2, 3)
+
+        # Verify that the expected result is retuned
+        self.assertTrue(np.array_equal(a.cross(b), expected_result))
+
+
 if __name__ == "__main__":
 
     unittest.main()