Repository: mars-project/mars

Issue:
[BUG]Cannot pass zero-dimensional tensor as both arguments into `mt.arctan2`
## Describe the bug
When I use the `mt.arctan2`, if i pass zero-dimensional tensors as both arguments, an error will be raised. If any argument is not zero-dimensional, the codes will work.

## To Reproduce
```
vx = mt.dot((1,0,0),(0,1,0))
vy = mt.dot((1,0,0),(0,0,1))
t = mt.arctan2(vx, vy)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~/anaconda3/lib/python3.7/site-packages/mars/core.py in __len__(self)
    533         try:
--> 534             return self.shape[0]
    535         except IndexError:

IndexError: tuple index out of range

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-23-09c63447ea86> in <module>
----> 1 mt.arctan2(vx, vy)

~/anaconda3/lib/python3.7/site-packages/mars/tensor/utils.py in h(*tensors, **kw)
    256                 kw['dtype'] = dtype
    257 
--> 258             ret = func(*tensors, **kw)
    259             if ret is NotImplemented:
    260                 reverse_func = getattr(inspect.getmodule(func), 'r{0}'.format(func.__name__), None) \

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/arctan2.py in arctan2(x1, x2, out, where, **kwargs)
    125     """
    126     op = TensorArctan2(**kwargs)
--> 127     return op(x1, x2, out=out, where=where)

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/core.py in __call__(self, x1, x2, out, where)
    268 
    269     def __call__(self, x1, x2, out=None, where=None):
--> 270         return self._call(x1, x2, out=out, where=where)
    271 
    272     def rcall(self, x1, x2, out=None, where=None):

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/core.py in _call(self, x1, x2, out, where)
    251 
    252         inputs = filter_inputs([x1, x2, out, where])
--> 253         t = self.new_tensor(inputs, shape, order=order)
    254 
    255         if out is None:

~/anaconda3/lib/python3.7/site-packages/mars/tensor/operands.py in new_tensor(self, inputs, shape, dtype, order, **kw)
     77             raise TypeError('cannot new tensor with more than 1 outputs')
     78 
---> 79         return self.new_tensors(inputs, shape=shape, dtype=dtype, order=order, **kw)[0]
     80 
     81     @classmethod

~/anaconda3/lib/python3.7/site-packages/mars/tensor/operands.py in new_tensors(self, inputs, shape, dtype, order, chunks, nsplits, output_limit, kws, **kw)
     71                     output_limit=None, kws=None, **kw):
     72         return self.new_tileables(inputs, shape=shape, chunks=chunks, nsplits=nsplits,
---> 73                                   output_limit=output_limit, kws=kws, dtype=dtype, order=order, **kw)
     74 
     75     def new_tensor(self, inputs, shape, dtype=None, order=None, **kw):

~/anaconda3/lib/python3.7/site-packages/mars/operands.py in new_tileables(self, inputs, kws, **kw)
    352         """
    353 
--> 354         tileables = self._new_tileables(inputs, kws=kws, **kw)
    355         if is_eager_mode():
    356             ExecutableTuple(tileables).execute(fetch=False)

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/core.py in _new_tileables(self, inputs, kws, **kw)
     70 
     71     def _new_tileables(self, inputs, kws=None, **kw):
---> 72         self._set_sparse(inputs)
     73         return super()._new_tileables(
     74             inputs, kws=kws, **kw)

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/core.py in _set_sparse(self, inputs)
    188         x1 = self._lhs if np.isscalar(self._lhs) else next(inputs_iter)
    189         x2 = self._rhs if np.isscalar(self._rhs) else next(inputs_iter)
--> 190         setattr(self, '_sparse', self._is_sparse(x1, x2))
    191 
    192     def _set_inputs(self, inputs):

~/anaconda3/lib/python3.7/site-packages/mars/tensor/arithmetic/arctan2.py in _is_sparse(cls, x1, x2)
     33         if hasattr(x1, 'issparse') and x1.issparse() and np.isscalar(x2):
     34             return True
---> 35         elif x1 == 0:
     36             return True
     37         return False

~/anaconda3/lib/python3.7/site-packages/mars/tensor/core.py in __len__(self)
    279 
    280     def __len__(self):
--> 281         return len(self._data)
    282 
    283     @property

~/anaconda3/lib/python3.7/site-packages/mars/core.py in __len__(self)
    536             if build_mode().is_build_mode:
    537                 return 0
--> 538             raise TypeError('len() of unsized object')
    539 
    540     @property

TypeError: len() of unsized object
```


Discussion:
Thank you for your report, seems the sparse logic in arctan2 made some mistakes. We will fix it ASAP.
BTW, if you are interested in resolving the issue, tell us in advance.
FYI, I am working on this issue.

Resolving patch:
diff --git a/mars/tensor/arithmetic/arctan2.py b/mars/tensor/arithmetic/arctan2.py
--- a/mars/tensor/arithmetic/arctan2.py
+++ b/mars/tensor/arithmetic/arctan2.py
@@ -29,11 +29,12 @@ class TensorArctan2(TensorBinOp):
 
     @classmethod
     def _is_sparse(cls, x1, x2):
-        # x2 is sparse or not does not matter
-        if hasattr(x1, 'issparse') and x1.issparse() and np.isscalar(x2):
-            return True
-        elif x1 == 0:
+        if hasattr(x1, 'issparse') and x1.issparse():
+            # if x1 is sparse, will be sparse always
             return True
+        elif np.isscalar(x1) and x1 == 0:
+            # x1 == 0, return sparse if x2 is
+            return x2.issparse() if hasattr(x2, 'issparse') else False
         return False
 
 


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
@@ -23,7 +23,7 @@
 from mars.utils import ignore_warning
 from mars.tensor.datasource import ones, tensor, zeros
 from mars.tensor.arithmetic import add, cos, truediv, frexp, \
-    modf, clip, isclose
+    modf, clip, isclose, arctan2
 from mars.tests.core import require_cupy, ExecutorForTest
 
 
@@ -282,6 +282,40 @@ def testAddWithOutExecution(self):
         expected = np.add(data2[:5, :], 1)
         self.assertTrue(np.array_equal(res, expected))
 
+    def testArctan2Execution(self):
+        x = tensor(1)  # scalar
+        y = arctan2(x, x)
+
+        self.assertFalse(y.issparse())
+        result = self.executor.execute_tensor(y, concat=True)[0]
+        np.testing.assert_equal(result, np.arctan2(1, 1))
+
+        y = arctan2(0, x)
+
+        self.assertFalse(y.issparse())
+        result = self.executor.execute_tensor(y, concat=True)[0]
+        np.testing.assert_equal(result, np.arctan2(0, 1))
+
+        raw1 = np.array([[0, 1, 2]])
+        raw2 = sps.csr_matrix([[0, 1, 0]])
+        y = arctan2(raw1, raw2)
+
+        self.assertFalse(y.issparse())
+        result = self.executor.execute_tensor(y, concat=True)[0]
+        np.testing.assert_equal(result, np.arctan2(raw1, raw2.A))
+
+        y = arctan2(raw2, raw2)
+
+        self.assertTrue(y.issparse())
+        result = self.executor.execute_tensor(y, concat=True)[0]
+        np.testing.assert_equal(result, np.arctan2(raw2.A, raw2.A))
+
+        y = arctan2(0, raw2)
+
+        self.assertTrue(y.issparse())
+        result = self.executor.execute_tensor(y, concat=True)[0]
+        np.testing.assert_equal(result, np.arctan2(0, raw2.A))
+
     def testFrexpExecution(self):
         data1 = np.random.random((5, 9, 4))