From 1643ce52976ff7f1e28360c8c8872f6e260b30e7 Mon Sep 17 00:00:00 2001 From: Joe Hamman Date: Tue, 5 Apr 2016 16:34:52 -0700 Subject: [PATCH 01/36] rasterio checkin --- xarray/backends/__init__.py | 1 + xarray/backends/api.py | 2 +- xarray/backends/rasterio_.py | 132 +++++++++++++++++++++++++++++ xarray/core/accessors.py | 150 --------------------------------- xarray/tests/__init__.py | 8 ++ xarray/tests/test_accessors.py | 96 --------------------- xarray/tests/test_backends.py | 31 ++++++- 7 files changed, 172 insertions(+), 248 deletions(-) create mode 100644 xarray/backends/rasterio_.py delete mode 100644 xarray/core/accessors.py delete mode 100644 xarray/tests/test_accessors.py diff --git a/xarray/backends/__init__.py b/xarray/backends/__init__.py index a082bd53e5e..192a3c57db2 100644 --- a/xarray/backends/__init__.py +++ b/xarray/backends/__init__.py @@ -10,3 +10,4 @@ from .pynio_ import NioDataStore from .scipy_ import ScipyDataStore from .h5netcdf_ import H5NetCDFStore +from .rasterio_ import RasterioDataStore diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 4929da2539c..3a3b9305c8b 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -174,7 +174,7 @@ def open_dataset(filename_or_obj, group=None, decode_cf=True, decode_coords : bool, optional If True, decode the 'coordinates' attribute to identify coordinates in the resulting dataset. - engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio'}, optional + engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', 'rasterio'}, optional Engine to use when reading files. If not provided, the default engine is chosen based on available dependencies, with a preference for 'netcdf4'. diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py new file mode 100644 index 00000000000..8f2491e858c --- /dev/null +++ b/xarray/backends/rasterio_.py @@ -0,0 +1,132 @@ +import numpy as np + +try: + import rasterio +except ImportError: + rasterio = False + +from .. import Variable, DataArray +from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin +from ..core import indexing + +from .common import AbstractDataStore + +_VARNAME = 'raster' + + +class RasterioArrayWrapper(NDArrayMixin): + def __init__(self, array, ds): + self.array = array + self._ds = ds # make an explicit reference because pynio uses weakrefs + + @property + def dtype(self): + return np.dtype(self.array.typecode()) + + def __getitem__(self, key): + if key == () and self.ndim == 0: + return self.array.get_value() + return self.array[key] + + +class RasterioDataStore(AbstractDataStore): + """Store for accessing datasets via Rasterio + """ + def __init__(self, filename, mode='r'): + + with rasterio.drivers(): + self.ds = rasterio.open(filename, mode=mode, ) + + # Get coords + nx, ny = self.ds.width, self.ds.height + x0, y0 = self.ds.bounds.left, self.ds.bounds.top + dx, dy = self.ds.res[0], -self.ds.res[1] + + coords = {'y': np.arange(start=y0, stop=(y0 + ny * dy), step=dy), + 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} + + # Get dims + if self.ds.count == 3: + self.dims = ('band', 'y', 'x') + coords['band'] = self.ds.indexes + elif self.ds.count == 2: + self.dims = ('y', 'x') + else: + raise ValueError('unknown dims') + + attrs = {} + for attr_name in ['crs', 'affine', 'proj']: + try: + attrs[attr_name] = getattr(self.ds, attr_name) + except AttributeError: + pass + + def get_vardata(self, var_id=1): + """Read the geotiff band. + Parameters + ---------- + var_id: the variable name (here the band number) + """ + wx = (self.sub_x[0], self.sub_x[1] + 1) + wy = (self.sub_y[0], self.sub_y[1] + 1) + with rasterio.drivers(): + band = self.ds.read(var_id, window=(wy, wx)) + return band + + def open_store_variable(self, var): + if var != _VARNAME: + raise ValueError('Rasterio variables are all named %s' % _VARNAME) + data = indexing.LazilyIndexedArray(RasterioArrayWrapper(var, self.ds)) + return Variable(var.dimensions, data, var.attributes) + + def get_variables(self): + return FrozenOrderedDict((k, self.open_store_variable(v)) + for k, v in self.ds.variables.items()) + + def get_attrs(self): + return Frozen(self.ds.attributes) + + def get_dimensions(self): + return Frozen(self.ds.dimensions) + + def close(self): + self.ds.close() + + +def _transform_proj(p1, p2, x, y, nocopy=False): + """Wrapper around the pyproj transform. + When two projections are equal, this function avoids quite a bunch of + useless calculations. See https://github.com/jswhit/pyproj/issues/15 + """ + import pyproj + import copy + + if p1.srs == p2.srs: + if nocopy: + return x, y + else: + return copy.deepcopy(x), copy.deepcopy(y) + + return pyproj.transform(p1, p2, x, y) + + +def _try_to_get_latlon_coords(da): + import pyproj + if 'crs' in da.attrs: + proj = pyproj.Proj(da.attrs['crs']) + x, y = np.meshgrid(da['x'], da['y']) + proj_out = pyproj.Proj("+init=EPSG:4326", preserve_units=True) + xc, yc = _transform_proj(proj, proj_out, x, y) + coords = dict(y=da['y'], x=da['x']) + dims = ('y', 'x') + + da.coords['latitude'] = \ + DataArray(yc, coords=coords, dims=dims, name='latitude', + attrs={'units': 'degrees_north', 'long_name': 'latitude', + 'standard_name': 'latitude'}) + da.coords['longitude'] = DataArray(xc, coords=coords, dims=dims, name='latitude', + attrs={'units': 'degrees_east', + 'long_name': 'longitude', + 'standard_name': 'longitude'}) + + return da diff --git a/xarray/core/accessors.py b/xarray/core/accessors.py deleted file mode 100644 index 7360b9764ae..00000000000 --- a/xarray/core/accessors.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from .common import is_datetime_like -from .pycompat import dask_array_type - -from functools import partial - -import numpy as np -import pandas as pd - - -def _season_from_months(months): - """Compute season (DJF, MAM, JJA, SON) from month ordinal - """ - # TODO: Move "season" accessor upstream into pandas - seasons = np.array(['DJF', 'MAM', 'JJA', 'SON']) - months = np.asarray(months) - return seasons[(months // 3) % 4] - - -def _access_through_series(values, name): - """Coerce an array of datetime-like values to a pandas Series and - access requested datetime component - """ - values_as_series = pd.Series(values.ravel()) - if name == "season": - months = values_as_series.dt.month.values - field_values = _season_from_months(months) - else: - field_values = getattr(values_as_series.dt, name).values - return field_values.reshape(values.shape) - - -def _get_date_field(values, name, dtype): - """Indirectly access pandas' libts.get_date_field by wrapping data - as a Series and calling through `.dt` attribute. - - Parameters - ---------- - values : np.ndarray or dask.array-like - Array-like container of datetime-like values - name : str - Name of datetime field to access - dtype : dtype-like - dtype for output date field values - - Returns - ------- - datetime_fields : same type as values - Array-like of datetime fields accessed for each element in values - - """ - if isinstance(values, dask_array_type): - from dask.array import map_blocks - return map_blocks(_access_through_series, - values, name, dtype=dtype) - else: - return _access_through_series(values, name) - - -class DatetimeAccessor(object): - """Access datetime fields for DataArrays with datetime-like dtypes. - - Similar to pandas, fields can be accessed through the `.dt` attribute - for applicable DataArrays: - - >>> ds = xarray.Dataset({'time': pd.date_range(start='2000/01/01', - ... freq='D', periods=100)}) - >>> ds.time.dt - - >>> ds.time.dt.dayofyear[:5] - - array([1, 2, 3, 4, 5], dtype=int32) - Coordinates: - * time (time) datetime64[ns] 2000-01-01 2000-01-02 2000-01-03 ... - - All of the pandas fields are accessible here. Note that these fields are - not calendar-aware; if your datetimes are encoded with a non-Gregorian - calendar (e.g. a 360-day calendar) using netcdftime, then some fields like - `dayofyear` may not be accurate. - - """ - def __init__(self, xarray_obj): - if not is_datetime_like(xarray_obj.dtype): - raise TypeError("'dt' accessor only available for " - "DataArray with datetime64 or timedelta64 dtype") - self._obj = xarray_obj - - def _tslib_field_accessor(name, docstring=None, dtype=None): - def f(self, dtype=dtype): - if dtype is None: - dtype = self._obj.dtype - obj_type = type(self._obj) - result = _get_date_field(self._obj.data, name, dtype) - return obj_type(result, name=name, - coords=self._obj.coords, dims=self._obj.dims) - - f.__name__ = name - f.__doc__ = docstring - return property(f) - - year = _tslib_field_accessor('year', "The year of the datetime", np.int64) - month = _tslib_field_accessor( - 'month', "The month as January=1, December=12", np.int64 - ) - day = _tslib_field_accessor('day', "The days of the datetime", np.int64) - hour = _tslib_field_accessor('hour', "The hours of the datetime", np.int64) - minute = _tslib_field_accessor( - 'minute', "The minutes of the datetime", np.int64 - ) - second = _tslib_field_accessor( - 'second', "The seconds of the datetime", np.int64 - ) - microsecond = _tslib_field_accessor( - 'microsecond', "The microseconds of the datetime", np.int64 - ) - nanosecond = _tslib_field_accessor( - 'nanosecond', "The nanoseconds of the datetime", np.int64 - ) - weekofyear = _tslib_field_accessor( - 'weekofyear', "The week ordinal of the year", np.int64 - ) - week = weekofyear - dayofweek = _tslib_field_accessor( - 'dayofweek', "The day of the week with Monday=0, Sunday=6", np.int64 - ) - weekday = dayofweek - - weekday_name = _tslib_field_accessor( - 'weekday_name', "The name of day in a week (ex: Friday)", object - ) - - dayofyear = _tslib_field_accessor( - 'dayofyear', "The ordinal day of the year", np.int64 - ) - quarter = _tslib_field_accessor('quarter', "The quarter of the date") - days_in_month = _tslib_field_accessor( - 'days_in_month', "The number of days in the month", np.int64 - ) - daysinmonth = days_in_month - - season = _tslib_field_accessor( - "season", "Season of the year (ex: DJF)", object - ) - - time = _tslib_field_accessor( - "time", "Timestamps corresponding to datetimes", object - ) \ No newline at end of file diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index 95af04b9e40..512deacbd5f 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -76,6 +76,12 @@ except ImportError: has_bottleneck = False +try: + import rasterio + has_rasterio = True +except ImportError: + has_rasterio = False + # slighly simpler construction that the full functions. # Generally `pytest.importorskip('package')` inline is even easier requires_matplotlib = pytest.mark.skipif( @@ -96,6 +102,8 @@ not has_dask, reason='requires dask') requires_bottleneck = pytest.mark.skipif( not has_bottleneck, reason='requires bottleneck') +requires_rasterio = pytest.mark.skipif( + not has_rasterio, reason='requires rasterio') try: diff --git a/xarray/tests/test_accessors.py b/xarray/tests/test_accessors.py deleted file mode 100644 index dd1d77cf0af..00000000000 --- a/xarray/tests/test_accessors.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import xarray as xr -import numpy as np -import pandas as pd - -from . import TestCase, requires_dask - - -class TestDatetimeAccessor(TestCase): - def setUp(self): - nt = 100 - data = np.random.rand(10, 10, nt) - lons = np.linspace(0, 11, 10) - lats = np.linspace(0, 20, 10) - self.times = pd.date_range(start="2000/01/01", freq='H', periods=nt) - - self.data = xr.DataArray(data, coords=[lons, lats, self.times], - dims=['lon', 'lat', 'time'], name='data') - - self.times_arr = np.random.choice(self.times, size=(10, 10, nt)) - self.times_data = xr.DataArray(self.times_arr, - coords=[lons, lats, self.times], - dims=['lon', 'lat', 'time'], - name='data') - - def test_field_access(self): - years = xr.DataArray(self.times.year, name='year', - coords=[self.times, ], dims=['time', ]) - months = xr.DataArray(self.times.month, name='month', - coords=[self.times, ], dims=['time', ]) - days = xr.DataArray(self.times.day, name='day', - coords=[self.times, ], dims=['time', ]) - hours = xr.DataArray(self.times.hour, name='hour', - coords=[self.times, ], dims=['time', ]) - - self.assertDataArrayEqual(years, self.data.time.dt.year) - self.assertDataArrayEqual(months, self.data.time.dt.month) - self.assertDataArrayEqual(days, self.data.time.dt.day) - self.assertDataArrayEqual(hours, self.data.time.dt.hour) - - def test_not_datetime_type(self): - nontime_data = self.data.copy() - int_data = np.arange(len(self.data.time)).astype('int8') - nontime_data['time'].values = int_data - with self.assertRaisesRegexp(TypeError, 'dt'): - nontime_data.time.dt - - @requires_dask - def test_dask_field_access(self): - import dask.array as da - - years = self.times_data.dt.year - months = self.times_data.dt.month - hours = self.times_data.dt.hour - days = self.times_data.dt.day - - dask_times_arr = da.from_array(self.times_arr, chunks=(5, 5, 50)) - dask_times_2d = xr.DataArray(dask_times_arr, - coords=self.data.coords, - dims=self.data.dims, - name='data') - dask_year = dask_times_2d.dt.year - dask_month = dask_times_2d.dt.month - dask_day = dask_times_2d.dt.day - dask_hour = dask_times_2d.dt.hour - - # Test that the data isn't eagerly evaluated - assert isinstance(dask_year.data, da.Array) - assert isinstance(dask_month.data, da.Array) - assert isinstance(dask_day.data, da.Array) - assert isinstance(dask_hour.data, da.Array) - - # Double check that outcome chunksize is unchanged - dask_chunks = dask_times_2d.chunks - self.assertEqual(dask_year.data.chunks, dask_chunks) - self.assertEqual(dask_month.data.chunks, dask_chunks) - self.assertEqual(dask_day.data.chunks, dask_chunks) - self.assertEqual(dask_hour.data.chunks, dask_chunks) - - # Check the actual output from the accessors - self.assertDataArrayEqual(years, dask_year.compute()) - self.assertDataArrayEqual(months, dask_month.compute()) - self.assertDataArrayEqual(days, dask_day.compute()) - self.assertDataArrayEqual(hours, dask_hour.compute()) - - def test_seasons(self): - dates = pd.date_range(start="2000/01/01", freq="M", periods=12) - dates = xr.DataArray(dates) - seasons = ["DJF", "DJF", "MAM", "MAM", "MAM", "JJA", "JJA", "JJA", - "SON", "SON", "SON", "DJF"] - seasons = xr.DataArray(seasons) - - self.assertArrayEqual(seasons.values, dates.dt.season.values) \ No newline at end of file diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index a4218a24d75..c06527271fc 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -27,7 +27,7 @@ from . import (TestCase, requires_scipy, requires_netCDF4, requires_pydap, requires_scipy_or_netCDF4, requires_dask, requires_h5netcdf, requires_pynio, has_netCDF4, has_scipy, assert_allclose, - flaky) + flaky, requires_rasterio) from .test_dataset import create_test_data try: @@ -1429,6 +1429,35 @@ class TestPyNioAutocloseTrue(TestPyNio): autoclose = True +@requires_rasterio +class TestRasterIO(CFEncodedDataTest, Only32BitTypes, TestCase): + def test_write_store(self): + # rasterio is read-only for now + pass + + def test_orthogonal_indexing(self): + # rasterio also does not support list-like indexing + pass + + @contextlib.contextmanager + def roundtrip(self, data, save_kwargs={}, open_kwargs={}): + with create_tmp_file() as tmp_file: + data.to_netcdf(tmp_file, engine='scipy', **save_kwargs) + with open_dataset(tmp_file, engine='pynio', **open_kwargs) as ds: + yield ds + + def test_weakrefs(self): + example = Dataset({'foo': ('x', np.arange(5.0))}) + expected = example.rename({'foo': 'bar', 'x': 'y'}) + + with create_tmp_file() as tmp_file: + example.to_netcdf(tmp_file, engine='scipy') + on_disk = open_dataset(tmp_file, engine='pynio') + actual = on_disk.rename({'foo': 'bar', 'x': 'y'}) + del on_disk # trigger garbage collection + self.assertDatasetIdentical(actual, expected) + + class TestEncodingInvalid(TestCase): def test_extract_nc4_variable_encoding(self): From 067dedbd542e9c05ea7a8c421420d6efc2a01fe4 Mon Sep 17 00:00:00 2001 From: NicWayand Date: Thu, 27 Oct 2016 15:55:56 -0600 Subject: [PATCH 02/36] temp fixes --- xarray/backends/rasterio_.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 8f2491e858c..7d5c53c67f1 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -34,7 +34,7 @@ class RasterioDataStore(AbstractDataStore): """ def __init__(self, filename, mode='r'): - with rasterio.drivers(): + with rasterio.Env(): self.ds = rasterio.open(filename, mode=mode, ) # Get coords @@ -46,10 +46,10 @@ def __init__(self, filename, mode='r'): 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} # Get dims - if self.ds.count == 3: + if self.ds.count >= 2: self.dims = ('band', 'y', 'x') coords['band'] = self.ds.indexes - elif self.ds.count == 2: + elif self.ds.count == 1: self.dims = ('y', 'x') else: raise ValueError('unknown dims') @@ -69,7 +69,7 @@ def get_vardata(self, var_id=1): """ wx = (self.sub_x[0], self.sub_x[1] + 1) wy = (self.sub_y[0], self.sub_y[1] + 1) - with rasterio.drivers(): + with rasterio.Env(): band = self.ds.read(var_id, window=(wy, wx)) return band From 7906cfdf91ffaa48f0eb0b0b3177406ade13c055 Mon Sep 17 00:00:00 2001 From: Joe Hamman Date: Fri, 28 Oct 2016 10:42:33 -0700 Subject: [PATCH 03/36] update rasterio reader, no lazy loading, no decoding of coords --- ci/requirements-py35.yml | 1 + xarray/backends/rasterio_.py | 100 +++++++++++++++++++--------------- xarray/tests/test_backends.py | 4 +- 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index f6a62ac72a6..1c7a4558c91 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -15,6 +15,7 @@ dependencies: - scipy - seaborn - toolz + - rasterio - pip: - coveralls - pytest-cov diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 7d5c53c67f1..521ca85d212 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -8,20 +8,21 @@ from .. import Variable, DataArray from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin from ..core import indexing +from ..core.pycompat import OrderedDict from .common import AbstractDataStore -_VARNAME = 'raster' +__rio_varname__ = 'raster' class RasterioArrayWrapper(NDArrayMixin): - def __init__(self, array, ds): - self.array = array - self._ds = ds # make an explicit reference because pynio uses weakrefs + def __init__(self, ds): + self._ds = ds + self.array = ds.read() @property def dtype(self): - return np.dtype(self.array.typecode()) + return np.dtype(self._ds.dtypes[0]) def __getitem__(self, key): if key == () and self.ndim == 0: @@ -42,52 +43,58 @@ def __init__(self, filename, mode='r'): x0, y0 = self.ds.bounds.left, self.ds.bounds.top dx, dy = self.ds.res[0], -self.ds.res[1] - coords = {'y': np.arange(start=y0, stop=(y0 + ny * dy), step=dy), - 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} + self.coords = {'y': np.arange(start=y0, stop=(y0 + ny * dy), step=dy), + 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} # Get dims if self.ds.count >= 2: self.dims = ('band', 'y', 'x') - coords['band'] = self.ds.indexes + self.coords['band'] = self.ds.indexes elif self.ds.count == 1: self.dims = ('y', 'x') else: raise ValueError('unknown dims') - attrs = {} - for attr_name in ['crs', 'affine', 'proj']: + self._attrs = OrderedDict() + for attr_name in ['crs', 'transform', 'proj']: try: - attrs[attr_name] = getattr(self.ds, attr_name) + self._attrs[attr_name] = getattr(self.ds, attr_name) except AttributeError: pass - def get_vardata(self, var_id=1): - """Read the geotiff band. - Parameters - ---------- - var_id: the variable name (here the band number) - """ - wx = (self.sub_x[0], self.sub_x[1] + 1) - wy = (self.sub_y[0], self.sub_y[1] + 1) - with rasterio.Env(): - band = self.ds.read(var_id, window=(wy, wx)) - return band + self.coords = _try_to_get_latlon_coords(self.coords, self._attrs) + + + + # def get_vardata(self, var_id=1): + # """Read the geotiff band. + # Parameters + # ---------- + # var_id: the variable name (here the band number) + # """ + # # wx = (self.sub_x[0], self.sub_x[1] + 1) + # # wy = (self.sub_y[0], self.sub_y[1] + 1) + # with rasterio.Env(): + # band = self.ds.read() # var_id, window=(wy, wx)) + # return band def open_store_variable(self, var): - if var != _VARNAME: - raise ValueError('Rasterio variables are all named %s' % _VARNAME) - data = indexing.LazilyIndexedArray(RasterioArrayWrapper(var, self.ds)) - return Variable(var.dimensions, data, var.attributes) + if var != __rio_varname__: + raise ValueError( + 'Rasterio variables are all named %s' % __rio_varname__) + data = indexing.LazilyIndexedArray( + RasterioArrayWrapper(self.ds)) + return Variable(self.dims, data, self._attrs) def get_variables(self): - return FrozenOrderedDict((k, self.open_store_variable(v)) - for k, v in self.ds.variables.items()) + return FrozenOrderedDict( + {__rio_varname__: self.open_store_variable(__rio_varname__)}) def get_attrs(self): - return Frozen(self.ds.attributes) + return Frozen(self._attrs) def get_dimensions(self): - return Frozen(self.ds.dimensions) + return Frozen(self.ds.dims) def close(self): self.ds.close() @@ -110,23 +117,26 @@ def _transform_proj(p1, p2, x, y, nocopy=False): return pyproj.transform(p1, p2, x, y) -def _try_to_get_latlon_coords(da): - import pyproj - if 'crs' in da.attrs: - proj = pyproj.Proj(da.attrs['crs']) - x, y = np.meshgrid(da['x'], da['y']) +def _try_to_get_latlon_coords(coords, attrs): + try: + import pyproj + except ImportError: + pyproj = False + if 'crs' in attrs and pyproj: + proj = pyproj.Proj(attrs['crs']) + x, y = np.meshgrid(coords['x'], coords['y']) proj_out = pyproj.Proj("+init=EPSG:4326", preserve_units=True) xc, yc = _transform_proj(proj, proj_out, x, y) - coords = dict(y=da['y'], x=da['x']) + coords = dict(y=coords['y'], x=coords['x']) dims = ('y', 'x') - da.coords['latitude'] = \ - DataArray(yc, coords=coords, dims=dims, name='latitude', - attrs={'units': 'degrees_north', 'long_name': 'latitude', - 'standard_name': 'latitude'}) - da.coords['longitude'] = DataArray(xc, coords=coords, dims=dims, name='latitude', - attrs={'units': 'degrees_east', - 'long_name': 'longitude', - 'standard_name': 'longitude'}) + coords['latitude'] = DataArray( + data=yc, coords=coords, dims=dims, name='latitude', + attrs={'units': 'degrees_north', 'long_name': 'latitude', + 'standard_name': 'latitude'}) + coords['longitude'] = DataArray( + data=xc, coords=coords, dims=dims, name='latitude', + attrs={'units': 'degrees_east', 'long_name': 'longitude', + 'standard_name': 'longitude'}) - return da + return coords diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index c06527271fc..958e68cddb8 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1443,7 +1443,7 @@ def test_orthogonal_indexing(self): def roundtrip(self, data, save_kwargs={}, open_kwargs={}): with create_tmp_file() as tmp_file: data.to_netcdf(tmp_file, engine='scipy', **save_kwargs) - with open_dataset(tmp_file, engine='pynio', **open_kwargs) as ds: + with open_dataset(tmp_file, engine='rasterio', **open_kwargs) as ds: yield ds def test_weakrefs(self): @@ -1452,7 +1452,7 @@ def test_weakrefs(self): with create_tmp_file() as tmp_file: example.to_netcdf(tmp_file, engine='scipy') - on_disk = open_dataset(tmp_file, engine='pynio') + on_disk = open_dataset(tmp_file, engine='rasterio') actual = on_disk.rename({'foo': 'bar', 'x': 'y'}) del on_disk # trigger garbage collection self.assertDatasetIdentical(actual, expected) From 2e1b5280f87cad18c30a178a615597618ef6b7de Mon Sep 17 00:00:00 2001 From: NicWayand Date: Sat, 29 Oct 2016 22:06:26 -0600 Subject: [PATCH 04/36] keep band dim even for single band. Fix longitude typo --- xarray/backends/rasterio_.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 521ca85d212..13038fe35e6 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -47,11 +47,9 @@ def __init__(self, filename, mode='r'): 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} # Get dims - if self.ds.count >= 2: + if self.ds.count >= 1: self.dims = ('band', 'y', 'x') self.coords['band'] = self.ds.indexes - elif self.ds.count == 1: - self.dims = ('y', 'x') else: raise ValueError('unknown dims') @@ -65,7 +63,6 @@ def __init__(self, filename, mode='r'): self.coords = _try_to_get_latlon_coords(self.coords, self._attrs) - # def get_vardata(self, var_id=1): # """Read the geotiff band. # Parameters @@ -135,8 +132,7 @@ def _try_to_get_latlon_coords(coords, attrs): attrs={'units': 'degrees_north', 'long_name': 'latitude', 'standard_name': 'latitude'}) coords['longitude'] = DataArray( - data=xc, coords=coords, dims=dims, name='latitude', + data=xc, coords=coords, dims=dims, name='longitude', attrs={'units': 'degrees_east', 'long_name': 'longitude', 'standard_name': 'longitude'}) - return coords From 532c5d32e0e6780f8e3f33b3f9da872aa82fac53 Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Mon, 31 Oct 2016 11:45:15 -0600 Subject: [PATCH 05/36] Fix lat/lon decoding. Remove requirment comment --- xarray/backends/rasterio_.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 13038fe35e6..b77274f1f4b 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -60,9 +60,6 @@ def __init__(self, filename, mode='r'): except AttributeError: pass - self.coords = _try_to_get_latlon_coords(self.coords, self._attrs) - - # def get_vardata(self, var_id=1): # """Read the geotiff band. # Parameters @@ -84,8 +81,11 @@ def open_store_variable(self, var): return Variable(self.dims, data, self._attrs) def get_variables(self): - return FrozenOrderedDict( - {__rio_varname__: self.open_store_variable(__rio_varname__)}) + # Get lat lon coordinates + coords = _try_to_get_latlon_coords(self.coords, self._attrs) + vars = {__rio_varname__: self.open_store_variable(__rio_varname__)} + vars.update(coords) + return FrozenOrderedDict(vars) def get_attrs(self): return Frozen(self._attrs) @@ -115,6 +115,7 @@ def _transform_proj(p1, p2, x, y, nocopy=False): def _try_to_get_latlon_coords(coords, attrs): + coords_out = {} try: import pyproj except ImportError: @@ -127,12 +128,12 @@ def _try_to_get_latlon_coords(coords, attrs): coords = dict(y=coords['y'], x=coords['x']) dims = ('y', 'x') - coords['latitude'] = DataArray( - data=yc, coords=coords, dims=dims, name='latitude', + coords_out['lat'] = DataArray( + data=yc, coords=coords, dims=dims, name='lat', attrs={'units': 'degrees_north', 'long_name': 'latitude', 'standard_name': 'latitude'}) - coords['longitude'] = DataArray( - data=xc, coords=coords, dims=dims, name='longitude', + coords_out['lon'] = DataArray( + data=xc, coords=coords, dims=dims, name='lon', attrs={'units': 'degrees_east', 'long_name': 'longitude', 'standard_name': 'longitude'}) - return coords + return coords_out From 8bc3da300f2436bb83099c778a1b96fefb71e4f0 Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Mon, 31 Oct 2016 12:31:16 -0600 Subject: [PATCH 06/36] Attr error suppression. DataArray to Variable objects. CI requirment updates --- ci/requirements-py27-cdat+pynio.yml | 2 ++ ci/requirements-py27-min.yml | 1 + ci/requirements-py34.yml | 1 + ci/requirements-py35.yml | 2 +- ci/requirements-py36-netcdf4-dev.yml | 1 + ci/requirements-py36-pydap.yml | 1 + xarray/backends/rasterio_.py | 31 ++++++---------------------- 7 files changed, 13 insertions(+), 26 deletions(-) diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+pynio.yml index c88263fcfba..dff5c8dad6a 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+pynio.yml @@ -18,6 +18,8 @@ dependencies: - scipy - seaborn - toolz + - cyordereddict + - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index 7499157dbe9..2be01f1fa1f 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -4,6 +4,7 @@ dependencies: - pytest - numpy==1.9.3 - pandas==0.15.0 + - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index a49611751ca..047021ff193 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -4,6 +4,7 @@ dependencies: - bottleneck - pytest - pandas + - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index 1c7a4558c91..fc3c8757e8f 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -15,7 +15,7 @@ dependencies: - scipy - seaborn - toolz - - rasterio + - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36-netcdf4-dev.yml b/ci/requirements-py36-netcdf4-dev.yml index 033d1f41b4d..e9d48ebfba8 100644 --- a/ci/requirements-py36-netcdf4-dev.yml +++ b/ci/requirements-py36-netcdf4-dev.yml @@ -14,6 +14,7 @@ dependencies: - pandas - scipy - toolz + - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36-pydap.yml b/ci/requirements-py36-pydap.yml index c10bd93f928..2c5bfea3fe9 100644 --- a/ci/requirements-py36-pydap.yml +++ b/ci/requirements-py36-pydap.yml @@ -13,6 +13,7 @@ dependencies: - numpy - scipy - toolz + - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index b77274f1f4b..f8fa628458e 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -8,7 +8,7 @@ from .. import Variable, DataArray from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin from ..core import indexing -from ..core.pycompat import OrderedDict +from ..core.pycompat import OrderedDict, suppress from .common import AbstractDataStore @@ -54,23 +54,9 @@ def __init__(self, filename, mode='r'): raise ValueError('unknown dims') self._attrs = OrderedDict() - for attr_name in ['crs', 'transform', 'proj']: - try: - self._attrs[attr_name] = getattr(self.ds, attr_name) - except AttributeError: - pass - - # def get_vardata(self, var_id=1): - # """Read the geotiff band. - # Parameters - # ---------- - # var_id: the variable name (here the band number) - # """ - # # wx = (self.sub_x[0], self.sub_x[1] + 1) - # # wy = (self.sub_y[0], self.sub_y[1] + 1) - # with rasterio.Env(): - # band = self.ds.read() # var_id, window=(wy, wx)) - # return band + with suppress(AttributeError): + for attr_name in ['crs', 'transform', 'proj']: + self._attrs[attr_name] = getattr(self.ds, attr_name) def open_store_variable(self, var): if var != __rio_varname__: @@ -125,15 +111,10 @@ def _try_to_get_latlon_coords(coords, attrs): x, y = np.meshgrid(coords['x'], coords['y']) proj_out = pyproj.Proj("+init=EPSG:4326", preserve_units=True) xc, yc = _transform_proj(proj, proj_out, x, y) - coords = dict(y=coords['y'], x=coords['x']) dims = ('y', 'x') - coords_out['lat'] = DataArray( - data=yc, coords=coords, dims=dims, name='lat', - attrs={'units': 'degrees_north', 'long_name': 'latitude', + coords_out['lat'] = Variable(dims,yc,attrs={'units': 'degrees_north', 'long_name': 'latitude', 'standard_name': 'latitude'}) - coords_out['lon'] = DataArray( - data=xc, coords=coords, dims=dims, name='lon', - attrs={'units': 'degrees_east', 'long_name': 'longitude', + coords_out['lon'] = Variable(dims,xc,attrs={'units': 'degrees_east', 'long_name': 'longitude', 'standard_name': 'longitude'}) return coords_out From 77cc0ca61a0b891ca040077153a96b454d682f22 Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Mon, 31 Oct 2016 12:57:51 -0600 Subject: [PATCH 07/36] remove >= requirment --- ci/requirements-py27-cdat+pynio.yml | 2 +- ci/requirements-py27-min.yml | 2 +- ci/requirements-py34.yml | 2 +- doc/_static/dataset-diagram-build.sh | 0 4 files changed, 3 insertions(+), 3 deletions(-) mode change 100755 => 100644 doc/_static/dataset-diagram-build.sh diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+pynio.yml index dff5c8dad6a..bad3b0a961b 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+pynio.yml @@ -19,7 +19,7 @@ dependencies: - seaborn - toolz - cyordereddict - - rasterio>=1.0 + - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index 2be01f1fa1f..7d795ce580c 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -4,7 +4,7 @@ dependencies: - pytest - numpy==1.9.3 - pandas==0.15.0 - - rasterio>=1.0 + - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index 047021ff193..1f41be029d3 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -4,7 +4,7 @@ dependencies: - bottleneck - pytest - pandas - - rasterio>=1.0 + - rasterio - pip: - coveralls - pytest-cov diff --git a/doc/_static/dataset-diagram-build.sh b/doc/_static/dataset-diagram-build.sh old mode 100755 new mode 100644 From 776cbd9b8c356b815a7ce438fe3f2e0858d280fd Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Mon, 31 Oct 2016 13:00:16 -0600 Subject: [PATCH 08/36] added conda-forge channel to CI check --- ci/requirements-py27-min.yml | 2 ++ ci/requirements-py34.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index 7d795ce580c..c82354ff5bc 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=2.7 - pytest diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index 1f41be029d3..b04a12c243c 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=3.4 - bottleneck From eb739de679ddd638b6cdb0eb7b4113659e70a087 Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Mon, 31 Oct 2016 13:35:37 -0600 Subject: [PATCH 09/36] add scipy requirement --- ci/requirements-py34.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index b04a12c243c..3ad45660b80 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -7,6 +7,7 @@ dependencies: - pytest - pandas - rasterio + - scipy - pip: - coveralls - pytest-cov From 3a394ae9446a372a1aad31e5f25ec43cfaba28fd Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Wed, 2 Nov 2016 13:07:26 -0400 Subject: [PATCH 10/36] roll back ci requirements. Rename vars --- ci/requirements-py27-cdat+pynio.yml | 1 - ci/requirements-py27-min.yml | 3 --- ci/requirements-py27-windows.yml | 6 ++++-- ci/requirements-py34.yml | 2 -- ci/requirements-py35.yml | 2 -- ci/requirements-py36-netcdf4-dev.yml | 2 -- ci/requirements-py36-pandas-dev.yml | 2 -- xarray/backends/rasterio_.py | 10 +++++----- 8 files changed, 9 insertions(+), 19 deletions(-) diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+pynio.yml index bad3b0a961b..8ebb3c5bb0d 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+pynio.yml @@ -19,7 +19,6 @@ dependencies: - seaborn - toolz - cyordereddict - - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index c82354ff5bc..7499157dbe9 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -1,12 +1,9 @@ name: test_env -channels: - - conda-forge dependencies: - python=2.7 - pytest - numpy==1.9.3 - pandas==0.15.0 - - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py27-windows.yml b/ci/requirements-py27-windows.yml index caa77627acc..3ef89e17a99 100644 --- a/ci/requirements-py27-windows.yml +++ b/ci/requirements-py27-windows.yml @@ -1,6 +1,4 @@ name: test_env -channels: - - conda-forge dependencies: - python=2.7 - dask @@ -15,3 +13,7 @@ dependencies: - scipy - seaborn - toolz + - pip: + - coveralls + - pytest-cov + - pydap diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index 3ad45660b80..8adae1afd15 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -1,6 +1,4 @@ name: test_env -channels: - - conda-forge dependencies: - python=3.4 - bottleneck diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index fc3c8757e8f..0200bf4e770 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -1,6 +1,4 @@ name: test_env -channels: - - conda-forge dependencies: - python=3.5 - dask diff --git a/ci/requirements-py36-netcdf4-dev.yml b/ci/requirements-py36-netcdf4-dev.yml index e9d48ebfba8..20147c33d12 100644 --- a/ci/requirements-py36-netcdf4-dev.yml +++ b/ci/requirements-py36-netcdf4-dev.yml @@ -1,6 +1,4 @@ name: test_env -channels: - - conda-forge dependencies: - python=3.6 - cython diff --git a/ci/requirements-py36-pandas-dev.yml b/ci/requirements-py36-pandas-dev.yml index ebcec868f76..bd09df3716b 100644 --- a/ci/requirements-py36-pandas-dev.yml +++ b/ci/requirements-py36-pandas-dev.yml @@ -1,6 +1,4 @@ name: test_env -channels: - - conda-forge dependencies: - python=3.6 - cython diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index f8fa628458e..5d32c25cb2c 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -44,7 +44,7 @@ def __init__(self, filename, mode='r'): dx, dy = self.ds.res[0], -self.ds.res[1] self.coords = {'y': np.arange(start=y0, stop=(y0 + ny * dy), step=dy), - 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} + 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} # Get dims if self.ds.count >= 1: @@ -56,7 +56,7 @@ def __init__(self, filename, mode='r'): self._attrs = OrderedDict() with suppress(AttributeError): for attr_name in ['crs', 'transform', 'proj']: - self._attrs[attr_name] = getattr(self.ds, attr_name) + self._attrs[attr_name] = getattr(self.ds, attr_name) def open_store_variable(self, var): if var != __rio_varname__: @@ -69,9 +69,9 @@ def open_store_variable(self, var): def get_variables(self): # Get lat lon coordinates coords = _try_to_get_latlon_coords(self.coords, self._attrs) - vars = {__rio_varname__: self.open_store_variable(__rio_varname__)} - vars.update(coords) - return FrozenOrderedDict(vars) + rio_vars = {__rio_varname__: self.open_store_variable(__rio_varname__)} + rio_vars.update(coords) + return FrozenOrderedDict(rio_vars) def get_attrs(self): return Frozen(self._attrs) From 061b8fd0814d81c9f3a956e1ba797d1a287711b4 Mon Sep 17 00:00:00 2001 From: Nicolas Wayand Date: Wed, 2 Nov 2016 13:11:47 -0400 Subject: [PATCH 11/36] roll back --- ci/requirements-py36.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index be78d32ddb1..e6a45b1cf19 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -1,6 +1,4 @@ name: test_env -channels: - - conda-forge dependencies: - python=3.6 - dask From c0962fabbb51f1110d46064e6b588eda9bbaa38a Mon Sep 17 00:00:00 2001 From: NicWayand Date: Tue, 8 Nov 2016 14:54:21 -0600 Subject: [PATCH 12/36] fixed coord spacing bug where x and y were +1 dim than raster. Uses np.linspace now. --- xarray/backends/rasterio_.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 5d32c25cb2c..1b49a182b07 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -43,8 +43,10 @@ def __init__(self, filename, mode='r'): x0, y0 = self.ds.bounds.left, self.ds.bounds.top dx, dy = self.ds.res[0], -self.ds.res[1] - self.coords = {'y': np.arange(start=y0, stop=(y0 + ny * dy), step=dy), - 'x': np.arange(start=x0, stop=(x0 + nx * dx), step=dx)} + self.coords = {'y': np.linspace(start=y0, num=ny, + stop=(y0 + (ny-1) * dy)), + 'x': np.linspace(start=x0, num=nx, + stop=(x0 + (nx-1) * dx))} # Get dims if self.ds.count >= 1: From 7275ffab1c87ed6765e9e348d520d612f1fa33fb Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Fri, 10 Feb 2017 14:26:19 +0100 Subject: [PATCH 13/36] change test env to py36 --- ci/requirements-py34.yml | 2 -- ci/requirements-py36.yml | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ci/requirements-py34.yml b/ci/requirements-py34.yml index 8adae1afd15..a49611751ca 100644 --- a/ci/requirements-py34.yml +++ b/ci/requirements-py34.yml @@ -4,8 +4,6 @@ dependencies: - bottleneck - pytest - pandas - - rasterio - - scipy - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index e6a45b1cf19..774bf42a644 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -13,6 +13,7 @@ dependencies: - scipy - seaborn - toolz + - rasterio - pip: - coveralls - pytest-cov From 51a60af72d34d1670737ed0273cf2e0ecfeb61fe Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Fri, 10 Feb 2017 21:46:30 +0100 Subject: [PATCH 14/36] first tests --- xarray/backends/rasterio_.py | 75 +++++++++++++++++------------------ xarray/tests/test_backends.py | 73 ++++++++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 56 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 1b49a182b07..ed79498c0ad 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -1,3 +1,4 @@ +import copy import numpy as np try: @@ -35,23 +36,27 @@ class RasterioDataStore(AbstractDataStore): """ def __init__(self, filename, mode='r'): + # TODO: is the rasterio.Env() really necessary, and if yes where? with rasterio.Env(): - self.ds = rasterio.open(filename, mode=mode, ) + self.ds = rasterio.open(filename, mode=mode) - # Get coords - nx, ny = self.ds.width, self.ds.height - x0, y0 = self.ds.bounds.left, self.ds.bounds.top - dx, dy = self.ds.res[0], -self.ds.res[1] + # Get coords + nx, ny = self.ds.width, self.ds.height + dx, dy = self.ds.res[0], -self.ds.res[1] + x0 = self.ds.bounds.right if dx < 0 else self.ds.bounds.left + y0 = self.ds.bounds.top if dy < 0 else self.ds.bounds.bottom + y = np.linspace(start=y0, num=ny, stop=(y0 + (ny-1) * dy)) + x = np.linspace(start=x0, num=nx, stop=(x0 + (nx-1) * dx)) - self.coords = {'y': np.linspace(start=y0, num=ny, - stop=(y0 + (ny-1) * dy)), - 'x': np.linspace(start=x0, num=nx, - stop=(x0 + (nx-1) * dx))} + self.coords = OrderedDict() + self.coords['y'] = Variable(('y', ), y) + self.coords['x'] = Variable(('x', ), x) # Get dims if self.ds.count >= 1: self.dims = ('band', 'y', 'x') - self.coords['band'] = self.ds.indexes + self.coords['band'] = Variable(('band', ), + np.atleast_1d(self.ds.indexes)) else: raise ValueError('unknown dims') @@ -85,38 +90,30 @@ def close(self): self.ds.close() -def _transform_proj(p1, p2, x, y, nocopy=False): - """Wrapper around the pyproj transform. - When two projections are equal, this function avoids quite a bunch of - useless calculations. See https://github.com/jswhit/pyproj/issues/15 - """ - import pyproj - import copy - - if p1.srs == p2.srs: - if nocopy: - return x, y - else: - return copy.deepcopy(x), copy.deepcopy(y) - - return pyproj.transform(p1, p2, x, y) +def _try_to_get_latlon_coords(coords, attrs): + from rasterio.warp import transform -def _try_to_get_latlon_coords(coords, attrs): - coords_out = {} - try: - import pyproj - except ImportError: - pyproj = False - if 'crs' in attrs and pyproj: - proj = pyproj.Proj(attrs['crs']) + coords_out = coords + if 'crs' in attrs: + proj = attrs['crs'] + # TODO: if the proj is already PlateCarree, making 2D coordinates + # is not the best thing to do here. + ny, nx = len(coords['y']), len(coords['x']) x, y = np.meshgrid(coords['x'], coords['y']) - proj_out = pyproj.Proj("+init=EPSG:4326", preserve_units=True) - xc, yc = _transform_proj(proj, proj_out, x, y) + # Rasterio works with 1D arrays + xc, yc = transform(proj, {'init': 'EPSG:4326'}, + x.flatten(), y.flatten()) + xc = np.asarray(xc).reshape((ny, nx)) + yc = np.asarray(yc).reshape((ny, nx)) dims = ('y', 'x') - coords_out['lat'] = Variable(dims,yc,attrs={'units': 'degrees_north', 'long_name': 'latitude', - 'standard_name': 'latitude'}) - coords_out['lon'] = Variable(dims,xc,attrs={'units': 'degrees_east', 'long_name': 'longitude', - 'standard_name': 'longitude'}) + coords_out['lat'] = Variable(dims, yc, + attrs={'units': 'degrees_north', + 'long_name': 'latitude', + 'standard_name': 'latitude'}) + coords_out['lon'] = Variable(dims, xc, + attrs={'units': 'degrees_east', + 'long_name': 'longitude', + 'standard_name': 'longitude'}) return coords_out diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 958e68cddb8..00848e34e62 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1431,31 +1431,70 @@ class TestPyNioAutocloseTrue(TestPyNio): @requires_rasterio class TestRasterIO(CFEncodedDataTest, Only32BitTypes, TestCase): + + # def setUp(self): + # + # + # name = 'test_latlong.tif' + # + # + # name = 'test_utm.tif' + # transform = from_origin(-300, 200, 1000, 1000) + # with rasterio.open( + # name, 'w', + # driver='GTiff', height=3, width=4, count=1, + # crs={'units': 'm', 'no_defs': True, 'ellps': 'WGS84', + # 'proj': 'utm', 'zone': 18}, + # transform=transform, + # dtype=rasterio.float32) as s: + # s.write(a, indexes=1) + def test_write_store(self): - # rasterio is read-only for now + # RasterIO is read-only for now pass def test_orthogonal_indexing(self): - # rasterio also does not support list-like indexing + # RasterIO also does not support list-like indexing pass - @contextlib.contextmanager - def roundtrip(self, data, save_kwargs={}, open_kwargs={}): - with create_tmp_file() as tmp_file: - data.to_netcdf(tmp_file, engine='scipy', **save_kwargs) - with open_dataset(tmp_file, engine='rasterio', **open_kwargs) as ds: - yield ds + def test_latlong_coords(self): - def test_weakrefs(self): - example = Dataset({'foo': ('x', np.arange(5.0))}) - expected = example.rename({'foo': 'bar', 'x': 'y'}) + import rasterio + from rasterio.transform import from_origin - with create_tmp_file() as tmp_file: - example.to_netcdf(tmp_file, engine='scipy') - on_disk = open_dataset(tmp_file, engine='rasterio') - actual = on_disk.rename({'foo': 'bar', 'x': 'y'}) - del on_disk # trigger garbage collection - self.assertDatasetIdentical(actual, expected) + # Create a geotiff file in latlong proj + data = np.arange(12, dtype=rasterio.float32).reshape(3, 4) + with create_tmp_file(suffix='.tif') as tmp_file: + transform = from_origin(1, 2, 0.5, 1.) + with rasterio.open( + tmp_file, 'w', + driver='GTiff', height=3, width=4, count=1, + crs='+proj=latlong', + transform=transform, + dtype=rasterio.float32) as s: + s.write(data, + indexes=1) + actual = xr.open_dataset(tmp_file, engine='rasterio') + + assert 'proj' in actual.crs + assert actual.crs['proj'] == 'longlat' + + expected = Dataset() + expected['x'] = ('x', [1, 1.5, 2, 2.5]) + expected['y'] = ('y', [2., 1, 0]) + expected['band'] = ('band', [1]) + expected['raster'] = (('band', 'y', 'x'), data[np.newaxis, ...]) + lon, lat = np.meshgrid(expected['x'], expected['y']) + expected['lon'] = (('y', 'x'), lon) + expected['lat'] = (('y', 'x'), lat) + + assert_allclose(actual.y, expected.y) + assert_allclose(actual.x, expected.x) + assert_allclose(actual.raster, expected.raster) + assert_allclose(actual.lon, expected.lon) + assert_allclose(actual.lat, expected.lat) + + print(actual) class TestEncodingInvalid(TestCase): From 515563406bb6118e52f82adc6397680c485a9882 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Fri, 10 Feb 2017 22:18:24 +0100 Subject: [PATCH 15/36] other tests --- xarray/tests/test_backends.py | 39 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 00848e34e62..1908d2e9241 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1443,8 +1443,6 @@ class TestRasterIO(CFEncodedDataTest, Only32BitTypes, TestCase): # with rasterio.open( # name, 'w', # driver='GTiff', height=3, width=4, count=1, - # crs={'units': 'm', 'no_defs': True, 'ellps': 'WGS84', - # 'proj': 'utm', 'zone': 18}, # transform=transform, # dtype=rasterio.float32) as s: # s.write(a, indexes=1) @@ -1472,13 +1470,9 @@ def test_latlong_coords(self): crs='+proj=latlong', transform=transform, dtype=rasterio.float32) as s: - s.write(data, - indexes=1) + s.write(data) actual = xr.open_dataset(tmp_file, engine='rasterio') - assert 'proj' in actual.crs - assert actual.crs['proj'] == 'longlat' - expected = Dataset() expected['x'] = ('x', [1, 1.5, 2, 2.5]) expected['y'] = ('y', [2., 1, 0]) @@ -1496,6 +1490,37 @@ def test_latlong_coords(self): print(actual) + def test_utm_coords(self): + + import rasterio + from rasterio.transform import from_origin + + # Create a geotiff file in utm proj + data = np.arange(24, dtype=rasterio.float32).reshape(2, 3, 4) + with create_tmp_file(suffix='.tif') as tmp_file: + transform = from_origin(-3000, 1000, 1000, 1000) + with rasterio.open( + tmp_file, 'w', + driver='GTiff', height=3, width=4, count=2, + crs={'units': 'm', 'no_defs': True, 'ellps': 'WGS84', + 'proj': 'utm', 'zone': 18}, + transform=transform, + dtype=rasterio.float32) as s: + s.write(data) + actual = xr.open_dataset(tmp_file, engine='rasterio') + + expected = Dataset() + expected['x'] = ('x', [-3000., -2000, -1000, 0]) + expected['y'] = ('y', [1000., 0, -1000]) + expected['band'] = ('band', [1, 2]) + expected['raster'] = (('band', 'y', 'x'), data) + + assert_allclose(actual.y, expected.y) + assert_allclose(actual.x, expected.x) + assert_allclose(actual.raster, expected.raster) + + print(actual) + class TestEncodingInvalid(TestCase): From 09196ee265c5096146da1556c7ac0261295110e7 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Fri, 10 Feb 2017 22:24:48 +0100 Subject: [PATCH 16/36] fix test --- xarray/tests/test_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 1908d2e9241..acbced9ee50 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1470,7 +1470,7 @@ def test_latlong_coords(self): crs='+proj=latlong', transform=transform, dtype=rasterio.float32) as s: - s.write(data) + s.write(data, indexes=1) actual = xr.open_dataset(tmp_file, engine='rasterio') expected = Dataset() From e2b67865fad9cdd0abb207426fc2f56b1af18836 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Fri, 10 Feb 2017 22:51:19 +0100 Subject: [PATCH 17/36] get the order right --- xarray/backends/rasterio_.py | 40 +++++++++++++++--------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index ed79498c0ad..6f7df475196 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -13,7 +13,7 @@ from .common import AbstractDataStore -__rio_varname__ = 'raster' +_rio_varname = 'raster' class RasterioArrayWrapper(NDArrayMixin): @@ -66,35 +66,30 @@ def __init__(self, filename, mode='r'): self._attrs[attr_name] = getattr(self.ds, attr_name) def open_store_variable(self, var): - if var != __rio_varname__: - raise ValueError( - 'Rasterio variables are all named %s' % __rio_varname__) - data = indexing.LazilyIndexedArray( - RasterioArrayWrapper(self.ds)) + if var != _rio_varname: + raise ValueError('Rasterio variables are named %s' % _rio_varname) + data = indexing.LazilyIndexedArray(RasterioArrayWrapper(self.ds)) return Variable(self.dims, data, self._attrs) def get_variables(self): # Get lat lon coordinates - coords = _try_to_get_latlon_coords(self.coords, self._attrs) - rio_vars = {__rio_varname__: self.open_store_variable(__rio_varname__)} - rio_vars.update(coords) - return FrozenOrderedDict(rio_vars) + vars = _try_to_get_latlon_coords(self.coords, self._attrs) + vars[_rio_varname] = self.open_store_variable(_rio_varname) + return FrozenOrderedDict(vars) def get_attrs(self): return Frozen(self._attrs) def get_dimensions(self): - return Frozen(self.ds.dims) + return Frozen(self.dims) def close(self): self.ds.close() def _try_to_get_latlon_coords(coords, attrs): - from rasterio.warp import transform - coords_out = coords if 'crs' in attrs: proj = attrs['crs'] # TODO: if the proj is already PlateCarree, making 2D coordinates @@ -107,13 +102,12 @@ def _try_to_get_latlon_coords(coords, attrs): xc = np.asarray(xc).reshape((ny, nx)) yc = np.asarray(yc).reshape((ny, nx)) dims = ('y', 'x') - - coords_out['lat'] = Variable(dims, yc, - attrs={'units': 'degrees_north', - 'long_name': 'latitude', - 'standard_name': 'latitude'}) - coords_out['lon'] = Variable(dims, xc, - attrs={'units': 'degrees_east', - 'long_name': 'longitude', - 'standard_name': 'longitude'}) - return coords_out + coords['lon'] = Variable(dims, xc, + attrs={'units': 'degrees_east', + 'long_name': 'longitude', + 'standard_name': 'longitude'}) + coords['lat'] = Variable(dims, yc, + attrs={'units': 'degrees_north', + 'long_name': 'latitude', + 'standard_name': 'latitude'}) + return coords From 228a5a33adbd70b67741dd8569b3cab55e7f8b14 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Tue, 14 Feb 2017 12:22:50 +0100 Subject: [PATCH 18/36] some progress with indexing --- xarray/backends/rasterio_.py | 102 ++++++++++++++----------- xarray/core/utils.py | 35 +++++++++ xarray/tests/test_backends.py | 140 ++++++++++++++++++++++++++-------- 3 files changed, 201 insertions(+), 76 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 6f7df475196..d05903bca85 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -15,20 +15,56 @@ _rio_varname = 'raster' +_error_mess = 'The kind of indexing operation you are trying to do is not ' \ + 'valid on RasterIO files. Try to load your data with ds.load()' \ + ' first' class RasterioArrayWrapper(NDArrayMixin): def __init__(self, ds): - self._ds = ds - self.array = ds.read() + self.ds = ds + self._shape = self.ds.count, self.ds.height, self.ds.width @property def dtype(self): - return np.dtype(self._ds.dtypes[0]) + return np.dtype(self.ds.dtypes[0]) \ + + @property + def shape(self): + return self._shape def __getitem__(self, key): - if key == () and self.ndim == 0: - return self.array.get_value() - return self.array[key] + + # make our job a bit easier + key = indexing.canonicalize_indexer(key, len(self.shape)) + + # bands cannot be windowed but they can be listed + bands, n = key[0], self.shape[0] + if isinstance(bands, slice): + start = bands.start if bands.start is not None else 0 + stop = bands.stop if bands.stop is not None else n + if bands.step is not None and bands.step != 1: + raise IndexError(_error_mess) + bands = np.arange(start, stop) + # be sure we give out a list + bands = (np.asarray(bands) + 1).tolist() + + # but other dims can + window = [] + for k, n in zip(key[1:], self.shape[1:]): + if isinstance(k, slice): + start = k.start if k.start is not None else 0 + stop = k.stop if k.stop is not None else n + if k.step is not None and k.step != 1: + raise IndexError(_error_mess) + else: + k = np.asarray(k).flatten() + start = k[0] + stop = k[-1] + 1 + if (stop - start) != len(k): + raise IndexError(_error_mess) + window.append((start, stop)) + + return self.ds.read(bands, window=window) class RasterioDataStore(AbstractDataStore): @@ -36,7 +72,7 @@ class RasterioDataStore(AbstractDataStore): """ def __init__(self, filename, mode='r'): - # TODO: is the rasterio.Env() really necessary, and if yes where? + # TODO: is the rasterio.Env() really necessary, and if yes: when? with rasterio.Env(): self.ds = rasterio.open(filename, mode=mode) @@ -45,26 +81,29 @@ def __init__(self, filename, mode='r'): dx, dy = self.ds.res[0], -self.ds.res[1] x0 = self.ds.bounds.right if dx < 0 else self.ds.bounds.left y0 = self.ds.bounds.top if dy < 0 else self.ds.bounds.bottom - y = np.linspace(start=y0, num=ny, stop=(y0 + (ny-1) * dy)) - x = np.linspace(start=x0, num=nx, stop=(x0 + (nx-1) * dx)) + x = np.linspace(start=x0, num=nx, stop=(x0 + (nx - 1) * dx)) + y = np.linspace(start=y0, num=ny, stop=(y0 + (ny - 1) * dy)) - self.coords = OrderedDict() - self.coords['y'] = Variable(('y', ), y) - self.coords['x'] = Variable(('x', ), x) + self._vars = OrderedDict() + self._vars['y'] = Variable(('y',), y) + self._vars['x'] = Variable(('x',), x) # Get dims if self.ds.count >= 1: self.dims = ('band', 'y', 'x') - self.coords['band'] = Variable(('band', ), - np.atleast_1d(self.ds.indexes)) + self._vars['band'] = Variable(('band',), + np.atleast_1d(self.ds.indexes)) else: - raise ValueError('unknown dims') + raise ValueError('Unknown dims') self._attrs = OrderedDict() with suppress(AttributeError): - for attr_name in ['crs', 'transform', 'proj']: + for attr_name in ['crs']: self._attrs[attr_name] = getattr(self.ds, attr_name) + # Get data + self._vars[_rio_varname] = self.open_store_variable(_rio_varname) + def open_store_variable(self, var): if var != _rio_varname: raise ValueError('Rasterio variables are named %s' % _rio_varname) @@ -72,10 +111,7 @@ def open_store_variable(self, var): return Variable(self.dims, data, self._attrs) def get_variables(self): - # Get lat lon coordinates - vars = _try_to_get_latlon_coords(self.coords, self._attrs) - vars[_rio_varname] = self.open_store_variable(_rio_varname) - return FrozenOrderedDict(vars) + return FrozenOrderedDict(self._vars) def get_attrs(self): return Frozen(self._attrs) @@ -85,29 +121,3 @@ def get_dimensions(self): def close(self): self.ds.close() - - -def _try_to_get_latlon_coords(coords, attrs): - from rasterio.warp import transform - - if 'crs' in attrs: - proj = attrs['crs'] - # TODO: if the proj is already PlateCarree, making 2D coordinates - # is not the best thing to do here. - ny, nx = len(coords['y']), len(coords['x']) - x, y = np.meshgrid(coords['x'], coords['y']) - # Rasterio works with 1D arrays - xc, yc = transform(proj, {'init': 'EPSG:4326'}, - x.flatten(), y.flatten()) - xc = np.asarray(xc).reshape((ny, nx)) - yc = np.asarray(yc).reshape((ny, nx)) - dims = ('y', 'x') - coords['lon'] = Variable(dims, xc, - attrs={'units': 'degrees_east', - 'long_name': 'longitude', - 'standard_name': 'longitude'}) - coords['lat'] = Variable(dims, yc, - attrs={'units': 'degrees_north', - 'long_name': 'latitude', - 'standard_name': 'latitude'}) - return coords diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 89d1462328c..54b14037a9f 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -489,3 +489,38 @@ def ensure_us_time_resolution(val): elif np.issubdtype(val.dtype, np.timedelta64): val = val.astype('timedelta64[us]') return val + + +def get_latlon_coords_from_crs(ds, crs=None): + """Currently very specific function, but coul dbe generalized.""" + + from .. import DataArray + + try: + from rasterio.warp import transform + except ImportError: + raise ImportError('add_latlon_coords_from_crs needs RasterIO.') + + if crs is None: + if 'crs' in ds.attrs: + crs = ds.attrs['crs'] + else: + raise ValueError('crs not found') + + ny, nx = len(ds['y']), len(ds['x']) + x, y = np.meshgrid(ds['x'], ds['y']) + # Rasterio works with 1D arrays + xc, yc = transform(crs, {'init': 'EPSG:4326'}, + x.flatten(), y.flatten()) + xc = np.asarray(xc).reshape((ny, nx)) + yc = np.asarray(yc).reshape((ny, nx)) + dims = ('y', 'x') + ds['lon'] = DataArray(xc, dims=dims, + attrs={'units': 'degrees_east', + 'long_name': 'longitude', + 'standard_name': 'longitude'}) + ds['lat'] = DataArray(yc, dims=dims, + attrs={'units': 'degrees_north', + 'long_name': 'latitude', + 'standard_name': 'latitude'}) + return ds diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index acbced9ee50..a7ff79283d9 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1432,21 +1432,6 @@ class TestPyNioAutocloseTrue(TestPyNio): @requires_rasterio class TestRasterIO(CFEncodedDataTest, Only32BitTypes, TestCase): - # def setUp(self): - # - # - # name = 'test_latlong.tif' - # - # - # name = 'test_utm.tif' - # transform = from_origin(-300, 200, 1000, 1000) - # with rasterio.open( - # name, 'w', - # driver='GTiff', height=3, width=4, count=1, - # transform=transform, - # dtype=rasterio.float32) as s: - # s.write(a, indexes=1) - def test_write_store(self): # RasterIO is read-only for now pass @@ -1455,53 +1440,64 @@ def test_orthogonal_indexing(self): # RasterIO also does not support list-like indexing pass - def test_latlong_coords(self): + def test_latlong_basics(self): import rasterio from rasterio.transform import from_origin + from ..core.utils import get_latlon_coords_from_crs # Create a geotiff file in latlong proj - data = np.arange(12, dtype=rasterio.float32).reshape(3, 4) with create_tmp_file(suffix='.tif') as tmp_file: - transform = from_origin(1, 2, 0.5, 1.) + # data + nx, ny = 8, 10 + data = np.arange(80, dtype=rasterio.float32).reshape(ny, nx) + transform = from_origin(1, 2, 0.5, 2.) with rasterio.open( tmp_file, 'w', - driver='GTiff', height=3, width=4, count=1, + driver='GTiff', height=ny, width=nx, count=1, crs='+proj=latlong', transform=transform, dtype=rasterio.float32) as s: s.write(data, indexes=1) actual = xr.open_dataset(tmp_file, engine='rasterio') + # ref expected = Dataset() - expected['x'] = ('x', [1, 1.5, 2, 2.5]) - expected['y'] = ('y', [2., 1, 0]) + expected['x'] = ('x', np.arange(nx)*0.5 + 1) + expected['y'] = ('y', -np.arange(ny)*2 + 2) expected['band'] = ('band', [1]) expected['raster'] = (('band', 'y', 'x'), data[np.newaxis, ...]) lon, lat = np.meshgrid(expected['x'], expected['y']) expected['lon'] = (('y', 'x'), lon) expected['lat'] = (('y', 'x'), lat) + # tests assert_allclose(actual.y, expected.y) assert_allclose(actual.x, expected.x) assert_allclose(actual.raster, expected.raster) + + actual = get_latlon_coords_from_crs(actual) assert_allclose(actual.lon, expected.lon) assert_allclose(actual.lat, expected.lat) - print(actual) + assert 'crs' in actual.attrs - def test_utm_coords(self): + def test_utm_basics(self): import rasterio from rasterio.transform import from_origin + from ..core.utils import get_latlon_coords_from_crs # Create a geotiff file in utm proj - data = np.arange(24, dtype=rasterio.float32).reshape(2, 3, 4) with create_tmp_file(suffix='.tif') as tmp_file: - transform = from_origin(-3000, 1000, 1000, 1000) + # data + nx, ny, nz = 4, 3, 3 + data = np.arange(nx*ny*nz, + dtype=rasterio.float32).reshape(nz, ny, nx) + transform = from_origin(5000, 80000, 1000, 2000.) with rasterio.open( tmp_file, 'w', - driver='GTiff', height=3, width=4, count=2, + driver='GTiff', height=ny, width=nx, count=nz, crs={'units': 'm', 'no_defs': True, 'ellps': 'WGS84', 'proj': 'utm', 'zone': 18}, transform=transform, @@ -1509,17 +1505,101 @@ def test_utm_coords(self): s.write(data) actual = xr.open_dataset(tmp_file, engine='rasterio') + # ref expected = Dataset() - expected['x'] = ('x', [-3000., -2000, -1000, 0]) - expected['y'] = ('y', [1000., 0, -1000]) - expected['band'] = ('band', [1, 2]) + expected['x'] = ('x', np.arange(nx)*1000 + 5000) + expected['y'] = ('y', -np.arange(ny)*2000 + 80000) + expected['band'] = ('band', [1, 2, 3]) expected['raster'] = (('band', 'y', 'x'), data) + # data obtained independently with pyproj + lon = np.array( + [[-79.44429834, -79.43533803, -79.42637762, -79.4174171], + [-79.44428102, -79.43532075, -79.42636037, -79.41739988], + [-79.44426413, -79.4353039, -79.42634355, -79.4173831]]) + lat = np.array( + [[0.72159393, 0.72160275, 0.72161156, 0.72162034], + [0.70355411, 0.70356271, 0.70357129, 0.70357986], + [0.68551428, 0.68552266, 0.68553103, 0.68553937]]) + expected['lon'] = (('y', 'x'), lon) + expected['lat'] = (('y', 'x'), lat) + + # tests assert_allclose(actual.y, expected.y) assert_allclose(actual.x, expected.x) assert_allclose(actual.raster, expected.raster) - print(actual) + actual = get_latlon_coords_from_crs(actual) + assert_allclose(actual.lon, expected.lon) + assert_allclose(actual.lat, expected.lat) + + assert 'crs' in actual.attrs + + def test_indexing(self): + + import rasterio + from rasterio.transform import from_origin + + # Create a geotiff file in latlong proj + with create_tmp_file(suffix='.tif') as tmp_file: + # data + nx, ny, nz = 8, 10, 3 + data = np.arange(nx*ny*nz, + dtype=rasterio.float32).reshape(nz, ny, nx) + transform = from_origin(1, 2, 0.5, 2.) + with rasterio.open( + tmp_file, 'w', + driver='GTiff', height=ny, width=nx, count=nz, + crs='+proj=latlong', + transform=transform, + dtype=rasterio.float32) as s: + s.write(data) + actual = xr.open_dataset(tmp_file, engine='rasterio') + + # ref + expected = Dataset() + expected['x'] = ('x', np.arange(nx)*0.5 + 1) + expected['y'] = ('y', -np.arange(ny)*2 + 2) + expected['band'] = ('band', [1, 2, 3]) + expected['raster'] = (('band', 'y', 'x'), data) + + # tests + _ex = expected.isel(band=1) + _ac = actual.isel(band=1) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.band, _ex.band) + assert_allclose(_ac.raster, _ex.raster) + + _ex = expected.isel(x=slice(2, 5), y=slice(5, 7)) + _ac = actual.isel(x=slice(2, 5), y=slice(5, 7)) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.raster, _ex.raster) + + _ex = expected.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) + _ac = actual.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.raster, _ex.raster) + + _ex = expected.isel(x=1, y=2) + _ac = actual.isel(x=1, y=2) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + # TODO: this doesnt work properly because of the shape + # assert_allclose(_ac.raster, _ex.raster) + np.testing.assert_allclose(_ac.raster.values.flatten(), + _ex.raster.values) + + _ex = expected.isel(band=0, x=1, y=2) + _ac = actual.isel(band=0, x=1, y=2) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + # TODO: this doesnt work properly because of the shape + # assert_allclose(_ac.raster, _ex.raster) + np.testing.assert_allclose(_ac.raster.values.flatten(), + _ex.raster.values) class TestEncodingInvalid(TestCase): From 4b57d1c1a9f97bbdd878db28fb856f05e7e4b16c Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Sun, 5 Mar 2017 10:46:22 +0100 Subject: [PATCH 19/36] cosmetic changes --- xarray/backends/rasterio_.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index d05903bca85..a7e992a21ab 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -1,4 +1,3 @@ -import copy import numpy as np try: @@ -6,7 +5,7 @@ except ImportError: rasterio = False -from .. import Variable, DataArray +from .. import Variable from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin from ..core import indexing from ..core.pycompat import OrderedDict, suppress @@ -15,18 +14,19 @@ _rio_varname = 'raster' -_error_mess = 'The kind of indexing operation you are trying to do is not ' \ - 'valid on RasterIO files. Try to load your data with ds.load()' \ - ' first' +_error_mess = 'The kind of indexing operation you are trying to do is not ' +'valid on RasterIO files. Try to load your data with ds.load()' +'first.' class RasterioArrayWrapper(NDArrayMixin): def __init__(self, ds): self.ds = ds self._shape = self.ds.count, self.ds.height, self.ds.width + self._ndims = len(self.shape) @property def dtype(self): - return np.dtype(self.ds.dtypes[0]) \ + return np.dtype(self.ds.dtypes[0]) @property def shape(self): @@ -35,7 +35,7 @@ def shape(self): def __getitem__(self, key): # make our job a bit easier - key = indexing.canonicalize_indexer(key, len(self.shape)) + key = indexing.canonicalize_indexer(key, self._ndims) # bands cannot be windowed but they can be listed bands, n = key[0], self.shape[0] From 2d21b4b6a9d60e2fa26497ab9f36e760c2f5162f Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Wed, 3 May 2017 22:19:31 +0200 Subject: [PATCH 20/36] Conflicts --- ci/requirements-py27-cdat+pynio.yml | 1 - ci/requirements-py27-windows.yml | 6 ++---- ci/requirements-py35.yml | 4 +++- ci/requirements-py36-netcdf4-dev.yml | 3 ++- ci/requirements-py36-pandas-dev.yml | 2 ++ ci/requirements-py36-pydap.yml | 1 - ci/requirements-py36.yml | 2 ++ 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+pynio.yml index 8ebb3c5bb0d..c88263fcfba 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+pynio.yml @@ -18,7 +18,6 @@ dependencies: - scipy - seaborn - toolz - - cyordereddict - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py27-windows.yml b/ci/requirements-py27-windows.yml index 3ef89e17a99..caa77627acc 100644 --- a/ci/requirements-py27-windows.yml +++ b/ci/requirements-py27-windows.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=2.7 - dask @@ -13,7 +15,3 @@ dependencies: - scipy - seaborn - toolz - - pip: - - coveralls - - pytest-cov - - pydap diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index 0200bf4e770..1c7a4558c91 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=3.5 - dask @@ -13,7 +15,7 @@ dependencies: - scipy - seaborn - toolz - - rasterio>=1.0 + - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36-netcdf4-dev.yml b/ci/requirements-py36-netcdf4-dev.yml index 20147c33d12..033d1f41b4d 100644 --- a/ci/requirements-py36-netcdf4-dev.yml +++ b/ci/requirements-py36-netcdf4-dev.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=3.6 - cython @@ -12,7 +14,6 @@ dependencies: - pandas - scipy - toolz - - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36-pandas-dev.yml b/ci/requirements-py36-pandas-dev.yml index bd09df3716b..ebcec868f76 100644 --- a/ci/requirements-py36-pandas-dev.yml +++ b/ci/requirements-py36-pandas-dev.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=3.6 - cython diff --git a/ci/requirements-py36-pydap.yml b/ci/requirements-py36-pydap.yml index 2c5bfea3fe9..c10bd93f928 100644 --- a/ci/requirements-py36-pydap.yml +++ b/ci/requirements-py36-pydap.yml @@ -13,7 +13,6 @@ dependencies: - numpy - scipy - toolz - - rasterio>=1.0 - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index 774bf42a644..5840a0157ba 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -1,4 +1,6 @@ name: test_env +channels: + - conda-forge dependencies: - python=3.6 - dask From c2cb927e141cd4628df5540516c555f5f31d9d13 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Wed, 3 May 2017 22:30:22 +0200 Subject: [PATCH 21/36] More rebase --- doc/_static/dataset-diagram-build.sh | 0 xarray/core/accessors.py | 150 +++++++++++++++++++++++++++ xarray/tests/test_accessors.py | 96 +++++++++++++++++ 3 files changed, 246 insertions(+) mode change 100644 => 100755 doc/_static/dataset-diagram-build.sh create mode 100644 xarray/core/accessors.py create mode 100644 xarray/tests/test_accessors.py diff --git a/doc/_static/dataset-diagram-build.sh b/doc/_static/dataset-diagram-build.sh old mode 100644 new mode 100755 diff --git a/xarray/core/accessors.py b/xarray/core/accessors.py new file mode 100644 index 00000000000..7360b9764ae --- /dev/null +++ b/xarray/core/accessors.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from .common import is_datetime_like +from .pycompat import dask_array_type + +from functools import partial + +import numpy as np +import pandas as pd + + +def _season_from_months(months): + """Compute season (DJF, MAM, JJA, SON) from month ordinal + """ + # TODO: Move "season" accessor upstream into pandas + seasons = np.array(['DJF', 'MAM', 'JJA', 'SON']) + months = np.asarray(months) + return seasons[(months // 3) % 4] + + +def _access_through_series(values, name): + """Coerce an array of datetime-like values to a pandas Series and + access requested datetime component + """ + values_as_series = pd.Series(values.ravel()) + if name == "season": + months = values_as_series.dt.month.values + field_values = _season_from_months(months) + else: + field_values = getattr(values_as_series.dt, name).values + return field_values.reshape(values.shape) + + +def _get_date_field(values, name, dtype): + """Indirectly access pandas' libts.get_date_field by wrapping data + as a Series and calling through `.dt` attribute. + + Parameters + ---------- + values : np.ndarray or dask.array-like + Array-like container of datetime-like values + name : str + Name of datetime field to access + dtype : dtype-like + dtype for output date field values + + Returns + ------- + datetime_fields : same type as values + Array-like of datetime fields accessed for each element in values + + """ + if isinstance(values, dask_array_type): + from dask.array import map_blocks + return map_blocks(_access_through_series, + values, name, dtype=dtype) + else: + return _access_through_series(values, name) + + +class DatetimeAccessor(object): + """Access datetime fields for DataArrays with datetime-like dtypes. + + Similar to pandas, fields can be accessed through the `.dt` attribute + for applicable DataArrays: + + >>> ds = xarray.Dataset({'time': pd.date_range(start='2000/01/01', + ... freq='D', periods=100)}) + >>> ds.time.dt + + >>> ds.time.dt.dayofyear[:5] + + array([1, 2, 3, 4, 5], dtype=int32) + Coordinates: + * time (time) datetime64[ns] 2000-01-01 2000-01-02 2000-01-03 ... + + All of the pandas fields are accessible here. Note that these fields are + not calendar-aware; if your datetimes are encoded with a non-Gregorian + calendar (e.g. a 360-day calendar) using netcdftime, then some fields like + `dayofyear` may not be accurate. + + """ + def __init__(self, xarray_obj): + if not is_datetime_like(xarray_obj.dtype): + raise TypeError("'dt' accessor only available for " + "DataArray with datetime64 or timedelta64 dtype") + self._obj = xarray_obj + + def _tslib_field_accessor(name, docstring=None, dtype=None): + def f(self, dtype=dtype): + if dtype is None: + dtype = self._obj.dtype + obj_type = type(self._obj) + result = _get_date_field(self._obj.data, name, dtype) + return obj_type(result, name=name, + coords=self._obj.coords, dims=self._obj.dims) + + f.__name__ = name + f.__doc__ = docstring + return property(f) + + year = _tslib_field_accessor('year', "The year of the datetime", np.int64) + month = _tslib_field_accessor( + 'month', "The month as January=1, December=12", np.int64 + ) + day = _tslib_field_accessor('day', "The days of the datetime", np.int64) + hour = _tslib_field_accessor('hour', "The hours of the datetime", np.int64) + minute = _tslib_field_accessor( + 'minute', "The minutes of the datetime", np.int64 + ) + second = _tslib_field_accessor( + 'second', "The seconds of the datetime", np.int64 + ) + microsecond = _tslib_field_accessor( + 'microsecond', "The microseconds of the datetime", np.int64 + ) + nanosecond = _tslib_field_accessor( + 'nanosecond', "The nanoseconds of the datetime", np.int64 + ) + weekofyear = _tslib_field_accessor( + 'weekofyear', "The week ordinal of the year", np.int64 + ) + week = weekofyear + dayofweek = _tslib_field_accessor( + 'dayofweek', "The day of the week with Monday=0, Sunday=6", np.int64 + ) + weekday = dayofweek + + weekday_name = _tslib_field_accessor( + 'weekday_name', "The name of day in a week (ex: Friday)", object + ) + + dayofyear = _tslib_field_accessor( + 'dayofyear', "The ordinal day of the year", np.int64 + ) + quarter = _tslib_field_accessor('quarter', "The quarter of the date") + days_in_month = _tslib_field_accessor( + 'days_in_month', "The number of days in the month", np.int64 + ) + daysinmonth = days_in_month + + season = _tslib_field_accessor( + "season", "Season of the year (ex: DJF)", object + ) + + time = _tslib_field_accessor( + "time", "Timestamps corresponding to datetimes", object + ) \ No newline at end of file diff --git a/xarray/tests/test_accessors.py b/xarray/tests/test_accessors.py new file mode 100644 index 00000000000..dd1d77cf0af --- /dev/null +++ b/xarray/tests/test_accessors.py @@ -0,0 +1,96 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import xarray as xr +import numpy as np +import pandas as pd + +from . import TestCase, requires_dask + + +class TestDatetimeAccessor(TestCase): + def setUp(self): + nt = 100 + data = np.random.rand(10, 10, nt) + lons = np.linspace(0, 11, 10) + lats = np.linspace(0, 20, 10) + self.times = pd.date_range(start="2000/01/01", freq='H', periods=nt) + + self.data = xr.DataArray(data, coords=[lons, lats, self.times], + dims=['lon', 'lat', 'time'], name='data') + + self.times_arr = np.random.choice(self.times, size=(10, 10, nt)) + self.times_data = xr.DataArray(self.times_arr, + coords=[lons, lats, self.times], + dims=['lon', 'lat', 'time'], + name='data') + + def test_field_access(self): + years = xr.DataArray(self.times.year, name='year', + coords=[self.times, ], dims=['time', ]) + months = xr.DataArray(self.times.month, name='month', + coords=[self.times, ], dims=['time', ]) + days = xr.DataArray(self.times.day, name='day', + coords=[self.times, ], dims=['time', ]) + hours = xr.DataArray(self.times.hour, name='hour', + coords=[self.times, ], dims=['time', ]) + + self.assertDataArrayEqual(years, self.data.time.dt.year) + self.assertDataArrayEqual(months, self.data.time.dt.month) + self.assertDataArrayEqual(days, self.data.time.dt.day) + self.assertDataArrayEqual(hours, self.data.time.dt.hour) + + def test_not_datetime_type(self): + nontime_data = self.data.copy() + int_data = np.arange(len(self.data.time)).astype('int8') + nontime_data['time'].values = int_data + with self.assertRaisesRegexp(TypeError, 'dt'): + nontime_data.time.dt + + @requires_dask + def test_dask_field_access(self): + import dask.array as da + + years = self.times_data.dt.year + months = self.times_data.dt.month + hours = self.times_data.dt.hour + days = self.times_data.dt.day + + dask_times_arr = da.from_array(self.times_arr, chunks=(5, 5, 50)) + dask_times_2d = xr.DataArray(dask_times_arr, + coords=self.data.coords, + dims=self.data.dims, + name='data') + dask_year = dask_times_2d.dt.year + dask_month = dask_times_2d.dt.month + dask_day = dask_times_2d.dt.day + dask_hour = dask_times_2d.dt.hour + + # Test that the data isn't eagerly evaluated + assert isinstance(dask_year.data, da.Array) + assert isinstance(dask_month.data, da.Array) + assert isinstance(dask_day.data, da.Array) + assert isinstance(dask_hour.data, da.Array) + + # Double check that outcome chunksize is unchanged + dask_chunks = dask_times_2d.chunks + self.assertEqual(dask_year.data.chunks, dask_chunks) + self.assertEqual(dask_month.data.chunks, dask_chunks) + self.assertEqual(dask_day.data.chunks, dask_chunks) + self.assertEqual(dask_hour.data.chunks, dask_chunks) + + # Check the actual output from the accessors + self.assertDataArrayEqual(years, dask_year.compute()) + self.assertDataArrayEqual(months, dask_month.compute()) + self.assertDataArrayEqual(days, dask_day.compute()) + self.assertDataArrayEqual(hours, dask_hour.compute()) + + def test_seasons(self): + dates = pd.date_range(start="2000/01/01", freq="M", periods=12) + dates = xr.DataArray(dates) + seasons = ["DJF", "DJF", "MAM", "MAM", "MAM", "JJA", "JJA", "JJA", + "SON", "SON", "SON", "DJF"] + seasons = xr.DataArray(seasons) + + self.assertArrayEqual(seasons.values, dates.dt.season.values) \ No newline at end of file From f86507e6cf1344a6c433323465f571ee17183bf1 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Wed, 17 May 2017 23:34:26 +0200 Subject: [PATCH 22/36] looking good now. Testing --- xarray/__init__.py | 2 +- xarray/backends/api.py | 12 +++++++- xarray/backends/rasterio_.py | 23 ++++++++++----- xarray/core/utils.py | 12 ++++++-- xarray/tests/test_backends.py | 54 ++++++++++++++++++++++++----------- 5 files changed, 75 insertions(+), 28 deletions(-) diff --git a/xarray/__init__.py b/xarray/__init__.py index c88a0176616..6817764433d 100644 --- a/xarray/__init__.py +++ b/xarray/__init__.py @@ -14,7 +14,7 @@ from .core.options import set_options from .backends.api import (open_dataset, open_dataarray, open_mfdataset, - save_mfdataset) + save_mfdataset, open_rasterio) from .conventions import decode_cf try: diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 3a3b9305c8b..7229524d011 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -174,7 +174,7 @@ def open_dataset(filename_or_obj, group=None, decode_cf=True, decode_coords : bool, optional If True, decode the 'coordinates' attribute to identify coordinates in the resulting dataset. - engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', 'rasterio'}, optional + engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio'}, optional Engine to use when reading files. If not provided, the default engine is chosen based on available dependencies, with a preference for 'netcdf4'. @@ -318,6 +318,16 @@ def maybe_decode_store(store, lock=False): return maybe_decode_store(store) +def open_rasterio(filename, add_latlon=True): + + store = backends.RasterioDataStore(filename) + ds = conventions.decode_cf(store) + if add_latlon: + from ..core.utils import add_latlon_coords_from_crs + ds = add_latlon_coords_from_crs(ds) + return ds + + def open_dataarray(*args, **kwargs): """Open an DataArray from a netCDF file containing a single data variable. diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index a7e992a21ab..e4616ff73fe 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -6,7 +6,7 @@ rasterio = False from .. import Variable -from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin +from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin, is_scalar from ..core import indexing from ..core.pycompat import OrderedDict, suppress @@ -14,9 +14,10 @@ _rio_varname = 'raster' -_error_mess = 'The kind of indexing operation you are trying to do is not ' -'valid on RasterIO files. Try to load your data with ds.load()' -'first.' +_error_mess = ('The kind of indexing operation you are trying to do is not ' + 'valid on RasterIO files. Try to load your data with ds.load()' + 'first.') + class RasterioArrayWrapper(NDArrayMixin): def __init__(self, ds): @@ -48,15 +49,20 @@ def __getitem__(self, key): # be sure we give out a list bands = (np.asarray(bands) + 1).tolist() - # but other dims can + # but other dims can only be windowed window = [] - for k, n in zip(key[1:], self.shape[1:]): + squeeze_axis = [] + for i, (k, n) in enumerate(zip(key[1:], self.shape[1:])): if isinstance(k, slice): start = k.start if k.start is not None else 0 stop = k.stop if k.stop is not None else n if k.step is not None and k.step != 1: raise IndexError(_error_mess) else: + if is_scalar(k): + # windowed operations will always return an array which + # we will have to squeeze later on + squeeze_axis.append(i+1) k = np.asarray(k).flatten() start = k[0] stop = k[-1] + 1 @@ -64,7 +70,10 @@ def __getitem__(self, key): raise IndexError(_error_mess) window.append((start, stop)) - return self.ds.read(bands, window=window) + out = self.ds.read(bands, window=window) + if squeeze_axis: + out = np.squeeze(out, axis=squeeze_axis) + return out class RasterioDataStore(AbstractDataStore): diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 54b14037a9f..6e0f720a1e1 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -491,8 +491,16 @@ def ensure_us_time_resolution(val): return val -def get_latlon_coords_from_crs(ds, crs=None): - """Currently very specific function, but coul dbe generalized.""" +def add_latlon_coords_from_crs(ds, crs=None): + """Computes the longitudes and latitudes out of the x and y coordinates + of a dataset and a coordinate reference system (crs). If crs isn't provided + it will look for "crs" in the dataset's attributes. + + Needs rasterIO to be installe. + + Note that this function could be generalized to other coordinates or + for all datasets with a valid crs. + """ from .. import DataArray diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index a7ff79283d9..701a58f2fd9 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1444,7 +1444,6 @@ def test_latlong_basics(self): import rasterio from rasterio.transform import from_origin - from ..core.utils import get_latlon_coords_from_crs # Create a geotiff file in latlong proj with create_tmp_file(suffix='.tif') as tmp_file: @@ -1459,7 +1458,7 @@ def test_latlong_basics(self): transform=transform, dtype=rasterio.float32) as s: s.write(data, indexes=1) - actual = xr.open_dataset(tmp_file, engine='rasterio') + actual = xr.open_rasterio(tmp_file) # ref expected = Dataset() @@ -1475,8 +1474,6 @@ def test_latlong_basics(self): assert_allclose(actual.y, expected.y) assert_allclose(actual.x, expected.x) assert_allclose(actual.raster, expected.raster) - - actual = get_latlon_coords_from_crs(actual) assert_allclose(actual.lon, expected.lon) assert_allclose(actual.lat, expected.lat) @@ -1486,7 +1483,6 @@ def test_utm_basics(self): import rasterio from rasterio.transform import from_origin - from ..core.utils import get_latlon_coords_from_crs # Create a geotiff file in utm proj with create_tmp_file(suffix='.tif') as tmp_file: @@ -1503,7 +1499,7 @@ def test_utm_basics(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_dataset(tmp_file, engine='rasterio') + actual = xr.open_rasterio(tmp_file) # ref expected = Dataset() @@ -1528,8 +1524,6 @@ def test_utm_basics(self): assert_allclose(actual.y, expected.y) assert_allclose(actual.x, expected.x) assert_allclose(actual.raster, expected.raster) - - actual = get_latlon_coords_from_crs(actual) assert_allclose(actual.lon, expected.lon) assert_allclose(actual.lat, expected.lat) @@ -1554,7 +1548,9 @@ def test_indexing(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_dataset(tmp_file, engine='rasterio') + actual = xr.open_rasterio(tmp_file, add_latlon=False) + assert 'lon' not in actual + assert 'lat' not in actual # ref expected = Dataset() @@ -1583,23 +1579,47 @@ def test_indexing(self): assert_allclose(_ac.x, _ex.x) assert_allclose(_ac.raster, _ex.raster) + # Selecting lists of bands is fine + _ex = expected.isel(band=[1, 2]) + _ac = actual.isel(band=[1, 2]) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.band, _ex.band) + assert_allclose(_ac.raster, _ex.raster) + + # but on x and y only windowed operations are allowed + with self.assertRaisesRegexp(IndexError, 'not valid on RasterIO'): + _ = actual.isel(x=[2, 4], y=[1, 3]).raster.values + _ex = expected.isel(x=1, y=2) _ac = actual.isel(x=1, y=2) assert_allclose(_ac.y, _ex.y) assert_allclose(_ac.x, _ex.x) - # TODO: this doesnt work properly because of the shape - # assert_allclose(_ac.raster, _ex.raster) - np.testing.assert_allclose(_ac.raster.values.flatten(), - _ex.raster.values) + assert_allclose(_ac.raster, _ex.raster) _ex = expected.isel(band=0, x=1, y=2) _ac = actual.isel(band=0, x=1, y=2) assert_allclose(_ac.y, _ex.y) assert_allclose(_ac.x, _ex.x) - # TODO: this doesnt work properly because of the shape - # assert_allclose(_ac.raster, _ex.raster) - np.testing.assert_allclose(_ac.raster.values.flatten(), - _ex.raster.values) + assert_allclose(_ac.raster, _ex.raster) + + _ex = expected.isel(band=0, x=1, y=slice(5, 7)) + _ac = actual.isel(band=0, x=1, y=slice(5, 7)) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.raster, _ex.raster) + + _ex = expected.isel(band=0, x=slice(2, 5), y=2) + _ac = actual.isel(band=0, x=slice(2, 5), y=2) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.raster, _ex.raster) + + _ex = expected.isel(band=[0], x=slice(2, 5), y=[2]) + _ac = actual.isel(band=[0], x=slice(2, 5), y=[2]) + assert_allclose(_ac.y, _ex.y) + assert_allclose(_ac.x, _ex.x) + assert_allclose(_ac.raster, _ex.raster) class TestEncodingInvalid(TestCase): From e1a5b31b20caa86d4c9e48b4f671131e0a4052fa Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 18 May 2017 00:49:02 +0200 Subject: [PATCH 23/36] docs --- doc/_static/rasterio_example.png | Bin 0 -> 94421 bytes doc/api.rst | 1 + doc/io.rst | 42 +++++++++++++++++++++++++++++++ xarray/backends/api.py | 23 ++++++++++++++--- xarray/backends/rasterio_.py | 5 ++-- xarray/core/utils.py | 2 +- xarray/tests/test_backends.py | 6 ++--- 7 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 doc/_static/rasterio_example.png diff --git a/doc/_static/rasterio_example.png b/doc/_static/rasterio_example.png new file mode 100644 index 0000000000000000000000000000000000000000..e72fc6e605c5c77dc8fb55f8978a07241b989e9c GIT binary patch literal 94421 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 WjS$mSiWUX + Dimensions: (band: 3, x: 791, y: 718) + Coordinates: + * y (y) float64 2.827e+06 2.827e+06 2.826e+06 2.826e+06 2.826e+06 ... + * x (x) float64 1.02e+05 1.023e+05 1.026e+05 1.029e+05 1.032e+05 ... + * band (band) int64 1 2 3 + lon (y, x) float64 -78.96 -78.96 -78.95 -78.95 -78.95 -78.94 -78.94 ... + lat (y, x) float64 25.51 25.51 25.51 25.51 25.51 25.51 25.51 25.51 ... + Data variables: + raster (band, y, x) uint8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... + Attributes: + crs: CRS({'init': 'epsg:32618'}) + + In [6]: ds.raster.sel(band=1).plot() + +.. image:: _static/rasterio_example.png + +.. warning:: + + This feature has been added in xarray v0.9.6 and should still be + considered as being experimental. Please report any bug you may find + on xarray's github repository. + +.. _rasterio: https://mapbox.github.io/rasterio/ +.. _test files: https://github.com/mapbox/rasterio/blob/master/tests/data/RGB.byte.tif + .. _io.pynio: Formats supported by PyNIO diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 7229524d011..831b243c2fb 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -318,10 +318,27 @@ def maybe_decode_store(store, lock=False): return maybe_decode_store(store) -def open_rasterio(filename, add_latlon=True): +def open_rasterio(filename, add_latlon=False): + """Open a file with RasterIO (experimental). - store = backends.RasterioDataStore(filename) - ds = conventions.decode_cf(store) + This should work with any file that rasterio can open (typically: + geoTIFF). The x and y coordinates are generated automatically out of the + file's geoinformation. + + Parameters + ---------- + filename : str + path to the file to open + add_latlon : bool, optional + if the file ships with valid geoinformation, longitudes and latitudes + can be computed and added to the dataset as non-dimension coordinates + + Returns + ------- + dataset : Dataset + The newly created dataset. + """ + ds = conventions.decode_cf(backends.RasterioDataStore(filename)) if add_latlon: from ..core.utils import add_latlon_coords_from_crs ds = add_latlon_coords_from_crs(ds) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index e4616ff73fe..2ebc9fa3e7a 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -60,8 +60,8 @@ def __getitem__(self, key): raise IndexError(_error_mess) else: if is_scalar(k): - # windowed operations will always return an array which - # we will have to squeeze later on + # windowed operations will always return an array + # we will have to squeeze it later squeeze_axis.append(i+1) k = np.asarray(k).flatten() start = k[0] @@ -81,7 +81,6 @@ class RasterioDataStore(AbstractDataStore): """ def __init__(self, filename, mode='r'): - # TODO: is the rasterio.Env() really necessary, and if yes: when? with rasterio.Env(): self.ds = rasterio.open(filename, mode=mode) diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 6e0f720a1e1..f4985a913cb 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -531,4 +531,4 @@ def add_latlon_coords_from_crs(ds, crs=None): attrs={'units': 'degrees_north', 'long_name': 'latitude', 'standard_name': 'latitude'}) - return ds + return ds.set_coords(['lon', 'lat']) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 701a58f2fd9..c4c29cd9572 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1458,7 +1458,7 @@ def test_latlong_basics(self): transform=transform, dtype=rasterio.float32) as s: s.write(data, indexes=1) - actual = xr.open_rasterio(tmp_file) + actual = xr.open_rasterio(tmp_file, add_latlon=True) # ref expected = Dataset() @@ -1499,7 +1499,7 @@ def test_utm_basics(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_rasterio(tmp_file) + actual = xr.open_rasterio(tmp_file, add_latlon=True) # ref expected = Dataset() @@ -1548,7 +1548,7 @@ def test_indexing(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_rasterio(tmp_file, add_latlon=False) + actual = xr.open_rasterio(tmp_file) assert 'lon' not in actual assert 'lat' not in actual From 7cb8baf37fa21409065aea25fa8612adf8782c8c Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 18 May 2017 00:50:50 +0200 Subject: [PATCH 24/36] whats new --- doc/whats-new.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 7eccecf541e..cdcfebe4004 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -30,6 +30,12 @@ Enhancements By `Chun-Wei Yuan `_ and `Kyle Heuton `_. +- New backend to open raster files with the + `rasterio `_ library. + By `Joe Hamman `_, + `Nic Wayand `_ and + `Fabien Maussion `_ + Bug fixes ~~~~~~~~~ From 48c7268828e2862dad28a30e5983c849c2c32151 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 18 May 2017 01:02:03 +0200 Subject: [PATCH 25/36] fix test --- xarray/tests/test_backends.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index c4c29cd9572..0a3a9d2e37a 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1469,6 +1469,7 @@ def test_latlong_basics(self): lon, lat = np.meshgrid(expected['x'], expected['y']) expected['lon'] = (('y', 'x'), lon) expected['lat'] = (('y', 'x'), lat) + expected = expected.set_coords(['lon', 'lat']) # tests assert_allclose(actual.y, expected.y) @@ -1519,6 +1520,7 @@ def test_utm_basics(self): [0.68551428, 0.68552266, 0.68553103, 0.68553937]]) expected['lon'] = (('y', 'x'), lon) expected['lat'] = (('y', 'x'), lat) + expected = expected.set_coords(['lon', 'lat']) # tests assert_allclose(actual.y, expected.y) From 70bd03a79fd24fd543a4db7a1e5c980a986c10e2 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Wed, 24 May 2017 18:32:50 +0200 Subject: [PATCH 26/36] reviews --- xarray/__init__.py | 2 +- xarray/backends/__init__.py | 1 - xarray/backends/api.py | 24 ++-- xarray/backends/rasterio_.py | 150 +++++++++------------ xarray/core/utils.py | 43 ------ xarray/tests/test_backends.py | 247 +++++++++++++++------------------- 6 files changed, 185 insertions(+), 282 deletions(-) diff --git a/xarray/__init__.py b/xarray/__init__.py index 6817764433d..23f16d03e03 100644 --- a/xarray/__init__.py +++ b/xarray/__init__.py @@ -14,7 +14,7 @@ from .core.options import set_options from .backends.api import (open_dataset, open_dataarray, open_mfdataset, - save_mfdataset, open_rasterio) + open_rasterio, save_mfdataset) from .conventions import decode_cf try: diff --git a/xarray/backends/__init__.py b/xarray/backends/__init__.py index 192a3c57db2..a082bd53e5e 100644 --- a/xarray/backends/__init__.py +++ b/xarray/backends/__init__.py @@ -10,4 +10,3 @@ from .pynio_ import NioDataStore from .scipy_ import ScipyDataStore from .h5netcdf_ import H5NetCDFStore -from .rasterio_ import RasterioDataStore diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 831b243c2fb..506d18717d1 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -318,31 +318,25 @@ def maybe_decode_store(store, lock=False): return maybe_decode_store(store) -def open_rasterio(filename, add_latlon=False): - """Open a file with RasterIO (experimental). +def open_rasterio(filename): + """Open a file with rasterio (experimental). - This should work with any file that rasterio can open (typically: - geoTIFF). The x and y coordinates are generated automatically out of the + This should work with any file that rasterio can open (most often: + geoTIFF). The x and y coordinates are generated automatically from the file's geoinformation. Parameters ---------- filename : str - path to the file to open - add_latlon : bool, optional - if the file ships with valid geoinformation, longitudes and latitudes - can be computed and added to the dataset as non-dimension coordinates + Path to the file to open. Returns ------- - dataset : Dataset - The newly created dataset. + data : DataArray + The newly created DataArray. """ - ds = conventions.decode_cf(backends.RasterioDataStore(filename)) - if add_latlon: - from ..core.utils import add_latlon_coords_from_crs - ds = add_latlon_coords_from_crs(ds) - return ds + from .rasterio_ import rasterio_to_dataarray + return rasterio_to_dataarray(filename) def open_dataarray(*args, **kwargs): diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 2ebc9fa3e7a..ca6e8e3a134 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -5,127 +5,109 @@ except ImportError: rasterio = False -from .. import Variable -from ..core.utils import FrozenOrderedDict, Frozen, NDArrayMixin, is_scalar +from .. import DataArray +from ..core.utils import NDArrayMixin, is_scalar from ..core import indexing -from ..core.pycompat import OrderedDict, suppress -from .common import AbstractDataStore - -_rio_varname = 'raster' - -_error_mess = ('The kind of indexing operation you are trying to do is not ' - 'valid on RasterIO files. Try to load your data with ds.load()' - 'first.') +_ERROR_MSG = ('The kind of indexing operation you are trying to do is not ' + 'valid on rasterio files. Try to load your data with ds.load()' + 'first.') class RasterioArrayWrapper(NDArrayMixin): - def __init__(self, ds): - self.ds = ds - self._shape = self.ds.count, self.ds.height, self.ds.width + """A wrapper around rasterio dataset objects""" + def __init__(self, riods): + self.riods = riods + self._shape = self.riods.count, self.riods.height, self.riods.width self._ndims = len(self.shape) @property def dtype(self): - return np.dtype(self.ds.dtypes[0]) + return np.dtype(self.riods.dtypes[0]) @property def shape(self): return self._shape + def __exit__(self, exception_type, exception_value, traceback): + self.riods.close() + def __getitem__(self, key): # make our job a bit easier key = indexing.canonicalize_indexer(key, self._ndims) # bands cannot be windowed but they can be listed - bands, n = key[0], self.shape[0] - if isinstance(bands, slice): - start = bands.start if bands.start is not None else 0 - stop = bands.stop if bands.stop is not None else n - if bands.step is not None and bands.step != 1: - raise IndexError(_error_mess) - bands = np.arange(start, stop) + band_key = key[0] + n_bands = self.shape[0] + if isinstance(band_key, slice): + start, stop, step = band_key.indices(n_bands) + if step is not None and step != 1: + raise IndexError(_ERROR_MSG) + band_key = np.arange(start, stop) # be sure we give out a list - bands = (np.asarray(bands) + 1).tolist() + band_key = (np.asarray(band_key) + 1).tolist() # but other dims can only be windowed window = [] squeeze_axis = [] for i, (k, n) in enumerate(zip(key[1:], self.shape[1:])): if isinstance(k, slice): - start = k.start if k.start is not None else 0 - stop = k.stop if k.stop is not None else n - if k.step is not None and k.step != 1: - raise IndexError(_error_mess) + start, stop, step = k.indices(n) + if step is not None and step != 1: + raise IndexError(_ERROR_MSG) else: if is_scalar(k): # windowed operations will always return an array # we will have to squeeze it later squeeze_axis.append(i+1) - k = np.asarray(k).flatten() - start = k[0] - stop = k[-1] + 1 - if (stop - start) != len(k): - raise IndexError(_error_mess) + start = k + stop = k+1 + else: + start = k[0] + stop = k[-1] + 1 + if not np.all(k == np.arange(start, stop)): + raise IndexError(_ERROR_MSG) window.append((start, stop)) - out = self.ds.read(bands, window=window) + out = self.riods.read(band_key, window=window) if squeeze_axis: out = np.squeeze(out, axis=squeeze_axis) return out -class RasterioDataStore(AbstractDataStore): - """Store for accessing datasets via Rasterio +def rasterio_to_dataarray(filename): + """Open a file with rasterio. + + This should work with any file that rasterio can open (most often: + geoTIFF). The x and y coordinates are generated automatically from the + file's geoinformation. """ - def __init__(self, filename, mode='r'): - - with rasterio.Env(): - self.ds = rasterio.open(filename, mode=mode) - - # Get coords - nx, ny = self.ds.width, self.ds.height - dx, dy = self.ds.res[0], -self.ds.res[1] - x0 = self.ds.bounds.right if dx < 0 else self.ds.bounds.left - y0 = self.ds.bounds.top if dy < 0 else self.ds.bounds.bottom - x = np.linspace(start=x0, num=nx, stop=(x0 + (nx - 1) * dx)) - y = np.linspace(start=y0, num=ny, stop=(y0 + (ny - 1) * dy)) - - self._vars = OrderedDict() - self._vars['y'] = Variable(('y',), y) - self._vars['x'] = Variable(('x',), x) - - # Get dims - if self.ds.count >= 1: - self.dims = ('band', 'y', 'x') - self._vars['band'] = Variable(('band',), - np.atleast_1d(self.ds.indexes)) - else: - raise ValueError('Unknown dims') - - self._attrs = OrderedDict() - with suppress(AttributeError): - for attr_name in ['crs']: - self._attrs[attr_name] = getattr(self.ds, attr_name) - - # Get data - self._vars[_rio_varname] = self.open_store_variable(_rio_varname) - - def open_store_variable(self, var): - if var != _rio_varname: - raise ValueError('Rasterio variables are named %s' % _rio_varname) - data = indexing.LazilyIndexedArray(RasterioArrayWrapper(self.ds)) - return Variable(self.dims, data, self._attrs) - - def get_variables(self): - return FrozenOrderedDict(self._vars) - - def get_attrs(self): - return Frozen(self._attrs) - - def get_dimensions(self): - return Frozen(self.dims) - - def close(self): - self.ds.close() + + riods = rasterio.open(filename, mode='r') + + # Get geo coords + nx, ny = riods.width, riods.height + dx, dy = riods.res[0], -riods.res[1] + x0 = riods.bounds.right if dx < 0 else riods.bounds.left + y0 = riods.bounds.top if dy < 0 else riods.bounds.bottom + x = np.linspace(start=x0, num=nx, stop=(x0 + (nx - 1) * dx)) + y = np.linspace(start=y0, num=ny, stop=(y0 + (ny - 1) * dy)) + + # Get bands + if riods.count < 1: + raise ValueError('Unknown dims') + bands = np.asarray(riods.indexes) + + # Attributes + attrs = {} + if hasattr(riods, 'crs'): + # CRS is a dict-like object specific to rasterio + # We convert it back to a PROJ4 string using rasterio itself + attrs['crs'] = riods.crs.to_string() + # Maybe we'd like to parse other attributes here (for later) + + data = indexing.LazilyIndexedArray(RasterioArrayWrapper(riods)) + return DataArray(data=data, dims=('band', 'y', 'x'), + coords={'band': bands, 'y': y, 'x': x}, + attrs=attrs) diff --git a/xarray/core/utils.py b/xarray/core/utils.py index f4985a913cb..89d1462328c 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -489,46 +489,3 @@ def ensure_us_time_resolution(val): elif np.issubdtype(val.dtype, np.timedelta64): val = val.astype('timedelta64[us]') return val - - -def add_latlon_coords_from_crs(ds, crs=None): - """Computes the longitudes and latitudes out of the x and y coordinates - of a dataset and a coordinate reference system (crs). If crs isn't provided - it will look for "crs" in the dataset's attributes. - - Needs rasterIO to be installe. - - Note that this function could be generalized to other coordinates or - for all datasets with a valid crs. - """ - - from .. import DataArray - - try: - from rasterio.warp import transform - except ImportError: - raise ImportError('add_latlon_coords_from_crs needs RasterIO.') - - if crs is None: - if 'crs' in ds.attrs: - crs = ds.attrs['crs'] - else: - raise ValueError('crs not found') - - ny, nx = len(ds['y']), len(ds['x']) - x, y = np.meshgrid(ds['x'], ds['y']) - # Rasterio works with 1D arrays - xc, yc = transform(crs, {'init': 'EPSG:4326'}, - x.flatten(), y.flatten()) - xc = np.asarray(xc).reshape((ny, nx)) - yc = np.asarray(yc).reshape((ny, nx)) - dims = ('y', 'x') - ds['lon'] = DataArray(xc, dims=dims, - attrs={'units': 'degrees_east', - 'long_name': 'longitude', - 'standard_name': 'longitude'}) - ds['lat'] = DataArray(yc, dims=dims, - attrs={'units': 'degrees_north', - 'long_name': 'latitude', - 'standard_name': 'latitude'}) - return ds.set_coords(['lon', 'lat']) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 0a3a9d2e37a..810ddf8cadc 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -22,12 +22,12 @@ from xarray.backends.common import robust_getitem from xarray.backends.netCDF4_ import _extract_nc4_variable_encoding from xarray.core import indexing -from xarray.core.pycompat import iteritems, PY2, PY3, ExitStack +from xarray.core.pycompat import iteritems, PY2, PY3, ExitStack, basestring from . import (TestCase, requires_scipy, requires_netCDF4, requires_pydap, requires_scipy_or_netCDF4, requires_dask, requires_h5netcdf, requires_pynio, has_netCDF4, has_scipy, assert_allclose, - flaky, requires_rasterio) + flaky, requires_rasterio, assert_identical) from .test_dataset import create_test_data try: @@ -1430,57 +1430,17 @@ class TestPyNioAutocloseTrue(TestPyNio): @requires_rasterio -class TestRasterIO(CFEncodedDataTest, Only32BitTypes, TestCase): +class TestRasterio(CFEncodedDataTest, Only32BitTypes, TestCase): def test_write_store(self): - # RasterIO is read-only for now + # rasterio is read-only for now pass def test_orthogonal_indexing(self): - # RasterIO also does not support list-like indexing + # rasterio also does not support list-like indexing pass - def test_latlong_basics(self): - - import rasterio - from rasterio.transform import from_origin - - # Create a geotiff file in latlong proj - with create_tmp_file(suffix='.tif') as tmp_file: - # data - nx, ny = 8, 10 - data = np.arange(80, dtype=rasterio.float32).reshape(ny, nx) - transform = from_origin(1, 2, 0.5, 2.) - with rasterio.open( - tmp_file, 'w', - driver='GTiff', height=ny, width=nx, count=1, - crs='+proj=latlong', - transform=transform, - dtype=rasterio.float32) as s: - s.write(data, indexes=1) - actual = xr.open_rasterio(tmp_file, add_latlon=True) - - # ref - expected = Dataset() - expected['x'] = ('x', np.arange(nx)*0.5 + 1) - expected['y'] = ('y', -np.arange(ny)*2 + 2) - expected['band'] = ('band', [1]) - expected['raster'] = (('band', 'y', 'x'), data[np.newaxis, ...]) - lon, lat = np.meshgrid(expected['x'], expected['y']) - expected['lon'] = (('y', 'x'), lon) - expected['lat'] = (('y', 'x'), lat) - expected = expected.set_coords(['lon', 'lat']) - - # tests - assert_allclose(actual.y, expected.y) - assert_allclose(actual.x, expected.x) - assert_allclose(actual.raster, expected.raster) - assert_allclose(actual.lon, expected.lon) - assert_allclose(actual.lat, expected.lat) - - assert 'crs' in actual.attrs - - def test_utm_basics(self): + def test_serialization(self): import rasterio from rasterio.transform import from_origin @@ -1500,36 +1460,55 @@ def test_utm_basics(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_rasterio(tmp_file, add_latlon=True) - # ref - expected = Dataset() - expected['x'] = ('x', np.arange(nx)*1000 + 5000) - expected['y'] = ('y', -np.arange(ny)*2000 + 80000) - expected['band'] = ('band', [1, 2, 3]) - expected['raster'] = (('band', 'y', 'x'), data) - - # data obtained independently with pyproj - lon = np.array( - [[-79.44429834, -79.43533803, -79.42637762, -79.4174171], - [-79.44428102, -79.43532075, -79.42636037, -79.41739988], - [-79.44426413, -79.4353039, -79.42634355, -79.4173831]]) - lat = np.array( - [[0.72159393, 0.72160275, 0.72161156, 0.72162034], - [0.70355411, 0.70356271, 0.70357129, 0.70357986], - [0.68551428, 0.68552266, 0.68553103, 0.68553937]]) - expected['lon'] = (('y', 'x'), lon) - expected['lat'] = (('y', 'x'), lat) - expected = expected.set_coords(['lon', 'lat']) + # Tests + expected = DataArray(data, dims=('band', 'y', 'x'), + coords={'band': [1, 2, 3], + 'y': -np.arange(ny) * 2000 + 80000, + 'x': np.arange(nx) * 1000 + 5000, + }) + rioda = xr.open_rasterio(tmp_file) + assert_allclose(rioda, expected) + assert 'crs' in rioda.attrs + assert isinstance(rioda.attrs['crs'], basestring) + + # Write it to a netcdf and read again (roundtrip) + with create_tmp_file(suffix='.nc') as tmp_nc_file: + rioda.to_netcdf(tmp_nc_file) + ncds = xr.open_dataarray(tmp_nc_file) + assert_identical(rioda, ncds) - # tests - assert_allclose(actual.y, expected.y) - assert_allclose(actual.x, expected.x) - assert_allclose(actual.raster, expected.raster) - assert_allclose(actual.lon, expected.lon) - assert_allclose(actual.lat, expected.lat) + # Create a geotiff file in latlong proj + with create_tmp_file(suffix='.tif') as tmp_file: + # data + nx, ny = 8, 10 + data = np.arange(80, dtype=rasterio.float32).reshape(ny, nx) + transform = from_origin(1, 2, 0.5, 2.) + with rasterio.open( + tmp_file, 'w', + driver='GTiff', height=ny, width=nx, count=1, + crs='+proj=latlong', + transform=transform, + dtype=rasterio.float32) as s: + s.write(data, indexes=1) - assert 'crs' in actual.attrs + # Tests + expected = DataArray(data[np.newaxis, ...], + dims=('band', 'y', 'x'), + coords={'band': [1], + 'y': -np.arange(ny)*2 + 2, + 'x': np.arange(nx)*0.5 + 1, + }) + rioda = xr.open_rasterio(tmp_file) + assert_allclose(rioda, expected) + assert 'crs' in rioda.attrs + assert isinstance(rioda.attrs['crs'], basestring) + + # Write it to a netcdf and read again (roundtrip) + with create_tmp_file(suffix='.nc') as tmp_nc_file: + rioda.to_netcdf(tmp_nc_file) + ncds = xr.open_dataarray(tmp_nc_file) + assert_identical(rioda, ncds) def test_indexing(self): @@ -1551,77 +1530,69 @@ def test_indexing(self): dtype=rasterio.float32) as s: s.write(data) actual = xr.open_rasterio(tmp_file) - assert 'lon' not in actual - assert 'lat' not in actual # ref - expected = Dataset() - expected['x'] = ('x', np.arange(nx)*0.5 + 1) - expected['y'] = ('y', -np.arange(ny)*2 + 2) - expected['band'] = ('band', [1, 2, 3]) - expected['raster'] = (('band', 'y', 'x'), data) + expected = DataArray(data, dims=('band', 'y', 'x'), + coords={'x': np.arange(nx)*0.5 + 1, + 'y': -np.arange(ny)*2 + 2, + 'band': [1, 2, 3]}) # tests - _ex = expected.isel(band=1) - _ac = actual.isel(band=1) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.band, _ex.band) - assert_allclose(_ac.raster, _ex.raster) - - _ex = expected.isel(x=slice(2, 5), y=slice(5, 7)) - _ac = actual.isel(x=slice(2, 5), y=slice(5, 7)) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) - - _ex = expected.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) - _ac = actual.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) + # assert_allclose checks all data + coordinates + assert_allclose(actual, expected) + + # Slicing + ex = expected.isel(x=slice(2, 5), y=slice(5, 7)) + ac = actual.isel(x=slice(2, 5), y=slice(5, 7)) + assert_allclose(ac, ex) + + ex = expected.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) + ac = actual.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) + assert_allclose(ac, ex) # Selecting lists of bands is fine - _ex = expected.isel(band=[1, 2]) - _ac = actual.isel(band=[1, 2]) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.band, _ex.band) - assert_allclose(_ac.raster, _ex.raster) - - # but on x and y only windowed operations are allowed - with self.assertRaisesRegexp(IndexError, 'not valid on RasterIO'): - _ = actual.isel(x=[2, 4], y=[1, 3]).raster.values - - _ex = expected.isel(x=1, y=2) - _ac = actual.isel(x=1, y=2) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) - - _ex = expected.isel(band=0, x=1, y=2) - _ac = actual.isel(band=0, x=1, y=2) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) - - _ex = expected.isel(band=0, x=1, y=slice(5, 7)) - _ac = actual.isel(band=0, x=1, y=slice(5, 7)) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) - - _ex = expected.isel(band=0, x=slice(2, 5), y=2) - _ac = actual.isel(band=0, x=slice(2, 5), y=2) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) - - _ex = expected.isel(band=[0], x=slice(2, 5), y=[2]) - _ac = actual.isel(band=[0], x=slice(2, 5), y=[2]) - assert_allclose(_ac.y, _ex.y) - assert_allclose(_ac.x, _ex.x) - assert_allclose(_ac.raster, _ex.raster) + ex = expected.isel(band=[1, 2]) + ac = actual.isel(band=[1, 2]) + assert_allclose(ac, ex) + ex = expected.isel(band=[0, 2]) + ac = actual.isel(band=[0, 2]) + assert_allclose(ac, ex) + + # but on x and y only windowed operations are allowed, more + # exotic slicing should raise an error + with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): + _ = actual.isel(x=[2, 4], y=[1, 3]).values + with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): + _ = actual.isel(x=[4, 2]).values + with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): + _ = actual.isel(x=slice(5, 2, -1)).values + + # Integer indexing + ex = expected.isel(band=1) + ac = actual.isel(band=1) + assert_allclose(ac, ex) + + ex = expected.isel(x=1, y=2) + ac = actual.isel(x=1, y=2) + assert_allclose(ac, ex) + + ex = expected.isel(band=0, x=1, y=2) + ac = actual.isel(band=0, x=1, y=2) + assert_allclose(ac, ex) + + # Mixed + ex = expected.isel(band=0, x=1, y=slice(5, 7)) + ac = actual.isel(band=0, x=1, y=slice(5, 7)) + assert_allclose(ac, ex) + + ex = expected.isel(band=0, x=slice(2, 5), y=2) + ac = actual.isel(band=0, x=slice(2, 5), y=2) + assert_allclose(ac, ex) + + # One-element lists + ex = expected.isel(band=[0], x=slice(2, 5), y=[2]) + ac = actual.isel(band=[0], x=slice(2, 5), y=[2]) + assert_allclose(ac, ex) class TestEncodingInvalid(TestCase): From 3f181442cb77b770942defd09cce4723ac17d483 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Wed, 24 May 2017 19:25:05 +0200 Subject: [PATCH 27/36] docs --- doc/_static/rasterio_example.png | Bin 94421 -> 0 bytes doc/gallery/plot_rasterio.py | 55 +++++++++++++++++++++++++++++++ doc/io.rst | 24 +++++++------- xarray/backends/rasterio_.py | 21 +++++++----- 4 files changed, 79 insertions(+), 21 deletions(-) delete mode 100644 doc/_static/rasterio_example.png create mode 100644 doc/gallery/plot_rasterio.py diff --git a/doc/_static/rasterio_example.png b/doc/_static/rasterio_example.png deleted file mode 100644 index e72fc6e605c5c77dc8fb55f8978a07241b989e9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94421 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 WjS$mSiWUX - Dimensions: (band: 3, x: 791, y: 718) + + [1703814 values with dtype=uint8] Coordinates: + * band (band) int64 1 2 3 * y (y) float64 2.827e+06 2.827e+06 2.826e+06 2.826e+06 2.826e+06 ... * x (x) float64 1.02e+05 1.023e+05 1.026e+05 1.029e+05 1.032e+05 ... - * band (band) int64 1 2 3 - lon (y, x) float64 -78.96 -78.96 -78.95 -78.95 -78.95 -78.94 -78.94 ... - lat (y, x) float64 25.51 25.51 25.51 25.51 25.51 25.51 25.51 25.51 ... - Data variables: - raster (band, y, x) uint8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... Attributes: - crs: CRS({'init': 'epsg:32618'}) - - In [6]: ds.raster.sel(band=1).plot() + crs: +init=epsg:32618 -.. image:: _static/rasterio_example.png +The ``x`` and ``y`` coordinates are generated out of the file's metadata +(``bounds``, ``width``, ``height``), and they can be understood as cartesian +coordinates defined in the file's projection provided by the ``crs`` attribute. +``crs`` is a PROJ4 string which can be parsed by e.g. `pyproj`_ or rasterio. +See :ref:`recipes.rasterio` for an example of how to convert these to +longitudes and latitudes. .. warning:: @@ -425,6 +424,7 @@ rasterio is installed. Here is an example of how to use .. _rasterio: https://mapbox.github.io/rasterio/ .. _test files: https://github.com/mapbox/rasterio/blob/master/tests/data/RGB.byte.tif +.. _pyproj: https://github.com/jswhit/pyproj .. _io.pynio: diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index ca6e8e3a134..dd3a09d2f74 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import numpy as np try: @@ -9,6 +10,7 @@ from ..core.utils import NDArrayMixin, is_scalar from ..core import indexing + _ERROR_MSG = ('The kind of indexing operation you are trying to do is not ' 'valid on rasterio files. Try to load your data with ds.load()' 'first.') @@ -86,18 +88,20 @@ def rasterio_to_dataarray(filename): riods = rasterio.open(filename, mode='r') + coords = OrderedDict() + + # Get bands + if riods.count < 1: + raise ValueError('Unknown dims') + coords['band'] = np.asarray(riods.indexes) + # Get geo coords nx, ny = riods.width, riods.height dx, dy = riods.res[0], -riods.res[1] x0 = riods.bounds.right if dx < 0 else riods.bounds.left y0 = riods.bounds.top if dy < 0 else riods.bounds.bottom - x = np.linspace(start=x0, num=nx, stop=(x0 + (nx - 1) * dx)) - y = np.linspace(start=y0, num=ny, stop=(y0 + (ny - 1) * dy)) - - # Get bands - if riods.count < 1: - raise ValueError('Unknown dims') - bands = np.asarray(riods.indexes) + coords['y'] = np.linspace(start=y0, num=ny, stop=(y0 + (ny - 1) * dy)) + coords['x'] = np.linspace(start=x0, num=nx, stop=(x0 + (nx - 1) * dx)) # Attributes attrs = {} @@ -109,5 +113,4 @@ def rasterio_to_dataarray(filename): data = indexing.LazilyIndexedArray(RasterioArrayWrapper(riods)) return DataArray(data=data, dims=('band', 'y', 'x'), - coords={'band': bands, 'y': y, 'x': x}, - attrs=attrs) + coords=coords, attrs=attrs) From 1ae2d9be24195c29238ad58d675e359e00e19b4f Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 25 May 2017 23:46:15 +0200 Subject: [PATCH 28/36] more reviews --- doc/gallery/plot_rasterio.py | 9 +++-- xarray/__init__.py | 4 ++- xarray/backends/api.py | 21 ------------ xarray/backends/rasterio_.py | 62 ++++++++++++++++++++--------------- xarray/tests/test_backends.py | 4 +++ 5 files changed, 49 insertions(+), 51 deletions(-) diff --git a/doc/gallery/plot_rasterio.py b/doc/gallery/plot_rasterio.py index 9334f58cca5..0281c07a243 100644 --- a/doc/gallery/plot_rasterio.py +++ b/doc/gallery/plot_rasterio.py @@ -14,6 +14,7 @@ original map projection. """ +import os import numpy as np import xarray as xr import cartopy.crs as ccrs @@ -40,9 +41,8 @@ # Convert the DataArray to a dataset and set them as non-dimension coordinates riods = rioda.to_dataset(name='img') -riods['lon'] = (('y', 'x'), lon) -riods['lat'] = (('y', 'x'), lat) -riods = riods.set_coords(['lon', 'lat']) +riods.coords['lon'] = (('y', 'x'), lon) +riods.coords['lat'] = (('y', 'x'), lat) # Compute a greyscale out of the rgb image riods['greyscale'] = riods.img.mean(dim='band') @@ -53,3 +53,6 @@ cmap='Greys_r', add_colorbar=False) ax.coastlines('10m', color='r'); plt.show() + +# Delete the file +os.remove('RGB.byte.tif') \ No newline at end of file diff --git a/xarray/__init__.py b/xarray/__init__.py index 23f16d03e03..19d9b63866d 100644 --- a/xarray/__init__.py +++ b/xarray/__init__.py @@ -14,7 +14,9 @@ from .core.options import set_options from .backends.api import (open_dataset, open_dataarray, open_mfdataset, - open_rasterio, save_mfdataset) + save_mfdataset) +from .backends.rasterio_ import open_rasterio + from .conventions import decode_cf try: diff --git a/xarray/backends/api.py b/xarray/backends/api.py index b862c73dc9b..c32a16ae4da 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -320,27 +320,6 @@ def maybe_decode_store(store, lock=False): return maybe_decode_store(store) -def open_rasterio(filename): - """Open a file with rasterio (experimental). - - This should work with any file that rasterio can open (most often: - geoTIFF). The x and y coordinates are generated automatically from the - file's geoinformation. - - Parameters - ---------- - filename : str - Path to the file to open. - - Returns - ------- - data : DataArray - The newly created DataArray. - """ - from .rasterio_ import rasterio_to_dataarray - return rasterio_to_dataarray(filename) - - def open_dataarray(*args, **kwargs): """Open an DataArray from a netCDF file containing a single data variable. diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index dd3a09d2f74..fa767aa330b 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -1,13 +1,8 @@ from collections import OrderedDict import numpy as np -try: - import rasterio -except ImportError: - rasterio = False - from .. import DataArray -from ..core.utils import NDArrayMixin, is_scalar +from ..core.utils import DunderArrayMixin, NdimSizeLenMixin, is_scalar from ..core import indexing @@ -16,24 +11,25 @@ 'first.') -class RasterioArrayWrapper(NDArrayMixin): +class RasterioArrayWrapper(NdimSizeLenMixin, DunderArrayMixin): """A wrapper around rasterio dataset objects""" - def __init__(self, riods): - self.riods = riods - self._shape = self.riods.count, self.riods.height, self.riods.width + def __init__(self, rasterio_ds): + self.rasterio_ds = rasterio_ds + self._shape = (rasterio_ds.count, rasterio_ds.height, + rasterio_ds.width) self._ndims = len(self.shape) @property def dtype(self): - return np.dtype(self.riods.dtypes[0]) + dtypes = self.rasterio_ds.dtypes + if not np.all(np.asarray(dtypes) == dtypes[0]): + raise ValueError('All bands should have the same dtype') + return np.dtype(dtypes[0]) @property def shape(self): return self._shape - def __exit__(self, exception_type, exception_value, traceback): - self.riods.close() - def __getitem__(self, key): # make our job a bit easier @@ -58,34 +54,46 @@ def __getitem__(self, key): start, stop, step = k.indices(n) if step is not None and step != 1: raise IndexError(_ERROR_MSG) - else: - if is_scalar(k): + elif is_scalar(k): # windowed operations will always return an array # we will have to squeeze it later squeeze_axis.append(i+1) start = k stop = k+1 - else: - start = k[0] - stop = k[-1] + 1 - if not np.all(k == np.arange(start, stop)): - raise IndexError(_ERROR_MSG) + else: + k = np.asarray(k) + start = k[0] + stop = k[-1] + 1 + ids = np.arange(start, stop) + if not ((k.shape == ids.shape) and np.all(k == ids)): + raise IndexError(_ERROR_MSG) window.append((start, stop)) - out = self.riods.read(band_key, window=window) + out = self.rasterio_ds.read(band_key, window=window) if squeeze_axis: out = np.squeeze(out, axis=squeeze_axis) return out -def rasterio_to_dataarray(filename): - """Open a file with rasterio. +def open_rasterio(filename): + """Open a file with rasterio (experimental). This should work with any file that rasterio can open (most often: geoTIFF). The x and y coordinates are generated automatically from the file's geoinformation. + + Parameters + ---------- + filename : str + Path to the file to open. + + Returns + ------- + data : DataArray + The newly created DataArray. """ + import rasterio riods = rasterio.open(filename, mode='r') coords = OrderedDict() @@ -112,5 +120,7 @@ def rasterio_to_dataarray(filename): # Maybe we'd like to parse other attributes here (for later) data = indexing.LazilyIndexedArray(RasterioArrayWrapper(riods)) - return DataArray(data=data, dims=('band', 'y', 'x'), - coords=coords, attrs=attrs) + result = DataArray(data=data, dims=('band', 'y', 'x'), + coords=coords, attrs=attrs) + result._file_obj = riods + return result diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 502b1f8aeed..2305314db7a 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1581,6 +1581,10 @@ def test_indexing(self): assert_allclose(ac, ex) # Mixed + ex = actual.isel(x=slice(2), y=slice(2)) + ac = actual.isel(x=[0, 1], y=[0, 1]) + assert_allclose(ac, ex) + ex = expected.isel(band=0, x=1, y=slice(5, 7)) ac = actual.isel(band=0, x=1, y=slice(5, 7)) assert_allclose(ac, ex) From 955f6b9f30ccfe7b380aea3fceb993e2fc1a8fe7 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Tue, 30 May 2017 12:39:26 +0200 Subject: [PATCH 29/36] chunking and caching --- xarray/backends/rasterio_.py | 55 +++++++++++++++++++++++- xarray/tests/test_backends.py | 81 ++++++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index fa767aa330b..de203c41892 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -1,4 +1,6 @@ +import os from collections import OrderedDict +from distutils.version import LooseVersion import numpy as np from .. import DataArray @@ -75,7 +77,7 @@ def __getitem__(self, key): return out -def open_rasterio(filename): +def open_rasterio(filename, chunks=None, cache=None, lock=False): """Open a file with rasterio (experimental). This should work with any file that rasterio can open (most often: @@ -91,11 +93,32 @@ def open_rasterio(filename): ------- data : DataArray The newly created DataArray. + chunks : int, tuple or dict, optional + Chunk sizes along each dimension, e.g., ``5``, ``(5, 5)`` or + ``{'x': 5, 'y': 5}``. If chunks is provided, it used to load the new + DataArray into a dask array. This is an experimental feature; see the + documentation for more details. + cache : bool, optional + If True, cache data loaded from the underlying datastore in memory as + NumPy arrays when accessed to avoid reading from the underlying data- + store multiple times. Defaults to True unless you specify the `chunks` + argument to use dask, in which case it defaults to False. Does not + change the behavior of coordinates corresponding to dimensions, which + always load their data from disk into a ``pandas.Index``. + lock : False, True or threading.Lock, optional + If chunks is provided, this argument is passed on to + :py:func:`dask.array.from_array`. By default, a per-variable lock is + used when reading data from netCDF files with the netcdf4 and h5netcdf + engines to avoid issues with concurrent access when using dask's + multithreaded backend. """ import rasterio riods = rasterio.open(filename, mode='r') + if cache is None: + cache = chunks is None + coords = OrderedDict() # Get bands @@ -120,7 +143,37 @@ def open_rasterio(filename): # Maybe we'd like to parse other attributes here (for later) data = indexing.LazilyIndexedArray(RasterioArrayWrapper(riods)) + + # this lets you write arrays loaded with rasterio + data = indexing.CopyOnWriteArray(data) + if cache and (chunks is None): + data = indexing.MemoryCachedArray(data) + result = DataArray(data=data, dims=('band', 'y', 'x'), coords=coords, attrs=attrs) + + if chunks is not None: + try: + from dask.base import tokenize + except ImportError: + # raise the usual error if dask is entirely missing + import dask + + if LooseVersion(dask.__version__) < LooseVersion('0.6'): + raise ImportError( + 'xarray requires dask version 0.6 or newer') + else: + raise + + # augment the token with the file modification time + mtime = os.path.getmtime(filename) + token = tokenize(filename, mtime, chunks) + name_prefix = 'open_rasterio-%s' % token + # result = result.chunk(chunks, name_prefix=name_prefix, token=token, + # lock=lock) + result = result.chunk(chunks) + + # Make the file closeable result._file_obj = riods + return result diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 2305314db7a..fc50b2ff7b1 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1529,7 +1529,7 @@ def test_indexing(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_rasterio(tmp_file) + actual = xr.open_rasterio(tmp_file, cache=False) # ref expected = DataArray(data, dims=('band', 'y', 'x'), @@ -1598,6 +1598,85 @@ def test_indexing(self): ac = actual.isel(band=[0], x=slice(2, 5), y=[2]) assert_allclose(ac, ex) + def test_caching(self): + + import rasterio + from rasterio.transform import from_origin + + # Create a geotiff file in latlong proj + with create_tmp_file(suffix='.tif') as tmp_file: + # data + nx, ny, nz = 8, 10, 3 + data = np.arange(nx*ny*nz, + dtype=rasterio.float32).reshape(nz, ny, nx) + transform = from_origin(1, 2, 0.5, 2.) + with rasterio.open( + tmp_file, 'w', + driver='GTiff', height=ny, width=nx, count=nz, + crs='+proj=latlong', + transform=transform, + dtype=rasterio.float32) as s: + s.write(data) + + # Cache is the default + actual = xr.open_rasterio(tmp_file) + + # ref + expected = DataArray(data, dims=('band', 'y', 'x'), + coords={'x': np.arange(nx)*0.5 + 1, + 'y': -np.arange(ny)*2 + 2, + 'band': [1, 2, 3]}) + + # Without cache an error is raised + with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): + _ = actual.isel(x=[2, 4]).values + + # This should cache everything + assert_allclose(actual, expected) + + # once cached, non-windowed indexing should become possible + ac = actual.isel(x=[2, 4]) + ex = expected.isel(x=[2, 4]) + assert_allclose(ac, ex) + + def test_chunks(self): + + import rasterio + from rasterio.transform import from_origin + + # Create a geotiff file in latlong proj + with create_tmp_file(suffix='.tif') as tmp_file: + # data + nx, ny, nz = 8, 10, 3 + data = np.arange(nx*ny*nz, + dtype=rasterio.float32).reshape(nz, ny, nx) + transform = from_origin(1, 2, 0.5, 2.) + with rasterio.open( + tmp_file, 'w', + driver='GTiff', height=ny, width=nx, count=nz, + crs='+proj=latlong', + transform=transform, + dtype=rasterio.float32) as s: + s.write(data) + + # Chunk at open time + actual = xr.open_rasterio(tmp_file, chunks=(1, 2, 2)) + + # ref + expected = DataArray(data, dims=('band', 'y', 'x'), + coords={'x': np.arange(nx)*0.5 + 1, + 'y': -np.arange(ny)*2 + 2, + 'band': [1, 2, 3]}) + + # do some arithmetic + ac = actual.mean() + ex = expected.mean() + assert_allclose(ac, ex) + + ac = actual.sel(band=1).mean(dim='x') + ex = expected.sel(band=1).mean(dim='x') + assert_allclose(ac, ex) + class TestEncodingInvalid(TestCase): From 223ce0c8436120e0d18a067099a4ca9e23585868 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Tue, 30 May 2017 20:17:42 +0200 Subject: [PATCH 30/36] Final tweaks --- ci/requirements-py35.yml | 1 - doc/gallery/plot_rasterio.py | 8 ++++---- xarray/backends/rasterio_.py | 17 ++++++++--------- xarray/tests/test_backends.py | 15 ++++++++++----- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index 1c7a4558c91..f6a62ac72a6 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -15,7 +15,6 @@ dependencies: - scipy - seaborn - toolz - - rasterio - pip: - coveralls - pytest-cov diff --git a/doc/gallery/plot_rasterio.py b/doc/gallery/plot_rasterio.py index 0281c07a243..cfa0786d3f9 100644 --- a/doc/gallery/plot_rasterio.py +++ b/doc/gallery/plot_rasterio.py @@ -15,11 +15,12 @@ """ import os +import urllib.request import numpy as np import xarray as xr import cartopy.crs as ccrs import matplotlib.pyplot as plt -import urllib.request +from rasterio.warp import transform # Download the file from rasterio's repository url = 'https://github.com/mapbox/rasterio/raw/master/tests/data/RGB.byte.tif' @@ -33,7 +34,6 @@ x, y = np.meshgrid(rioda['x'], rioda['y']) # Rasterio works with 1D arrays -from rasterio.warp import transform lon, lat = transform(rioda.crs, {'init': 'EPSG:4326'}, x.flatten(), y.flatten()) lon = np.asarray(lon).reshape((ny, nx)) @@ -51,8 +51,8 @@ ax = plt.subplot(projection=ccrs.PlateCarree()) riods.greyscale.plot(ax=ax, x='lon', y='lat', transform=ccrs.PlateCarree(), cmap='Greys_r', add_colorbar=False) -ax.coastlines('10m', color='r'); +ax.coastlines('10m', color='r') plt.show() # Delete the file -os.remove('RGB.byte.tif') \ No newline at end of file +os.remove('RGB.byte.tif') diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index de203c41892..d1c5e233e00 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -80,8 +80,8 @@ def __getitem__(self, key): def open_rasterio(filename, chunks=None, cache=None, lock=False): """Open a file with rasterio (experimental). - This should work with any file that rasterio can open (most often: - geoTIFF). The x and y coordinates are generated automatically from the + This should work with any file that rasterio can open (most often: + geoTIFF). The x and y coordinates are generated automatically from the file's geoinformation. Parameters @@ -94,9 +94,9 @@ def open_rasterio(filename, chunks=None, cache=None, lock=False): data : DataArray The newly created DataArray. chunks : int, tuple or dict, optional - Chunk sizes along each dimension, e.g., ``5``, ``(5, 5)`` or - ``{'x': 5, 'y': 5}``. If chunks is provided, it used to load the new - DataArray into a dask array. This is an experimental feature; see the + Chunk sizes along each dimension, e.g., ``5``, ``(5, 5)`` or + ``{'x': 5, 'y': 5}``. If chunks is provided, it used to load the new + DataArray into a dask array. This is an experimental feature; see the documentation for more details. cache : bool, optional If True, cache data loaded from the underlying datastore in memory as @@ -153,12 +153,12 @@ def open_rasterio(filename, chunks=None, cache=None, lock=False): coords=coords, attrs=attrs) if chunks is not None: + # Logic borrowed from open_dataset try: from dask.base import tokenize except ImportError: # raise the usual error if dask is entirely missing import dask - if LooseVersion(dask.__version__) < LooseVersion('0.6'): raise ImportError( 'xarray requires dask version 0.6 or newer') @@ -169,9 +169,8 @@ def open_rasterio(filename, chunks=None, cache=None, lock=False): mtime = os.path.getmtime(filename) token = tokenize(filename, mtime, chunks) name_prefix = 'open_rasterio-%s' % token - # result = result.chunk(chunks, name_prefix=name_prefix, token=token, - # lock=lock) - result = result.chunk(chunks) + result = result.chunk(chunks, name_prefix=name_prefix, token=token, + lock=lock) # Make the file closeable result._file_obj = riods diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 5e798510fe9..888a78ba067 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -22,7 +22,7 @@ from xarray.backends.common import robust_getitem from xarray.backends.netCDF4_ import _extract_nc4_variable_encoding from xarray.core import indexing -from xarray.core.pycompat import iteritems, PY2, PY3, ExitStack, basestring +from xarray.core.pycompat import iteritems, PY2, ExitStack, basestring from . import (TestCase, requires_scipy, requires_netCDF4, requires_pydap, requires_scipy_or_netCDF4, requires_dask, requires_h5netcdf, @@ -1570,11 +1570,11 @@ def test_indexing(self): # but on x and y only windowed operations are allowed, more # exotic slicing should raise an error with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - _ = actual.isel(x=[2, 4], y=[1, 3]).values + actual.isel(x=[2, 4], y=[1, 3]).values with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - _ = actual.isel(x=[4, 2]).values + actual.isel(x=[4, 2]).values with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - _ = actual.isel(x=slice(5, 2, -1)).values + actual.isel(x=slice(5, 2, -1)).values # Integer indexing ex = expected.isel(band=1) @@ -1638,7 +1638,7 @@ def test_caching(self): # Without cache an error is raised with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - _ = actual.isel(x=[2, 4]).values + actual.isel(x=[2, 4]).values # This should cache everything assert_allclose(actual, expected) @@ -1648,6 +1648,7 @@ def test_caching(self): ex = expected.isel(x=[2, 4]) assert_allclose(ac, ex) + @requires_dask def test_chunks(self): import rasterio @@ -1671,6 +1672,10 @@ def test_chunks(self): # Chunk at open time actual = xr.open_rasterio(tmp_file, chunks=(1, 2, 2)) + import dask.array as da + self.assertIsInstance(actual.data, da.Array) + assert 'open_rasterio' in actual.data.name + # ref expected = DataArray(data, dims=('band', 'y', 'x'), coords={'x': np.arange(nx)*0.5 + 1, From 6cf2ce9949e9c8bdbb4d35bac174b84176adf8f2 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Wed, 31 May 2017 18:10:23 +0200 Subject: [PATCH 31/36] Lock-doc tweaks --- xarray/backends/api.py | 22 +++++----------------- xarray/backends/rasterio_.py | 30 +++++++++--------------------- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/xarray/backends/api.py b/xarray/backends/api.py index c32a16ae4da..616471ca7aa 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -181,11 +181,10 @@ def open_dataset(filename_or_obj, group=None, decode_cf=True, chunks : int or dict, optional If chunks is provided, it used to load the new dataset into dask arrays. ``chunks={}`` loads the dataset with dask using a single - chunk for all arrays. This is an experimental feature; see the - documentation for more details. + chunk for all arrays. lock : False, True or threading.Lock, optional If chunks is provided, this argument is passed on to - :py:func:`dask.array.from_array`. By default, a per-variable lock is + :py:func:`dask.array.from_array`. By default, a global lock is used when reading data from netCDF files with the netcdf4 and h5netcdf engines to avoid issues with concurrent access when using dask's multithreaded backend. @@ -228,17 +227,7 @@ def maybe_decode_store(store, lock=False): _protect_dataset_variables_inplace(ds, cache) if chunks is not None: - try: - from dask.base import tokenize - except ImportError: - # raise the usual error if dask is entirely missing - import dask - if LooseVersion(dask.__version__) < LooseVersion('0.6'): - raise ImportError( - 'xarray requires dask version 0.6 or newer') - else: - raise - + from dask.base import tokenize # if passed an actual file path, augment the token with # the file modification time if (isinstance(filename_or_obj, basestring) and @@ -369,11 +358,10 @@ def open_dataarray(*args, **kwargs): 'netcdf4'. chunks : int or dict, optional If chunks is provided, it used to load the new dataset into dask - arrays. This is an experimental feature; see the documentation for more - details. + arrays. lock : False, True or threading.Lock, optional If chunks is provided, this argument is passed on to - :py:func:`dask.array.from_array`. By default, a per-variable lock is + :py:func:`dask.array.from_array`. By default, a global lock is used when reading data from netCDF files with the netcdf4 and h5netcdf engines to avoid issues with concurrent access when using dask's multithreaded backend. diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index d1c5e233e00..8873e0dff49 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -6,6 +6,7 @@ from .. import DataArray from ..core.utils import DunderArrayMixin, NdimSizeLenMixin, is_scalar from ..core import indexing +from .common import GLOBAL_LOCK _ERROR_MSG = ('The kind of indexing operation you are trying to do is not ' @@ -96,21 +97,17 @@ def open_rasterio(filename, chunks=None, cache=None, lock=False): chunks : int, tuple or dict, optional Chunk sizes along each dimension, e.g., ``5``, ``(5, 5)`` or ``{'x': 5, 'y': 5}``. If chunks is provided, it used to load the new - DataArray into a dask array. This is an experimental feature; see the - documentation for more details. + DataArray into a dask array. cache : bool, optional If True, cache data loaded from the underlying datastore in memory as NumPy arrays when accessed to avoid reading from the underlying data- store multiple times. Defaults to True unless you specify the `chunks` - argument to use dask, in which case it defaults to False. Does not - change the behavior of coordinates corresponding to dimensions, which - always load their data from disk into a ``pandas.Index``. + argument to use dask, in which case it defaults to False. lock : False, True or threading.Lock, optional If chunks is provided, this argument is passed on to - :py:func:`dask.array.from_array`. By default, a per-variable lock is - used when reading data from netCDF files with the netcdf4 and h5netcdf - engines to avoid issues with concurrent access when using dask's - multithreaded backend. + :py:func:`dask.array.from_array`. By default, a global lock is + used to avoid issues with concurrent access to the same file when using + dask's multithreaded backend. """ import rasterio @@ -153,22 +150,13 @@ def open_rasterio(filename, chunks=None, cache=None, lock=False): coords=coords, attrs=attrs) if chunks is not None: - # Logic borrowed from open_dataset - try: - from dask.base import tokenize - except ImportError: - # raise the usual error if dask is entirely missing - import dask - if LooseVersion(dask.__version__) < LooseVersion('0.6'): - raise ImportError( - 'xarray requires dask version 0.6 or newer') - else: - raise - + from dask.base import tokenize # augment the token with the file modification time mtime = os.path.getmtime(filename) token = tokenize(filename, mtime, chunks) name_prefix = 'open_rasterio-%s' % token + if lock is None: + lock = GLOBAL_LOCK result = result.chunk(chunks, name_prefix=name_prefix, token=token, lock=lock) From 9193a2b7c5170529b5665a09a6c498f425f43383 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 1 Jun 2017 10:37:30 +0200 Subject: [PATCH 32/36] Add rasterio to other test suites --- ci/requirements-py27-cdat+pynio.yml | 1 + ci/requirements-py27-windows.yml | 1 + ci/requirements-py35.yml | 1 + ci/requirements-py36-windows.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/ci/requirements-py27-cdat+pynio.yml b/ci/requirements-py27-cdat+pynio.yml index c88263fcfba..113714cbfd6 100644 --- a/ci/requirements-py27-cdat+pynio.yml +++ b/ci/requirements-py27-cdat+pynio.yml @@ -18,6 +18,7 @@ dependencies: - scipy - seaborn - toolz + - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py27-windows.yml b/ci/requirements-py27-windows.yml index caa77627acc..cfd3d4262cc 100644 --- a/ci/requirements-py27-windows.yml +++ b/ci/requirements-py27-windows.yml @@ -15,3 +15,4 @@ dependencies: - scipy - seaborn - toolz + - rasterio diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml index f6a62ac72a6..1c7a4558c91 100644 --- a/ci/requirements-py35.yml +++ b/ci/requirements-py35.yml @@ -15,6 +15,7 @@ dependencies: - scipy - seaborn - toolz + - rasterio - pip: - coveralls - pytest-cov diff --git a/ci/requirements-py36-windows.yml b/ci/requirements-py36-windows.yml index e84a9346b59..70ff3e50a1b 100644 --- a/ci/requirements-py36-windows.yml +++ b/ci/requirements-py36-windows.yml @@ -15,3 +15,4 @@ dependencies: - scipy - seaborn - toolz + - rasterio From c7789483b4910b2eb9658c40ff63704badcf55cd Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 1 Jun 2017 11:07:50 +0200 Subject: [PATCH 33/36] use context managers in tests for windows --- xarray/tests/test_backends.py | 227 +++++++++++++++++----------------- 1 file changed, 116 insertions(+), 111 deletions(-) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 888a78ba067..026ab9d3582 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1476,16 +1476,16 @@ def test_serialization(self): 'y': -np.arange(ny) * 2000 + 80000, 'x': np.arange(nx) * 1000 + 5000, }) - rioda = xr.open_rasterio(tmp_file) - assert_allclose(rioda, expected) - assert 'crs' in rioda.attrs - assert isinstance(rioda.attrs['crs'], basestring) + with xr.open_rasterio(tmp_file) as rioda: + assert_allclose(rioda, expected) + assert 'crs' in rioda.attrs + assert isinstance(rioda.attrs['crs'], basestring) - # Write it to a netcdf and read again (roundtrip) - with create_tmp_file(suffix='.nc') as tmp_nc_file: - rioda.to_netcdf(tmp_nc_file) - ncds = xr.open_dataarray(tmp_nc_file) - assert_identical(rioda, ncds) + # Write it to a netcdf and read again (roundtrip) + with create_tmp_file(suffix='.nc') as tmp_nc_file: + rioda.to_netcdf(tmp_nc_file) + with xr.open_dataarray(tmp_nc_file) as ncds: + assert_identical(rioda, ncds) # Create a geotiff file in latlong proj with create_tmp_file(suffix='.tif') as tmp_file: @@ -1508,16 +1508,16 @@ def test_serialization(self): 'y': -np.arange(ny)*2 + 2, 'x': np.arange(nx)*0.5 + 1, }) - rioda = xr.open_rasterio(tmp_file) - assert_allclose(rioda, expected) - assert 'crs' in rioda.attrs - assert isinstance(rioda.attrs['crs'], basestring) + with xr.open_rasterio(tmp_file) as rioda: + assert_allclose(rioda, expected) + assert 'crs' in rioda.attrs + assert isinstance(rioda.attrs['crs'], basestring) - # Write it to a netcdf and read again (roundtrip) - with create_tmp_file(suffix='.nc') as tmp_nc_file: - rioda.to_netcdf(tmp_nc_file) - ncds = xr.open_dataarray(tmp_nc_file) - assert_identical(rioda, ncds) + # Write it to a netcdf and read again (roundtrip) + with create_tmp_file(suffix='.nc') as tmp_nc_file: + rioda.to_netcdf(tmp_nc_file) + with xr.open_dataarray(tmp_nc_file) as ncds: + assert_identical(rioda, ncds) def test_indexing(self): @@ -1538,7 +1538,6 @@ def test_indexing(self): transform=transform, dtype=rasterio.float32) as s: s.write(data) - actual = xr.open_rasterio(tmp_file, cache=False) # ref expected = DataArray(data, dims=('band', 'y', 'x'), @@ -1546,66 +1545,71 @@ def test_indexing(self): 'y': -np.arange(ny)*2 + 2, 'band': [1, 2, 3]}) - # tests - # assert_allclose checks all data + coordinates - assert_allclose(actual, expected) - - # Slicing - ex = expected.isel(x=slice(2, 5), y=slice(5, 7)) - ac = actual.isel(x=slice(2, 5), y=slice(5, 7)) - assert_allclose(ac, ex) - - ex = expected.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) - ac = actual.isel(band=slice(1, 2), x=slice(2, 5), y=slice(5, 7)) - assert_allclose(ac, ex) - - # Selecting lists of bands is fine - ex = expected.isel(band=[1, 2]) - ac = actual.isel(band=[1, 2]) - assert_allclose(ac, ex) - ex = expected.isel(band=[0, 2]) - ac = actual.isel(band=[0, 2]) - assert_allclose(ac, ex) - - # but on x and y only windowed operations are allowed, more - # exotic slicing should raise an error - with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - actual.isel(x=[2, 4], y=[1, 3]).values - with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - actual.isel(x=[4, 2]).values - with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - actual.isel(x=slice(5, 2, -1)).values - - # Integer indexing - ex = expected.isel(band=1) - ac = actual.isel(band=1) - assert_allclose(ac, ex) - - ex = expected.isel(x=1, y=2) - ac = actual.isel(x=1, y=2) - assert_allclose(ac, ex) - - ex = expected.isel(band=0, x=1, y=2) - ac = actual.isel(band=0, x=1, y=2) - assert_allclose(ac, ex) - - # Mixed - ex = actual.isel(x=slice(2), y=slice(2)) - ac = actual.isel(x=[0, 1], y=[0, 1]) - assert_allclose(ac, ex) - - ex = expected.isel(band=0, x=1, y=slice(5, 7)) - ac = actual.isel(band=0, x=1, y=slice(5, 7)) - assert_allclose(ac, ex) - - ex = expected.isel(band=0, x=slice(2, 5), y=2) - ac = actual.isel(band=0, x=slice(2, 5), y=2) - assert_allclose(ac, ex) - - # One-element lists - ex = expected.isel(band=[0], x=slice(2, 5), y=[2]) - ac = actual.isel(band=[0], x=slice(2, 5), y=[2]) - assert_allclose(ac, ex) + with xr.open_rasterio(tmp_file, cache=False) as actual: + + # tests + # assert_allclose checks all data + coordinates + assert_allclose(actual, expected) + + # Slicing + ex = expected.isel(x=slice(2, 5), y=slice(5, 7)) + ac = actual.isel(x=slice(2, 5), y=slice(5, 7)) + assert_allclose(ac, ex) + + ex = expected.isel(band=slice(1, 2), x=slice(2, 5), + y=slice(5, 7)) + ac = actual.isel(band=slice(1, 2), x=slice(2, 5), + y=slice(5, 7)) + assert_allclose(ac, ex) + + # Selecting lists of bands is fine + ex = expected.isel(band=[1, 2]) + ac = actual.isel(band=[1, 2]) + assert_allclose(ac, ex) + ex = expected.isel(band=[0, 2]) + ac = actual.isel(band=[0, 2]) + assert_allclose(ac, ex) + + # but on x and y only windowed operations are allowed, more + # exotic slicing should raise an error + err_msg = 'not valid on rasterio' + with self.assertRaisesRegexp(IndexError, err_msg): + actual.isel(x=[2, 4], y=[1, 3]).values + with self.assertRaisesRegexp(IndexError, err_msg): + actual.isel(x=[4, 2]).values + with self.assertRaisesRegexp(IndexError, err_msg): + actual.isel(x=slice(5, 2, -1)).values + + # Integer indexing + ex = expected.isel(band=1) + ac = actual.isel(band=1) + assert_allclose(ac, ex) + + ex = expected.isel(x=1, y=2) + ac = actual.isel(x=1, y=2) + assert_allclose(ac, ex) + + ex = expected.isel(band=0, x=1, y=2) + ac = actual.isel(band=0, x=1, y=2) + assert_allclose(ac, ex) + + # Mixed + ex = actual.isel(x=slice(2), y=slice(2)) + ac = actual.isel(x=[0, 1], y=[0, 1]) + assert_allclose(ac, ex) + + ex = expected.isel(band=0, x=1, y=slice(5, 7)) + ac = actual.isel(band=0, x=1, y=slice(5, 7)) + assert_allclose(ac, ex) + + ex = expected.isel(band=0, x=slice(2, 5), y=2) + ac = actual.isel(band=0, x=slice(2, 5), y=2) + assert_allclose(ac, ex) + + # One-element lists + ex = expected.isel(band=[0], x=slice(2, 5), y=[2]) + ac = actual.isel(band=[0], x=slice(2, 5), y=[2]) + assert_allclose(ac, ex) def test_caching(self): @@ -1627,26 +1631,27 @@ def test_caching(self): dtype=rasterio.float32) as s: s.write(data) - # Cache is the default - actual = xr.open_rasterio(tmp_file) - # ref expected = DataArray(data, dims=('band', 'y', 'x'), coords={'x': np.arange(nx)*0.5 + 1, 'y': -np.arange(ny)*2 + 2, 'band': [1, 2, 3]}) - # Without cache an error is raised - with self.assertRaisesRegexp(IndexError, 'not valid on rasterio'): - actual.isel(x=[2, 4]).values + # Cache is the default + with xr.open_rasterio(tmp_file) as actual: + + # Without cache an error is raised + err_msg = 'not valid on rasterio' + with self.assertRaisesRegexp(IndexError, err_msg): + actual.isel(x=[2, 4]).values - # This should cache everything - assert_allclose(actual, expected) + # This should cache everything + assert_allclose(actual, expected) - # once cached, non-windowed indexing should become possible - ac = actual.isel(x=[2, 4]) - ex = expected.isel(x=[2, 4]) - assert_allclose(ac, ex) + # once cached, non-windowed indexing should become possible + ac = actual.isel(x=[2, 4]) + ex = expected.isel(x=[2, 4]) + assert_allclose(ac, ex) @requires_dask def test_chunks(self): @@ -1670,26 +1675,26 @@ def test_chunks(self): s.write(data) # Chunk at open time - actual = xr.open_rasterio(tmp_file, chunks=(1, 2, 2)) - - import dask.array as da - self.assertIsInstance(actual.data, da.Array) - assert 'open_rasterio' in actual.data.name - - # ref - expected = DataArray(data, dims=('band', 'y', 'x'), - coords={'x': np.arange(nx)*0.5 + 1, - 'y': -np.arange(ny)*2 + 2, - 'band': [1, 2, 3]}) - - # do some arithmetic - ac = actual.mean() - ex = expected.mean() - assert_allclose(ac, ex) - - ac = actual.sel(band=1).mean(dim='x') - ex = expected.sel(band=1).mean(dim='x') - assert_allclose(ac, ex) + with xr.open_rasterio(tmp_file, chunks=(1, 2, 2)) as actual: + + import dask.array as da + self.assertIsInstance(actual.data, da.Array) + assert 'open_rasterio' in actual.data.name + + # ref + expected = DataArray(data, dims=('band', 'y', 'x'), + coords={'x': np.arange(nx)*0.5 + 1, + 'y': -np.arange(ny)*2 + 2, + 'band': [1, 2, 3]}) + + # do some arithmetic + ac = actual.mean() + ex = expected.mean() + assert_allclose(ac, ex) + + ac = actual.sel(band=1).mean(dim='x') + ex = expected.sel(band=1).mean(dim='x') + assert_allclose(ac, ex) class TestEncodingInvalid(TestCase): From 42999571a5e8e23289028968ae4e308ef5ec58c0 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Thu, 1 Jun 2017 11:52:04 +0200 Subject: [PATCH 34/36] Change example to use an accessor --- doc/gallery/plot_rasterio.py | 53 ++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/doc/gallery/plot_rasterio.py b/doc/gallery/plot_rasterio.py index cfa0786d3f9..9ec2cb1a9b9 100644 --- a/doc/gallery/plot_rasterio.py +++ b/doc/gallery/plot_rasterio.py @@ -6,7 +6,10 @@ Parsing rasterio's geocoordinates ================================= -Convert cartesian coordinates into 2D longitudes and latitudes + +The example illustrates how to use an accessor (see :ref:`internals.accessors`) +to convert a projection's cartesian coordinates into 2D longitudes and +latitudes. These new coordinates might be handy for plotting and indexing, but it should be kept in mind that a grid which is regular in projection coordinates will @@ -22,6 +25,32 @@ import matplotlib.pyplot as plt from rasterio.warp import transform + +# Define the accessor +@xr.register_dataarray_accessor('rasterio') +class RasterioAccessor(object): + def __init__(self, xarray_obj): + self._obj = xarray_obj + + def add_lonlat_coords(self): + """Compute the lon/lat coordinates out of the dataset's crs. + + This adds two non-dimension coordinates ('lon' and 'lat') to the + original dataarray. + """ + + ny, nx = len(self._obj['y']), len(self._obj['x']) + x, y = np.meshgrid(self._obj['x'], self._obj['y']) + + # Rasterio works with 1D arrays + lon, lat = transform(self._obj.crs, {'init': 'EPSG:4326'}, + x.flatten(), y.flatten()) + lon = np.asarray(lon).reshape((ny, nx)) + lat = np.asarray(lat).reshape((ny, nx)) + self._obj.coords['lon'] = (('y', 'x'), lon) + self._obj.coords['lat'] = (('y', 'x'), lat) + + # Download the file from rasterio's repository url = 'https://github.com/mapbox/rasterio/raw/master/tests/data/RGB.byte.tif' urllib.request.urlretrieve(url, 'RGB.byte.tif') @@ -29,28 +58,16 @@ # Read the data rioda = xr.open_rasterio('RGB.byte.tif') -# Compute the lons and lats using rasterio -ny, nx = len(rioda['y']), len(rioda['x']) -x, y = np.meshgrid(rioda['x'], rioda['y']) - -# Rasterio works with 1D arrays -lon, lat = transform(rioda.crs, {'init': 'EPSG:4326'}, - x.flatten(), y.flatten()) -lon = np.asarray(lon).reshape((ny, nx)) -lat = np.asarray(lat).reshape((ny, nx)) - -# Convert the DataArray to a dataset and set them as non-dimension coordinates -riods = rioda.to_dataset(name='img') -riods.coords['lon'] = (('y', 'x'), lon) -riods.coords['lat'] = (('y', 'x'), lat) +# Compute the coordinates +rioda.rasterio.add_lonlat_coords() # Compute a greyscale out of the rgb image -riods['greyscale'] = riods.img.mean(dim='band') +greyscale = rioda.mean(dim='band') # Plot on a map ax = plt.subplot(projection=ccrs.PlateCarree()) -riods.greyscale.plot(ax=ax, x='lon', y='lat', transform=ccrs.PlateCarree(), - cmap='Greys_r', add_colorbar=False) +greyscale.plot(ax=ax, x='lon', y='lat', transform=ccrs.PlateCarree(), + cmap='Greys_r', add_colorbar=False) ax.coastlines('10m', color='r') plt.show() From fcdd8947c8aab521b362874474ca5aafd24f1373 Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Mon, 5 Jun 2017 11:08:24 +0200 Subject: [PATCH 35/36] Reviews --- doc/gallery/plot_rasterio.py | 45 +++++++++++------------------------ xarray/backends/rasterio_.py | 10 +++++--- xarray/tests/test_backends.py | 17 ++++++------- 3 files changed, 28 insertions(+), 44 deletions(-) diff --git a/doc/gallery/plot_rasterio.py b/doc/gallery/plot_rasterio.py index 9ec2cb1a9b9..b42970db970 100644 --- a/doc/gallery/plot_rasterio.py +++ b/doc/gallery/plot_rasterio.py @@ -7,8 +7,7 @@ ================================= -The example illustrates how to use an accessor (see :ref:`internals.accessors`) -to convert a projection's cartesian coordinates into 2D longitudes and +Converting a projection's cartesian coordinates into 2D longitudes and latitudes. These new coordinates might be handy for plotting and indexing, but it should @@ -26,43 +25,27 @@ from rasterio.warp import transform -# Define the accessor -@xr.register_dataarray_accessor('rasterio') -class RasterioAccessor(object): - def __init__(self, xarray_obj): - self._obj = xarray_obj - - def add_lonlat_coords(self): - """Compute the lon/lat coordinates out of the dataset's crs. - - This adds two non-dimension coordinates ('lon' and 'lat') to the - original dataarray. - """ - - ny, nx = len(self._obj['y']), len(self._obj['x']) - x, y = np.meshgrid(self._obj['x'], self._obj['y']) - - # Rasterio works with 1D arrays - lon, lat = transform(self._obj.crs, {'init': 'EPSG:4326'}, - x.flatten(), y.flatten()) - lon = np.asarray(lon).reshape((ny, nx)) - lat = np.asarray(lat).reshape((ny, nx)) - self._obj.coords['lon'] = (('y', 'x'), lon) - self._obj.coords['lat'] = (('y', 'x'), lat) - - # Download the file from rasterio's repository url = 'https://github.com/mapbox/rasterio/raw/master/tests/data/RGB.byte.tif' urllib.request.urlretrieve(url, 'RGB.byte.tif') # Read the data -rioda = xr.open_rasterio('RGB.byte.tif') +da = xr.open_rasterio('RGB.byte.tif') + +# Compute the lon/lat coordinates with rasterio.warp.transform +ny, nx = len(da['y']), len(da['x']) +x, y = np.meshgrid(da['x'], da['y']) -# Compute the coordinates -rioda.rasterio.add_lonlat_coords() +# Rasterio works with 1D arrays +lon, lat = transform(da.crs, {'init': 'EPSG:4326'}, + x.flatten(), y.flatten()) +lon = np.asarray(lon).reshape((ny, nx)) +lat = np.asarray(lat).reshape((ny, nx)) +da.coords['lon'] = (('y', 'x'), lon) +da.coords['lat'] = (('y', 'x'), lat) # Compute a greyscale out of the rgb image -greyscale = rioda.mean(dim='band') +greyscale = da.mean(dim='band') # Plot on a map ax = plt.subplot(projection=ccrs.PlateCarree()) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index 8873e0dff49..e753165796b 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -6,8 +6,12 @@ from .. import DataArray from ..core.utils import DunderArrayMixin, NdimSizeLenMixin, is_scalar from ..core import indexing -from .common import GLOBAL_LOCK +try: + from dask.utils import SerializableLock as Lock +except ImportError: + from threading import Lock +RASTERIO_LOCK = Lock() _ERROR_MSG = ('The kind of indexing operation you are trying to do is not ' 'valid on rasterio files. Try to load your data with ds.load()' @@ -78,7 +82,7 @@ def __getitem__(self, key): return out -def open_rasterio(filename, chunks=None, cache=None, lock=False): +def open_rasterio(filename, chunks=None, cache=None, lock=None): """Open a file with rasterio (experimental). This should work with any file that rasterio can open (most often: @@ -156,7 +160,7 @@ def open_rasterio(filename, chunks=None, cache=None, lock=False): token = tokenize(filename, mtime, chunks) name_prefix = 'open_rasterio-%s' % token if lock is None: - lock = GLOBAL_LOCK + lock = RASTERIO_LOCK result = result.chunk(chunks, name_prefix=name_prefix, token=token, lock=lock) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 026ab9d3582..5401d0e3a60 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -1439,17 +1439,9 @@ class TestPyNioAutocloseTrue(TestPyNio): @requires_rasterio -class TestRasterio(CFEncodedDataTest, Only32BitTypes, TestCase): +class TestRasterio(TestCase): - def test_write_store(self): - # rasterio is read-only for now - pass - - def test_orthogonal_indexing(self): - # rasterio also does not support list-like indexing - pass - - def test_serialization(self): + def test_serialization_utm(self): import rasterio from rasterio.transform import from_origin @@ -1487,6 +1479,11 @@ def test_serialization(self): with xr.open_dataarray(tmp_nc_file) as ncds: assert_identical(rioda, ncds) + def test_serialization_platecarree(self): + + import rasterio + from rasterio.transform import from_origin + # Create a geotiff file in latlong proj with create_tmp_file(suffix='.tif') as tmp_file: # data From d5c964e882e68470946eca9b97ac1908c7578e4b Mon Sep 17 00:00:00 2001 From: Fabien Maussion Date: Mon, 5 Jun 2017 13:08:32 +0200 Subject: [PATCH 36/36] typo --- xarray/backends/rasterio_.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xarray/backends/rasterio_.py b/xarray/backends/rasterio_.py index e753165796b..bb50a6b0b5e 100644 --- a/xarray/backends/rasterio_.py +++ b/xarray/backends/rasterio_.py @@ -62,11 +62,11 @@ def __getitem__(self, key): if step is not None and step != 1: raise IndexError(_ERROR_MSG) elif is_scalar(k): - # windowed operations will always return an array - # we will have to squeeze it later - squeeze_axis.append(i+1) - start = k - stop = k+1 + # windowed operations will always return an array + # we will have to squeeze it later + squeeze_axis.append(i+1) + start = k + stop = k+1 else: k = np.asarray(k) start = k[0]