Repository: dask/dask

Issue:
Computing `.var(axis=())` fails
Seeing the following failure with using `.var(axis=())` on `master`. This does not occur when using 1.2.2.

```python
In [1]: import dask.array as da                                                 

In [2]: a = da.random.random((10, 11, 12), chunks=5)                            

In [3]: a.var(axis=())                                                          
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-3-60e5d6560565> in <module>
----> 1 a.var(axis=())

~/Developer/dask/dask/array/core.py in var(self, axis, dtype, keepdims, ddof, split_every, out)
   1720         from .reductions import var
   1721         return var(self, axis=axis, dtype=dtype, keepdims=keepdims, ddof=ddof,
-> 1722                    split_every=split_every, out=out)
   1723 
   1724     def moment(self, order, axis=None, dtype=None, keepdims=False, ddof=0,

~/Developer/dask/dask/array/reductions.py in var(a, axis, dtype, keepdims, ddof, split_every, out)
    585                      keepdims=keepdims, dtype=dt, split_every=split_every,
    586                      combine=moment_combine, name='var', out=out,
--> 587                      concatenate=False)
    588 
    589 

~/Developer/dask/dask/array/reductions.py in reduction(x, chunk, aggregate, axis, keepdims, dtype, split_every, combine, name, out, concatenate, output_size, meta)
    161     result = _tree_reduce(tmp, aggregate, axis, keepdims, dtype, split_every,
    162                           combine, name=name, concatenate=concatenate,
--> 163                           reduced_meta=reduced_meta)
    164     if keepdims and output_size != 1:
    165         result._chunks = tuple((output_size, ) if i in axis else c

~/Developer/dask/dask/array/reductions.py in _tree_reduce(x, aggregate, axis, keepdims, dtype, split_every, combine, name, concatenate, reduced_meta)
    203     return partial_reduce(func, x, split_every, keepdims=keepdims, dtype=dtype,
    204                           name=(name or funcname(aggregate)) + '-aggregate',
--> 205                           reduced_meta=reduced_meta)
    206 
    207 

~/Developer/dask/dask/array/reductions.py in partial_reduce(func, x, split_every, keepdims, dtype, name, reduced_meta)
    247     if reduced_meta is not None:
    248         try:
--> 249             meta = func(reduced_meta, meta=True)
    250         # no meta keyword argument exists for func, and it isn't required
    251         except TypeError:

~/Developer/dask/dask/array/reductions.py in moment_agg(pairs, order, ddof, dtype, sum, axis, meta, **kwargs)
    524 
    525     ns = deepmap(lambda pair: pair['n'], pairs) if not meta else pairs
--> 526     ns = _concatenate2(ns, axes=axis)
    527     n = ns.sum(axis=axis, **keepdim_kw)
    528 

~/Developer/dask/dask/array/core.py in _concatenate2(arrays, axes)
    226         arrays = [_concatenate2(a, axes=axes[1:]) for a in arrays]
    227     concatenate = concatenate_lookup.dispatch(type(max(arrays, key=lambda x: getattr(x, '__array_priority__', 0))))
--> 228     return concatenate(arrays, axis=axes[0])
    229 
    230 

IndexError: tuple index out of range
```

Note: This also happens with `.std(axis=())`.


Discussion:
Running `git bisect` points to PR ( https://github.com/dask/dask/pull/4543 ) as the cause.

cc @pentschev
Looking a little closer it seems this failure will happen with 1.2.2 if `.compute()` is added. So this may be an older issue that `git bisect` suggests.

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -224,6 +224,8 @@ def _concatenate2(arrays, axes=[]):
 
     Each entry in axes corresponds to each level of the nested list.  The
     length of axes should correspond to the level of nesting of arrays.
+    If axes is an empty list or tuple, return arrays, or arrays[0] if
+    arrays is a list.
 
     >>> x = np.array([[1, 2], [3, 4]])
     >>> _concatenate2([x, x], axes=[0])
@@ -246,7 +248,18 @@ def _concatenate2(arrays, axes=[]):
     >>> _concatenate2(iter([x, x]), axes=[1])
     array([[1, 2, 1, 2],
            [3, 4, 3, 4]])
+
+    Special Case
+    >>> _concatenate2([x, x], axes=())
+    array([[1, 2],
+           [3, 4]])
     """
+    if axes == ():
+        if isinstance(arrays, list):
+            return arrays[0]
+        else:
+            return arrays
+
     if isinstance(arrays, Iterator):
         arrays = list(arrays)
     if not isinstance(arrays, (list, tuple)):


Test patch:
diff --git a/dask/array/tests/test_reductions.py b/dask/array/tests/test_reductions.py
--- a/dask/array/tests/test_reductions.py
+++ b/dask/array/tests/test_reductions.py
@@ -24,6 +24,10 @@ def assert_eq(a, b):
 def test_numel(dtype, keepdims):
     x = np.ones((2, 3, 4))
 
+    assert_eq(
+        da.reductions.numel(x, axis=(), keepdims=keepdims, dtype=dtype),
+        np.sum(x, axis=(), keepdims=keepdims, dtype=dtype),
+    )
     assert_eq(
         da.reductions.numel(x, axis=0, keepdims=keepdims, dtype=dtype),
         np.sum(x, axis=0, keepdims=keepdims, dtype=dtype),
@@ -48,6 +52,7 @@ def test_numel(dtype, keepdims):
 def reduction_1d_test(da_func, darr, np_func, narr, use_dtype=True, split_every=True):
     assert_eq(da_func(darr), np_func(narr))
     assert_eq(da_func(darr, keepdims=True), np_func(narr, keepdims=True))
+    assert_eq(da_func(darr, axis=()), np_func(narr, axis=()))
     assert same_keys(da_func(darr), da_func(darr))
     assert same_keys(da_func(darr, keepdims=True), da_func(darr, keepdims=True))
     if use_dtype:
@@ -94,6 +99,7 @@ def reduction_2d_test(da_func, darr, np_func, narr, use_dtype=True, split_every=
         warnings.simplefilter("ignore")  # overflow
         assert_eq(da_func(darr), np_func(narr))
         assert_eq(da_func(darr, keepdims=True), np_func(narr, keepdims=True))
+        assert_eq(da_func(darr, axis=()), np_func(narr, axis=()))
         assert_eq(da_func(darr, axis=0), np_func(narr, axis=0))
         assert_eq(da_func(darr, axis=1), np_func(narr, axis=1))
         assert_eq(da_func(darr, axis=-1), np_func(narr, axis=-1))
@@ -101,8 +107,12 @@ def reduction_2d_test(da_func, darr, np_func, narr, use_dtype=True, split_every=
         assert_eq(
             da_func(darr, axis=1, keepdims=True), np_func(narr, axis=1, keepdims=True)
         )
+        assert_eq(
+            da_func(darr, axis=(), keepdims=True), np_func(narr, axis=(), keepdims=True)
+        )
         assert_eq(da_func(darr, axis=(1, 0)), np_func(narr, axis=(1, 0)))
 
+        assert same_keys(da_func(darr, axis=()), da_func(darr, axis=()))
         assert same_keys(da_func(darr, axis=1), da_func(darr, axis=1))
         assert same_keys(da_func(darr, axis=(1, 0)), da_func(darr, axis=(1, 0)))
 
@@ -120,7 +130,12 @@ def reduction_2d_test(da_func, darr, np_func, narr, use_dtype=True, split_every=
                 da_func(darr, keepdims=True, split_every=4),
                 np_func(narr, keepdims=True),
             )
+            assert_eq(da_func(darr, axis=(), split_every=2), np_func(narr, axis=()))
             assert_eq(da_func(darr, axis=0, split_every=2), np_func(narr, axis=0))
+            assert_eq(
+                da_func(darr, axis=(), keepdims=True, split_every=2),
+                np_func(narr, axis=(), keepdims=True),
+            )
             assert_eq(
                 da_func(darr, axis=0, keepdims=True, split_every=2),
                 np_func(narr, axis=0, keepdims=True),
@@ -385,12 +400,21 @@ def test_nan_object(func):
         )
         d = da.from_array(x, chunks=(2, 2))
 
+        if func in {"nanmin", "nanmax"}:
+            warnings.simplefilter("ignore", RuntimeWarning)
+
+        assert_eq(getattr(np, func)(x, axis=()), getattr(da, func)(d, axis=()))
+
+        if func in {"nanmin", "nanmax"}:
+            warnings.simplefilter("default", RuntimeWarning)
+
         assert_eq(getattr(np, func)(x, axis=0), getattr(da, func)(d, axis=0))
         assert_eq(getattr(np, func)(x, axis=1), getattr(da, func)(d, axis=1))
         assert_eq(getattr(np, func)(x), getattr(da, func)(d))
 
 
 def test_0d_array():
+    x = da.mean(da.ones(4, chunks=4), axis=()).compute()
     x = da.mean(da.ones(4, chunks=4), axis=0).compute()
     y = np.mean(np.ones(4))
     assert type(x) == type(y)
@@ -414,6 +438,7 @@ def test_reductions_with_empty_array():
     for dx, x in [(dx1, x1), (dx2, x2)]:
         with pytest.warns(None):  # empty slice warning
             assert_eq(dx.mean(), x.mean())
+            assert_eq(dx.mean(axis=()), x.mean(axis=()))
             assert_eq(dx.mean(axis=0), x.mean(axis=0))
             assert_eq(dx.mean(axis=1), x.mean(axis=1))
             assert_eq(dx.mean(axis=2), x.mean(axis=2))
@@ -432,9 +457,11 @@ def test_tree_reduce_depth():
     x = da.from_array(np.arange(242).reshape((11, 22)), chunks=(3, 4))
     thresh = {0: 2, 1: 3}
     assert_max_deps(x.sum(split_every=thresh), 2 * 3)
+    assert_max_deps(x.sum(axis=(), split_every=thresh), 1)
     assert_max_deps(x.sum(axis=0, split_every=thresh), 2)
     assert_max_deps(x.sum(axis=1, split_every=thresh), 3)
     assert_max_deps(x.sum(split_every=20), 20, False)
+    assert_max_deps(x.sum(axis=(), split_every=20), 1)
     assert_max_deps(x.sum(axis=0, split_every=20), 4)
     assert_max_deps(x.sum(axis=1, split_every=20), 6)
 
@@ -442,6 +469,7 @@ def test_tree_reduce_depth():
     x = da.from_array(np.arange(11 * 22 * 29).reshape((11, 22, 29)), chunks=(3, 4, 5))
     thresh = {0: 2, 1: 3, 2: 4}
     assert_max_deps(x.sum(split_every=thresh), 2 * 3 * 4)
+    assert_max_deps(x.sum(axis=(), split_every=thresh), 1)
     assert_max_deps(x.sum(axis=0, split_every=thresh), 2)
     assert_max_deps(x.sum(axis=1, split_every=thresh), 3)
     assert_max_deps(x.sum(axis=2, split_every=thresh), 4)
@@ -449,6 +477,7 @@ def test_tree_reduce_depth():
     assert_max_deps(x.sum(axis=(0, 2), split_every=thresh), 2 * 4)
     assert_max_deps(x.sum(axis=(1, 2), split_every=thresh), 3 * 4)
     assert_max_deps(x.sum(split_every=20), 20, False)
+    assert_max_deps(x.sum(axis=(), split_every=20), 1)
     assert_max_deps(x.sum(axis=0, split_every=20), 4)
     assert_max_deps(x.sum(axis=1, split_every=20), 6)
     assert_max_deps(x.sum(axis=2, split_every=20), 6)
@@ -464,6 +493,7 @@ def test_tree_reduce_set_options():
     x = da.from_array(np.arange(242).reshape((11, 22)), chunks=(3, 4))
     with config.set(split_every={0: 2, 1: 3}):
         assert_max_deps(x.sum(), 2 * 3)
+        assert_max_deps(x.sum(axis=()), 1)
         assert_max_deps(x.sum(axis=0), 2)
 
 
@@ -601,6 +631,9 @@ def test_regres_3940(func):
     assert func(a).name != func(a + 1).name
     assert func(a, axis=0).name != func(a).name
     assert func(a, axis=0).name != func(a, axis=1).name
+    if func not in {da.cumsum, da.cumprod, da.argmin, da.argmax}:
+        assert func(a, axis=()).name != func(a).name
+        assert func(a, axis=()).name != func(a, axis=0).name
 
 
 def test_trace():