Repository: mars-project/mars

Issue:
[BUG] Fuse operand's sparse value is wrong
<!--
Thank you for your contribution!

Please review https://github.com/mars-project/mars/blob/master/CONTRIBUTING.rst before opening an issue.
-->

**Describe the bug**
A fuse operand's sparseness should be the same as tail node's, it is not set correctly now.

**To Reproduce**
``` Python
In [1]: import scipy.sparse as sps                                                                                                 

In [2]: import mars.tensor as mt                                                                                                   

In [3]: data = sps.rand(10, 10, density=0.05)                                                                                      

In [4]: a = mt.tensor(data, chunk_size=3)                                                                                          

In [5]: b = (a * 2) * 2                                                                                                            

In [6]: g = b.build_graph(tiled=True, compose=True)                                                                                

In [7]: list(g)[0].op.sparse                                                                                                       
Out[7]: False

In [8]: list(g)[0].op                                                                                                              
Out[8]: <mars.tensor.expressions.fuse.core.TensorFuseChunk at 0xa208b7048>

In [9]: list(g)[0].composed[-1].op.sparse                                                                                         
Out[9]: True
```




Discussion:

Resolving patch:
diff --git a/mars/tensor/expressions/fuse/core.py b/mars/tensor/expressions/fuse/core.py
--- a/mars/tensor/expressions/fuse/core.py
+++ b/mars/tensor/expressions/fuse/core.py
@@ -20,8 +20,8 @@
 
 
 class TensorFuseChunk(operands.Fuse, TensorOperandMixin):
-    def __init__(self, dtype=None, **kw):
-        super(TensorFuseChunk, self).__init__(_dtype=dtype, **kw)
+    def __init__(self, dtype=None, sparse=False, **kw):
+        super(TensorFuseChunk, self).__init__(_dtype=dtype, _sparse=sparse, **kw)
 
     def calc_shape(self, *inputs_shape):
         in_shapes = inputs_shape


Test patch:
diff --git a/mars/tensor/expressions/tests/test_fuse.py b/mars/tensor/expressions/tests/test_fuse.py
new file mode 100644
--- /dev/null
+++ b/mars/tensor/expressions/tests/test_fuse.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright 1999-2018 Alibaba Group Holding Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import scipy.sparse as sps
+
+import mars.tensor as mt
+from mars.tensor.expressions.fuse.core import TensorFuseChunk
+from mars.tensor.expressions.datasource import CSRMatrixDataSource
+from mars import operands
+
+
+class Test(unittest.TestCase):
+    def testBuildFusedGraph(self):
+        t = mt.random.rand(9, 9, chunk_size=3)
+        t1 = (t + 1) * 2
+
+        g = t1.build_graph(tiled=True, compose=True)
+        graph_nodes = list(g)
+        self.assertTrue(all(isinstance(n.op, TensorFuseChunk) for n in graph_nodes))
+        self.assertTrue(all(n.shape == (3, 3) for n in graph_nodes))
+
+        fuse_node = graph_nodes[0]
+        self.assertEqual(fuse_node.shape, (3, 3))
+        self.assertEqual(len(fuse_node.composed), 3)
+        self.assertIsInstance(fuse_node.composed[0].op, operands.Rand)
+        self.assertIsInstance(fuse_node.composed[1].op, operands.AddConstant)
+        self.assertIsInstance(fuse_node.composed[2].op, operands.MulConstant)
+
+        t2 = mt.sum((t / 2) - 1, axis=0)
+
+        g = t2.build_graph(tiled=True, compose=True)
+        graph_nodes = list(g.bfs())
+        self.assertTrue(all(isinstance(n.op, TensorFuseChunk) for n in graph_nodes))
+
+        reduction_node = graph_nodes[-1]
+        self.assertEqual(reduction_node.shape, (3,))
+        self.assertEqual(len(reduction_node.composed), 2)
+        self.assertEqual(reduction_node.inputs, reduction_node.composed[0].inputs)
+        self.assertIsInstance(reduction_node.composed[0].op, operands.Concatenate)
+        self.assertIsInstance(reduction_node.composed[1].op, operands.Sum)
+
+        agg_node = graph_nodes[0]
+        self.assertEqual(agg_node.shape, (1, 3))
+        self.assertEqual(len(agg_node.composed), 4)
+        self.assertIsInstance(agg_node.composed[0].op, operands.Rand)
+        self.assertIsInstance(agg_node.composed[1].op, (operands.TDivConstant, operands.DivConstant))
+        self.assertIsInstance(agg_node.composed[2].op, operands.SubConstant)
+        self.assertIsInstance(agg_node.composed[3].op, operands.Sum)
+
+    def testSparse(self):
+        data = sps.rand(9, 9, density=0.1)
+        t = mt.tensor(data, chunk_size=3)
+
+        t1 = t * 2 / 3
+        g = t1.build_graph(tiled=True, compose=True)
+        graph_nodes = list(g)
+        self.assertTrue(all(isinstance(n.op, TensorFuseChunk) for n in graph_nodes))
+        self.assertTrue(all(n.op.sparse for n in graph_nodes))
+        self.assertTrue(all(n.shape == (3, 3) for n in graph_nodes))
+
+        fuse_node = graph_nodes[0]
+        self.assertEqual(fuse_node.shape, (3, 3))
+        self.assertEqual(len(fuse_node.composed), 3)
+        self.assertIsInstance(fuse_node.composed[0].op, CSRMatrixDataSource)
+        self.assertIsInstance(fuse_node.composed[1].op, operands.MulConstant)
+        self.assertIsInstance(fuse_node.composed[2].op, (operands.TDivConstant, operands.DivConstant))
+        self.assertTrue(all(c.op.sparse for c in fuse_node.composed))
+
+        # add constant will convert sparse matrix to dense matrix
+        t2 = t * 2 + 3
+        g = t2.build_graph(tiled=True, compose=True)
+        graph_nodes = list(g)
+        self.assertTrue(all([isinstance(n.op, TensorFuseChunk) for n in graph_nodes]))
+        self.assertTrue(all([not n.op.sparse for n in graph_nodes]))
+        self.assertTrue(all(n.shape == (3, 3) for n in graph_nodes))
+
+        fuse_node = graph_nodes[0]
+        self.assertEqual(fuse_node.shape, (3, 3))
+        self.assertEqual(len(fuse_node.composed), 3)
+        self.assertIsInstance(fuse_node.composed[0].op, CSRMatrixDataSource)
+        self.assertIsInstance(fuse_node.composed[1].op, operands.MulConstant)
+        self.assertTrue(fuse_node.composed[1].op.sparse)
+        self.assertIsInstance(fuse_node.composed[2].op, operands.AddConstant)
+        self.assertFalse(fuse_node.composed[2].op.sparse)