Repository: dask/dask

Issue:
std() fails on masked array of all masked elements
```python
In [1]: import numpy as np                                                                                                                                                                                 

In [2]: import dask.array as da                                                                                                                                                                            

In [3]: all_masked = np.ma.masked_array([1, 2], mask=[True, True])                                                                                                                                         

In [4]: all_masked.std()                                                                                                                                                                                   
Out[4]: masked

In [5]: dask_all_masked = da.ma.masked_array([1, 2], mask=[True, True])                                                                                                                                    

In [6]: dask_all_masked.std().compute()                                                                                                                                                                    
---------------------------------------------------------------------------
MaskError                                 Traceback (most recent call last)
<ipython-input-6-dd8d98362ed2> in <module>
----> 1 dask_all_masked.std().compute()

~/dl/forks/dask/dask/base.py in compute(self, **kwargs)
    173         dask.base.compute
    174         """
--> 175         (result,) = compute(self, traverse=False, **kwargs)
    176         return result
    177 

~/dl/forks/dask/dask/base.py in compute(*args, **kwargs)
    444     keys = [x.__dask_keys__() for x in collections]
    445     postcomputes = [x.__dask_postcompute__() for x in collections]
--> 446     results = schedule(dsk, keys, **kwargs)
    447     return repack([f(r, *a) for r, (f, a) in zip(results, postcomputes)])
    448 

~/dl/forks/dask/dask/threaded.py in get(dsk, result, cache, num_workers, pool, **kwargs)
     80         get_id=_thread_get_id,
     81         pack_exception=pack_exception,
---> 82         **kwargs
     83     )
     84 

~/dl/forks/dask/dask/local.py in get_async(apply_async, num_workers, dsk, result, cache, get_id, rerun_exceptions_locally, pack_exception, raise_exception, callbacks, dumps, loads, **kwargs)
    489                         _execute_task(task, data)  # Re-execute locally
    490                     else:
--> 491                         raise_exception(exc, tb)
    492                 res, worker_id = loads(res_info)
    493                 state["cache"][key] = res

~/dl/forks/dask/dask/compatibility.py in reraise(exc, tb)
    128         if exc.__traceback__ is not tb:
    129             raise exc.with_traceback(tb)
--> 130         raise exc
    131 
    132     import pickle as cPickle

~/dl/forks/dask/dask/local.py in execute_task(key, task_info, dumps, loads, get_id, pack_exception)
    231     try:
    232         task, data = loads(task_info)
--> 233         result = _execute_task(task, data)
    234         id = get_id()
    235         result = dumps((result, id))

~/dl/forks/dask/dask/core.py in _execute_task(arg, cache, dsk)
    116     elif istask(arg):
    117         func, args = arg[0], arg[1:]
--> 118         args2 = [_execute_task(a, cache) for a in args]
    119         return func(*args2)
    120     elif not ishashable(arg):

~/dl/forks/dask/dask/core.py in <listcomp>(.0)
    116     elif istask(arg):
    117         func, args = arg[0], arg[1:]
--> 118         args2 = [_execute_task(a, cache) for a in args]
    119         return func(*args2)
    120     elif not ishashable(arg):

~/dl/forks/dask/dask/core.py in _execute_task(arg, cache, dsk)
    117         func, args = arg[0], arg[1:]
    118         args2 = [_execute_task(a, cache) for a in args]
--> 119         return func(*args2)
    120     elif not ishashable(arg):
    121         return arg

~/dl/forks/dask/dask/array/reductions.py in moment_agg(pairs, order, ddof, dtype, sum, axis, computing_meta, **kwargs)
    731             denominator = np.nan
    732     else:
--> 733         denominator[denominator < 0] = np.nan
    734 
    735     return divide(M, denominator, dtype=dtype)

~/.local/share/virtualenvs/dask-dyglJSq3/lib/python3.7/site-packages/numpy/ma/core.py in __setitem__(self, indx, value)
   3288         """
   3289         if self is masked:
-> 3290             raise MaskError('Cannot alter the masked element.')
   3291         _data = self._data
   3292         _mask = self._mask

MaskError: Cannot alter the masked element.
```


Discussion:
Thank you for the excellent minimal bug report @gjoseph92 !

Do you have any thoughts on how this might be resolved?  The array operation that you've highlighted here is just a normal Numpy array.  

```python
denominator[denominator < 0] = np.nan
```

I wonder if there is a simple solution for someone who is familiar with masked arrays.  

Resolving patch:
diff --git a/dask/array/reductions.py b/dask/array/reductions.py
--- a/dask/array/reductions.py
+++ b/dask/array/reductions.py
@@ -15,7 +15,6 @@
 from .blockwise import blockwise
 from ..blockwise import lol_tuples
 from .creation import arange, diagonal
-from .ufunc import sqrt
 from .utils import full_like_safe, validate_axis, compute_meta, is_arraylike
 from .wrap import zeros, ones
 from .numpy_compat import ma_divide, divide as np_divide
@@ -727,7 +726,7 @@ def moment_agg(
     if isinstance(denominator, Number):
         if denominator < 0:
             denominator = np.nan
-    else:
+    elif denominator is not np.ma.masked:
         denominator[denominator < 0] = np.nan
 
     return divide(M, denominator, dtype=dtype)
@@ -812,9 +811,30 @@ def nanvar(
     nanvar = derived_from(np)(nanvar)
 
 
+def _sqrt(a):
+    o = np.sqrt(a)
+    if isinstance(o, np.ma.masked_array) and not o.shape and o.mask.all():
+        return np.ma.masked
+    return o
+
+
+def safe_sqrt(a):
+    """A version of sqrt that properly handles scalar masked arrays.
+
+    To mimic ``np.ma`` reductions, we need to convert scalar masked arrays that
+    have an active mask to the ``np.ma.masked`` singleton. This is properly
+    handled automatically for reduction code, but not for ufuncs. We implement
+    a simple version here, since calling `np.ma.sqrt` everywhere is
+    significantly more expensive.
+    """
+    if hasattr(a, "_elemwise"):
+        return a._elemwise(_sqrt, a)
+    return _sqrt(a)
+
+
 @derived_from(np)
 def std(a, axis=None, dtype=None, keepdims=False, ddof=0, split_every=None, out=None):
-    result = sqrt(
+    result = safe_sqrt(
         var(
             a,
             axis=axis,
@@ -834,7 +854,7 @@ def std(a, axis=None, dtype=None, keepdims=False, ddof=0, split_every=None, out=
 def nanstd(
     a, axis=None, dtype=None, keepdims=False, ddof=0, split_every=None, out=None
 ):
-    result = sqrt(
+    result = safe_sqrt(
         nanvar(
             a,
             axis=axis,


Test patch:
diff --git a/dask/array/tests/test_masked.py b/dask/array/tests/test_masked.py
--- a/dask/array/tests/test_masked.py
+++ b/dask/array/tests/test_masked.py
@@ -225,12 +225,15 @@ def test_filled():
 
 def assert_eq_ma(a, b):
     res = a.compute()
-    assert type(res) == type(b)
-    if hasattr(res, "mask"):
-        np.testing.assert_equal(res.mask, b.mask)
-        a = da.ma.filled(a)
-        b = np.ma.filled(b)
-    assert_eq(a, b, equal_nan=True)
+    if res is np.ma.masked:
+        assert res is b
+    else:
+        assert type(res) == type(b)
+        if hasattr(res, "mask"):
+            np.testing.assert_equal(res.mask, b.mask)
+            a = da.ma.filled(a)
+            b = np.ma.filled(b)
+        assert_eq(a, b, equal_nan=True)
 
 
 @pytest.mark.parametrize("dtype", ("i8", "f8"))
@@ -261,6 +264,20 @@ def test_reductions(dtype, reduction):
     )
 
 
+@pytest.mark.parametrize("dtype", ("i8", "f8"))
+@pytest.mark.parametrize(
+    "reduction", ["sum", "prod", "mean", "var", "std", "min", "max", "any", "all"]
+)
+def test_reductions_allmasked(dtype, reduction):
+    x = np.ma.masked_array([1, 2], mask=True)
+    dx = da.from_array(x, asarray=False)
+
+    dfunc = getattr(da, reduction)
+    func = getattr(np, reduction)
+
+    assert_eq_ma(dfunc(dx), func(x))
+
+
 @pytest.mark.parametrize("reduction", ["argmin", "argmax"])
 def test_arg_reductions(reduction):
     x = np.random.random((10, 10, 10))