diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+pynio.yml index 53aafb058e1..abfb55fc593 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+pynio.yml @@ -14,3 +14,4 @@ dependencies: - cyordereddict - pip: - coveralls + - quantities diff --git a/ci/requirements-py27-netcdf4-dev.yml b/ci/requirements-py27-netcdf4-dev.yml index a64782de235..14fe2bd3209 100644 --- a/ci/requirements-py27-netcdf4-dev.yml +++ b/ci/requirements-py27-netcdf4-dev.yml @@ -12,4 +12,5 @@ dependencies: - coveralls - pytest-cov - h5netcdf + - quantities - git+https://github.com/Unidata/netcdf4-python.git diff --git a/ci/requirements-py27-pydap.yml b/ci/requirements-py27-pydap.yml index 459f049c76a..160581acdc3 100644 --- a/ci/requirements-py27-pydap.yml +++ b/ci/requirements-py27-pydap.yml @@ -12,3 +12,4 @@ dependencies: - coveralls - pytest-cov - pydap + - quantities diff --git a/ci/requirements-py33.yml b/ci/requirements-py33.yml index 7ff08a5794f..98b089e6df5 100644 --- a/ci/requirements-py33.yml +++ b/ci/requirements-py33.yml @@ -6,3 +6,4 @@ dependencies: - pip: - coveralls - pytest-cov + - quantities diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index a49611751ca..e2ab1d0cc3c 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -7,3 +7,4 @@ dependencies: - pip: - coveralls - pytest-cov + - quantities diff --git a/ci/requirements-py35-dask-dev.yml b/ci/requirements-py35-dask-dev.yml index eab9ad21877..ed935294401 100644 --- a/ci/requirements-py35-dask-dev.yml +++ b/ci/requirements-py35-dask-dev.yml @@ -10,4 +10,5 @@ dependencies: - pip: - coveralls - pytest-cov + - quantities - git+https://github.com/blaze/dask.git diff --git a/ci/requirements-py35-pandas-dev.yml b/ci/requirements-py35-pandas-dev.yml index 74f8abe84f0..4a7f4979f52 100644 --- a/ci/requirements-py35-pandas-dev.yml +++ b/ci/requirements-py35-pandas-dev.yml @@ -11,4 +11,5 @@ dependencies: - coveralls - pytest-cov - dask + - quantities - git+https://github.com/pydata/pandas.git diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index 0f3b005ea6a..8c76279c060 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -15,3 +15,4 @@ dependencies: - coveralls - pytest-cov - h5netcdf + - quantities diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 6aafbcaab82..51eb5cf67c5 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -193,6 +193,19 @@ def _as_array_or_item(data): data = np.timedelta64(data, 'ns') return data +def _as_any_array_or_item(data): + """Return the given values as a numpy array subclass instance, or as an + individual item if it's a 0d datetime64 or timedelta64 array. + + The same caveats as for :py:meth:`_as_array_or_item` apply. + """ + data = np.asanyarray(data) + if data.ndim == 0: + if data.dtype.kind == 'M': + data = np.datetime64(data, 'ns') + elif data.dtype.kind == 'm': + data = np.timedelta64(data, 'ns') + return data class Variable(common.AbstractArray, common.SharedMethodsMixin, utils.NdimSizeLenMixin): @@ -267,7 +280,7 @@ def data(self): if isinstance(self._data, dask_array_type): return self._data else: - return self.values + return _as_any_array_or_item(self._data_cached()) @data.setter def data(self, data): diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py new file mode 100644 index 00000000000..1769a81fc57 --- /dev/null +++ b/xarray/test/test_quantities.py @@ -0,0 +1,106 @@ +""" test_quantities: Test storing and using instances of +:py:class:`quantities.Quantity` inside :py:class`xarray.DataArray`. + +It can be considered a stand-in for other :py:class:`numpy.ndarray` +subclasses, particularly other units implementations such as +astropy's. As preservation of subclasses is not a guaranteed feature of +`xarray`, some operations will discard subclasses. This test +also serves as documnetation which operations do preserve subclasses +and which don't. +""" + +import numpy as np +import pandas as pd + +from xarray import (align, broadcast, Dataset, DataArray, Variable) + +from xarray.test import (TestCase, unittest) + +try: + import quantities as pq + + has_quantities = True +except ImportError: + has_quantities = False + + +def requires_quantities(test): + return ( + test if has_quantities else + unittest.skip('requires python-quantities')(test) + ) + + +@requires_quantities +class TestWithQuantities(TestCase): + def setUp(self): + self.x = np.arange(10) * pq.A + self.y = np.arange(20) + self.xp = np.arange(10) * pq.J + self.v = np.arange(10 * 20).reshape(10, 20) * pq.V + self.da = DataArray(self.v, dims=['x', 'y'], + coords=dict(x=self.x, y=self.y, xp=(['x'], self.xp))) + + def assertEqualWUnits(self, a, b): + # DataArray's are supposed to preserve Quantity instances + # but they (currently?) do not expose their behaviour. + # We thus need to extract the contained subarray via .data + if isinstance(a, DataArray): + a = a.data + if isinstance(b, DataArray): + b = b.data + self.assertIsNotNone(getattr(a, 'units', None)) + self.assertIsNotNone(getattr(b, 'units', None)) + self.assertEqual(a.units, b.units) + np.testing.assert_allclose(a.magnitude, b.magnitude) + + def test_units_in_data_and_coords(self): + da = self.da + self.assertEqualWUnits(da.xp.data, self.xp) + self.assertEqualWUnits(da.data, self.v) + + def test_arithmetics(self): + x = self.x + y = self.y + v = self.v + da = self.da + + f = np.arange(10 * 20).reshape(10, 20) * pq.A + g = DataArray(f, dims=['x', 'y'], coords=dict(x=x, y=y)) + self.assertEqualWUnits(da * g, v * f) + + # swapped dimension order + f = np.arange(20 * 10).reshape(20, 10) * pq.V + g = DataArray(f, dims=['y', 'x'], coords=dict(x=x, y=y)) + self.assertEqualWUnits(da + g, v + f.T) + + # broadcasting + f = np.arange(10) * pq.m + g = DataArray(f, dims=['x'], coords=dict(x=x)) + self.assertEqualWUnits(da / g, v / f[:,None]) + + def test_unit_checking(self): + da = self.da + f = np.arange(10 * 20).reshape(10, 20) * pq.A + g = DataArray(f, dims=['x', 'y'], coords=dict(x=self.x, y=self.y)) + with self.assertRaisesRegexp(ValueError, + 'Unable to convert between units'): + da + g + + @unittest.expectedFailure + def test_units_in_indexes(self): + """ Test if units survive through xarray indexes. + + Indexes are borrowed from Pandas, and Pandas does not support units. + Therefore, we currently don't intend to support units on indexes either. + """ + da = self.da + self.assertEqualWUnits(da.x, self.x) + + @unittest.expectedFailure + def test_sel(self): + self.assertEqualWUnits(self.da.sel(y=self.y[0]), self.v[:, 0]) + + @unittest.expectedFailure + def test_mean(self): + self.assertEqualWUnits(self.da.mean('x'), self.v.mean(0))