Repository: OpenMined/PySyft

Issue:
Add Functionality to FixedPrecisionTensor
In https://github.com/OpenMined/PySyft/pull/1530 and https://github.com/OpenMined/PySyft/pull/1531, we created a new class called FixedPrecisionTensor which is able to take any pytorch tensor and compute over it using a limited range of precision. For example (https://github.com/OpenMined/PySyft/blob/master/examples/torch/Fixed%20Precision%20Tensor%20Testing.ipynb)

However, only addition has been implemented. In this work, we'd like to add the following functions.

- \_\_mult__ - multiplication by another fixed precision tensor
- \_\_mult__ - multiplication by another non-fixed precision tensor
- prod() - multiplication across a dimension
- cumprod() - cumulative multiplication across a dimension
- \_\_add__ - addition by a non-fixed precision tensor
- sum() - addition across a dimension
- cumsum() - cumulative addition across a dimension
- inheritance - all undefined methods should inherit from the tensor underlying FixedPrecisionTensor (self.child). Test that this works using __getitem__


Discussion:
I would like to work on this. I'm just getting started with this project though.

Please reach out if you get stuck on anything - I'm here to help :)
I got this 😁

Resolving patch:
diff --git a/syft/core/frameworks/torch/tensor.py b/syft/core/frameworks/torch/tensor.py
--- a/syft/core/frameworks/torch/tensor.py
+++ b/syft/core/frameworks/torch/tensor.py
@@ -783,7 +783,7 @@ def sum_get(self):
         return res
 
     def workers(self):
-        return list(self.pointer_tensor_dict.keys()) 
+        return list(self.pointer_tensor_dict.keys())
 
     def on(self, wrapper):
         """
@@ -1177,33 +1177,69 @@ def handle_call(cls, command, owner):
         args = command['args']
         kwargs = command['kwargs']
         has_self = command['has_self']
-
         if has_self:
             self = command['self']
+            # Overriding prod, sum and cumsum or similar methods, which may have an argument but
+            # do not include the parameters when they're called. i.e. given an argument prod(
+            # dim=3) the value of args[0] is just 3, therefore it does not satisfy the second
+            # part of the if statement.
+            if attr in ('prod', 'sum', 'cumsum'):
+                if args == ():
+                    raise AttributeError('Please provide a dimension')
+                if attr == 'prod':
+                    response = cls.prod(self, *args, **kwargs)
+                elif attr == 'sum':
+                    response = cls.sum(self, *args, **kwargs)
+                elif attr == 'cumsum':
+                    response = cls.cumsum(self, *args, **kwargs)
+                return _FixedPrecisionTensor(response).wrap(True)
+
             if attr == 'share':
                 response = self.share(*args, **kwargs)
                 return response
             else:
-                if attr in ('__add__', '__mul__', 'mm') and isinstance(args[0], sy._FixedPrecisionTensor):
+                if attr in ('__add__', '__mul__', '__sub__', '__div__', '__truediv__', 'mm',
+                            'matmul') and\
+                        isinstance(args[0], sy._FixedPrecisionTensor):
+                    # Compute the precision to keep
                     other = args[0]
                     assert (self.base == other.base) and (self.bits == other.bits), \
                         'Arguments should share the same base and field'
-
+                    self_precision = self.precision_fractional
+                    other_precision = other.precision_fractional
+
+                    # If the precision fractional of self is different than other's raise an exception
+                    # You may uncomment this line out if you do care about different precisions,
+                    # the code will work either way.
+                    # if not(self_precision == other_precision):
+                    #     raise ArithmeticError("The tensors have different precisions")
+                    precision = max(self_precision, other_precision)
+                    precision_loss = min(self_precision, other_precision)
                     # Perform the computation
                     torch_tensorvar = None
-                    if attr == '__add__':
-                        torch_tensorvar = cls.__add__(self, *args, **kwargs)
-                        torch_tensorvar = torch_tensorvar % self.field
-                    elif attr == '__mul__':
+                    if attr == '__mul__':
                         torch_tensorvar = cls.__mul__(self, other)
-                    elif attr in ('mm',):
+                    elif attr in ('mm',) or attr == 'matmul':
                         torch_tensorvar = cls.mm(self, other)
-
-                    # We could check overflow, but it is a pb for shared values.
-                    # if (torch_tensorvar > self.field).any():
-                    #     torch_tensorvar = torch_tensorvar % self.field
-                    #     logging.warning('{} on FixPrecision Tensor/Variable overflowed, '
-                    #                     'try reducing precision_fractional.'.format(attr))
+                    elif attr == '__add__':
+                        torch_tensorvar = cls.__add__(self, *args, **kwargs)
+                    elif attr == '__sub__':
+                        torch_tensorvar = cls.__sub__(self, *args, **kwargs)
+                    elif attr == '__div__' or '__truediv__':
+                        torch_tensorvar = cls.__div__(self, *args, **kwargs)
+                    if attr not in ('mm',) and attr != '__mul__':
+                        response = torch_tensorvar.fix_precision(
+                            already_encoded=True,
+                            precision_fractional=precision
+                        )
+                        return response
+
+
+                        # We could check overflow, but it is a pb for shared values.
+                        # if (torch_tensorvar > self.field).any():
+                        #     torch_tensorvar = torch_tensorvar % self.field
+                        #     logging.warning('{} on FixPrecision Tensor/Variable overflowed, '
+                        #                     'try reducing precision_fractional.'.format(attr))
 
                 else:  # Standard procedure for methods
                     # Get the next node type and update in command tensorvar with tensorvar.child
@@ -1220,7 +1256,7 @@ def handle_call(cls, command, owner):
 
                 # Compute the precision to keep
                 precision = self.precision_fractional
-                if attr in ('__mul__', 'mm', 'matmul'):
+                if attr in ('mm', 'matmul', '__mul__'):
                     other = args[0]
                     if isinstance(other, sy._FixedPrecisionTensor):
                         self_precision = self.precision_fractional
@@ -1272,12 +1308,74 @@ def get(self, *args, **kwargs):
             return self.parent
 
     def __add__(self, other):
-        response = self.child + other.child
-        return response
+        # if other is not a fixed tensor, convert it to a fixed one
+        if (not hasattr(other, 'precision_fractional')):
+            other = other.fix_precision(precision_fractional = self.precision_fractional)
+        # check for inconsistencies in precision points
+        if (self.precision_fractional == other.precision_fractional):
+            gp_response = (self.child + other.child) % self.field
+        elif (self.precision_fractional > other.precision_fractional):
+            gp_response = (self.child + other.child * 10 ** (self.precision_fractional -
+                                                             other.precision_fractional)) % self.field
+        elif (self.precision_fractional < other.precision_fractional):
+            gp_response = (self.child * 10 ** (other.precision_fractional -
+                                               self.precision_fractional)+ other.child) % self.field
+        return gp_response
+
+    def __div__(self, other):
+        # if other is not a fixed tensor, convert it to a fixed one
+        if (not hasattr(other, 'precision_fractional')):
+            other = other.fix_precision(precision_fractional = self.precision_fractional)
+
+        if (self.precision_fractional == other.precision_fractional):
+            gp_response = (self.child * 10 ** self.precision_fractional / other.child) % \
+                          self.field
+        elif (self.precision_fractional > other.precision_fractional):
+            gp_response = (self.child / other.child * 10 ** other.precision_fractional) % \
+                          self.field
+
+        elif (self.precision_fractional < other.precision_fractional):
+            gp_response = ((self.child *10 ** (2 * other.precision_fractional
+                           - self.precision_fractional)) / other.child) % \
+                          self.field
+        return gp_response
 
     def __mul__(self, other):
-        response = self.child * other.child
-        return response
+        # if other is not a fixed tensor, convert it to a fixed one
+        if (not hasattr(other, 'precision_fractional')):
+            other = other.fix_precision(precision_fractional = self.precision_fractional)
+        return self.child * other.child
+
+    def __sub__(self, other):
+        # if other is not a fixed tensor, convert it to a fixed one
+        if (not hasattr(other, 'precision_fractional')):
+            other = other.fix_precision(precision_fractional = self.precision_fractional)
+
+        if (self.precision_fractional == other.precision_fractional):
+            gp_response = self.child - other.child
+        elif (self.precision_fractional > other.precision_fractional):
+            gp_response = (self.child - other.child * 10 ** (self.precision_fractional -
+                                                             other.precision_fractional)) % self.field
+
+            other.precision_fractional = self.precision_fractional
+        elif (self.precision_fractional < other.precision_fractional):
+            gp_response = (self.child * 10 ** (other.precision_fractional -
+                                               self.precision_fractional) - other.child) % \
+                          self.field
+
+        return gp_response
+
+    def prod(self, *args, **kwargs):
+        # getting the dimension of the tensor which prod will be applied to. (needed for fixing
+        # the precision precision problems)
+        dim = self.child.size()[args[0]]
+        return self.child.prod(*args, **kwargs) / 10 ** (self.precision_fractional * dim)
+
+    def sum(self, *args, **kwargs):
+        return (self.child.sum(*args, *kwargs) / 10 ** self.precision_fractional)
+
+    def cumsum(self, *args, **kwargs):
+        return (self.child.cumsum(*args, *kwargs) / 10 ** self.precision_fractional)
 
     def mm(self, other):
         response = self.child.mm(other.child)
@@ -2303,6 +2401,3 @@ def decode_(self):
             self.data.child = self.data.child.child.child
         self.child = self.child.child.child
         torch_utils.fix_chain_ends(self)
-
-
-


Test patch:
diff --git a/test/torch_test.py b/test/torch_test.py
--- a/test/torch_test.py
+++ b/test/torch_test.py
@@ -1176,13 +1176,138 @@ def test_fix_precision_decode(self):
         assert torch_utils.chain_print(x, display=False) == display_chain.tensor.local
 
     def test_fix_precision_mul(self):
-        x = torch.FloatTensor([2.1, 1])
-        y = torch.FloatTensor([1.2, 1.111])
+        x = torch.FloatTensor([1, 2, 0.4])
+        y = torch.FloatTensor([1, 1, 2])
+        x = x.fix_precision(precision_fractional=3)
+        y = y.fix_precision(precision_fractional=3)
+        z = x * y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([1, 2, 0.8])).all()
+
+        # with different precision fractions x's > y's
+        x = torch.FloatTensor([1, 2, 0.4])
+        y = torch.FloatTensor([1, 1, 2])
+        x = x.fix_precision(precision_fractional=3)
+        y = y.fix_precision(precision_fractional=4)
+        z = x * y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([1, 2, 0.8])).all()
+
+        # with different precision fractions x's < y's
+        x = torch.FloatTensor([1, 2, 0.4])
+        y = torch.FloatTensor([1, 1, 2])
+        x = x.fix_precision(precision_fractional=3)
+        y = y.fix_precision(precision_fractional=2)
+        z = x * y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([1, 2, 0.8])).all()
+
+
+
+    def test_fix_precision_add(self):
+        x = torch.FloatTensor([[1, 0.2], [0.9, 11]])
+        y = torch.FloatTensor([[0.8, 1], [1, 3]])
         x = x.fix_precision()
         y = y.fix_precision()
         z = x + y
         z = z.decode()
-        assert torch.eq(z, torch.FloatTensor([3.3, 2.111])).all()
+        assert torch.eq(z, torch.FloatTensor([[1.8, 1.2], [1.9, 14]])).all()
+
+        # with different precision fractions x's > y's
+        x = torch.FloatTensor([[1, 0.2], [0.9, 11]])
+        y = torch.FloatTensor([[0.8, 1], [1, 3]])
+        x = x.fix_precision(precision_fractional=4)
+        y = y.fix_precision(precision_fractional=3)
+        z = x + y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[1.8, 1.2], [1.9, 14]])).all()
+
+        # with different precision fractions x's < y's
+        x = torch.FloatTensor([[1, 0.2], [0.9, 11]])
+        y = torch.FloatTensor([[0.8, 1], [1, 3]])
+        x = x.fix_precision(precision_fractional=3)
+        y = y.fix_precision(precision_fractional=4)
+        z = x + y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[1.8, 1.2], [1.9, 14]])).all()
+
+    def test_fix_precision_sub(self):
+        x = torch.FloatTensor([[1, 1.2], [1.9, 11]])
+        y = torch.FloatTensor([[0.8, 1], [1, 3]])
+        x = x.fix_precision()
+        y = y.fix_precision()
+        z = x - y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[0.2, .2], [.9, 8]])).all()
+
+        # with different precision fractions x's > y's
+        x = torch.FloatTensor([[1, 1.2], [1.9, 11]])
+        y = torch.FloatTensor([[0.8, 1], [1, 3]])
+        x = x.fix_precision(precision_fractional=4)
+        y = y.fix_precision(precision_fractional=3)
+        z = x - y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[0.2, .2], [.9, 8]])).all()
+
+        # with different precision fractions x's < y's
+        x = torch.FloatTensor([[1, 1.2], [1.9, 11]])
+        y = torch.FloatTensor([[0.8, 1], [1, 3]])
+        x = x.fix_precision(precision_fractional=3)
+        y = y.fix_precision(precision_fractional=4)
+        z = x - y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[0.2, .2], [.9, 8]])).all()
+
+    def test_fix_precision_div(self):
+        x = torch.FloatTensor([[1, 1.2], [1.9, 12]])
+        y = torch.FloatTensor([[0.8, 0.4], [1, 3]])
+        x = x.fix_precision()
+        y = y.fix_precision()
+        z = x / y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[1.2500, 3], [1.9, 4]])).all()
+
+        # with different precision fractions x's > y's
+        x = torch.FloatTensor([[1, 1.2], [1.9, 12]])
+        y = torch.FloatTensor([[0.8, 0.4], [1, 3]])
+        x = x.fix_precision(precision_fractional=4)
+        y = y.fix_precision(precision_fractional=3)
+        z = x / y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[1.2000, 3], [1.9, 4]])).all()
+
+        # with different precision fractions x's < y's
+        x = torch.FloatTensor([[1, 1.2], [1.9, 12]])
+        y = torch.FloatTensor([[0.8, 0.4], [1, 3]])
+        x = x.fix_precision(precision_fractional=3)
+        y = y.fix_precision(precision_fractional=4)
+        z = x / y
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[1.2500, 3], [1.9, 4]])).all()
+
+    def test_fix_precision_sum(self):
+        x = torch.FloatTensor([[1, 1.2], [1.9, 12]])
+        y = torch.FloatTensor([[0.8, 0.4], [1, 3]])
+        x = x.fix_precision(precision_fractional=4)
+        z = x.sum(0)
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([2, 13])).all()
+
+    def test_fix_precision_cumsum(self):
+        x = torch.FloatTensor([[1, 1.2], [1.9, 12]])
+        y = torch.FloatTensor([[0.8, 0.4], [1, 3]])
+        x = x.fix_precision(precision_fractional=4)
+        z = x.cumsum(0)
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([[1, 1], [2, 13]])).all()
+
+    def test_fix_precision_prod(self):
+        x = torch.FloatTensor([[1, 1.2], [1.9, 12]])
+        y = torch.FloatTensor([[0.8, 0.4], [1, 3]])
+        x = x.fix_precision(precision_fractional=4)
+        z = x.prod(0)
+        z = z.decode()
+        assert torch.eq(z, torch.FloatTensor([1, 14])).all()
 
     def test_var_fix_precision_decode(self):
         x = sy.Variable(torch.FloatTensor([0.1, 0.2, 0.1, 0.2]))
@@ -1540,28 +1665,28 @@ def test_addition_remote_fix_precision_share(self):
         self.remote_fix_precision_share_operation([2.5, 3.2], [5.4, -1.1])
         self.remote_fix_precision_share_operation([-2.8, -3.9], [-1, -1])
         self.remote_fix_precision_share_operation([-2, 3.3], [-1.9, 1])
-        self.remote_fix_precision_share_operation([-19000, 3.3], [-1.9, 17654])
+        self.remote_fix_precision_share_operation([-190, 3.3], [-1.9, 174])
 
     def test_var_addition_remote_fix_precision_share(self):
         self.remote_fix_precision_share_operation([3.3], [5.1], var=True)
         self.remote_fix_precision_share_operation([2.5, 3.2], [5.4, -1.1], var=True)
         self.remote_fix_precision_share_operation([-2.8, -3.9], [-1, -1], var=True)
         self.remote_fix_precision_share_operation([-2, 3.3], [-1.9, 1], var=True)
-        self.remote_fix_precision_share_operation([-19000, 3.3], [-1.9, 17654], var=True)
+        self.remote_fix_precision_share_operation([-190, 3.3], [-1.9, 174], var=True)
 
     def test_mult_remote_fix_precision_share(self):
         self.remote_fix_precision_share_operation([3.3], [5.1], op='mul')
         self.remote_fix_precision_share_operation([2.5, 3.2], [5.4, -1.1], op='mul')
         self.remote_fix_precision_share_operation([-2.8, -3.9], [-1, -1], op='mul')
         self.remote_fix_precision_share_operation([-2, 3.3], [-1.9, 1], op='mul')
-        #self.remote_fix_precision_share_operation([-19000, 3.3], [-1.9, 17654], op='mul')
+        self.remote_fix_precision_share_operation([-190, 3.3], [-1.9, 174], op='mul')
 
     def test_var_mult_remote_fix_precision_share(self):
         self.remote_fix_precision_share_operation([3.3], [5.1], var=True, op='mul')
         self.remote_fix_precision_share_operation([2.5, 3.2], [5.4, -1.1], var=True, op='mul')
         self.remote_fix_precision_share_operation([-2.8, -3.9], [-1, -1], var=True, op='mul')
         self.remote_fix_precision_share_operation([-2, 3.3], [-1.9, 1], var=True, op='mul')
-        #self.remote_fix_precision_share_operation([-19000, 3.3], [-1.9, 17654], var=True, op='mul')
+        self.remote_fix_precision_share_operation([-190, 3.3], [-1.9, 174], var=True, op='mul')
 
     def test_matmul_remote_fix_precision_share(self):
         self.remote_fix_precision_share_operation([[3.3, 2.1],
@@ -1635,7 +1760,7 @@ def test_gpc_unwrapped_add(self):
         results = y.get()
 
         assert (results[0] == (x.get() * 2)).all()
-    
+
     def test_gpc_workers(self):
         x = torch.LongTensor([1, 2, 3, 4, 5])
         y = torch.LongTensor([1, 2, 3, 4, 5])
@@ -1647,7 +1772,7 @@ def test_gpc_workers(self):
         x_gp = _GeneralizedPointerTensor(x_pointer_tensor_dict)
 
         results = x_gp.workers()
-    
+
         assert(results == [k.id for k in x_pointer_tensor_dict.keys()])
 
 
@@ -1657,4 +1782,4 @@ def test_gpc_workers(self):
 
 
 if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+    unittest.main()