Repository: OpenMined/PySyft
Pull request #388 (2017-10-30 21:15:08)

Dense to sparse matrix
#### Reference Issue
Implements #274 


#### What does your implementation fix? 
Implements dense to sparse matrix conversion

#### Explain your changes.
Implement sparse function in math.py

implemented function uses scipy sompressed sparse row matrix format to create sparse matrix
function returns TensorBase object
Implement spars functions in tensor.py
sparse() returns TensorBase object same as sparse() in math.py
sparse_() operates inline
Add unit tests for both sparse() functions and for sparse_() 

#### include other comments?(if any)
A few days ago I created original pull request (#376), but I got a bit confused trying to push my changes after rebase and changes according to comments from reviewer so I accidentally deleted branch from my fork which automatically closed that pull request.


Patch:
diff --git a/notebooks/Syft - Paillier Encrypted Linear Classification.ipynb b/notebooks/Syft - Paillier Encrypted Linear Classification.ipynb
index 98f6a3a530..d0f4555c59 100644
--- a/notebooks/Syft - Paillier Encrypted Linear Classification.ipynb
+++ b/notebooks/Syft - Paillier Encrypted Linear Classification.ipynb
@@ -6,9 +6,7 @@
    "source": [
     "# Paillier Encrypted Linear Classification Example\n",
     "\n",
-    "DISCLAIMER: This is a proof-of-concept implementation. It does not represent a remotely product ready implementation or follow proper conventions for security, convenience, or scalability. It is part of a broader proof-of-concept demonstrating the vision of the OpenMined project, its major moving parts, and how they might work together.\n",
-    "\n",
-    "NOTE: One needs [capusle](https://github.com/OpenMined/Capsule) to establish a client for decryption and re-encryption of weights.\n"
+    "DISCLAIMER: This is a proof-of-concept implementation. It does not represent a remotely product ready implementation or follow proper conventions for security, convenience, or scalability. It is part of a broader proof-of-concept demonstrating the vision of the OpenMined project, its major moving parts, and how they might work together."
    ]
   },
   {
@@ -21,9 +19,8 @@
    "source": [
     "from syft.he.paillier import KeyPair\n",
     "from syft.nn.linear import LinearClassifier\n",
-    "import numpy as np\n",
-    "\n",
-    "from capsule.zmq_client import LocalCapsuleClient "
+    "from syft.he.keys import Paillier\n",
+    "import numpy as np"
    ]
   },
   {
@@ -34,8 +31,8 @@
    },
    "outputs": [],
    "source": [
-    "capsule = LocalCapsuleClient()\n",
-    "model = LinearClassifier(n_inputs=4, n_labels=2, capsule_client=capsule)\n",
+    "pk, sk = Paillier()\n",
+    "model = LinearClassifier(n_inputs=4, n_labels=2)\n",
     "epochs = 3"
    ]
   },
@@ -47,7 +44,7 @@
    },
    "outputs": [],
    "source": [
-    "model = model.encrypt()"
+    "model = model.encrypt(pk)"
    ]
   },
   {
@@ -58,8 +55,8 @@
    },
    "outputs": [],
    "source": [
-    "input = np.array([[0,0,1,1],[0,0,1,0],[1,0,1,1],[0,0,1,0]])\n",
-    "target = np.array([[0,1],[0,0],[1,1],[0,0]])"
+    "input = np.array([[0,0,1,1],[0,0,1,0]])\n",
+    "target = np.array([[0,1],[0,0]])"
    ]
   },
   {
@@ -82,7 +79,7 @@
    },
    "outputs": [],
    "source": [
-    "model = model.decrypt()"
+    "model = model.decrypt(sk)"
    ]
   },
   {
diff --git a/notebooks/Syft - Paillier Homographic Encrypted Linear Classification example .ipynb b/notebooks/Syft - Paillier Homographic Encrypted Linear Classification example .ipynb
index d833a89321..c351d566a4 100644
--- a/notebooks/Syft - Paillier Homographic Encrypted Linear Classification example .ipynb
+++ b/notebooks/Syft - Paillier Homographic Encrypted Linear Classification example .ipynb
@@ -20,10 +20,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from capsule.zmq_client import LocalCapsuleClient\n",
     "import syft as sy\n",
     "from sklearn.datasets import load_diabetes\n",
-    "from syft.nn.linear import LinearClassifier"
+    "from syft.nn.linear import LinearClassifier\n",
+    "from syft.he.keys import Paillier"
    ]
   },
   {
@@ -32,8 +32,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "cc = LocalCapsuleClient()\n",
-    "pk = cc.keygen()"
+    "pk, sk = Paillier()"
    ]
   },
   {
@@ -44,7 +43,8 @@
    },
    "outputs": [],
    "source": [
-    "model = LinearClassifier(n_inputs=10,n_labels=1).encrypt(pk)"
+    "n_inputs = 2\n",
+    "model = LinearClassifier(n_inputs=n_inputs,n_labels=1).encrypt(pk)"
    ]
   },
   {
@@ -66,7 +66,7 @@
    "source": [
     "diabetes = load_diabetes()\n",
     "target = diabetes.target\n",
-    "input = diabetes.data"
+    "input = diabetes.data[:, :n_inputs]"
    ]
   },
   {
@@ -76,7 +76,7 @@
    "outputs": [],
    "source": [
     "for i in range(5):\n",
-    "    print(\"Grads:\" + str((model.learn(input=input[i],target=target[i],alpha=0.5))))"
+    "    model.learn(input=input,target=target,alpha=0.5)"
    ]
   },
   {
@@ -87,7 +87,7 @@
    },
    "outputs": [],
    "source": [
-    "model = model.decrypt(cc)"
+    "model = model.decrypt(sk)"
    ]
   },
   {
diff --git a/syft/__init__.py b/syft/__init__.py
index 43b3fa7b10..29ff0b8afb 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, sparse, cross
 
 s = str(he)
 s += str(nn)
@@ -26,3 +27,5 @@ s += str(fmod)
 s += str(lerp)
 s += str(numel)
 s += str(renorm)
+s += str(sparse)
+s += str(cross)
diff --git a/syft/he/paillier/__init__.py b/syft/he/paillier/__init__.py
index 7599f7b0cc..9348727467 100644
--- a/syft/he/paillier/__init__.py
+++ b/syft/he/paillier/__init__.py
@@ -1,5 +1,5 @@
-from .basic import PaillierTensor, PrecisionConf
+from .basic import PaillierTensor
 from .keys import SecretKey, PublicKey, KeyPair
 
-s = str(PaillierTensor) + str(PrecisionConf) + str(SecretKey) + str(PublicKey)
+s = str(PaillierTensor) + str(SecretKey) + str(PublicKey)
 s += str(KeyPair)
diff --git a/syft/he/paillier/basic.py b/syft/he/paillier/basic.py
index ea68b596e7..72983c7578 100644
--- a/syft/he/paillier/basic.py
+++ b/syft/he/paillier/basic.py
@@ -4,29 +4,21 @@ from ...tensor import TensorBase
 
 class PaillierTensor(TensorBase):
 
-    def __init__(self, public_key, data=None, input_is_decrypted=True, fixed_point_conf=None):
+    def __init__(self, public_key, data=None, input_is_decrypted=True):
         """
-        Initializes a Tensor. If `data` is encrypted, `fixed_point_conf` needs to match that of `data`.
-        That is `fixed_point_conf` should be the same one that was used for encryption of `data`.
-        If no `fixed_point_conf` is provided, it creates its own default fixed-point configuration.
+        Initializes a Tensor.
         :param public_key: Public key to encrypt the data by
         :param data: Array-like data
         :param input_is_decrypted: To indicate whether `data` is encrypted
-        :param fixed_point_conf: Precision configuration of type `FXfamily`
         """
         self.encrypted = True
-        if fixed_point_conf is None:
-            # creates default fixed point configuration
-            self.fixed_point_conf = PrecisionConf(32)
-        else:
-            self.fixed_point_conf = fixed_point_conf
 
         self.public_key = public_key
         if(type(data) == np.ndarray or type(data) == TensorBase) and input_is_decrypted:
             if type(data) == np.ndarray:
-                self.data = public_key.encrypt(data, True, precision_conf=self.fixed_point_conf)
+                self.data = public_key.encrypt(data, True)
             else:
-                self.data = public_key.encrypt(data.data, True, precision_conf=self.fixed_point_conf)
+                self.data = public_key.encrypt(data.data, True)
         else:
             self.data = data
 
@@ -35,14 +27,14 @@ class PaillierTensor(TensorBase):
 
         if(not isinstance(tensor, TensorBase)):
             # try encrypting it
-            tensor = PaillierTensor(self.public_key, np.array([tensor]).astype('float'), fixed_point_conf=self.fixed_point_conf)
-            return PaillierTensor(self.public_key, self.data + tensor.data, False, fixed_point_conf=self.fixed_point_conf)
+            tensor = PaillierTensor(self.public_key, np.array([tensor]).astype('float'))
+            return PaillierTensor(self.public_key, self.data + tensor.data, False)
 
         if(type(tensor) == TensorBase):
-            tensor = PaillierTensor(self.public_key, tensor.data, fixed_point_conf=self.fixed_point_conf)
+            tensor = PaillierTensor(self.public_key, tensor.data)
 
         result_data = self.data + tensor.data
-        ptensor = PaillierTensor(self.public_key, result_data, False, fixed_point_conf=self.fixed_point_conf)
+        ptensor = PaillierTensor(self.public_key, result_data, False)
         ptensor._calc_add_depth(self, tensor)
         return ptensor
 
@@ -60,7 +52,7 @@ class PaillierTensor(TensorBase):
         if(isinstance(tensor, TensorBase)):
             if(not tensor.encrypted):
                 result = self.data * tensor.data
-                o = PaillierTensor(self.public_key, result, False, fixed_point_conf=self.fixed_point_conf)
+                o = PaillierTensor(self.public_key, result, False)
                 o._calc_mul_depth(self, tensor)
                 return o
             else:
@@ -68,7 +60,7 @@ class PaillierTensor(TensorBase):
         elif np.isscalar(tensor):
             # scalar is encode to match the precision of self before multiplication.
             op = self.data * tensor
-            ptensor = PaillierTensor(self.public_key, op, False, fixed_point_conf=self.fixed_point_conf)
+            ptensor = PaillierTensor(self.public_key, op, False)
             ptensor._calc_mul_depth(self, tensor)
             return ptensor
         else:
@@ -102,13 +94,13 @@ class PaillierTensor(TensorBase):
         if(isinstance(tensor, TensorBase)):
             if(not tensor.encrypted):
                 result = self.data * (1 / tensor.data)
-                o = PaillierTensor(self.public_key, result, False, fixed_point_conf=self.fixed_point_conf)
+                o = PaillierTensor(self.public_key, result, False)
                 return o
             else:
                 return NotImplemented
         elif np.isscalar(tensor):
             op = self.data * (1 / tensor)
-            return PaillierTensor(self.public_key, op, False, fixed_point_conf=self.fixed_point_conf)
+            return PaillierTensor(self.public_key, op, False)
         else:
             return NotImplemented
 
@@ -124,15 +116,7 @@ class PaillierTensor(TensorBase):
             return NotImplemented
 
         if dim is None:
-            return PaillierTensor(self.public_key, self.data.sum(), False, fixed_point_conf=self.fixed_point_conf)
+            return PaillierTensor(self.public_key, self.data.sum(), False)
         else:
             op = self.data.sum(axis=dim)
-            return PaillierTensor(self.public_key, op, False, fixed_point_conf=self.fixed_point_conf)
-
-
-class PrecisionConf:
-    def __init__(self, fraction_bits=None):
-        self.fraction_bits = fraction_bits
-
-    def __eq__(self, other):
-        return self.franction_bits == other.franction_bits
+            return PaillierTensor(self.public_key, op, False)
diff --git a/syft/he/paillier/keys.py b/syft/he/paillier/keys.py
index efdd4ecbdb..d94923032b 100644
--- a/syft/he/paillier/keys.py
+++ b/syft/he/paillier/keys.py
@@ -1,5 +1,4 @@
 import phe as paillier
-from phe.util import invert
 import numpy as np
 import json
 import syft
@@ -7,7 +6,6 @@ from .basic import PaillierTensor
 from ...tensor import TensorBase
 from ..abstract.keys import AbstractSecretKey, AbstractPublicKey
 from ..abstract.keys import AbstractKeyPair
-import math
 
 
 class SecretKey(AbstractSecretKey):
@@ -15,7 +13,7 @@ class SecretKey(AbstractSecretKey):
     def __init__(self, sk):
         self.sk = sk
 
-    def decrypt(self, x, precision_conf=None):
+    def decrypt(self, x):
         """Decrypts x. X can be either an encrypted int or a numpy
         vector/matrix/tensor."""
 
@@ -68,13 +66,13 @@ class PublicKey(AbstractPublicKey):
         pk = paillier.PaillierPublicKey(n=int(pk_record['n']))
         return PublicKey(pk)
 
-    def encrypt(self, x, same_type=False, precision_conf=None):
+    def encrypt(self, x, same_type=False):
         """Encrypts x. X can be either an encrypted int or a numpy
         vector/matrix/tensor."""
         if(type(x) == int or type(x) == float or type(x) == np.float64):
             if(same_type):
                 return NotImplemented
-            return self.pk.encrypt(x, precision_conf)
+            return self.pk.encrypt(x)
         elif(type(x) == TensorBase):
             if(x.encrypted or same_type):
                 return NotImplemented
@@ -84,7 +82,7 @@ class PublicKey(AbstractPublicKey):
             x_ = x.reshape(-1)
             out = list()
             for v in x_:
-                out.append(self.pk.encrypt(v, precision_conf))
+                out.append(self.pk.encrypt(v))
             if(same_type):
                 return np.array(out).reshape(sh)
             else:
@@ -137,92 +135,3 @@ class KeyPair(AbstractKeyPair):
         self.secret_key = SecretKey(prikey)
 
         return (self.public_key, self.secret_key)
-
-
-# This is our custom implementations of `encode` and `decode`. It replaces those of `phe`'s EncodedNumber.
-# This gives us control over the precision of numbers.
-@classmethod
-def encode(cls, public_key, scalar, precision=None, max_exponent=None):
-
-    # Calculate the maximum exponent for desired precision
-    if precision is None:
-        if isinstance(scalar, int):
-            exponent = 0
-        elif isinstance(scalar, float):
-            # Encode with *at least* as much precision as the python float
-            # What's the base-2 exponent on the float?
-            bin_flt_exponent = math.frexp(scalar)[1]
-
-            # What's the base-2 exponent of the least significant bit?
-            # The least significant bit has value 2 ** bin_lsb_exponent
-            bin_lsb_exponent = bin_flt_exponent - cls.FLOAT_MANTISSA_BITS
-
-            # What's the corresponding base BASE exponent? Round that down.
-            exponent = math.floor(bin_lsb_exponent / cls.LOG2_BASE)
-        else:
-            raise TypeError("Don't know the precision of type %s."
-                            % type(scalar))
-    else:
-        exponent = -precision.fraction_bits
-
-    int_rep = int(scalar * pow(cls.BASE, -exponent))
-    if abs(int_rep) > public_key.max_int:
-        raise ValueError('Integer needs to be within +/- %d but got %d'
-                         % (public_key.max_int, int_rep))
-    # Wrap negative numbers by adding n
-    return cls(public_key, int_rep % public_key.n, exponent)
-
-
-def decode(self):
-    """Decode fixed-point plaintext and return the result.
-
-    Returns:
-      returns an int if `exponent` is < 1. Returns a float otherwise.
-
-    Raises:
-      OverflowError: if overflow is detected in the decrypted number.
-    """
-    if self.encoding >= self.public_key.n:
-        # Should be mod n
-        raise ValueError('Attempted to decode corrupted number')
-    elif self.encoding <= self.public_key.max_int:
-        # Positive
-        mantissa = self.encoding
-    elif self.encoding >= self.public_key.n - self.public_key.max_int:
-        # Negative
-        mantissa = self.encoding - self.public_key.n
-    else:
-        raise OverflowError('Overflow detected in decrypted number')
-    if self.exponent >= -1:
-        return int(math.ceil(mantissa * pow(self.BASE, self.exponent)))
-    else:
-        return float(mantissa * pow(self.BASE, self.exponent))
-
-
-def my__mul__(self, other):
-    """Multiply by an int, float, or EncodedNumber.
-    If `other` is a scalar, it is first encoded to fixed-point precision of `self`"""
-    if isinstance(other, paillier.EncryptedNumber):
-        raise NotImplementedError('Good luck with that...')
-    if isinstance(other, paillier.EncodedNumber):
-        encoding = other
-    else:
-        int_rep = int(other * pow(paillier.EncodedNumber.BASE, -self.exponent))
-        if abs(int_rep) > self.public_key.max_int:
-            raise ValueError('Integer needs to be within +/- %d but got %d'
-                             % (self.public_key.max_int, int_rep))
-        # Wrap negative numbers by adding n
-        encoding = paillier.EncodedNumber(self.public_key, int_rep % self.public_key.n, self.exponent)
-    product = self._raw_mul(encoding.encoding)
-    exponent = self.exponent + encoding.exponent
-    unscaled_result = paillier.EncryptedNumber(self.public_key, product, exponent)
-    # moving the decimal point to end up having the same number of digits to its right
-    scaling_multiplier = invert(paillier.EncodedNumber.BASE**(-self.exponent), self.public_key.n)
-    scaled_product = unscaled_result._raw_mul(scaling_multiplier)
-    return paillier.EncryptedNumber(self.public_key, scaled_product, self.exponent)
-
-
-paillier.EncodedNumber.encode = encode
-paillier.EncodedNumber.decode = decode
-paillier.EncodedNumber.BASE = 2
-paillier.EncryptedNumber.__mul__ = my__mul__
diff --git a/syft/math.py b/syft/math.py
index 4d395a6a0d..e682644975 100644
--- a/syft/math.py
+++ b/syft/math.py
@@ -11,16 +11,16 @@
         http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html
 
 """
+import scipy as sp
 import numpy as np
-
 from .tensor import TensorBase
 from .tensor import _ensure_tensorbase
 
 __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'
+    'sin', 'sinh', 'sparse', 'cos', 'cosh', 'tan', 'tanh', 'zeros', 'ones',
+    'rand', 'randn', 'mm', 'fmod', 'diag', 'lerp', 'renorm', 'numel', 'cross'
 ]
 
 
@@ -805,6 +805,28 @@ def sinh(tensor):
     return TensorBase(np.sinh(np.array(tensor.data)))
 
 
+def sparse(tensor):
+    """
+    Converts dense matrix to sparse, returning a new matrix as a tensor
+
+    Parameters
+    ----------
+    tensor: TensorBase
+
+    Returns
+    -------
+    TensorBase:
+        Output Tensor
+    """
+    tensor = _ensure_tensorbase(tensor)
+    if tensor.encrypted:
+        return NotImplemented
+    else:
+        sparse_tensor = sp.sparse.csr_matrix(tensor)
+        sparse_tensor = _ensure_tensorbase(sparse_tensor)
+        return sparse_tensor
+
+
 def cos(tensor):
     """
     Returns a new tensor holding values of Trigonometric cosine
@@ -1008,3 +1030,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..3bbdaffd6f 100644
--- a/syft/tensor.py
+++ b/syft/tensor.py
@@ -3038,6 +3038,42 @@ class TensorBase(object):
         else:
             return self.data.shape
 
+    def sparse(self):
+        """
+        Converts dense matrix to sparse, returning a new matrix as a tensor
+
+        Parameters
+        ----------
+        tensor: TensorBase
+
+        Returns
+        -------
+        TensorBase:
+            Output Tensor
+        """
+        if self.encrypted:
+            return NotImplemented
+
+        return syft.math.sparse(self)
+
+    def sparse_(self):
+        """
+        Converts dense matrix to sparse, returning a new matrix as a tensor
+
+        Parameters
+        ----------
+        tensor: TensorBase
+
+        Returns
+        -------
+            Caller with values in-place
+        """
+        if self.encrypted:
+            return NotImplemented
+
+        self.data = syft.math.sparse(self)
+        return self
+
     def stride(self, dim=None):
         """
         Returns the jump necessary to go from one element to the next one in the specified dimension dim.
@@ -3605,3 +3641,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..e842314ba8 100644
--- a/tests/test_math.py
+++ b/tests/test_math.py
@@ -1,7 +1,6 @@
 import unittest
 
 import numpy as np
-
 import syft
 from syft import TensorBase
 
@@ -429,6 +428,20 @@ class MultinomialTests(unittest.TestCase):
         self.assertTrue(np.all(t2.data >= 0) and np.all(t2.data <= len(t1)))
 
 
+class SparseTests(unittest.TestCase):
+    def test_sparse_sparseMatrix(self):
+        matrix = np.array([[1, 0], [0, 0]])
+        sparseMatrix = "{0}".format(syft.math.sparse(matrix))
+        expectedOutput = 'BaseTensor:   (0, 0)\t1'
+        self.assertEqual(sparseMatrix, expectedOutput)
+
+    def test_sparse_denseMatrix(self):
+        matrix = np.array([[1, 2], [3, 4]])
+        sparseMatrix = "{0}".format(syft.math.sparse(matrix))
+        expectedOutput = 'BaseTensor:   (0, 0)\t1\n  (0, 1)\t2\n  (1, 0)\t3\n  (1, 1)\t4'
+        self.assertEqual(sparseMatrix, expectedOutput)
+
+
 class SplitTests(unittest.TestCase):
     def test_split(self):
         t = TensorBase(np.random.rand(7, 4))
@@ -441,3 +454,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..e7296b87c8 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -1575,6 +1575,21 @@ class RenormTests(unittest.TestCase):
         self.assertTrue(np.allclose(t, np.array([[1.0, 2.0, 3.0], [2.735054, 3.418817, 4.102581]])))
 
 
+class SparseTests(unittest.TestCase):
+    def test_sparse(self):
+        matrix = TensorBase(np.array([[1, 0], [0, 0]]))
+        sparseMatrix = "{0}".format(matrix.sparse())
+        expectedOutput = 'BaseTensor:   (0, 0)\t1'
+        self.assertEqual(expectedOutput, sparseMatrix)
+
+    def test_sparse_(self):
+        matrix = TensorBase(np.array([[1, 0], [0, 0]]))
+        matrix.sparse_()
+        returnedOutput = "{0}".format(matrix)
+        expectedOutput = 'BaseTensor: BaseTensor:   (0, 0)\t1'
+        self.assertEqual(returnedOutput, expectedOutput)
+
+
 class StrideTests(unittest.TestCase):
     def test_stride(self):
         t = TensorBase([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
@@ -1635,6 +1650,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()