From 40df7b0b73d97c053b4e8c7194b9bf2419a66f08 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Tue, 20 Sep 2016 16:49:56 +0200 Subject: [PATCH 1/8] use asanyarray instead of asarray in data accessor of Variable. --- xarray/core/variable.py | 2 +- xarray/test/test_quantities.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 xarray/test/test_quantities.py diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 6aafbcaab82..66985f91896 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -185,7 +185,7 @@ def _as_array_or_item(data): TODO: remove this (replace with np.asarray) once these issues are fixed """ - data = np.asarray(data) + data = np.asanyarray(data) if data.ndim == 0: if data.dtype.kind == 'M': data = np.datetime64(data, 'ns') diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py new file mode 100644 index 00000000000..7d8f61b562e --- /dev/null +++ b/xarray/test/test_quantities.py @@ -0,0 +1,55 @@ +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 dask')(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): + 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): + da = self.da + f = DataArray(np.arange(10*20).reshape(10,20)*pq.A,dims=['x','y'],coords=dict(x=self.x,y=self.y)) + self.assertEqualWUnits((da*f).data, da.data*f.data) + + def test_unit_checking(self): + da = self.da + f = DataArray(np.arange(10*20).reshape(10,20)*pq.A,dims=['x','y'],coords=dict(x=self.x,y=self.y)) + with self.assertRaisesRegex(ValueError,'Unable to convert between units'): + da + f + + @unittest.expectedFailure + def test_units_in_indexes(self): + """ + Indexes are borrowed from Pandas, and Pandas does not support units. + Therefore, we currently don't intend to support units on idexes either. + """ + da = self.da + self.assertEqualWUnits(da.x.data,self.x) \ No newline at end of file From 37b484c8e0336c6ddb2c2c332b2b7c2a557272fd Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Wed, 5 Oct 2016 19:51:15 +0200 Subject: [PATCH 2/8] added tests for a few operations --- xarray/test/test_quantities.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py index 7d8f61b562e..58281731b59 100644 --- a/xarray/test/test_quantities.py +++ b/xarray/test/test_quantities.py @@ -52,4 +52,11 @@ def test_units_in_indexes(self): Therefore, we currently don't intend to support units on idexes either. """ da = self.da - self.assertEqualWUnits(da.x.data,self.x) \ No newline at end of file + self.assertEqualWUnits(da.x.data,self.x) + + def test_sel(self): + self.assertEqualWUnits(self.da.sel(y=self.y[0]).values,self.v[:,0]) + + def test_mean(self): + self.assertEqualWUnits(self.da.mean('x').values,self.v.mean(0)) + From 921915ed45d40d9b8a694fb5d8a7e2d8efe34693 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Mon, 14 Nov 2016 11:49:50 +0100 Subject: [PATCH 3/8] small typo --- xarray/test/test_quantities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py index 58281731b59..a0d4e20f44d 100644 --- a/xarray/test/test_quantities.py +++ b/xarray/test/test_quantities.py @@ -49,7 +49,7 @@ def test_unit_checking(self): def test_units_in_indexes(self): """ Indexes are borrowed from Pandas, and Pandas does not support units. - Therefore, we currently don't intend to support units on idexes either. + Therefore, we currently don't intend to support units on indexes either. """ da = self.da self.assertEqualWUnits(da.x.data,self.x) From b3a49b6410895051c5a8a7fa37d7a2e0e9ffa743 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Mon, 14 Nov 2016 12:57:47 +0100 Subject: [PATCH 4/8] marked failing tests as expected --- xarray/test/test_quantities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py index a0d4e20f44d..916e801241d 100644 --- a/xarray/test/test_quantities.py +++ b/xarray/test/test_quantities.py @@ -54,9 +54,11 @@ def test_units_in_indexes(self): da = self.da self.assertEqualWUnits(da.x.data,self.x) + @unittest.expectedFailure def test_sel(self): self.assertEqualWUnits(self.da.sel(y=self.y[0]).values,self.v[:,0]) + @unittest.expectedFailure def test_mean(self): self.assertEqualWUnits(self.da.mean('x').values,self.v.mean(0)) From 28ef3996acf402cb9918f0b4e982f146a85d39f6 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Wed, 16 Nov 2016 11:28:12 +0100 Subject: [PATCH 5/8] fixed dependency message for quantities testing, and added quantities to CI requirements --- ci/requirements-py27-cdat+pynio.yml | 1 + ci/requirements-py27-netcdf4-dev.yml | 1 + ci/requirements-py27-pydap.yml | 1 + ci/requirements-py33.yml | 1 + ci/requirements-py34.yml | 1 + ci/requirements-py35-dask-dev.yml | 1 + ci/requirements-py35-pandas-dev.yml | 1 + ci/requirements-py35.yml | 1 + xarray/test/test_quantities.py | 2 +- 9 files changed, 9 insertions(+), 1 deletion(-) 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/test/test_quantities.py b/xarray/test/test_quantities.py index 916e801241d..96c0101bc2e 100644 --- a/xarray/test/test_quantities.py +++ b/xarray/test/test_quantities.py @@ -12,7 +12,7 @@ has_quantities = False def requires_quantities(test): - return test if has_quantities else unittest.skip('requires dask')(test) + return test if has_quantities else unittest.skip('requires python-quantities')(test) @requires_quantities class TestWithQuantities(TestCase): From c849b677bffb4faed6f30c73a1a0d51b141462c2 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Wed, 16 Nov 2016 11:45:49 +0100 Subject: [PATCH 6/8] fixing python 2.7 unittests compatibility issue --- xarray/test/test_quantities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py index 96c0101bc2e..adce6a3040d 100644 --- a/xarray/test/test_quantities.py +++ b/xarray/test/test_quantities.py @@ -42,7 +42,7 @@ def test_arithmetics(self): def test_unit_checking(self): da = self.da f = DataArray(np.arange(10*20).reshape(10,20)*pq.A,dims=['x','y'],coords=dict(x=self.x,y=self.y)) - with self.assertRaisesRegex(ValueError,'Unable to convert between units'): + with self.assertRaisesRegexp(ValueError,'Unable to convert between units'): da + f @unittest.expectedFailure From 39dedb149086619331ee9f967878b06ce4e3c21a Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Fri, 18 Nov 2016 14:59:58 +0100 Subject: [PATCH 7/8] implemented some of the improvements suggested by shoyer in the unittests --- xarray/test/test_quantities.py | 78 ++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/xarray/test/test_quantities.py b/xarray/test/test_quantities.py index adce6a3040d..1769a81fc57 100644 --- a/xarray/test/test_quantities.py +++ b/xarray/test/test_quantities.py @@ -1,3 +1,14 @@ +""" 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 @@ -7,12 +18,18 @@ 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) + return ( + test if has_quantities else + unittest.skip('requires python-quantities')(test) + ) + @requires_quantities class TestWithQuantities(TestCase): @@ -20,45 +37,70 @@ 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))) + 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): + 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) + 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) + 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 = DataArray(np.arange(10*20).reshape(10,20)*pq.A,dims=['x','y'],coords=dict(x=self.x,y=self.y)) - self.assertEqualWUnits((da*f).data, da.data*f.data) + + 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 = DataArray(np.arange(10*20).reshape(10,20)*pq.A,dims=['x','y'],coords=dict(x=self.x,y=self.y)) - with self.assertRaisesRegexp(ValueError,'Unable to convert between units'): - da + f + 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.data,self.x) + self.assertEqualWUnits(da.x, self.x) @unittest.expectedFailure def test_sel(self): - self.assertEqualWUnits(self.da.sel(y=self.y[0]).values,self.v[:,0]) + self.assertEqualWUnits(self.da.sel(y=self.y[0]), self.v[:, 0]) @unittest.expectedFailure def test_mean(self): - self.assertEqualWUnits(self.da.mean('x').values,self.v.mean(0)) - + self.assertEqualWUnits(self.da.mean('x'), self.v.mean(0)) From c58f92ebccf76253d2eed6b9ec48b835a389209f Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Fri, 18 Nov 2016 15:41:33 +0100 Subject: [PATCH 8/8] changed preserving ndarray subclasses only for .data but not for .values --- xarray/core/variable.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 66985f91896..51eb5cf67c5 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -185,7 +185,7 @@ def _as_array_or_item(data): TODO: remove this (replace with np.asarray) once these issues are fixed """ - data = np.asanyarray(data) + data = np.asarray(data) if data.ndim == 0: if data.dtype.kind == 'M': data = np.datetime64(data, 'ns') @@ -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):