Repository: helmholtz-analytics/heat

Issue:
Implement allclose
https://docs.scipy.org/doc/numpy/reference/generated/numpy.allclose.html


Discussion:
Needs to provide an implementation for the reduction still
allclose() should allow scalar values as lhs and rhs operands

Resolving patch:
diff --git a/heat/core/operations.py b/heat/core/operations.py
--- a/heat/core/operations.py
+++ b/heat/core/operations.py
@@ -159,13 +159,47 @@ def allclose(x, y, rtol=1e-05, atol=1e-08, equal_nan=False):
     True
 
     """
-    if not isinstance(x, tensor.tensor):
-        raise TypeError('Expected x to be a ht.tensor, but was {}'.format(type(x)))
 
-    if not isinstance(y, tensor.tensor):
-        raise TypeError('Expected y to be a ht.tensor, but was {}'.format(type(y)))
+    if np.isscalar(x):
+        try:
+            x = tensor.array([float(x)])
+        except (ValueError, TypeError,):
+            raise TypeError('Data type not supported, input was {}'.format(type(x)))
+
+    elif not isinstance(x, tensor.tensor):
+        raise TypeError('Only tensors and numeric scalars are supported, but input was {}'.format(type(x)))
+
+    if np.isscalar(y):
+        try:
+            y = tensor.array([float(y)])
+        except (ValueError, TypeError,):
+            raise TypeError('Data type not supported, input was {}'.format(type(y)))
+
+    elif not isinstance(y, tensor.tensor):
+        raise TypeError('Only tensors and numeric scalars are supported, but input was {}'.format(type(y)))
+
+    # If only one of the tensors is distributed, unsplit/gather it
+    if (x.split is not None) and (y.split is None):
+        x.resplit(axis=None)
+    if (x.split is None) and (y.split is not None):
+        y.resplit(axis=None)
+
+    # If both x and y are split, but along different axes, y is redistributed to be split along the same axis as x
+    if (x.split is not None) and (y.split is not None) and (x.split!=y.split):
+        y.resplit(axis=x.split)
+
+    # no sanitization for shapes of x and y needed, torch.allclose raises relevant errors
+    _local_allclose = torch.tensor(torch.allclose(x._tensor__array, y._tensor__array, rtol, atol, equal_nan))
+
+    # If x is distributed, then y is also distributed along the same axis
+    if x.comm.is_distributed():
+        x.comm.Allreduce(MPI.IN_PLACE, _local_allclose, MPI.LAND)
+
+    else:
+        pass
+
+    return bool(_local_allclose.item())
 
-    return torch.allclose(x._tensor__array, y._tensor__array, rtol, atol, equal_nan)
 
 
 def any(x, axis=None, out=None):


Test patch:
diff --git a/heat/core/tests/test_exponential.py b/heat/core/tests/test_exponential.py
--- a/heat/core/tests/test_exponential.py
+++ b/heat/core/tests/test_exponential.py
@@ -10,7 +10,8 @@
 class TestOperations(unittest.TestCase):
     def test_exp(self):
         elements = 10
-        comparison = torch.arange(elements, dtype=torch.float64).exp()
+        tmp = torch.arange(elements, dtype=torch.float64).exp()
+        comparison = ht.array(tmp)
 
         # exponential of float32
         float32_tensor = ht.arange(elements, dtype=ht.float32)
@@ -18,8 +19,7 @@ def test_exp(self):
         self.assertIsInstance(float32_exp, ht.tensor)
         self.assertEqual(float32_exp.dtype, ht.float32)
         self.assertEqual(float32_exp.dtype, ht.float32)
-        in_range = (float32_exp._tensor__array - comparison.type(torch.float32)) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float32_exp, comparison.astype(ht.float32), atol= FLOAT_EPSILON))
 
         # exponential of float64
         float64_tensor = ht.arange(elements, dtype=ht.float64)
@@ -27,8 +27,7 @@ def test_exp(self):
         self.assertIsInstance(float64_exp, ht.tensor)
         self.assertEqual(float64_exp.dtype, ht.float64)
         self.assertEqual(float64_exp.dtype, ht.float64)
-        in_range = (float64_exp._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float64_exp , comparison, atol= FLOAT_EPSILON))
 
         # exponential of ints, automatic conversion to intermediate floats
         int32_tensor = ht.arange(elements, dtype=ht.int32)
@@ -36,8 +35,7 @@ def test_exp(self):
         self.assertIsInstance(int32_exp, ht.tensor)
         self.assertEqual(int32_exp.dtype, ht.float64)
         self.assertEqual(int32_exp.dtype, ht.float64)
-        in_range = (int32_exp._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int32_exp, comparison, atol= FLOAT_EPSILON))
 
         # exponential of longs, automatic conversion to intermediate floats
         int64_tensor = ht.arange(elements, dtype=ht.int64)
@@ -45,8 +43,7 @@ def test_exp(self):
         self.assertIsInstance(int64_exp, ht.tensor)
         self.assertEqual(int64_exp.dtype, ht.float64)
         self.assertEqual(int64_exp.dtype, ht.float64)
-        in_range = (int64_exp._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int64_exp, comparison, atol= FLOAT_EPSILON))
 
         # check exceptions
         with self.assertRaises(TypeError):
@@ -56,7 +53,8 @@ def test_exp(self):
 
     def test_exp2(self):
         elements = 10
-        comparison = np.exp2(torch.arange(elements, dtype=torch.float64))
+        tmp = np.exp2(torch.arange(elements, dtype=torch.float64))
+        comparison = ht.array(tmp)
 
         # exponential of float32
         float32_tensor = ht.arange(elements, dtype=ht.float32)
@@ -64,8 +62,8 @@ def test_exp2(self):
         self.assertIsInstance(float32_exp2, ht.tensor)
         self.assertEqual(float32_exp2.dtype, ht.float32)
         self.assertEqual(float32_exp2.dtype, ht.float32)
-        in_range = (float32_exp2._tensor__array - comparison.type(torch.float32)) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float32_exp2, comparison.astype(ht.float32), atol= FLOAT_EPSILON))
+
 
         # exponential of float64
         float64_tensor = ht.arange(elements, dtype=ht.float64)
@@ -73,8 +71,7 @@ def test_exp2(self):
         self.assertIsInstance(float64_exp2, ht.tensor)
         self.assertEqual(float64_exp2.dtype, ht.float64)
         self.assertEqual(float64_exp2.dtype, ht.float64)
-        in_range = (float64_exp2._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float64_exp2, comparison, atol = FLOAT_EPSILON))
 
         # exponential of ints, automatic conversion to intermediate floats
         int32_tensor = ht.arange(elements, dtype=ht.int32)
@@ -82,8 +79,8 @@ def test_exp2(self):
         self.assertIsInstance(int32_exp2, ht.tensor)
         self.assertEqual(int32_exp2.dtype, ht.float64)
         self.assertEqual(int32_exp2.dtype, ht.float64)
-        in_range = (int32_exp2._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int32_exp2, comparison, atol = FLOAT_EPSILON))
+
 
         # exponential of longs, automatic conversion to intermediate floats
         int64_tensor = ht.arange(elements, dtype=ht.int64)
@@ -91,8 +88,8 @@ def test_exp2(self):
         self.assertIsInstance(int64_exp2, ht.tensor)
         self.assertEqual(int64_exp2.dtype, ht.float64)
         self.assertEqual(int64_exp2.dtype, ht.float64)
-        in_range = (int64_exp2._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int64_exp2, comparison, atol = FLOAT_EPSILON))
+
 
         # check exceptions
         with self.assertRaises(TypeError):
@@ -102,7 +99,8 @@ def test_exp2(self):
 
     def test_log(self):
         elements = 15
-        comparison = torch.arange(1, elements, dtype=torch.float64).log2()
+        tmp = torch.arange(1, elements, dtype=torch.float64).log2()
+        comparison = ht.array(tmp)
 
         # logarithm of float32
         float32_tensor = ht.arange(1, elements, dtype=ht.float32)
@@ -110,8 +108,7 @@ def test_log(self):
         self.assertIsInstance(float32_log2, ht.tensor)
         self.assertEqual(float32_log2.dtype, ht.float32)
         self.assertEqual(float32_log2.dtype, ht.float32)
-        in_range = (float32_log2._tensor__array - comparison.type(torch.float32)) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float32_log2, comparison.astype(ht.float32), atol = FLOAT_EPSILON))
 
         # logarithm of float64
         float64_tensor = ht.arange(1, elements, dtype=ht.float64)
@@ -119,8 +116,7 @@ def test_log(self):
         self.assertIsInstance(float64_log2, ht.tensor)
         self.assertEqual(float64_log2.dtype, ht.float64)
         self.assertEqual(float64_log2.dtype, ht.float64)
-        in_range = (float64_log2._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float64_log2, comparison, atol = FLOAT_EPSILON))
 
         # logarithm of ints, automatic conversion to intermediate floats
         int32_tensor = ht.arange(1, elements, dtype=ht.int32)
@@ -128,8 +124,7 @@ def test_log(self):
         self.assertIsInstance(int32_log2, ht.tensor)
         self.assertEqual(int32_log2.dtype, ht.float64)
         self.assertEqual(int32_log2.dtype, ht.float64)
-        in_range = (int32_log2._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int32_log2, comparison, atol = FLOAT_EPSILON))
 
         # log2arithm of longs, automatic conversion to intermediate floats
         int64_tensor = ht.arange(1, elements, dtype=ht.int64)
@@ -137,8 +132,7 @@ def test_log(self):
         self.assertIsInstance(int64_log2, ht.tensor)
         self.assertEqual(int64_log2.dtype, ht.float64)
         self.assertEqual(int64_log2.dtype, ht.float64)
-        in_range = (int64_log2._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int64_log2, comparison, atol = FLOAT_EPSILON))
 
         # check exceptions
         with self.assertRaises(TypeError):
@@ -148,7 +142,8 @@ def test_log(self):
 
     def test_log2(self):
         elements = 15
-        comparison = torch.arange(1, elements, dtype=torch.float64).log()
+        tmp = torch.arange(1, elements, dtype=torch.float64).log()
+        comparison = ht.array(tmp)
 
         # logarithm of float32
         float32_tensor = ht.arange(1, elements, dtype=ht.float32)
@@ -156,8 +151,7 @@ def test_log2(self):
         self.assertIsInstance(float32_log, ht.tensor)
         self.assertEqual(float32_log.dtype, ht.float32)
         self.assertEqual(float32_log.dtype, ht.float32)
-        in_range = (float32_log._tensor__array - comparison.type(torch.float32)) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float32_log, comparison.astype(ht.float32), atol = FLOAT_EPSILON))
 
         # logarithm of float64
         float64_tensor = ht.arange(1, elements, dtype=ht.float64)
@@ -165,8 +159,7 @@ def test_log2(self):
         self.assertIsInstance(float64_log, ht.tensor)
         self.assertEqual(float64_log.dtype, ht.float64)
         self.assertEqual(float64_log.dtype, ht.float64)
-        in_range = (float64_log._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float64_log, comparison, atol = FLOAT_EPSILON))
 
         # logarithm of ints, automatic conversion to intermediate floats
         int32_tensor = ht.arange(1, elements, dtype=ht.int32)
@@ -174,8 +167,7 @@ def test_log2(self):
         self.assertIsInstance(int32_log, ht.tensor)
         self.assertEqual(int32_log.dtype, ht.float64)
         self.assertEqual(int32_log.dtype, ht.float64)
-        in_range = (int32_log._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int32_log, comparison, atol = FLOAT_EPSILON))
 
         # logarithm of longs, automatic conversion to intermediate floats
         int64_tensor = ht.arange(1, elements, dtype=ht.int64)
@@ -183,8 +175,7 @@ def test_log2(self):
         self.assertIsInstance(int64_log, ht.tensor)
         self.assertEqual(int64_log.dtype, ht.float64)
         self.assertEqual(int64_log.dtype, ht.float64)
-        in_range = (int64_log._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int64_log, comparison, atol = FLOAT_EPSILON))
 
         # check exceptions
         with self.assertRaises(TypeError):
@@ -194,7 +185,8 @@ def test_log2(self):
 
     def test_log10(self):
         elements = 15
-        comparison = torch.arange(1, elements, dtype=torch.float64).log10()
+        tmp = torch.arange(1, elements, dtype=torch.float64).log10()
+        comparison = ht.array(tmp)
 
         # logarithm of float32
         float32_tensor = ht.arange(1, elements, dtype=ht.float32)
@@ -202,8 +194,7 @@ def test_log10(self):
         self.assertIsInstance(float32_log10, ht.tensor)
         self.assertEqual(float32_log10.dtype, ht.float32)
         self.assertEqual(float32_log10.dtype, ht.float32)
-        in_range = (float32_log10._tensor__array - comparison.type(torch.float32)) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float32_log10, comparison.astype(ht.float32), atol = FLOAT_EPSILON))
 
         # logarithm of float64
         float64_tensor = ht.arange(1, elements, dtype=ht.float64)
@@ -211,8 +202,7 @@ def test_log10(self):
         self.assertIsInstance(float64_log10, ht.tensor)
         self.assertEqual(float64_log10.dtype, ht.float64)
         self.assertEqual(float64_log10.dtype, ht.float64)
-        in_range = (float64_log10._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(float64_log10, comparison, atol = FLOAT_EPSILON))
 
         # logarithm of ints, automatic conversion to intermediate floats
         int32_tensor = ht.arange(1, elements, dtype=ht.int32)
@@ -220,8 +210,7 @@ def test_log10(self):
         self.assertIsInstance(int32_log10, ht.tensor)
         self.assertEqual(int32_log10.dtype, ht.float64)
         self.assertEqual(int32_log10.dtype, ht.float64)
-        in_range = (int32_log10._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int32_log10, comparison, atol = FLOAT_EPSILON))
 
         # logarithm of longs, automatic conversion to intermediate floats
         int64_tensor = ht.arange(1, elements, dtype=ht.int64)
@@ -229,8 +218,7 @@ def test_log10(self):
         self.assertIsInstance(int64_log10, ht.tensor)
         self.assertEqual(int64_log10.dtype, ht.float64)
         self.assertEqual(int64_log10.dtype, ht.float64)
-        in_range = (int64_log10._tensor__array - comparison) < FLOAT_EPSILON
-        self.assertTrue(in_range.all())
+        self.assertTrue(ht.allclose(int64_log10, comparison, atol = FLOAT_EPSILON))
 
         # check exceptions
         with self.assertRaises(TypeError):
@@ -240,7 +228,8 @@ def test_log10(self):
 
     def test_sqrt(self):
         elements = 25
-        comparison = ht.arange(elements, dtype=ht.float64).sqrt()
+        tmp = torch.arange(elements, dtype=torch.float64).sqrt()
+        comparison = ht.array(tmp)
 
         # square roots of float32
         float32_tensor = ht.arange(elements, dtype=ht.float32)
@@ -282,7 +271,8 @@ def test_sqrt(self):
 
     def test_sqrt_method(self):
         elements = 25
-        comparison = ht.arange(elements, dtype=ht.float64).sqrt()
+        tmp = torch.arange(elements, dtype=torch.float64).sqrt()
+        comparison = ht.array(tmp)
 
         # square roots of float32
         float32_sqrt = ht.arange(elements, dtype=ht.float32).sqrt()
diff --git a/heat/core/tests/test_operations.py b/heat/core/tests/test_operations.py
--- a/heat/core/tests/test_operations.py
+++ b/heat/core/tests/test_operations.py
@@ -167,13 +167,26 @@ def test_all(self):
     def test_allclose(self):
         a = ht.float32([[2, 2], [2, 2]])
         b = ht.float32([[2.00005, 2.00005], [2.00005, 2.00005]])
+        c = ht.zeros((4, 6,), split=0)
+        d = ht.zeros((4, 6,), split=1)
+        e = ht.zeros((4, 6,))
 
         self.assertFalse(ht.allclose(a, b))
         self.assertTrue(ht.allclose(a, b, atol=1e-04))
         self.assertTrue(ht.allclose(a, b, rtol=1e-04))
+        self.assertTrue(ht.allclose(a, 2))
+        self.assertTrue(ht.allclose(a, 2.0))
+        self.assertTrue(ht.allclose(2,a))
+        self.assertTrue(ht.allclose(c, d))
+        self.assertTrue(ht.allclose(c, e))
+        self.assertTrue(ht.allclose(e, c))
 
         with self.assertRaises(TypeError):
             ht.allclose(a, (2, 2, 2, 2))
+        with self.assertRaises(TypeError):
+            ht.allclose(a, '?')
+        with self.assertRaises(TypeError):
+            ht.allclose('?', a)
 
     def test_any(self):
         x = ht.float32([[2.7, 0, 0],