Repository: OpenMined/PySyft

Issue:
Implement addr Functionality in FloatTensor with CPU/GPU Backend Support
### User Story: 
As a Data Scientist using PySyft's FloatTensor type, I want to leverage a wide range of methods which use our new Unity backend. For this ticket to be complete, the addr() should be added to our FloatTensor class with the appropriate functionality, returning a new tensor.

Furthermore, the function should automatically determine which backend to use (CPU/GPU) based on where the data is located. If the data is located on the CPU, a performant CPU implementation should run but if the data for a given FloatTensor is located on a GPU, it should be run using an HLSL kernel where appropriate. Obviously, if no GPU is available, it should automatically fall back to the CPU implementation.

### Every Reference You Might Need for this Issue:

- For a reference on the operation this performs check out [PyTorch](http://pytorch.org/docs/master/tensors.html)'s documentation.
- For a reference on how to program in Unity, check out [this basic tutorial](https://unity3d.com/learn/tutorials/projects/roll-ball-tutorial)
- For a reference on how to write HLSL code, check out [this basic tutorial](http://kylehalladay.com/blog/tutorial/2014/06/27/Compute-Shaders-Are-Nifty.html)
- For a complete tutorial on how to add functions to FloatTensor (step by step guide) see [this Google Document](https://docs.google.com/document/d/1WRd7gGLFN0Awtf86AICYIHtg3gfFWLBa5wYTthsB3i0/edit)
- For a reference on how other functions like this have been implemented check out the functions in [this notebook](https://github.com/OpenMined/OpenMined/blob/master/notebooks/Syft%20Tensor%20Example%20Notebook.ipynb) as well as the corresponding files that made it possible ([SyftController](https://github.com/OpenMined/OpenMined/blob/master/Assets/OpenMined/Network/Controllers/SyftController.cs), [FloatTensor.Ops](https://github.com/OpenMined/OpenMined/blob/master/Assets/OpenMined/Syft/Tensor/FloatTensor.Ops.cs), [FloatTensor.ShaderOps](https://github.com/OpenMined/OpenMined/blob/master/Assets/OpenMined/Syft/Tensor/FloatTensor.ShaderOps.cs), [FloatTensorShaders](https://github.com/OpenMined/OpenMined/blob/master/Assets/OpenMined/Syft/Math/Shaders/FloatTensorShaders.compute), and [FloatTensorTest](https://github.com/OpenMined/OpenMined/blob/master/Assets/OpenMined.Tests/Editor/FloatTensorTest.cs)).
- And of course, please consider our [Contributor Guidelines](https://github.com/OpenMined/Docs/blob/master/contributing/guidelines.md) for all contributions.
### Acceptance Criteria:

- [ ] an integration test in PySyft demonstrating the correct CPU and GPU operation implemented over a FloatTensor while connected to a Unity backend
- [ ] a Unit Test in OpenMined/OpenMined demonstrating the correct operation on a FloatTensor
- [ ] [inline](http://pytorch.org/docs/master/tensors.html) documentation in the python code. For inspiration on inline documentation, please check out PyTorch's documentation for this operator.
- [ ] Link your Pull Request back to this Issue so that it gets closed appropriately when the PR is merged.


Discussion:
i'll take this and #425!

Resolving patch:
diff --git a/syft/tensor/float_tensor.py b/syft/tensor/float_tensor.py
--- a/syft/tensor/float_tensor.py
+++ b/syft/tensor/float_tensor.py
@@ -149,6 +149,97 @@ def addmv(self, x, y):
         copy.params_func("addmv_", [x.id, y.id])
         return copy
 
+    def addr(self, *args):
+        """
+        Returns a new Tensor as the sum of beta*self and alpha*(vec1*vec2^T)
+
+        Parameters
+        ----------
+        beta: float
+            scalar to optionally multiply each element of mat
+        vec1: FloatTensor
+            first vector in outer product with vec2
+        vec2: FloatTensor
+            second vector in outer product with vec1
+        alpha: float
+            scalar to optionally multiply each element of the outer product of vec1, vec2
+
+        Returns
+        -------
+        FloatTensor
+            Output tensor
+        """
+
+        for arg in args:
+            if type(arg) == float:
+                if 'beta' not in locals() and 'vec1' not in locals():
+                    beta = arg
+                elif 'alpha' not in locals() and 'vec2' in locals():
+                    alpha = arg
+                else:
+                    raise TypeException('Method `addr` accepts only 2 float params; they may be out of order with respect to the Tensors.')
+            elif type(arg) == FloatTensor:
+                if 'vec1' not in locals():
+                    vec1 = arg
+                elif 'vec2' not in locals():
+                    vec2 = arg
+                else:
+                    raise TypeException('Method `addr` accepts only 2 FloatTensors')
+            else:
+                raise TypeException('Unexpected argument type')
+
+        if 'beta' not in locals():
+            beta = 1
+        if 'alpha' not in locals():
+            alpha = 1
+
+        return self.params_func("addr", [beta, vec1.id, vec2.id, alpha], return_response=True)
+
+    def addr_(self, *args):
+        """
+        Returns the sum of beta*self and alpha*(vec1*vec2') inline
+
+        Parameters
+        ----------
+        beta: float
+            scalar to optionally multiply each element of mat
+        vec1: FloatTensor
+            first vector in outer product with vec2
+        vec2: FloatTensor
+            second vector in outer product with vec1
+        alpha: float
+            scalar to optionally multiply each element of the outer product of vec1, vec2
+
+        Returns
+        -------
+        FloatTensor inline
+        """
+        
+        for arg in args:
+            if type(arg) == float:
+                if 'beta' not in locals() and 'vec1' not in locals():
+                    beta = arg
+                elif 'alpha' not in locals() and 'vec2' in locals():
+                    alpha = arg
+                else:
+                    raise TypeException('Method `addr` accepts only 2 float params; they may be out of order with respect to the Tensors.')
+            elif type(arg) == FloatTensor:
+                if 'vec1' not in locals():
+                    vec1 = arg
+                elif 'vec2' not in locals():
+                    vec2 = arg
+                else:
+                    raise TypeException('Method `addr` accepts only 2 FloatTensors')
+            else:
+                raise TypeException('Unexpected argument type')
+
+        if 'beta' not in locals():
+            beta = 1
+        if 'alpha' not in locals():
+            alpha = 1
+
+        return self.params_func("addr_",  [beta, vec1.id, vec2.id, alpha])
+
     def asin(self):
         """
         Returns a new Tensor with the arcsine of the elements of input.


Test patch: