Repository: mars-project/mars

Issue:
[BUG]mt.isclose does not broadcast correctly sometimes
## Describe the bug
`mt.isclose` sometimes does not broadcast correctly and raises a ValueError, where numpy does well. See the codes below for details

```
In []: mt.isclose((0,0), (0,0)).execute()
Out[]: array([True, True])
In []: mt.isclose((0,0), (0,)).execute()
Out[]: arary([True, True])
In []: np.isclose((0,0), (0))
Out[]: array([True, True])
In []: mt.isclose((0,0), (0)).execute()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-20-a3e463eb7acf> in <module>
----> 1 mt.isclose((0,0), (0)).execute()

~/anaconda3/lib/python3.7/site-packages/mars/core.py in execute(self, session, **kw)
    580 
    581     def execute(self, session=None, **kw):
--> 582         self._data.execute(session, **kw)
    583         return self
    584 

~/anaconda3/lib/python3.7/site-packages/mars/core.py in execute(self, session, **kw)
    368 
    369         # no more fetch, thus just fire run
--> 370         session.run(self, **kw)
    371         # return Tileable or ExecutableTuple itself
    372         return self

~/anaconda3/lib/python3.7/site-packages/mars/session.py in run(self, *tileables, **kw)
    460         tileables = tuple(mt.tensor(t) if not isinstance(t, (Entity, Base)) else t
    461                           for t in tileables)
--> 462         result = self._sess.run(*tileables, **kw)
    463 
    464         for t in tileables:

~/anaconda3/lib/python3.7/site-packages/mars/session.py in run(self, *tileables, **kw)
    105             # set number of running cores
    106             self.context.set_ncores(kw['n_parallel'])
--> 107             res = self._executor.execute_tileables(tileables, **kw)
    108             return res
    109 

~/anaconda3/lib/python3.7/site-packages/mars/utils.py in _wrapped(*args, **kwargs)
    406                 _kernel_mode.eager = False
    407             _kernel_mode.eager_count = enter_eager_count + 1
--> 408             return func(*args, **kwargs)
    409         finally:
    410             _kernel_mode.eager_count -= 1

~/anaconda3/lib/python3.7/site-packages/mars/utils.py in inner(*args, **kwargs)
    500     def inner(*args, **kwargs):
    501         with build_mode():
--> 502             return func(*args, **kwargs)
    503     return inner
    504 

~/anaconda3/lib/python3.7/site-packages/mars/executor.py in execute_tileables(self, tileables, fetch, n_parallel, n_thread, print_progress, mock, compose, name)
    878                                    n_parallel=n_parallel or n_thread,
    879                                    print_progress=print_progress, mock=mock,
--> 880                                    chunk_result=chunk_result)
    881 
    882                 # update shape of tileable and its chunks whatever it's successful or not

~/anaconda3/lib/python3.7/site-packages/mars/executor.py in execute_graph(self, graph, keys, n_parallel, print_progress, mock, no_intermediate, compose, retval, chunk_result)
    688             print_progress=print_progress, mock=mock, mock_max_memory=self._mock_max_memory,
    689             fetch_keys=fetch_keys, no_intermediate=no_intermediate)
--> 690         res = graph_execution.execute(retval)
    691         self._mock_max_memory = max(self._mock_max_memory, graph_execution._mock_max_memory)
    692         if mock:

~/anaconda3/lib/python3.7/site-packages/mars/executor.py in execute(self, retval)
    569         # wait until all the futures completed
    570         for future in executed_futures:
--> 571             future.result()
    572 
    573         if retval:

~/anaconda3/lib/python3.7/concurrent/futures/_base.py in result(self, timeout)
    426                 raise CancelledError()
    427             elif self._state == FINISHED:
--> 428                 return self.__get_result()
    429 
    430             self._condition.wait(timeout)

~/anaconda3/lib/python3.7/concurrent/futures/_base.py in __get_result(self)
    382     def __get_result(self):
    383         if self._exception:
--> 384             raise self._exception
    385         else:
    386             return self._result

~/anaconda3/lib/python3.7/concurrent/futures/thread.py in run(self)
     55 
     56         try:
---> 57             result = self.fn(*self.args, **self.kwargs)
     58         except BaseException as exc:
     59             self.future.set_exception(exc)

~/anaconda3/lib/python3.7/site-packages/mars/executor.py in _execute_operand(self, op)
    441             # so we pass the first operand's first output to Executor.handle
    442             first_op = ops[0]
--> 443             Executor.handle(first_op, results, self._mock)
    444 
    445             # update maximal memory usage during execution

~/anaconda3/lib/python3.7/site-packages/mars/executor.py in handle(cls, op, results, mock)
    639                 # The `UFuncTypeError` was introduced by numpy#12593 since v1.17.0.
    640                 try:
--> 641                     return runner(results, op)
    642                 except UFuncTypeError as e:
    643                     raise TypeError(str(e)).with_traceback(sys.exc_info()[2]) from None

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/isclose.py in execute(cls, ctx, op)
     61     def execute(cls, ctx, op):
     62         (a, b), device_id, xp = as_same_device(
---> 63             [ctx[c.key] for c in op.inputs], device=op.device, ret_extra=True)
     64 
     65         with device(device_id):

ValueError: not enough values to unpack (expected 2, got 1)
```



Discussion:

Resolving patch:
diff --git a/mars/tensor/arithmetic/isclose.py b/mars/tensor/arithmetic/isclose.py
--- a/mars/tensor/arithmetic/isclose.py
+++ b/mars/tensor/arithmetic/isclose.py
@@ -59,10 +59,13 @@ def _is_sparse(cls, x1, x2):
 
     @classmethod
     def execute(cls, ctx, op):
-        (a, b), device_id, xp = as_same_device(
+        inputs, device_id, xp = as_same_device(
             [ctx[c.key] for c in op.inputs], device=op.device, ret_extra=True)
 
         with device(device_id):
+            a = op.lhs if np.isscalar(op.lhs) else inputs[0]
+            b = op.rhs if np.isscalar(op.rhs) else inputs[-1]
+
             ctx[op.outputs[0].key] = xp.isclose(a, b, atol=op.atol, rtol=op.rtol,
                                                 equal_nan=op.equal_nan)
 
diff --git a/mars/utils.py b/mars/utils.py
--- a/mars/utils.py
+++ b/mars/utils.py
@@ -1101,5 +1101,5 @@ def arrow_array_to_objects(obj):
         obj = pd.DataFrame(out_cols, columns=list(obj.dtypes.keys()))
     elif isinstance(obj, pd.Series):
         if isinstance(obj.dtype, ArrowDtype):
-            obj = pd.Series(obj.to_numpy())
+            obj = pd.Series(obj.to_numpy(), index=obj.index, name=obj.name)
     return obj


Test patch:
diff --git a/mars/tensor/arithmetic/tests/test_arithmetic_execution.py b/mars/tensor/arithmetic/tests/test_arithmetic_execution.py
--- a/mars/tensor/arithmetic/tests/test_arithmetic_execution.py
+++ b/mars/tensor/arithmetic/tests/test_arithmetic_execution.py
@@ -510,18 +510,29 @@ def testIsCloseExecution(self):
 
         res = self.executor.execute_tensor(z, concat=True)[0]
         expected = np.isclose(data, data2, atol=.01)
-
         np.testing.assert_equal(res, expected)
 
         z = isclose(x, y, atol=.01, equal_nan=True)
 
         res = self.executor.execute_tensor(z, concat=True)[0]
         expected = np.isclose(data, data2, atol=.01, equal_nan=True)
+        np.testing.assert_equal(res, expected)
 
+        # test tensor with scalar
+        z = isclose(x, 1.0, atol=.01)
+        res = self.executor.execute_tensor(z, concat=True)[0]
+        expected = np.isclose(data, 1.0, atol=.01)
+        np.testing.assert_equal(res, expected)
+        z = isclose(1.0, y, atol=.01)
+        res = self.executor.execute_tensor(z, concat=True)[0]
+        expected = np.isclose(1.0, data2, atol=.01)
+        np.testing.assert_equal(res, expected)
+        z = isclose(1.0, 2.0, atol=.01)
+        res = self.executor.execute_tensor(z, concat=True)[0]
+        expected = np.isclose(1.0, 2.0, atol=.01)
         np.testing.assert_equal(res, expected)
 
         # test sparse
-
         data = sps.csr_matrix(np.array([0, 1.0, 1.01, np.nan]))
         data2 = sps.csr_matrix(np.array([0, 1.0, 1.03, np.nan]))
 
@@ -532,14 +543,12 @@ def testIsCloseExecution(self):
 
         res = self.executor.execute_tensor(z, concat=True)[0]
         expected = np.isclose(data.toarray(), data2.toarray(), atol=.01)
-
         np.testing.assert_equal(res, expected)
 
         z = isclose(x, y, atol=.01, equal_nan=True)
 
         res = self.executor.execute_tensor(z, concat=True)[0]
         expected = np.isclose(data.toarray(), data2.toarray(), atol=.01, equal_nan=True)
-
         np.testing.assert_equal(res, expected)
 
     @ignore_warning
diff --git a/mars/web/tests/test_api.py b/mars/web/tests/test_api.py
--- a/mars/web/tests/test_api.py
+++ b/mars/web/tests/test_api.py
@@ -213,15 +213,17 @@ def testWebApi(self):
                 value = sess.run(c, timeout=timeout)
                 np.testing.assert_array_equal(value, np.ones((10, 10)) * 10)
 
-                raw = pd.DataFrame(np.random.rand(10, 5), columns=list('ABCDE'))
+                raw = pd.DataFrame(np.random.rand(10, 5), columns=list('ABCDE'),
+                                   index=pd.RangeIndex(10, 0, -1))
                 data = md.DataFrame(raw).astype({'E': 'arrow_string'})
                 ret_data = data.execute(session=sess).fetch(session=sess)
                 self.assertEqual(ret_data.dtypes['E'], np.dtype('O'))
                 pd.testing.assert_frame_equal(
                     ret_data.astype({'E': 'float'}), raw, check_less_precise=True)
 
-                raw = pd.Series(np.random.rand(10))
-                data = md.Series(raw).astype('arrow_string')
+                raw = pd.Series(np.random.rand(10), index=pd.RangeIndex(10, 0, -1),
+                                name='r')
+                data = md.Series(raw).astype('Arrow[string]')
                 ret_data = data.execute(session=sess).fetch(session=sess)
                 self.assertEqual(ret_data.dtype, np.dtype('O'))
                 pd.testing.assert_series_equal(ret_data.astype('float'), raw)