Repository: pydata/xarray

Issue:
__array_ufunc__ for xarray
Now that `__array_ufunc__` has appeared in NumPy 1.13, we should be able to resolve a long-standing annoyance with xarray: the inability to use NumPy ufuncs like `np.sin()` directly on all xarray objects, such as `xarray.Dataset`.

I believe this could be straightforwardly accomplished by wrapping `xarray.core.computation.apply_ufunc`.

Some care should be taken to ensure that properly defer to other array types, as suggested [in the NEP](https://docs.scipy.org/doc/numpy-1.13.0/neps/ufunc-overrides.html). This also could be a good time to resolve some of the xarray's broader consistency issues when interacting with and wrapping other array-like types.

Discussion:
dopplershift: This looks awesome. Thoughts on where you think units fits in here?
shoyer: @dopplershift  see https://github.com/pydata/xarray/issues/1938. Units should be able to make use of the same machinery as sparse arrays (`__array_ufunc__` and multipledispatch), but xarray itself implementing `__array_ufunc__` is not particularly useful, because you still can't put your own custom array types inside xarray objects.
dopplershift: At this point I'd be happy to have hooks that let me intercept/wrap ufunc operations, though I guess that's what #1938 is supporting in a more systematic way.
shoyer: If you try to put a pint array into xarray.DataArray right now, I'm pretty sure it will get cast into a NumPy array.
dopplershift: Right. But such hooks would be sufficient to properly maintain the `units` attribute on a `DataArray` and check whether math made sense. This could use `pint` under the covers.

Patch:
diff --git a/.stickler.yml b/.stickler.yml
index db8f5f25..79d8b7fb 100644
--- a/.stickler.yml
+++ b/.stickler.yml
@@ -6,7 +6,6 @@ linters:
     # stickler doesn't support 'exclude' for flake8 properly, so we disable it
     # below with files.ignore:
     # https://github.com/markstory/lint-review/issues/184
-  py3k:
 files:
   ignore:
     - doc/**/*.py
diff --git a/asv_bench/benchmarks/rolling.py b/asv_bench/benchmarks/rolling.py
index 79d06019..52814ad3 100644
--- a/asv_bench/benchmarks/rolling.py
+++ b/asv_bench/benchmarks/rolling.py
@@ -1,9 +1,8 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
+from __future__ import absolute_import, division, print_function
 
 import numpy as np
 import pandas as pd
+
 import xarray as xr
 
 from . import parameterized, randn, requires_dask
diff --git a/doc/api.rst b/doc/api.rst
index 1814b874..5f0d9d39 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -358,6 +358,13 @@ Reshaping and reorganizing
 Universal functions
 ===================
 
+.. warning::
+
+   With recent versions of numpy, dask and xarray, NumPy ufuncs are now
+   supported directly on all xarray and dask objects. This obliviates the need
+   for the ``xarray.ufuncs`` module, which should not be used for new code
+   unless compatibility with versions of NumPy prior to v1.13 is required.
+
 This functions are copied from NumPy, but extended to work on NumPy arrays,
 dask arrays and all xarray objects. You can find them in the ``xarray.ufuncs``
 module:
diff --git a/doc/computation.rst b/doc/computation.rst
index bd0343b2..589df8ea 100644
--- a/doc/computation.rst
+++ b/doc/computation.rst
@@ -341,21 +341,15 @@ Datasets support most of the same methods found on data arrays:
     ds.mean(dim='x')
     abs(ds)
 
-Unfortunately, we currently do not support NumPy ufuncs for datasets [1]_.
-:py:meth:`~xarray.Dataset.apply` works around this
-limitation, by applying the given function to each variable in the dataset:
+Datasets also support NumPy ufuncs (requires NumPy v1.13 or newer), or
+alternatively you can use :py:meth:`~xarray.Dataset.apply` to apply a function
+to each variable in a dataset:
 
 .. ipython:: python
 
+    np.sin(ds)
     ds.apply(np.sin)
 
-You can also use the wrapped functions in the ``xarray.ufuncs`` module:
-
-.. ipython:: python
-
-    import xarray.ufuncs as xu
-    xu.sin(ds)
-
 Datasets also use looping over variables for *broadcasting* in binary
 arithmetic. You can do arithmetic between any ``DataArray`` and a dataset:
 
@@ -373,10 +367,6 @@ Arithmetic between two datasets matches data variables of the same name:
 Similarly to index based alignment, the result has the intersection of all
 matching data variables.
 
-.. [1] This was previously due to a limitation of NumPy, but with NumPy 1.13
-       we should be able to support this by leveraging ``__array_ufunc__``
-       (:issue:`1617`).
-
 .. _comput.wrapping-custom:
 
 Wrapping custom computation
diff --git a/doc/gallery/control_plot_colorbar.py b/doc/gallery/control_plot_colorbar.py
index a09d825f..5802a57c 100644
--- a/doc/gallery/control_plot_colorbar.py
+++ b/doc/gallery/control_plot_colorbar.py
@@ -7,9 +7,10 @@ Control the plot's colorbar
 Use ``cbar_kwargs`` keyword to specify the number of ticks.
 The ``spacing`` kwarg can be used to draw proportional ticks.
 """
-import xarray as xr
 import matplotlib.pyplot as plt
 
+import xarray as xr
+
 # Load the data
 air_temp = xr.tutorial.load_dataset('air_temperature')
 air2d = air_temp.air.isel(time=500)
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index e60d9834..947d22a9 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -32,27 +32,53 @@ v0.10.2 (unreleased)
 
 The minor release includes a number of bug-fixes and backwards compatible enhancements.
 
+Backwards incompatible changes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- The addition of ``__array_ufunc__`` for xarray objects (see below) means that
+  NumPy `ufunc methods`_ (e.g., ``np.add.reduce``) that previously worked on
+  ``xarray.DataArray`` objects by converting them into NumPy arrays will now
+  raise ``NotImplementedError`` instead. In all cases, the work-around is
+  simple: convert your objects explicitly into NumPy arrays before calling the
+  ufunc (e.g., with ``.values``).
+
+.. _ufunc methods: https://docs.scipy.org/doc/numpy/reference/ufuncs.html#methods
+
 Documentation
 ~~~~~~~~~~~~~
 
 Enhancements
 ~~~~~~~~~~~~
 
-- Addition of :py:func:`~xarray.dot`, equivalent to ``np.einsum``.
+- Added :py:func:`~xarray.dot`, equivalent to :py:func:`np.einsum`.
   Also, :py:func:`~xarray.DataArray.dot` now supports ``dims`` option,
   which specifies the dimensions to sum over.
   (:issue:`1951`)
+  By `Keisuke Fujii <https://github.com/fujiisoup>`_.
+
 - Support for writing xarray datasets to netCDF files (netcdf4 backend only)
   when using the `dask.distributed <https://distributed.readthedocs.io>`_
   scheduler (:issue:`1464`).
   By `Joe Hamman <https://github.com/jhamman>`_.
 
-
-- Fixed to_netcdf when using dask distributed
 - Support lazy vectorized-indexing. After this change, flexible indexing such
   as orthogonal/vectorized indexing, becomes possible for all the backend
   arrays. Also, lazy ``transpose`` is now also supported. (:issue:`1897`)
   By `Keisuke Fujii <https://github.com/fujiisoup>`_.
+
+- Implemented NumPy's ``__array_ufunc__`` protocol for all xarray objects
+  (:issue:`1617`). This enables using NumPy ufuncs directly on
+  ``xarray.Dataset`` objects with recent versions of NumPy (v1.13 and newer):
+
+  .. ipython:: python
+
+    ds = xr.Dataset({'a': 1})
+    np.sin(ds)
+
+  This obliviates the need for the ``xarray.ufuncs`` module, which will be
+  deprecated in the future when xarray drops support for older versions of
+  NumPy. By `Stephan Hoyer <https://github.com/shoyer>`_.
+
 - Improve :py:func:`~xarray.DataArray.rolling` logic.
   :py:func:`~xarray.DataArrayRolling` object now supports
   :py:func:`~xarray.DataArrayRolling.construct` method that returns a view
diff --git a/xarray/core/arithmetic.py b/xarray/core/arithmetic.py
new file mode 100644
index 00000000..3988d1ab
--- /dev/null
+++ b/xarray/core/arithmetic.py
@@ -0,0 +1,71 @@
+"""Base classes implementing arithmetic for xarray objects."""
+from __future__ import absolute_import, division, print_function
+
+import numbers
+
+import numpy as np
+
+from .options import OPTIONS
+from .pycompat import bytes_type, dask_array_type, unicode_type
+from .utils import not_implemented
+
+
+class SupportsArithmetic(object):
+    """Base class for xarray types that support arithmetic.
+
+    Used by Dataset, DataArray, Variable and GroupBy.
+    """
+
+    # TODO: implement special methods for arithmetic here rather than injecting
+    # them in xarray/core/ops.py. Ideally, do so by inheriting from
+    # numpy.lib.mixins.NDArrayOperatorsMixin.
+
+    # TODO: allow extending this with some sort of registration system
+    _HANDLED_TYPES = (np.ndarray, np.generic, numbers.Number, bytes_type,
+                      unicode_type) + dask_array_type
+
+    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
+        from .computation import apply_ufunc
+
+        # See the docstring example for numpy.lib.mixins.NDArrayOperatorsMixin.
+        out = kwargs.get('out', ())
+        for x in inputs + out:
+            if not isinstance(x, self._HANDLED_TYPES + (SupportsArithmetic,)):
+                return NotImplemented
+
+        if ufunc.signature is not None:
+            raise NotImplementedError(
+                '{} not supported: xarray objects do not directly implement '
+                'generalized ufuncs. Instead, use xarray.apply_ufunc.'
+                .format(ufunc))
+
+        if method != '__call__':
+            # TODO: support other methods, e.g., reduce and accumulate.
+            raise NotImplementedError(
+                '{} method for ufunc {} is not implemented on xarray objects, '
+                'which currently only support the __call__ method.'
+                .format(method, ufunc))
+
+        if any(isinstance(o, SupportsArithmetic) for o in out):
+            # TODO: implement this with logic like _inplace_binary_op. This
+            # will be necessary to use NDArrayOperatorsMixin.
+            raise NotImplementedError(
+                'xarray objects are not yet supported in the `out` argument '
+                'for ufuncs.')
+
+        join = dataset_join = OPTIONS['arithmetic_join']
+
+        return apply_ufunc(ufunc, *inputs,
+                           input_core_dims=((),) * ufunc.nin,
+                           output_core_dims=((),) * ufunc.nout,
+                           join=join,
+                           dataset_join=dataset_join,
+                           dataset_fill_value=np.nan,
+                           kwargs=kwargs,
+                           dask='allowed')
+
+    # this has no runtime function - these are listed so IDEs know these
+    # methods are defined and don't warn on these operations
+    __lt__ = __le__ = __ge__ = __gt__ = __add__ = __sub__ = __mul__ = \
+        __truediv__ = __floordiv__ = __mod__ = __pow__ = __and__ = __xor__ = \
+        __or__ = __div__ = __eq__ = __ne__ = not_implemented
diff --git a/xarray/core/common.py b/xarray/core/common.py
index 85ac0bf9..337c1c51 100644
--- a/xarray/core/common.py
+++ b/xarray/core/common.py
@@ -6,8 +6,9 @@ import numpy as np
 import pandas as pd
 
 from . import dtypes, formatting, ops
+from .arithmetic import SupportsArithmetic
 from .pycompat import OrderedDict, basestring, dask_array_type, suppress
-from .utils import Frozen, SortedKeysDict, not_implemented
+from .utils import Frozen, SortedKeysDict
 
 
 class ImplementsArrayReduce(object):
@@ -235,7 +236,7 @@ def get_squeeze_dims(xarray_obj, dim, axis=None):
     return dim
 
 
-class BaseDataObject(AttrAccessMixin):
+class DataWithCoords(SupportsArithmetic, AttrAccessMixin):
     """Shared base class for Dataset and DataArray."""
 
     def squeeze(self, dim=None, drop=False, axis=None):
@@ -749,12 +750,6 @@ class BaseDataObject(AttrAccessMixin):
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
 
-    # this has no runtime function - these are listed so IDEs know these
-    # methods are defined and don't warn on these operations
-    __lt__ = __le__ = __ge__ = __gt__ = __add__ = __sub__ = __mul__ = \
-        __truediv__ = __floordiv__ = __mod__ = __pow__ = __and__ = __xor__ = \
-        __or__ = __div__ = __eq__ = __ne__ = not_implemented
-
 
 def full_like(other, fill_value, dtype=None):
     """Return a new object with the same shape and type as a given object.
diff --git a/xarray/core/dask_array_ops.py b/xarray/core/dask_array_ops.py
index 5524efb4..4bd3766c 100644
--- a/xarray/core/dask_array_ops.py
+++ b/xarray/core/dask_array_ops.py
@@ -1,8 +1,7 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
+from __future__ import absolute_import, division, print_function
 
 import numpy as np
+
 from . import nputils
 
 try:
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index 3c022752..efa8b7d7 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -10,7 +10,7 @@ from . import computation, groupby, indexing, ops, resample, rolling, utils
 from ..plot.plot import _PlotMethods
 from .accessors import DatetimeAccessor
 from .alignment import align, reindex_like_indexers
-from .common import AbstractArray, BaseDataObject
+from .common import AbstractArray, DataWithCoords
 from .coordinates import (
     DataArrayCoordinates, Indexes, LevelCoordinatesSource,
     assert_coordinate_consistent, remap_label_indexers)
@@ -117,7 +117,7 @@ class _LocIndexer(object):
 _THIS_ARRAY = utils.ReprObject('<this-array>')
 
 
-class DataArray(AbstractArray, BaseDataObject):
+class DataArray(AbstractArray, DataWithCoords):
     """N-dimensional array with labeled coordinates and dimensions.
 
     DataArray provides a wrapper around numpy ndarrays that uses labeled
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 2a2c4e38..03bc8fd6 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -17,7 +17,7 @@ from . import (
     rolling, utils)
 from .. import conventions
 from .alignment import align
-from .common import BaseDataObject, ImplementsDatasetReduce
+from .common import DataWithCoords, ImplementsDatasetReduce
 from .coordinates import (
     DatasetCoordinates, Indexes, LevelCoordinatesSource,
     assert_coordinate_consistent, remap_label_indexers)
@@ -298,7 +298,7 @@ class _LocIndexer(object):
         return self.dataset.sel(**key)
 
 
-class Dataset(Mapping, ImplementsDatasetReduce, BaseDataObject,
+class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
               formatting.ReprMixin):
     """A multi-dimensional, in memory, array database.
 
@@ -2362,7 +2362,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, BaseDataObject,
             array = self._variables[k]
             if dim in array.dims:
                 dims = [d for d in array.dims if d != dim]
-                count += array.count(dims)
+                count += np.asarray(array.count(dims))
                 size += np.prod([self.dims[d] for d in dims])
 
         if thresh is not None:
diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py
index b722a01e..7068f8e6 100644
--- a/xarray/core/groupby.py
+++ b/xarray/core/groupby.py
@@ -6,6 +6,7 @@ import numpy as np
 import pandas as pd
 
 from . import dtypes, duck_array_ops, nputils, ops
+from .arithmetic import SupportsArithmetic
 from .combine import concat
 from .common import ImplementsArrayReduce, ImplementsDatasetReduce
 from .pycompat import integer_types, range, zip
@@ -151,7 +152,7 @@ def _unique_and_monotonic(group):
         return index.is_unique and index.is_monotonic
 
 
-class GroupBy(object):
+class GroupBy(SupportsArithmetic):
     """A object that implements the split-apply-combine pattern.
 
     Modeled after `pandas.GroupBy`. The `GroupBy` object can be iterated over
diff --git a/xarray/core/npcompat.py b/xarray/core/npcompat.py
index 8f1f3821..af722924 100644
--- a/xarray/core/npcompat.py
+++ b/xarray/core/npcompat.py
@@ -1,8 +1,8 @@
 from __future__ import absolute_import, division, print_function
 
-import numpy as np
 from distutils.version import LooseVersion
 
+import numpy as np
 
 if LooseVersion(np.__version__) >= LooseVersion('1.12'):
     as_strided = np.lib.stride_tricks.as_strided
diff --git a/xarray/core/variable.py b/xarray/core/variable.py
index 66a4e781..5ec85c15 100644
--- a/xarray/core/variable.py
+++ b/xarray/core/variable.py
@@ -10,7 +10,8 @@ import pandas as pd
 
 import xarray as xr  # only for Dataset and DataArray
 
-from . import common, dtypes, duck_array_ops, indexing, nputils, ops, utils
+from . import (
+    arithmetic, common, dtypes, duck_array_ops, indexing, nputils, ops, utils,)
 from .indexing import (
     BasicIndexer, OuterIndexer, PandasIndexAdapter, VectorizedIndexer,
     as_indexable)
@@ -216,8 +217,8 @@ def _as_array_or_item(data):
     return data
 
 
-class Variable(common.AbstractArray, utils.NdimSizeLenMixin):
-
+class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
+               utils.NdimSizeLenMixin):
     """A netcdf-like variable consisting of dimensions, data and attributes
     which describe a single Array. A single Variable object is not fully
     described outside the context of its parent Dataset (if you want such a
diff --git a/xarray/ufuncs.py b/xarray/ufuncs.py
index f7f17aed..d9fcc1ea 100644
--- a/xarray/ufuncs.py
+++ b/xarray/ufuncs.py
@@ -15,6 +15,8 @@ hopefully no longer be necessary.
 """
 from __future__ import absolute_import, division, print_function
 
+import warnings as _warnings
+
 import numpy as _np
 
 from .core.dataarray import DataArray as _DataArray
@@ -42,6 +44,11 @@ class _UFuncDispatcher(object):
         self._name = name
 
     def __call__(self, *args, **kwargs):
+        _warnings.warn(
+            'xarray.ufuncs will be deprecated when xarray no longer supports '
+            'versions of numpy older than v1.13. Instead, use numpy ufuncs '
+            'directly.', PendingDeprecationWarning, stacklevel=2)
+
         new_args = args
         f = _dask_or_eager_func(self._name, n_array_args=len(args))
         if len(args) > 2 or len(args) == 0:


Test patch:
diff --git a/xarray/tests/test_nputils.py b/xarray/tests/test_nputils.py
index d3ef02a0..d3ad87d0 100644
--- a/xarray/tests/test_nputils.py
+++ b/xarray/tests/test_nputils.py
@@ -1,8 +1,8 @@
 import numpy as np
 from numpy.testing import assert_array_equal
 
-from xarray.core.nputils import (NumpyVIndexAdapter, _is_contiguous,
-                                 rolling_window)
+from xarray.core.nputils import (
+    NumpyVIndexAdapter, _is_contiguous, rolling_window)
 
 
 def test_is_contiguous():
diff --git a/xarray/tests/test_ufuncs.py b/xarray/tests/test_ufuncs.py
index 64a24695..91ec1142 100644
--- a/xarray/tests/test_ufuncs.py
+++ b/xarray/tests/test_ufuncs.py
@@ -1,67 +1,185 @@
 from __future__ import absolute_import, division, print_function
 
+from distutils.version import LooseVersion
 import pickle
 
 import numpy as np
+import pytest
 
 import xarray as xr
 import xarray.ufuncs as xu
 
-from . import TestCase, assert_array_equal, assert_identical, raises_regex
-
-
-class TestOps(TestCase):
-    def assert_identical(self, a, b):
-        assert type(a) is type(b) or (float(a) == float(b))  # noqa
-        try:
-            assert a.identical(b), (a, b)
-        except AttributeError:
-            assert_array_equal(a, b)
-
-    def test_unary(self):
-        args = [0,
-                np.zeros(2),
-                xr.Variable(['x'], [0, 0]),
-                xr.DataArray([0, 0], dims='x'),
-                xr.Dataset({'y': ('x', [0, 0])})]
-        for a in args:
-            self.assert_identical(a + 1, xu.cos(a))
-
-    def test_binary(self):
-        args = [0,
-                np.zeros(2),
-                xr.Variable(['x'], [0, 0]),
-                xr.DataArray([0, 0], dims='x'),
-                xr.Dataset({'y': ('x', [0, 0])})]
-        for n, t1 in enumerate(args):
-            for t2 in args[n:]:
-                self.assert_identical(t2 + 1, xu.maximum(t1, t2 + 1))
-                self.assert_identical(t2 + 1, xu.maximum(t2, t1 + 1))
-                self.assert_identical(t2 + 1, xu.maximum(t1 + 1, t2))
-                self.assert_identical(t2 + 1, xu.maximum(t2 + 1, t1))
-
-    def test_groupby(self):
-        ds = xr.Dataset({'a': ('x', [0, 0, 0])}, {'c': ('x', [0, 0, 1])})
-        ds_grouped = ds.groupby('c')
-        group_mean = ds_grouped.mean('x')
-        arr_grouped = ds['a'].groupby('c')
-
-        assert_identical(ds, xu.maximum(ds_grouped, group_mean))
-        assert_identical(ds, xu.maximum(group_mean, ds_grouped))
-
-        assert_identical(ds, xu.maximum(arr_grouped, group_mean))
-        assert_identical(ds, xu.maximum(group_mean, arr_grouped))
-
-        assert_identical(ds, xu.maximum(ds_grouped, group_mean['a']))
-        assert_identical(ds, xu.maximum(group_mean['a'], ds_grouped))
-
-        assert_identical(ds.a, xu.maximum(arr_grouped, group_mean.a))
-        assert_identical(ds.a, xu.maximum(group_mean.a, arr_grouped))
-
-        with raises_regex(TypeError, 'only support binary ops'):
-            xu.maximum(ds.a.variable, ds_grouped)
-
-    def test_pickle(self):
-        a = 1.0
-        cos_pickled = pickle.loads(pickle.dumps(xu.cos))
-        self.assert_identical(cos_pickled(a), xu.cos(a))
+from . import (
+    assert_array_equal, assert_identical as assert_identical_, mock,
+    raises_regex,
+)
+
+
+requires_numpy113 = pytest.mark.skipif(LooseVersion(np.__version__) < '1.13',
+                                       reason='numpy 1.13 or newer required')
+
+
+def assert_identical(a, b):
+    assert type(a) is type(b) or (float(a) == float(b))  # noqa
+    if isinstance(a, (xr.DataArray, xr.Dataset, xr.Variable)):
+        assert_identical_(a, b)
+    else:
+        assert_array_equal(a, b)
+
+
+@requires_numpy113
+def test_unary():
+    args = [0,
+            np.zeros(2),
+            xr.Variable(['x'], [0, 0]),
+            xr.DataArray([0, 0], dims='x'),
+            xr.Dataset({'y': ('x', [0, 0])})]
+    for a in args:
+        assert_identical(a + 1, np.cos(a))
+
+
+@requires_numpy113
+def test_binary():
+    args = [0,
+            np.zeros(2),
+            xr.Variable(['x'], [0, 0]),
+            xr.DataArray([0, 0], dims='x'),
+            xr.Dataset({'y': ('x', [0, 0])})]
+    for n, t1 in enumerate(args):
+        for t2 in args[n:]:
+            assert_identical(t2 + 1, np.maximum(t1, t2 + 1))
+            assert_identical(t2 + 1, np.maximum(t2, t1 + 1))
+            assert_identical(t2 + 1, np.maximum(t1 + 1, t2))
+            assert_identical(t2 + 1, np.maximum(t2 + 1, t1))
+
+
+@requires_numpy113
+def test_binary_out():
+    args = [1,
+            np.ones(2),
+            xr.Variable(['x'], [1, 1]),
+            xr.DataArray([1, 1], dims='x'),
+            xr.Dataset({'y': ('x', [1, 1])})]
+    for arg in args:
+        actual_mantissa, actual_exponent = np.frexp(arg)
+        assert_identical(actual_mantissa, 0.5 * arg)
+        assert_identical(actual_exponent, arg)
+
+
+@requires_numpy113
+def test_groupby():
+    ds = xr.Dataset({'a': ('x', [0, 0, 0])}, {'c': ('x', [0, 0, 1])})
+    ds_grouped = ds.groupby('c')
+    group_mean = ds_grouped.mean('x')
+    arr_grouped = ds['a'].groupby('c')
+
+    assert_identical(ds, np.maximum(ds_grouped, group_mean))
+    assert_identical(ds, np.maximum(group_mean, ds_grouped))
+
+    assert_identical(ds, np.maximum(arr_grouped, group_mean))
+    assert_identical(ds, np.maximum(group_mean, arr_grouped))
+
+    assert_identical(ds, np.maximum(ds_grouped, group_mean['a']))
+    assert_identical(ds, np.maximum(group_mean['a'], ds_grouped))
+
+    assert_identical(ds.a, np.maximum(arr_grouped, group_mean.a))
+    assert_identical(ds.a, np.maximum(group_mean.a, arr_grouped))
+
+    with raises_regex(ValueError, 'mismatched lengths for dimension'):
+        np.maximum(ds.a.variable, ds_grouped)
+
+
+@requires_numpy113
+def test_alignment():
+    ds1 = xr.Dataset({'a': ('x', [1, 2])}, {'x': [0, 1]})
+    ds2 = xr.Dataset({'a': ('x', [2, 3]), 'b': 4}, {'x': [1, 2]})
+
+    actual = np.add(ds1, ds2)
+    expected = xr.Dataset({'a': ('x', [4])}, {'x': [1]})
+    assert_identical_(actual, expected)
+
+    with xr.set_options(arithmetic_join='outer'):
+        actual = np.add(ds1, ds2)
+        expected = xr.Dataset({'a': ('x', [np.nan, 4, np.nan]), 'b': np.nan},
+                              coords={'x': [0, 1, 2]})
+        assert_identical_(actual, expected)
+
+
+@requires_numpy113
+def test_kwargs():
+    x = xr.DataArray(0)
+    result = np.add(x, 1, dtype=np.float64)
+    assert result.dtype == np.float64
+
+
+@requires_numpy113
+def test_xarray_defers_to_unrecognized_type():
+
+    class Other(object):
+        def __array_ufunc__(self, *args, **kwargs):
+            return 'other'
+
+    xarray_obj = xr.DataArray([1, 2, 3])
+    other = Other()
+    assert np.maximum(xarray_obj, other) == 'other'
+    assert np.sin(xarray_obj, out=other) == 'other'
+
+
+@requires_numpy113
+def test_xarray_handles_dask():
+    da = pytest.importorskip('dask.array')
+    x = xr.DataArray(np.ones((2, 2)), dims=['x', 'y'])
+    y = da.ones((2, 2), chunks=(2, 2))
+    result = np.add(x, y)
+    assert result.chunks == ((2,), (2,))
+    assert isinstance(result, xr.DataArray)
+
+
+@requires_numpy113
+def test_dask_defers_to_xarray():
+    da = pytest.importorskip('dask.array')
+    x = xr.DataArray(np.ones((2, 2)), dims=['x', 'y'])
+    y = da.ones((2, 2), chunks=(2, 2))
+    result = np.add(y, x)
+    assert result.chunks == ((2,), (2,))
+    assert isinstance(result, xr.DataArray)
+
+
+@requires_numpy113
+def test_gufunc_methods():
+    xarray_obj = xr.DataArray([1, 2, 3])
+    with raises_regex(NotImplementedError, 'reduce method'):
+        np.add.reduce(xarray_obj, 1)
+
+
+@requires_numpy113
+def test_out():
+    xarray_obj = xr.DataArray([1, 2, 3])
+
+    # xarray out arguments should raise
+    with raises_regex(NotImplementedError, '`out` argument'):
+        np.add(xarray_obj, 1, out=xarray_obj)
+
+    # but non-xarray should be OK
+    other = np.zeros((3,))
+    np.add(other, xarray_obj, out=other)
+    assert_identical(other, np.array([1, 2, 3]))
+
+
+@requires_numpy113
+def test_gufuncs():
+    xarray_obj = xr.DataArray([1, 2, 3])
+    fake_gufunc = mock.Mock(signature='(n)->()', autospec=np.sin)
+    with raises_regex(NotImplementedError, 'generalized ufuncs'):
+        xarray_obj.__array_ufunc__(fake_gufunc, '__call__', xarray_obj)
+
+
+def test_xarray_ufuncs_deprecation():
+    with pytest.warns(PendingDeprecationWarning, match='xarray.ufuncs'):
+        xu.cos(xr.DataArray([0, 1]))
+
+
+def test_xarray_ufuncs_pickle():
+    a = 1.0
+    cos_pickled = pickle.loads(pickle.dumps(xu.cos))
+    assert_identical(cos_pickled(a), xu.cos(a))