diff --git a/datacube/api/core.py b/datacube/api/core.py index 0fda946db..4ebdf0469 100644 --- a/datacube/api/core.py +++ b/datacube/api/core.py @@ -2,6 +2,7 @@ # # Copyright (c) 2015-2021 ODC Contributors # SPDX-License-Identifier: Apache-2.0 +import logging import uuid import collections.abc from itertools import groupby @@ -15,7 +16,7 @@ from datacube.config import LocalConfig from datacube.storage import reproject_and_fuse, BandInfo from datacube.utils import ignore_exceptions_if -from odc.geo import CRS, XY, Resolution +from odc.geo import CRS, yx_, res_, resyx_ from odc.geo.xr import xr_coords from datacube.utils.dates import normalise_dt from odc.geo.geom import intersects, box, bbox_union @@ -27,6 +28,8 @@ from ..index import index_connect from ..drivers import new_datasource +_LOG = logging.getLogger(__name__) + class TerminateCurrentLoad(Exception): # noqa: N818 """ This exception is raised by user code from `progress_cbk` @@ -280,7 +283,7 @@ def load(self, product=None, measurements=None, output_crs=None, resolution=None y=(-35.15, -35.2), time=('1990', '1991'), output_crs='EPSG:3577`, - resolution=(-30, 30), + resolution=30, resampling='cubic' ) @@ -307,12 +310,11 @@ def load(self, product=None, measurements=None, output_crs=None, resolution=None This differs from the ``crs`` parameter desribed above, which is used to define the CRS of the coordinates in the query itself. - :param (float,float) resolution: - A tuple of the spatial resolution of the returned data. Units are in the coordinate - space of ``output_crs``. - - This includes the direction (as indicated by a positive or negative number). - For most CRSs, the first number will be negative, e.g. ``(-30, 30)``. + :param int|float|(float,float) resolution: + The spatial resolution of the returned data. If using square pixels with an inverted Y axis, it + should be provided as an int or float. If not, it should be provided as a tuple. + Units are in the coordinate space of ``output_crs``. This includes the direction + (as indicated by a positive or negative number). :param str|dict resampling: The resampling method to use if re-projection is required. This could be a string or @@ -333,6 +335,7 @@ def load(self, product=None, measurements=None, output_crs=None, resolution=None :param (float,float) align: Load data such that point 'align' lies on the pixel boundary. Units are in the coordinate space of ``output_crs``. + Expected in `(x, y)` order. Default is ``(0, 0)`` @@ -430,6 +433,16 @@ def filter_jan(dataset): return dataset.time.begin.month == 1 if extra_dims.has_empty_dim(): return xarray.Dataset() + if type(resolution) is tuple: + _LOG.warning("Resolution should be provided as a single int or float, or the axis order specified " + "using odc.geo.resxy_ or odc.geo.resyx_") + if resolution[0] == -resolution[1]: + resolution = res_(resolution[1]) + else: + _LOG.warning("Assuming resolution has been provided in (y, x) ordering. Please specify the order " + "with odc.geo.resxy_ or odc.geo.resyx_") + resolution = resyx_(*resolution) + geobox = output_geobox(like=like, output_crs=output_crs, resolution=resolution, align=align, grid_spec=datacube_product.grid_spec, load_hints=datacube_product.load_hints(), @@ -866,7 +879,7 @@ def output_geobox(like=None, output_crs=None, resolution=None, align=None, if isinstance(like, GeoBox): return like - return like.geobox + return like.odc.geobox if load_hints: if output_crs is None: @@ -908,11 +921,19 @@ def output_geobox(like=None, output_crs=None, resolution=None, align=None, geopolygon = get_bounds(datasets, crs) - if resolution is not None and type(resolution) is not Resolution: - resolution = Resolution(*resolution) + if type(resolution) is tuple: + _LOG.warning("Resolution should be provided as a single int or float, or the axis order specified " + "using odc.geo.resxy_ or odc.geo.resyx_") + if resolution[0] == -resolution[1]: + resolution = resolution[1] + else: + _LOG.warning("Assuming resolution has been provided in (y, x) ordering. Please specify the order " + "with odc.geo.resxy_ or odc.geo.resyx_") + resolution = resyx_(*resolution) + resolution = res_(resolution) if align is not None: - align = XY(*align) + align = yx_(align) return GeoBox.from_geopolygon(geopolygon, resolution, crs, align) diff --git a/datacube/api/grid_workflow.py b/datacube/api/grid_workflow.py index e5c67cefd..e38d55766 100644 --- a/datacube/api/grid_workflow.py +++ b/datacube/api/grid_workflow.py @@ -172,7 +172,7 @@ def cell_observations(self, cell_index=None, geopolygon=None, tile_buffer=None, :param odc.geo.geom.Geometry geopolygon: Only return observations with data inside polygon. :param (float,float) tile_buffer: - buffer tiles by (y, x) in CRS units + buffer tiles by (x, y) in CRS units :param (int,int) cell_index: The cell index. E.g. (14, -40) :param indexers: diff --git a/datacube/drivers/netcdf/_write.py b/datacube/drivers/netcdf/_write.py index 26a2f4a79..d759f20d4 100644 --- a/datacube/drivers/netcdf/_write.py +++ b/datacube/drivers/netcdf/_write.py @@ -108,13 +108,13 @@ def write_dataset_to_netcdf(dataset, filename, global_attributes=None, variable_ if not dataset.data_vars.keys(): raise DatacubeException('Cannot save empty dataset to disk.') - if dataset.geobox is None: + if dataset.odc.geobox is None: raise DatacubeException('Dataset geobox property is None, cannot write to NetCDF file.') try: HDF5_LOCK.acquire(blocking=True) nco = create_netcdf_storage_unit(filename, - dataset.geobox.crs, + dataset.odc.geobox.crs, dataset.coords, dataset.data_vars, variable_params, diff --git a/datacube/drivers/netcdf/writer.py b/datacube/drivers/netcdf/writer.py index c84ab4d1c..397fa2e78 100644 --- a/datacube/drivers/netcdf/writer.py +++ b/datacube/drivers/netcdf/writer.py @@ -13,11 +13,11 @@ import numpy from datacube.utils.masking import describe_flags_def -from datacube.utils import data_resolution_and_offset from netCDF4 import Dataset from odc.geo import CRS from odc.geo.geom import box +from odc.geo.math import data_resolution_and_offset from datacube import __version__ diff --git a/datacube/model/__init__.py b/datacube/model/__init__.py index dd20ab621..18d2249aa 100644 --- a/datacube/model/__init__.py +++ b/datacube/model/__init__.py @@ -211,7 +211,8 @@ def bounds(self) -> Optional[BoundingBox]: return BoundingBox(left=min(bounds['ur']['x'], bounds['ll']['x']), right=max(bounds['ur']['x'], bounds['ll']['x']), top=max(bounds['ur']['y'], bounds['ll']['y']), - bottom=min(bounds['ur']['y'], bounds['ll']['y'])) + bottom=min(bounds['ur']['y'], bounds['ll']['y']), + crs=self.crs) @property def transform(self) -> Optional[Affine]: @@ -542,6 +543,7 @@ def extract_point(name): xx = storage.get(name, None) return None if xx is None else tuple(xx[dim] for dim in crs.dimensions) + # extract both tile_size and tile_shape for backwards compatibility gs_params = {name: extract_point(name) for name in ('tile_size', 'resolution', 'origin')} @@ -750,6 +752,10 @@ class IngestorConfig: pass +@deprecat( + reason='This version of GridSpec has been deprecated. Please use the GridSpec class definted in odc-geo.', + version='1.9.0' +) class GridSpec: """ Definition for a regular spatial grid @@ -777,8 +783,13 @@ def __init__(self, resolution: Tuple[float, float], origin: Optional[Tuple[float, float]] = None): self.crs = crs + _LOG.warning('In odc-geo GridSpec, tile_size has been renamed tile_shape and should be provided in pixels.') self.tile_size = tile_size + _LOG.warning('In odc-geo GridSpec, resolution is expected in (X, Y) order, ' + 'or simply X if using square pixels with inverted Y axis.') self.resolution = resolution + if origin is not None: + _LOG.warning('In odc-geo GridSpec, origin is expected in (X, Y) order.') self.origin = origin or (0.0, 0.0) def __eq__(self, other): diff --git a/datacube/storage/_load.py b/datacube/storage/_load.py index ddd4ab6ed..a94e0d1a6 100644 --- a/datacube/storage/_load.py +++ b/datacube/storage/_load.py @@ -45,7 +45,7 @@ def _default_fuser(dst: np.ndarray, src: np.ndarray, dst_nodata) -> None: def reproject_and_fuse(datasources: List[DataSource], destination: np.ndarray, - dst_gbox: GeoBox, + dst_geobox: GeoBox, dst_nodata: Optional[Union[int, float]], resampling: str = 'nearest', fuse_func: Optional[FuserFunction] = None, @@ -57,7 +57,7 @@ def reproject_and_fuse(datasources: List[DataSource], :param datasources: Data sources to open and read from :param destination: ndarray of appropriate size to read data into - :param dst_gbox: GeoBox defining destination region + :param dst_geobox: GeoBox defining destination region :param skip_broken_datasets: Carry on in the face of adversity and failing reads. :param progress_cbk: If supplied will be called with 2 integers `Items processed, Total Items` after reading each file. @@ -77,7 +77,7 @@ def copyto_fuser(dest: np.ndarray, src: np.ndarray) -> None: elif len(datasources) == 1: with ignore_exceptions_if(skip_broken_datasets): with datasources[0].open() as rdr: - read_time_slice(rdr, destination, dst_gbox, resampling, dst_nodata, extra_dim_index) + read_time_slice(rdr, destination, dst_geobox, resampling, dst_nodata, extra_dim_index) if progress_cbk: progress_cbk(1, 1) @@ -89,7 +89,7 @@ def copyto_fuser(dest: np.ndarray, src: np.ndarray) -> None: for n_so_far, source in enumerate(datasources, 1): with ignore_exceptions_if(skip_broken_datasets): with source.open() as rdr: - roi = read_time_slice(rdr, buffer_, dst_gbox, resampling, dst_nodata, extra_dim_index) + roi = read_time_slice(rdr, buffer_, dst_geobox, resampling, dst_nodata, extra_dim_index) if not roi_is_empty(roi): fuse_func(destination[roi], buffer_[roi]) diff --git a/datacube/storage/_read.py b/datacube/storage/_read.py index 1f6136301..08e88ce01 100644 --- a/datacube/storage/_read.py +++ b/datacube/storage/_read.py @@ -7,7 +7,7 @@ import numpy as np from typing import Optional, Tuple -from ..utils.math import is_almost_int, valid_mask +from ..utils.math import valid_mask from odc.geo import wh_ from odc.geo.roi import ( @@ -26,6 +26,7 @@ Nodata, ) from odc.geo.overlap import compute_reproject_roi, is_affine_st +from odc.geo.math import is_almost_int def rdr_geobox(rdr) -> GeoBox: @@ -61,7 +62,7 @@ def pick_read_scale(scale: float, rdr=None, tol=1e-3): def read_time_slice(rdr, dst: np.ndarray, - dst_gbox: GeoBox, + dst_geobox: GeoBox, resampling: Resampling, dst_nodata: Nodata, extra_dim_index: Optional[int] = None) -> Tuple[slice, slice]: @@ -69,12 +70,12 @@ def read_time_slice(rdr, :returns: affected destination region """ - assert dst.shape == dst_gbox.shape - src_gbox = rdr_geobox(rdr) + assert dst.shape == dst_geobox.shape + src_geobox = rdr_geobox(rdr) is_nn = is_resampling_nn(resampling) - rr = compute_reproject_roi(src_gbox, dst_gbox, ttol=0.9 if is_nn else 0.01) + rr = compute_reproject_roi(src_geobox, dst_geobox, ttol=0.9 if is_nn else 0.01) if roi_is_empty(rr.roi_dst): return rr.roi_dst @@ -121,42 +122,42 @@ def norm_read_args(roi, shape, extra_dim_index): if is_st: # add padding on src/dst ROIs, it was set to tight bounds # TODO: this should probably happen inside compute_reproject_roi - rr.roi_dst = roi_pad(rr.roi_dst, 1, dst_gbox.shape) - rr.roi_src = roi_pad(rr.roi_src, 1, src_gbox.shape) + rr.roi_dst = roi_pad(rr.roi_dst, 1, dst_geobox.shape) + rr.roi_src = roi_pad(rr.roi_src, 1, src_geobox.shape) dst = dst[rr.roi_dst] - dst_gbox = dst_gbox[rr.roi_dst] - src_gbox = src_gbox[rr.roi_src] + dst_geobox = dst_geobox[rr.roi_dst] + src_geobox = src_geobox[rr.roi_src] if scale > 1: - src_gbox = zoom_out(src_gbox, scale) + src_geobox = zoom_out(src_geobox, scale) - pix = rdr.read(*norm_read_args(rr.roi_src, src_gbox.shape, extra_dim_index)) + pix = rdr.read(*norm_read_args(rr.roi_src, src_geobox.shape, extra_dim_index)) if rr.transform.linear is not None: - A = (~src_gbox.transform)*dst_gbox.transform + A = (~src_geobox.transform)*dst_geobox.transform warp_affine(pix, dst, A, resampling, src_nodata=rdr.nodata, dst_nodata=dst_nodata) else: - rio_reproject(pix, dst, src_gbox, dst_gbox, resampling, + rio_reproject(pix, dst, src_geobox, dst_geobox, resampling, src_nodata=rdr.nodata, dst_nodata=dst_nodata) return rr.roi_dst def read_time_slice_v2(rdr, - dst_gbox: GeoBox, + dst_geobox: GeoBox, resampling: Resampling, dst_nodata: Nodata) -> Tuple[Optional[np.ndarray], Tuple[slice, slice]]: """ From opened reader object read into `dst` - :returns: pixels read and ROI of dst_gbox that was affected + :returns: pixels read and ROI of dst_geobox that was affected """ # pylint: disable=too-many-locals - src_gbox = rdr_geobox(rdr) + src_geobox = rdr_geobox(rdr) is_nn = is_resampling_nn(resampling) - rr = compute_reproject_roi(src_gbox, dst_gbox, ttol=0.9 if is_nn else 0.01) + rr = compute_reproject_roi(src_geobox, dst_geobox, ttol=0.9 if is_nn else 0.01) if roi_is_empty(rr.roi_dst): return None, rr.roi_dst @@ -194,23 +195,23 @@ def norm_read_args(roi, shape): if is_st: # add padding on src/dst ROIs, it was set to tight bounds # TODO: this should probably happen inside compute_reproject_roi - rr.roi_dst = roi_pad(rr.roi_dst, 1, dst_gbox.shape) - rr.roi_src = roi_pad(rr.roi_src, 1, src_gbox.shape) + rr.roi_dst = roi_pad(rr.roi_dst, 1, dst_geobox.shape) + rr.roi_src = roi_pad(rr.roi_src, 1, src_geobox.shape) - dst_gbox = dst_gbox[rr.roi_dst] - src_gbox = src_gbox[rr.roi_src] + dst_geobox = dst_geobox[rr.roi_dst] + src_geobox = src_geobox[rr.roi_src] if scale > 1: - src_gbox = zoom_out(src_gbox, scale) + src_geobox = zoom_out(src_geobox, scale) - dst = np.full(dst_gbox.shape, dst_nodata, dtype=rdr.dtype) - pix = rdr.read(*norm_read_args(rr.roi_src, src_gbox.shape)).result() + dst = np.full(dst_geobox.shape, dst_nodata, dtype=rdr.dtype) + pix = rdr.read(*norm_read_args(rr.roi_src, src_geobox.shape)).result() if rr.transform.linear is not None: - A = (~src_gbox.transform)*dst_gbox.transform + A = (~src_geobox.transform)*dst_geobox.transform warp_affine(pix, dst, A, resampling, src_nodata=rdr.nodata, dst_nodata=dst_nodata) else: - rio_reproject(pix, dst, src_gbox, dst_gbox, resampling, + rio_reproject(pix, dst, src_geobox, dst_geobox, resampling, src_nodata=rdr.nodata, dst_nodata=dst_nodata) return dst, rr.roi_dst diff --git a/datacube/testutils/__init__.py b/datacube/testutils/__init__.py index 5461e0a92..45792224f 100644 --- a/datacube/testutils/__init__.py +++ b/datacube/testutils/__init__.py @@ -405,7 +405,7 @@ def gen_tiff_dataset(bands, bands = (bands,) # write arrays to disk and construct compatible measurement definitions - gbox = None + geobox = None mm = [] for band in bands: name = band.name @@ -415,7 +415,7 @@ def gen_tiff_dataset(bands, overwrite=True, **kwargs) - gbox = meta.gbox + geobox = meta.geobox mm.append(dict(name=name, path=fname, @@ -426,8 +426,8 @@ def gen_tiff_dataset(bands, ds = mk_sample_dataset(mm, uri=uri, timestamp=timestamp, - geobox=gbox) - return ds, gbox + geobox=geobox) + return ds, geobox def mk_sample_xr_dataset(crs="EPSG:3578", diff --git a/datacube/testutils/io.py b/datacube/testutils/io.py index 8c346c8cb..8b2f8fc4d 100644 --- a/datacube/testutils/io.py +++ b/datacube/testutils/io.py @@ -145,7 +145,7 @@ def native_load(ds, measurements=None, basis=None, **kw): def dc_read(path, band=1, - gbox=None, + geobox=None, resampling='nearest', dtype=None, dst_nodata=None, @@ -156,8 +156,8 @@ def dc_read(path, source = RasterFileDataSource(path, band, nodata=fallback_nodata) with source.open() as rdr: dtype = rdr.dtype if dtype is None else dtype - if gbox is None: - gbox = rdr_geobox(rdr) + if geobox is None: + geobox = rdr_geobox(rdr) if dst_nodata is None: dst_nodata = rdr.nodata @@ -166,8 +166,8 @@ def dc_read(path, if dst_nodata is None: dst_nodata = 0 - im = np.full(gbox.shape, dst_nodata, dtype=dtype) - reproject_and_fuse([source], im, gbox, dst_nodata, resampling=resampling) + im = np.full(geobox.shape, dst_nodata, dtype=dtype) + reproject_and_fuse([source], im, geobox, dst_nodata, resampling=resampling) return im @@ -179,14 +179,14 @@ def write_gtiff(fname, nodata=None, overwrite=False, blocksize=None, - gbox=None, + geobox=None, **extra_rio_opts): """ Write ndarray to GeoTiff file. Geospatial info can be supplied either via - resolution, offset, crs or - - gbox (takes precedence if supplied) + - geobox (takes precedence if supplied) """ # pylint: disable=too-many-locals @@ -213,11 +213,11 @@ def write_gtiff(fname, else: raise IOError("File exists") - if gbox is not None: - assert gbox.shape == (h, w) + if geobox is not None: + assert geobox.shape == (h, w) - A = gbox.transform - crs = str(gbox.crs) + A = geobox.transform + crs = str(geobox.crs) else: sx, sy = resolution tx, ty = offset @@ -248,7 +248,7 @@ def write_gtiff(fname, dst.write(pix, band) meta = dst.meta - meta['gbox'] = gbox if gbox is not None else rio_geobox(meta) + meta['geobox'] = geobox if geobox is not None else rio_geobox(meta) meta['path'] = fname return SimpleNamespace(**meta) @@ -280,7 +280,7 @@ def _fix_resampling(kw): kw['resampling'] = resampling_s2rio(r) -def rio_slurp_reproject(fname, gbox, dtype=None, dst_nodata=None, **kw): +def rio_slurp_reproject(fname, geobox, dtype=None, dst_nodata=None, **kw): """ Read image with reprojection """ @@ -291,10 +291,10 @@ def rio_slurp_reproject(fname, gbox, dtype=None, dst_nodata=None, **kw): with rasterio.open(str(fname), 'r') as src: if src.count == 1: - shape = gbox.shape + shape = geobox.shape src_band = rasterio.band(src, 1) else: - shape = (src.count, *gbox.shape) + shape = (src.count, *geobox.shape) src_band = rasterio.band(src, tuple(range(1, src.count+1))) if dtype is None: @@ -308,14 +308,14 @@ def rio_slurp_reproject(fname, gbox, dtype=None, dst_nodata=None, **kw): reproject(src_band, pix, dst_nodata=dst_nodata, - dst_transform=gbox.transform, - dst_crs=str(gbox.crs), + dst_transform=geobox.transform, + dst_crs=str(geobox.crs), **kw) meta = src.meta - meta['src_gbox'] = rio_geobox(meta) + meta['src_geobox'] = rio_geobox(meta) meta['path'] = fname - meta['gbox'] = gbox + meta['geobox'] = geobox return pix, SimpleNamespace(**meta) @@ -336,13 +336,13 @@ def rio_slurp_read(fname, out_shape=None, **kw): with rasterio.open(str(fname), 'r') as src: data = src.read(1, **kw) if src.count == 1 else src.read(**kw) meta = src.meta - src_gbox = rio_geobox(meta) + src_geobox = rio_geobox(meta) - same_gbox = out_shape is None or out_shape == src_gbox.shape - gbox = src_gbox if same_gbox else zoom_to(src_gbox, out_shape) + same_geobox = out_shape is None or out_shape == src_geobox.shape + geobox = src_geobox if same_geobox else zoom_to(src_geobox, out_shape) - meta['src_gbox'] = src_gbox - meta['gbox'] = gbox + meta['src_geobox'] = src_geobox + meta['geobox'] = geobox meta['path'] = fname return data, SimpleNamespace(**meta) @@ -352,11 +352,11 @@ def rio_slurp(fname, *args, **kw): Dispatches to either: rio_slurp_read(fname, out_shape, ..) - rio_slurp_reproject(fname, gbox, ...) + rio_slurp_reproject(fname, geobox, ...) """ if len(args) == 0: - if 'gbox' in kw: + if 'geobox' in kw: return rio_slurp_reproject(fname, **kw) else: return rio_slurp_read(fname, **kw) @@ -372,14 +372,14 @@ def rio_slurp_xarray(fname, *args, rgb='auto', **kw): Dispatches to either: rio_slurp_read(fname, out_shape, ..) - rio_slurp_reproject(fname, gbox, ...) + rio_slurp_reproject(fname, geobox, ...) then wraps it all in xarray.DataArray with .crs,.nodata etc. """ from xarray import DataArray if len(args) == 0: - if 'gbox' in kw: + if 'geobox' in kw: im, mm = rio_slurp_reproject(fname, **kw) else: im, mm = rio_slurp_read(fname, **kw) @@ -390,15 +390,15 @@ def rio_slurp_xarray(fname, *args, rgb='auto', **kw): im, mm = rio_slurp_read(fname, *args, **kw) if im.ndim == 3: - dims = ('band', *mm.gbox.dims) + dims = ('band', *mm.geobox.dims) if rgb and im.shape[0] in (3, 4): im = im.transpose([1, 2, 0]) dims = tuple(dims[i] for i in [1, 2, 0]) else: - dims = mm.gbox.dims + dims = mm.geobox.dims return DataArray(im, dims=dims, - coords=xr_coords(mm.gbox), + coords=xr_coords(mm.geobox), attrs=dict( nodata=mm.nodata)) diff --git a/datacube/utils/__init__.py b/datacube/utils/__init__.py index 1596a1e7b..cf70b55f9 100644 --- a/datacube/utils/__init__.py +++ b/datacube/utils/__init__.py @@ -31,7 +31,6 @@ unsqueeze_data_array, spatial_dims, iter_slices, - data_resolution_and_offset, ) from ._misc import ( DatacubeException, @@ -66,7 +65,6 @@ "unsqueeze_dataset", "spatial_dims", "iter_slices", - "data_resolution_and_offset", "DatacubeException", "schema_validated", "write_user_secret_file", diff --git a/datacube/utils/cog.py b/datacube/utils/cog.py index 9a4bd77ae..6483e78e4 100644 --- a/datacube/utils/cog.py +++ b/datacube/utils/cog.py @@ -17,6 +17,8 @@ from .geometry import GeoBox from .geometry.tools import align_up +from deprecat import deprecat + __all__ = ("write_cog", "to_cog") @@ -207,6 +209,7 @@ def _write(pix, band, dst): ) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def write_cog( geo_im: xr.DataArray, fname: Union[str, Path], @@ -304,6 +307,7 @@ def write_cog( ) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def to_cog( geo_im: xr.DataArray, blocksize: Optional[int] = None, diff --git a/datacube/utils/geometry/gbox.py b/datacube/utils/geometry/gbox.py index 11a2efab6..2cdeb8081 100644 --- a/datacube/utils/geometry/gbox.py +++ b/datacube/utils/geometry/gbox.py @@ -12,7 +12,7 @@ from . import Geometry, GeoBox, BoundingBox from .tools import align_up -from datacube.utils.math import clamp +from odc.geo.math import clamp # pylint: disable=invalid-name MaybeInt = Optional[int] diff --git a/datacube/utils/geometry/tools.py b/datacube/utils/geometry/tools.py index e6dc8516e..15cca6006 100644 --- a/datacube/utils/geometry/tools.py +++ b/datacube/utils/geometry/tools.py @@ -446,7 +446,7 @@ def box_overlap(src_shape, dst_shape, ST, tol): # noqa: N803 direction is: Xsrc = ST*Xdst :param tol: Sub-pixel translation tolerance that's scaled by resolution. """ - from datacube.utils.math import maybe_int, snap_scale + from odc.geo.math import maybe_int, snap_scale (sx, _, tx, _, sy, ty, diff --git a/datacube/utils/math.py b/datacube/utils/math.py index 037845085..d97419d90 100644 --- a/datacube/utils/math.py +++ b/datacube/utils/math.py @@ -2,12 +2,15 @@ # # Copyright (c) 2015-2020 ODC Contributors # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple, Union, Optional, Any, cast -from math import ceil, fmod +from typing import Tuple, Union, Optional, Any +from math import ceil import numpy import xarray as xr -from affine import Affine +import odc.geo.math as geomath +from odc.geo.xr import spatial_dims as xr_spatial_dims + +from deprecat import deprecat def unsqueeze_data_array(da: xr.DataArray, @@ -39,101 +42,35 @@ def unsqueeze_dataset(ds: xr.Dataset, dim: str, coord: int = 0, pos: int = 0) -> return ds +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def spatial_dims(xx: Union[xr.DataArray, xr.Dataset], relaxed: bool = False) -> Optional[Tuple[str, str]]: - """ Find spatial dimensions of `xx`. - - Checks for presence of dimensions named: - y, x | latitude, longitude | lat, lon - - Returns - ======= - None -- if no dimensions with expected names are found - ('y', 'x') | ('latitude', 'longitude') | ('lat', 'lon') - - If *relaxed* is True and none of the above dimension names are found, - assume that last two dimensions are spatial dimensions. - """ - guesses = [('y', 'x'), - ('latitude', 'longitude'), - ('lat', 'lon')] - - dims = set(xx.dims) - for guess in guesses: - if dims.issuperset(guess): - return guess - - if relaxed and len(xx.dims) >= 2: - # This operation is pushing mypy's type-inference engine. - return cast(Tuple[str, str], cast(Tuple[str, ...], xx.dims)[-2:]) - - return None + return xr_spatial_dims(xx, relaxed) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def maybe_zero(x: float, tol: float) -> float: - """ Turn almost zeros to actual zeros - """ - if abs(x) < tol: - return 0 - return x + return geomath.maybe_zero(x, tol) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def maybe_int(x: float, tol: float) -> Union[int, float]: - """ Turn almost ints to actual ints, pass through other values unmodified - """ - def split(x): - x_part = fmod(x, 1.0) - x_whole = x - x_part - if x_part > 0.5: - x_part -= 1 - x_whole += 1 - elif x_part < -0.5: - x_part += 1 - x_whole -= 1 - return (x_whole, x_part) - - x_whole, x_part = split(x) - - if abs(x_part) < tol: # almost int - return int(x_whole) - else: - return x + return geomath.maybe_int(x, tol) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def snap_scale(s, tol=1e-6): - """ Snap scale to the nearest integer and simple fractions in the form 1/ - """ - if abs(s) >= 1 - tol: - return maybe_int(s, tol) - else: - # Check of s is 0 - if abs(s) < tol: - return s - - # Check for simple fractions - s_inv = 1 / s - s_inv_snapped = maybe_int(s_inv, tol) - if s_inv_snapped is s_inv: - return s - return 1 / s_inv_snapped + return geomath.snap_scale(s, tol) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def clamp(x, lo, up): - """ - clamp x to be lo <= x <= up - """ - assert lo <= up - return lo if x < lo else up if x > up else x + return geomath.clamp(x, lo, up) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def is_almost_int(x: float, tol: float): - """ - Check if number is close enough to an integer - """ - x = abs(fmod(x, 1)) - if x > 0.5: - x = 1 - x - return x < tol + return geomath.is_almost_int(x, tol) def dtype_is_float(dtype) -> bool: @@ -199,65 +136,14 @@ def num2numpy(x, dtype, ignore_range=None): return None +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def data_resolution_and_offset(data, fallback_resolution=None): - """ Compute resolution and offset from x/y axis data. - - Only uses first two coordinate values, assumes that data is regularly - sampled. - - Returns - ======= - (resolution: float, offset: float) - """ - if data.size < 2: - if data.size < 1: - raise ValueError("Can't calculate resolution for empty data") - if fallback_resolution is None: - raise ValueError("Can't calculate resolution with data size < 2") - res = fallback_resolution - else: - res = (data[data.size - 1] - data[0]) / (data.size - 1.0) - res = res.item() - - off = data[0] - 0.5 * res - return res, off.item() + return geomath.data_resolution_and_offset(data, fallback_resolution) +@deprecat(reason='This method has been moved to odc-geo.', version='1.9.0') def affine_from_axis(xx, yy, fallback_resolution=None): - """ Compute Affine transform from pixel to real space given X,Y coordinates. - - :param xx: X axis coordinates - :param yy: Y axis coordinates - :param fallback_resolution: None|float|(resx:float, resy:float) resolution to - assume for single element axis. - - (0, 0) in pixel space is defined as top left corner of the top left pixel - \ - `` 0 1 - +---+---+ - 0 | | | - +---+---+ - 1 | | | - +---+---+ - - Only uses first two coordinate values, assumes that data is regularly - sampled. - - raises ValueError when any axis is empty - raises ValueError when any axis has single value and fallback resolution was not supplied. - """ - if fallback_resolution is not None: - if isinstance(fallback_resolution, (float, int)): - frx, fry = fallback_resolution, fallback_resolution - else: - frx, fry = fallback_resolution - else: - frx, fry = None, None - - xres, xoff = data_resolution_and_offset(xx, frx) - yres, yoff = data_resolution_and_offset(yy, fry) - - return Affine.translation(xoff, yoff) * Affine.scale(xres, yres) + return geomath.affine_from_axis(xx, yy, fallback_resolution) def iter_slices(shape, chunk_size): diff --git a/datacube/utils/xarray_geoextensions.py b/datacube/utils/xarray_geoextensions.py index b183f1b24..536abcad5 100644 --- a/datacube/utils/xarray_geoextensions.py +++ b/datacube/utils/xarray_geoextensions.py @@ -15,8 +15,9 @@ """ import warnings import xarray -from datacube.utils import spatial_dims -from datacube.utils.math import affine_from_axis +from odc.geo import resxy_ +from odc.geo.math import affine_from_axis +from odc.geo.xr import spatial_dims from odc.geo._xr_interop import _xarray_geobox as _xr_geobox @@ -27,7 +28,8 @@ def _xarray_affine_impl(obj): yy, xx = (obj[dim] for dim in sdims) fallback_res = (coord.attrs.get('resolution', None) for coord in (xx, yy)) - + res_x, res_y = next(fallback_res), next(fallback_res) + fallback_res = None if (res_x, res_y) == (None, None) else resxy_(res_x, res_y) return affine_from_axis(xx.values, yy.values, fallback_res), sdims diff --git a/datacube/virtual/impl.py b/datacube/virtual/impl.py index 1a37892a4..624028c24 100644 --- a/datacube/virtual/impl.py +++ b/datacube/virtual/impl.py @@ -885,7 +885,7 @@ def reproject_array(src, nodata, s_geobox, d_geobox, resampling): """ Reproject a numpy array. """ dst = numpy.full(d_geobox.shape, fill_value=nodata, dtype=src.dtype) rio_reproject(src=src, dst=dst, - s_gbox=s_geobox, d_gbox=d_geobox, + s_gbox=s_geobox, d_gbox=d_geobox, # TODO: rename s_gbox and d_gbox once odc-geo has been updated resampling=resampling_s2rio(resampling), src_nodata=nodata, dst_nodata=nodata) diff --git a/docs/about/whats_new.rst b/docs/about/whats_new.rst index 1747d5db7..902c25e76 100644 --- a/docs/about/whats_new.rst +++ b/docs/about/whats_new.rst @@ -13,6 +13,8 @@ v1.9.next - Migrate to SQLAlchemy 2.0 (:pull:`#1432`) - Clean up deprecated code and add deprecation warnings to legacy methods, simplify DocReader logic (:pull:`#1406`) - Mark geometry module as deprecated and replace all usage with odc-geo (:pull:`#1424`) +- Mark GridSpec as deprecated, replace math and cog functions with odc-geo equivalents, enforce new odc-geo conventions (:pull:`#1441`) +- Rename `gbox` to `geobox` in parameter names (:pull:`#1441`) v1.8.next diff --git a/integration_tests/test_end_to_end.py b/integration_tests/test_end_to_end.py index 54ec91621..708e77381 100644 --- a/integration_tests/test_end_to_end.py +++ b/integration_tests/test_end_to_end.py @@ -179,8 +179,8 @@ def check_open_with_dc(index): assert 0 < solar_day_dataset.time.size <= dataset.time.size dataset = dc.load(product='ls5_nbar_albers', latitude=(-35.2, -35.3), longitude=(149.1, 149.2), align=(20, 5)) - assert dataset.geobox.affine.f % abs(dataset.geobox.affine.e) == 5 - assert dataset.geobox.affine.c % abs(dataset.geobox.affine.a) == 20 + assert dataset.odc.geobox.affine.f % abs(dataset.odc.geobox.affine.e) == 20 + assert dataset.odc.geobox.affine.c % abs(dataset.odc.geobox.affine.a) == 5 dataset_like = dc.load(product='ls5_nbar_albers', measurements=['blue'], like=dataset) assert (dataset.blue == dataset_like.blue).all() diff --git a/tests/storage/test_storage.py b/tests/storage/test_storage.py index 0168a7ae0..761d29654 100644 --- a/tests/storage/test_storage.py +++ b/tests/storage/test_storage.py @@ -186,14 +186,14 @@ def test_read_from_broken_source(): output_data = np.full(shape, fill_value=no_data, dtype='int16') - gbox = mk_gbox(shape, crs=crs) + geobox = mk_gbox(shape, crs=crs) # Check exception is raised with pytest.raises(OSError): - reproject_and_fuse(sources, output_data, gbox, dst_nodata=no_data) + reproject_and_fuse(sources, output_data, geobox, dst_nodata=no_data) # Check can ignore errors - reproject_and_fuse(sources, output_data, gbox, dst_nodata=no_data, + reproject_and_fuse(sources, output_data, geobox, dst_nodata=no_data, skip_broken_datasets=True) assert (output_data == [[2, 2], [2, 2]]).all() @@ -238,11 +238,11 @@ def assert_same_read_results(source, dst_shape, dst_dtype, dst_transform, dst_no result = np.full(dst_shape, dst_nodata, dtype=dst_dtype) H, W = dst_shape - dst_gbox = GeoBox(wh_(W, H), dst_transform, dst_projection) + dst_geobox = GeoBox(wh_(W, H), dst_transform, dst_projection) with source.open() as rdr: read_time_slice(rdr, result, - dst_gbox, + dst_geobox, dst_nodata=dst_nodata, resampling=resampling) @@ -410,10 +410,10 @@ def _read_from_source(source, dest, dst_transform, dst_nodata, dst_projection, r Adapt old signature to new function, so that we can keep old tests at least for now """ H, W = dest.shape - gbox = GeoBox(wh_(W, H), dst_transform, dst_projection) + geobox = GeoBox(wh_(W, H), dst_transform, dst_projection) dest[:] = dst_nodata # new code assumes pre-populated image with source.open() as rdr: - read_time_slice(rdr, dest, gbox, resampling=resampling, dst_nodata=dst_nodata) + read_time_slice(rdr, dest, geobox, resampling=resampling, dst_nodata=dst_nodata) class TestRasterDataReading(object): @@ -657,11 +657,11 @@ def test_rasterio_nodata(tmpdir): mm = write_gtiff(pp/'absent_nodata.tiff', xx, nodata=None) - yy = dc_read(mm.path, gbox=mm.gbox, fallback_nodata=None) + yy = dc_read(mm.path, geobox=mm.geobox, fallback_nodata=None) np.testing.assert_array_equal(xx, yy) # fallback nodata is outside source range so it shouldn't be used - yy = dc_read(mm.path, gbox=mm.gbox, fallback_nodata=-1, dst_nodata=-999, dtype='int16') + yy = dc_read(mm.path, geobox=mm.geobox, fallback_nodata=-1, dst_nodata=-999, dtype='int16') np.testing.assert_array_equal(xx.astype('int16'), yy) # treat zeros as no-data + type conversion while reading diff --git a/tests/storage/test_storage_load.py b/tests/storage/test_storage_load.py index 8aafb550d..3277079a3 100644 --- a/tests/storage/test_storage_load.py +++ b/tests/storage/test_storage_load.py @@ -72,7 +72,7 @@ def band_info_collector(bands, ctx): measurements = [ds.product.measurements[n] for n in ('a', 'b')] measurements[1]['fuser'] = lambda dst, src: _default_fuser(dst, src, measurements[1].nodata) - xx, _ = xr_load(sources, meta.gbox, measurements, rdr) + xx, _ = xr_load(sources, meta.geobox, measurements, rdr) assert len(_bands) == 2 diff --git a/tests/storage/test_storage_read.py b/tests/storage/test_storage_read.py index 682aa8355..f0ba445ee 100644 --- a/tests/storage/test_storage_read.py +++ b/tests/storage/test_storage_read.py @@ -46,68 +46,68 @@ def test_read_paste(tmpdir): mm = write_gtiff(pp/'tst-read-paste-128x64-int16.tif', xx, nodata=None) - def _read(gbox, resampling='nearest', + def _read(geobox, resampling='nearest', fallback_nodata=-999, dst_nodata=-999, check_paste=False): with RasterFileDataSource(mm.path, 1, nodata=fallback_nodata).open() as rdr: if check_paste: # check that we are using paste - rr = compute_reproject_roi(rdr_geobox(rdr), gbox) + rr = compute_reproject_roi(rdr_geobox(rdr), geobox) assert rr.paste_ok is True - yy = np.full(gbox.shape, dst_nodata, dtype=rdr.dtype) - roi = read_time_slice(rdr, yy, gbox, resampling, dst_nodata) + yy = np.full(geobox.shape, dst_nodata, dtype=rdr.dtype) + roi = read_time_slice(rdr, yy, geobox, resampling, dst_nodata) return yy, roi # read native whole - yy, roi = _read(mm.gbox) + yy, roi = _read(mm.geobox) np.testing.assert_array_equal(xx, yy) assert roi == np.s_[0:64, 0:128] # read native whole, no nodata case - yy, roi = _read(mm.gbox, fallback_nodata=None) + yy, roi = _read(mm.geobox, fallback_nodata=None) np.testing.assert_array_equal(xx, yy) assert roi == np.s_[0:64, 0:128] # read native whole, ignoring small sub-pixel translation - yy, roi = _read(gbx.translate_pix(mm.gbox, 0.3, -0.4), fallback_nodata=-33) + yy, roi = _read(gbx.translate_pix(mm.geobox, 0.3, -0.4), fallback_nodata=-33) np.testing.assert_array_equal(xx, yy) assert roi == np.s_[0:64, 0:128] # no overlap between src and dst - yy, roi = _read(gbx.translate_pix(mm.gbox, 10000, -10000)) + yy, roi = _read(gbx.translate_pix(mm.geobox, 10000, -10000)) assert roi_is_empty(roi) # read with Y flipped - yy, roi = _read(gbx.flipy(mm.gbox)) + yy, roi = _read(gbx.flipy(mm.geobox)) np.testing.assert_array_equal(xx[::-1, :], yy) assert roi == np.s_[0:64, 0:128] # read with X flipped - yy, roi = _read(gbx.flipx(mm.gbox)) + yy, roi = _read(gbx.flipx(mm.geobox)) np.testing.assert_array_equal(xx[:, ::-1], yy) assert roi == np.s_[0:64, 0:128] # read with X and Y flipped - yy, roi = _read(gbx.flipy(gbx.flipx(mm.gbox))) + yy, roi = _read(gbx.flipy(gbx.flipx(mm.geobox))) assert roi == np.s_[0:64, 0:128] np.testing.assert_array_equal(xx[::-1, ::-1], yy[roi]) # dst is fully inside src sroi = np.s_[10:19, 31:47] - yy, roi = _read(mm.gbox[sroi]) + yy, roi = _read(mm.geobox[sroi]) np.testing.assert_array_equal(xx[sroi], yy[roi]) # partial overlap - yy, roi = _read(gbx.translate_pix(mm.gbox, -3, -10)) + yy, roi = _read(gbx.translate_pix(mm.geobox, -3, -10)) assert roi == np.s_[10:64, 3:128] np.testing.assert_array_equal(xx[:-10, :-3], yy[roi]) assert (yy[:10, :] == -999).all() assert (yy[:, :3] == -999).all() # scaling paste - yy, roi = _read(gbx.zoom_out(mm.gbox, 2), check_paste=True) + yy, roi = _read(gbx.zoom_out(mm.geobox, 2), check_paste=True) assert roi == np.s_[0:32, 0:64] np.testing.assert_array_equal(xx[1::2, 1::2], yy) @@ -128,44 +128,44 @@ def test_read_with_reproject(tmpdir): resolution=tile.resolution.xy, offset=tile.transform*(0, 0), nodata=-999) - assert mm.gbox == tile + assert mm.geobox == tile - def _read(gbox, + def _read(geobox, resampling='nearest', fallback_nodata=None, dst_nodata=-999): with RasterFileDataSource(mm.path, 1, nodata=fallback_nodata).open() as rdr: - yy = np.full(gbox.shape, dst_nodata, dtype=rdr.dtype) - roi = read_time_slice(rdr, yy, gbox, resampling, dst_nodata) + yy = np.full(geobox.shape, dst_nodata, dtype=rdr.dtype) + roi = read_time_slice(rdr, yy, geobox, resampling, dst_nodata) return yy, roi - gbox = gbx.pad(mm.gbox, 10) - gbox = gbx.zoom_out(gbox, 0.873) - yy, roi = _read(gbox) + geobox = gbx.pad(mm.geobox, 10) + geobox = gbx.zoom_out(geobox, 0.873) + yy, roi = _read(geobox) assert roi[0].start > 0 and roi[1].start > 0 assert (yy[0] == -999).all() - yy_expect, _ = rio_slurp(mm.path, gbox) + yy_expect, _ = rio_slurp(mm.path, geobox) np.testing.assert_array_equal(yy, yy_expect) - gbox = gbx.zoom_out(mm.gbox[3:-3, 10:-10], 2.1) - yy, roi = _read(gbox) + geobox = gbx.zoom_out(mm.geobox[3:-3, 10:-10], 2.1) + yy, roi = _read(geobox) - assert roi_shape(roi) == gbox.shape + assert roi_shape(roi) == geobox.shape assert not (yy == -999).any() - gbox = GeoBox.from_geopolygon(mm.gbox.extent.to_crs(epsg3857).buffer(50), - resolution=mm.gbox.resolution) + geobox = GeoBox.from_geopolygon(mm.geobox.extent.to_crs(epsg3857).buffer(50), + resolution=mm.geobox.resolution) - assert gbox.extent.contains(mm.gbox.extent.to_crs(epsg3857)) - assert gbox.crs != mm.gbox.crs - yy, roi = _read(gbox) + assert geobox.extent.contains(mm.geobox.extent.to_crs(epsg3857)) + assert geobox.crs != mm.geobox.crs + yy, roi = _read(geobox) assert roi[0].start > 0 and roi[1].start > 0 assert (yy[0] == -999).all() - gbox = gbx.zoom_out(gbox, 4) - yy, roi = _read(gbox, resampling='average') + geobox = gbx.zoom_out(geobox, 4) + yy, roi = _read(geobox, resampling='average') nvalid = (yy != -999).sum() nempty = (yy == -999).sum() assert nvalid > nempty @@ -184,7 +184,7 @@ def test_read_paste_v2(tmpdir): mm = write_gtiff(pp/'tst-read-paste-128x64-int16.tif', xx, nodata=None) - def _read(gbox, resampling='nearest', + def _read(geobox, resampling='nearest', fallback_nodata=-999, dst_nodata=-999, check_paste=False): @@ -193,11 +193,11 @@ def _read(gbox, resampling='nearest', nodata=fallback_nodata) if check_paste: # check that we are using paste - rr = compute_reproject_roi(rdr_geobox(rdr), gbox) + rr = compute_reproject_roi(rdr_geobox(rdr), geobox) assert rr.paste_ok is True - yy = np.full(gbox.shape, dst_nodata, dtype=rdr.dtype) - yy_, roi = read_time_slice_v2(rdr, gbox, resampling, dst_nodata) + yy = np.full(geobox.shape, dst_nodata, dtype=rdr.dtype) + yy_, roi = read_time_slice_v2(rdr, geobox, resampling, dst_nodata) if yy_ is None: print(f"Got None out of read_time_slice_v2: {roi} must be empty") else: @@ -205,53 +205,53 @@ def _read(gbox, resampling='nearest', return yy, roi # read native whole - yy, roi = _read(mm.gbox) + yy, roi = _read(mm.geobox) np.testing.assert_array_equal(xx, yy) assert roi == np.s_[0:64, 0:128] # read native whole, no nodata case - yy, roi = _read(mm.gbox, fallback_nodata=None) + yy, roi = _read(mm.geobox, fallback_nodata=None) np.testing.assert_array_equal(xx, yy) assert roi == np.s_[0:64, 0:128] # read native whole, ignoring small sub-pixel translation - yy, roi = _read(gbx.translate_pix(mm.gbox, 0.3, -0.4), fallback_nodata=-33) + yy, roi = _read(gbx.translate_pix(mm.geobox, 0.3, -0.4), fallback_nodata=-33) np.testing.assert_array_equal(xx, yy) assert roi == np.s_[0:64, 0:128] # no overlap between src and dst - yy, roi = _read(gbx.translate_pix(mm.gbox, 10000, -10000)) + yy, roi = _read(gbx.translate_pix(mm.geobox, 10000, -10000)) assert roi_is_empty(roi) # read with Y flipped - yy, roi = _read(gbx.flipy(mm.gbox)) + yy, roi = _read(gbx.flipy(mm.geobox)) np.testing.assert_array_equal(xx[::-1, :], yy) assert roi == np.s_[0:64, 0:128] # read with X flipped - yy, roi = _read(gbx.flipx(mm.gbox)) + yy, roi = _read(gbx.flipx(mm.geobox)) np.testing.assert_array_equal(xx[:, ::-1], yy) assert roi == np.s_[0:64, 0:128] # read with X and Y flipped - yy, roi = _read(gbx.flipy(gbx.flipx(mm.gbox))) + yy, roi = _read(gbx.flipy(gbx.flipx(mm.geobox))) assert roi == np.s_[0:64, 0:128] np.testing.assert_array_equal(xx[::-1, ::-1], yy[roi]) # dst is fully inside src sroi = np.s_[10:19, 31:47] - yy, roi = _read(mm.gbox[sroi]) + yy, roi = _read(mm.geobox[sroi]) np.testing.assert_array_equal(xx[sroi], yy[roi]) # partial overlap - yy, roi = _read(gbx.translate_pix(mm.gbox, -3, -10)) + yy, roi = _read(gbx.translate_pix(mm.geobox, -3, -10)) assert roi == np.s_[10:64, 3:128] np.testing.assert_array_equal(xx[:-10, :-3], yy[roi]) assert (yy[:10, :] == -999).all() assert (yy[:, :3] == -999).all() # scaling paste - yy, roi = _read(gbx.zoom_out(mm.gbox, 2), check_paste=True) + yy, roi = _read(gbx.zoom_out(mm.geobox, 2), check_paste=True) assert roi == np.s_[0:32, 0:64] np.testing.assert_array_equal(xx[1::2, 1::2], yy) @@ -268,15 +268,15 @@ def test_read_with_reproject_v2(tmpdir): assert (xx != -999).all() tile = AlbersGS.tile_geobox((17, -40))[:64, :128] - def _read(gbox, resampling='nearest', + def _read(geobox, resampling='nearest', fallback_nodata=-999, dst_nodata=-999): rdr = open_reader(mm.path, nodata=fallback_nodata) - yy = np.full(gbox.shape, dst_nodata, dtype=rdr.dtype) - yy_, roi = read_time_slice_v2(rdr, gbox, resampling, dst_nodata) + yy = np.full(geobox.shape, dst_nodata, dtype=rdr.dtype) + yy_, roi = read_time_slice_v2(rdr, geobox, resampling, dst_nodata) yy[roi] = yy_ return yy, roi @@ -285,35 +285,35 @@ def _read(gbox, resampling='nearest', resolution=tile.resolution.xy, offset=tile.transform*(0, 0), nodata=-999) - assert mm.gbox == tile + assert mm.geobox == tile - gbox = gbx.pad(mm.gbox, 10) - gbox = gbx.zoom_out(gbox, 0.873) - yy, roi = _read(gbox) + geobox = gbx.pad(mm.geobox, 10) + geobox = gbx.zoom_out(geobox, 0.873) + yy, roi = _read(geobox) assert roi[0].start > 0 and roi[1].start > 0 assert (yy[0] == -999).all() - yy_expect, _ = rio_slurp(mm.path, gbox) + yy_expect, _ = rio_slurp(mm.path, geobox) np.testing.assert_array_equal(yy, yy_expect) - gbox = gbx.zoom_out(mm.gbox[3:-3, 10:-10], 2.1) - yy, roi = _read(gbox) + geobox = gbx.zoom_out(mm.geobox[3:-3, 10:-10], 2.1) + yy, roi = _read(geobox) - assert roi_shape(roi) == gbox.shape + assert roi_shape(roi) == geobox.shape assert not (yy == -999).any() - gbox = GeoBox.from_geopolygon(mm.gbox.extent.to_crs(epsg3857).buffer(50), - resolution=mm.gbox.resolution) + geobox = GeoBox.from_geopolygon(mm.geobox.extent.to_crs(epsg3857).buffer(50), + resolution=mm.geobox.resolution) - assert gbox.extent.contains(mm.gbox.extent.to_crs(epsg3857)) - assert gbox.crs != mm.gbox.crs - yy, roi = _read(gbox) + assert geobox.extent.contains(mm.geobox.extent.to_crs(epsg3857)) + assert geobox.crs != mm.geobox.crs + yy, roi = _read(geobox) assert roi[0].start > 0 and roi[1].start > 0 assert (yy[0] == -999).all() - gbox = gbx.zoom_out(gbox, 4) - yy, roi = _read(gbox, resampling='average') + geobox = gbx.zoom_out(geobox, 4) + yy, roi = _read(geobox, resampling='average') nvalid = (yy != -999).sum() nempty = (yy == -999).sum() assert nvalid > nempty diff --git a/tests/test_load_data.py b/tests/test_load_data.py index e4589cdd5..d1c1305b6 100644 --- a/tests/test_load_data.py +++ b/tests/test_load_data.py @@ -29,11 +29,11 @@ def test_load_data(tmpdir): nodata = -999 aa = mk_test_image(96, 64, 'int16', nodata=nodata) - ds, gbox = gen_tiff_dataset([SimpleNamespace(name='aa', values=aa, nodata=nodata)], - tmpdir, - prefix='ds1-', - timestamp='2018-07-19', - **spatial) + ds, geobox = gen_tiff_dataset([SimpleNamespace(name='aa', values=aa, nodata=nodata)], + tmpdir, + prefix='ds1-', + timestamp='2018-07-19', + **spatial) assert ds.time is not None ds2, _ = gen_tiff_dataset([SimpleNamespace(name='aa', values=aa, nodata=nodata)], @@ -50,7 +50,7 @@ def test_load_data(tmpdir): mm = ['aa'] mm = [ds.product.measurements[k] for k in mm] - ds_data = Datacube.load_data(sources, gbox, mm) + ds_data = Datacube.load_data(sources, geobox, mm) assert ds_data.aa.nodata == nodata np.testing.assert_array_equal(aa, ds_data.aa.values[0]) @@ -66,7 +66,7 @@ def custom_fuser(dest, delta): def progress_cbk(n, nt): progress_call_data.append((n, nt)) - ds_data = Datacube.load_data(sources2, gbox, mm, fuse_func=custom_fuser, + ds_data = Datacube.load_data(sources2, geobox, mm, fuse_func=custom_fuser, progress_cbk=progress_cbk) assert ds_data.aa.nodata == nodata assert custom_fuser_call_count > 0 @@ -91,12 +91,12 @@ def url_mangler(raw): nodata = -999 aa = mk_test_image(96, 64, 'int16', nodata=nodata) - ds, gbox = gen_tiff_dataset([SimpleNamespace(name='aa', values=aa, nodata=nodata)], - tmpdir, - prefix='ds1-', - timestamp='2018-07-19', - base_folder_of_record=recorded_tmpdir, - **spatial) + ds, geobox = gen_tiff_dataset([SimpleNamespace(name='aa', values=aa, nodata=nodata)], + tmpdir, + prefix='ds1-', + timestamp='2018-07-19', + base_folder_of_record=recorded_tmpdir, + **spatial) assert ds.time is not None ds2, _ = gen_tiff_dataset([SimpleNamespace(name='aa', values=aa, nodata=nodata)], @@ -114,11 +114,11 @@ def url_mangler(raw): mm = ['aa'] mm = [ds.product.measurements[k] for k in mm] - ds_data = Datacube.load_data(sources, gbox, mm, patch_url=url_mangler) + ds_data = Datacube.load_data(sources, geobox, mm, patch_url=url_mangler) assert ds_data.aa.nodata == nodata np.testing.assert_array_equal(aa, ds_data.aa.values[0]) - ds_data = Datacube.load_data(sources, gbox, mm, dask_chunks={'x': 8, 'y': 8}, patch_url=url_mangler) + ds_data = Datacube.load_data(sources, geobox, mm, dask_chunks={'x': 8, 'y': 8}, patch_url=url_mangler) assert ds_data.aa.nodata == nodata np.testing.assert_array_equal(aa, ds_data.aa.values[0]) @@ -134,7 +134,7 @@ def custom_fuser(dest, delta): def progress_cbk(n, nt): progress_call_data.append((n, nt)) - ds_data = Datacube.load_data(sources2, gbox, mm, fuse_func=custom_fuser, + ds_data = Datacube.load_data(sources2, geobox, mm, fuse_func=custom_fuser, progress_cbk=progress_cbk, patch_url=url_mangler) assert ds_data.aa.nodata == nodata assert custom_fuser_call_count > 0 @@ -157,11 +157,11 @@ def test_load_data_cbk(tmpdir): bands = [SimpleNamespace(name=name, values=aa, nodata=nodata) for name in ['aa', 'bb']] - ds, gbox = gen_tiff_dataset(bands, - tmpdir, - prefix='ds1-', - timestamp='2018-07-19', - **spatial) + ds, geobox = gen_tiff_dataset(bands, + tmpdir, + prefix='ds1-', + timestamp='2018-07-19', + **spatial) assert ds.time is not None ds2, _ = gen_tiff_dataset(bands, @@ -178,7 +178,7 @@ def test_load_data_cbk(tmpdir): def progress_cbk(n, nt): progress_call_data.append((n, nt)) - ds_data = Datacube.load_data(sources, gbox, ds.product.measurements, + ds_data = Datacube.load_data(sources, geobox, ds.product.measurements, progress_cbk=progress_cbk) assert progress_call_data == [(1, 4), (2, 4), (3, 4), (4, 4)] @@ -195,7 +195,7 @@ def progress_cbk_fail_early2(n, nt): raise KeyboardInterrupt() progress_call_data = [] - ds_data = Datacube.load_data(sources, gbox, ds.product.measurements, + ds_data = Datacube.load_data(sources, geobox, ds.product.measurements, progress_cbk=progress_cbk_fail_early) assert progress_call_data == [(1, 4)] @@ -204,7 +204,7 @@ def progress_cbk_fail_early2(n, nt): np.testing.assert_array_equal(nodata, ds_data.bb.values[0]) progress_call_data = [] - ds_data = Datacube.load_data(sources, gbox, ds.product.measurements, + ds_data = Datacube.load_data(sources, geobox, ds.product.measurements, progress_cbk=progress_cbk_fail_early2) assert ds_data.dc_partial_load is True @@ -251,53 +251,53 @@ def test_rio_slurp(tmpdir): aa, mm = rio_slurp(mm0.path) np.testing.assert_array_equal(aa, aa0) - assert mm.gbox == mm0.gbox - assert aa.shape == mm.gbox.shape + assert mm.geobox == mm0.geobox + assert aa.shape == mm.geobox.shape xx = rio_slurp_xarray(mm0.path) - assert mm.gbox == xx.geobox + assert mm.geobox == xx.geobox np.testing.assert_array_equal(xx.values, aa0) aa, mm = rio_slurp(mm0.path, aa0.shape) np.testing.assert_array_equal(aa, aa0) - assert aa.shape == mm.gbox.shape - assert mm.gbox is mm.src_gbox + assert aa.shape == mm.geobox.shape + assert mm.geobox is mm.src_geobox xx = rio_slurp_xarray(mm0.path, aa0.shape) - assert mm.gbox == xx.geobox + assert mm.geobox == xx.geobox np.testing.assert_array_equal(xx.values, aa0) aa, mm = rio_slurp(mm0.path, (3, 7)) assert aa.shape == (3, 7) - assert aa.shape == mm.gbox.shape - assert mm.gbox != mm.src_gbox - assert mm.src_gbox == mm0.gbox - assert mm.gbox.extent == mm0.gbox.extent + assert aa.shape == mm.geobox.shape + assert mm.geobox != mm.src_geobox + assert mm.src_geobox == mm0.geobox + assert mm.geobox.extent == mm0.geobox.extent aa, mm = rio_slurp(mm0.path, aa0.shape) np.testing.assert_array_equal(aa, aa0) - assert aa.shape == mm.gbox.shape + assert aa.shape == mm.geobox.shape - aa, mm = rio_slurp(mm0.path, mm0.gbox, resampling='nearest') + aa, mm = rio_slurp(mm0.path, mm0.geobox, resampling='nearest') np.testing.assert_array_equal(aa, aa0) - xx = rio_slurp_xarray(mm0.path, mm0.gbox) - assert mm.gbox == xx.geobox + xx = rio_slurp_xarray(mm0.path, mm0.geobox) + assert mm.geobox == xx.geobox np.testing.assert_array_equal(xx.values, aa0) - aa, mm = rio_slurp(mm0.path, gbox=mm0.gbox, dtype='float32') + aa, mm = rio_slurp(mm0.path, geobox=mm0.geobox, dtype='float32') assert aa.dtype == 'float32' np.testing.assert_array_equal(aa, aa0.astype('float32')) - xx = rio_slurp_xarray(mm0.path, gbox=mm0.gbox) - assert mm.gbox == xx.geobox + xx = rio_slurp_xarray(mm0.path, geobox=mm0.geobox) + assert mm.geobox == xx.geobox assert mm.nodata == xx.nodata np.testing.assert_array_equal(xx.values, aa0) - aa, mm = rio_slurp(mm0.path, mm0.gbox, dst_nodata=-33) + aa, mm = rio_slurp(mm0.path, mm0.geobox, dst_nodata=-33) np.testing.assert_array_equal(aa == -33, aa0 == -999) - aa, mm = rio_slurp(mm00.path, mm00.gbox, dst_nodata=None) + aa, mm = rio_slurp(mm00.path, mm00.geobox, dst_nodata=None) np.testing.assert_array_equal(aa, aa0) -def test_rio_slurp_with_gbox(tmpdir): +def test_rio_slurp_with_geobox(tmpdir): w, h, dtype, nodata, ndw = 96, 64, 'int16', -999, 7 pp = Path(str(tmpdir)) @@ -313,7 +313,7 @@ def test_rio_slurp_with_gbox(tmpdir): mm = write_gtiff(pp/"rio-slurp-aa.tif", aa, nodata=-999, overwrite=True) assert mm.count == 2 - aa, mm = rio_slurp(mm.path, mm.gbox) + aa, mm = rio_slurp(mm.path, mm.geobox) assert aa.shape == aa0.shape np.testing.assert_array_equal(aa, aa0) @@ -351,24 +351,24 @@ def test_native_load(tmpdir): for name in ['aa', 'bb']] bands.append(SimpleNamespace(name='cc', values=cc, nodata=nodata)) - ds, gbox = gen_tiff_dataset(bands[:2], - tmpdir, - prefix='ds1-', - timestamp='2018-07-19', - **spatial) + ds, geobox = gen_tiff_dataset(bands[:2], + tmpdir, + prefix='ds1-', + timestamp='2018-07-19', + **spatial) assert set(get_raster_info(ds)) == set(ds.measurements) xx = native_load(ds) - assert xx.geobox == gbox + assert xx.geobox == geobox np.testing.assert_array_equal(aa, xx.isel(time=0).aa.values) np.testing.assert_array_equal(aa, xx.isel(time=0).bb.values) - ds, gbox_cc = gen_tiff_dataset(bands, - tmpdir, - prefix='ds2-', - timestamp='2018-07-19', - **spatial) + ds, geobox_cc = gen_tiff_dataset(bands, + tmpdir, + prefix='ds2-', + timestamp='2018-07-19', + **spatial) # cc is different size from aa,bb with pytest.raises(ValueError): @@ -379,20 +379,20 @@ def test_native_load(tmpdir): xx = native_geobox(ds) # aa and bb are the same - assert native_geobox(ds, ['aa', 'bb']) == gbox + assert native_geobox(ds, ['aa', 'bb']) == geobox xx = native_load(ds, ['aa', 'bb']) - assert xx.geobox == gbox + assert xx.geobox == geobox np.testing.assert_array_equal(aa, xx.isel(time=0).aa.values) np.testing.assert_array_equal(aa, xx.isel(time=0).bb.values) # cc will be reprojected - assert native_geobox(ds, basis='aa') == gbox + assert native_geobox(ds, basis='aa') == geobox xx = native_load(ds, basis='aa') - assert xx.geobox == gbox + assert xx.geobox == geobox np.testing.assert_array_equal(aa, xx.isel(time=0).aa.values) np.testing.assert_array_equal(aa, xx.isel(time=0).bb.values) # cc is compatible with self xx = native_load(ds, ['cc']) - assert xx.geobox == gbox_cc + assert xx.geobox == geobox_cc np.testing.assert_array_equal(cc, xx.isel(time=0).cc.values) diff --git a/tests/test_testutils.py b/tests/test_testutils.py index ee91249ea..fd168fe2c 100644 --- a/tests/test_testutils.py +++ b/tests/test_testutils.py @@ -6,7 +6,7 @@ from datacube.testutils.threads import FakeThreadPoolExecutor from datacube.testutils import mk_sample_xr_dataset, mk_sample_dataset from datacube.testutils.io import native_geobox -from odc.geo import XY +from odc.geo import xy_ def test_fakethreadpool(): @@ -49,14 +49,14 @@ def test_mk_sample_xr(): assert 'band' in xx.data_vars assert list(xx.coords) == ['time', 'y', 'x', 'spatial_ref'] assert xx.band.dims == ('time', 'y', 'x') - assert xx.geobox is not None + assert xx.odc.geobox is not None assert mk_sample_xr_dataset(name='xx', shape=(3, 7)).xx.shape == (1, 3, 7) assert mk_sample_xr_dataset(name='xx', time=None, shape=(3, 7)).xx.shape == (3, 7) assert mk_sample_xr_dataset(name='xx', time=None).xx.dims == ('y', 'x') - assert mk_sample_xr_dataset(resolution=(1, 100)).geobox.resolution == XY(100, 1) - assert mk_sample_xr_dataset(resolution=(1, 100), xy=(3, 55)).geobox.transform*(0, 0) == (3, 55) + assert mk_sample_xr_dataset(resolution=(1, 100)).odc.geobox.resolution == xy_(100, 1) + assert mk_sample_xr_dataset(resolution=(1, 100), xy=(3, 55)).odc.geobox.transform*(0, 0) == (3, 55) def test_native_geobox_ingested(): diff --git a/tests/test_utils_cog.py b/tests/test_utils_cog.py index 7d2b86282..3e7b773ec 100644 --- a/tests/test_utils_cog.py +++ b/tests/test_utils_cog.py @@ -79,7 +79,7 @@ def test_cog_file(tmpdir, opts): ) yy, mm = rio_slurp(pp / "cog-2-bands.tif") - assert mm.gbox == xx.geobox + assert mm.geobox == xx.geobox assert yy.shape == (2, *xx.shape) np.testing.assert_array_equal(yy[0], xx.values) np.testing.assert_array_equal(yy[1], xx.values) diff --git a/tests/test_utils_other.py b/tests/test_utils_other.py index 76646dcb5..c4bdb9d9d 100644 --- a/tests/test_utils_other.py +++ b/tests/test_utils_other.py @@ -25,18 +25,10 @@ from datacube.utils.dates import date_sequence from datacube.utils.math import ( num2numpy, - is_almost_int, - maybe_zero, - maybe_int, - snap_scale, valid_mask, invalid_mask, - clamp, unsqueeze_data_array, unsqueeze_dataset, - spatial_dims, - data_resolution_and_offset, - affine_from_axis, ) from datacube.utils.py import sorted_items from datacube.utils.uris import (uri_to_local_path, mk_part_uri, get_part_from_uri, as_url, is_url, @@ -153,18 +145,6 @@ def test_pick_uri(): pick_uri([s, h], 'file:') -@given(integers(), integers(), integers()) -def test_clamp(x, lower_bound, upper_bound): - if lower_bound > upper_bound: - lower_bound, upper_bound = upper_bound, lower_bound - new_x = clamp(x, lower_bound, upper_bound) - - # If x was already between the bounds, it shouldn't have changed - if lower_bound <= x <= upper_bound: - assert new_x == x - assert lower_bound <= new_x <= upper_bound - - @given(integers(min_value=10, max_value=30)) def test_gen_pass(n_bytes): password1 = gen_password(n_bytes) @@ -391,7 +371,7 @@ def test_testutils_gtif(tmpdir): assert fname.exists() assert fname5.exists() - assert aa_meta.gbox.shape == (h, w) + assert aa_meta.geobox.shape == (h, w) assert aa_meta.path is fname aa_, aa_meta_ = rio_slurp(fname) @@ -418,13 +398,13 @@ def test_testutils_gtif(tmpdir): # check that overwrite re-writes file write_gtiff(fname, bb[:32, :32], - gbox=aa_meta.gbox[:32, :32], + geobox=aa_meta.geobox[:32, :32], overwrite=True) bb_, mm = rio_slurp(fname, (32, 32)) np.testing.assert_array_equal(bb[:32, :32], bb_) - assert mm.gbox == aa_meta.gbox[:32, :32] + assert mm.geobox == aa_meta.geobox[:32, :32] with pytest.raises(ValueError): write_gtiff(fname, np.zeros((3, 4, 5, 6))) @@ -494,42 +474,6 @@ def test_is_url(test_input, expected): assert as_url(test_input) is test_input -def test_is_almost_int(): - assert is_almost_int(1, 1e-10) - assert is_almost_int(1.001, .1) - assert is_almost_int(2 - 0.001, .1) - assert is_almost_int(-1.001, .1) - - -def test_maybe_zero(): - assert maybe_zero(0.0001, 0.1) == 0 - assert maybe_zero(-0.0001, 0.1) == 0 - assert maybe_zero(1.5, 0.1) == 1.5 - - -def test_maybe_int(): - assert maybe_int(1, 1e-10) == 1 - assert maybe_int(1.6, .1) == 1.6 - assert maybe_int(-1.6, .1) == -1.6 - assert maybe_int(1.001, .1) == 1 - assert maybe_int(2 - 0.001, .1) == 2 - assert maybe_int(-1.001, .1) == -1 - assert maybe_int(1.1, .1) == 1.1 - for x in [3/7, 7/3, -13.7878]: - assert maybe_int(x, 1e-10) is x - - -def test_snap_scale(): - assert snap_scale(0.9999999) == 1 - assert snap_scale(-0.9999999) == -1 - for x in [0.0, 0.999, 0.621612621868, 3/7, 7/3]: - assert snap_scale(x) is x - x = -x - assert snap_scale(x) is x - assert snap_scale(0.33333331) == 1/3 - assert snap_scale(-0.33333331) == -1/3 - - def test_valid_mask(): xx = np.zeros((4, 8), dtype='float32') mm = valid_mask(xx, 0) @@ -601,55 +545,6 @@ def test_num2numpy(): assert num2numpy(3.3, np.float64).dtype == np.dtype('float64') -def test_utils_datares(): - assert data_resolution_and_offset(np.array([1.5, 2.5, 3.5])) == (1.0, 1.0) - assert data_resolution_and_offset(np.array([5, 3, 1])) == (-2.0, 6.0) - assert data_resolution_and_offset(np.array([5, 3])) == (-2.0, 6.0) - assert data_resolution_and_offset(np.array([1.5]), 1) == (1.0, 1.0) - - with pytest.raises(ValueError): - data_resolution_and_offset(np.array([])) - - with pytest.raises(ValueError): - data_resolution_and_offset(np.array([]), 10) - - with pytest.raises(ValueError): - data_resolution_and_offset(np.array([1])) - - -def test_utils_affine_from_axis(): - assert affine_from_axis(np.asarray([1.5, 2.5, 3.5]), - np.asarray([10.5, 11.5])) * (0, 0) == (1.0, 10.0) - - assert affine_from_axis(np.asarray([1.5, 2.5, 3.5]), - np.asarray([10.5, 11.5])) * (2, 1) == (3, 11) - - (sx, z1, tx, - z2, sy, ty, *_) = affine_from_axis(np.asarray([1, 2, 3]), - np.asarray([10, 20])) - assert z1 == 0 and z2 == 0 - assert sx == 1 and sy == 10 - assert tx == 0.5 and ty == 5 - - (sx, _, tx, - _, sy, ty, *_) = affine_from_axis(np.asarray([1]), - np.asarray([10, 20]), 1) - assert sx == 1 and sy == 10 - assert tx == 0.5 and ty == 5 - - (sx, _, tx, - _, sy, ty, *_) = affine_from_axis(np.asarray([1]), - np.asarray([10, 20]), (1, 1)) - assert sx == 1 and sy == 10 - assert tx == 0.5 and ty == 5 - - (sx, _, tx, - _, sy, ty, *_) = affine_from_axis(np.asarray([1]), - np.asarray([10]), (2, 10)) - assert sx == 2 and sy == 10 - assert tx == 0 and ty == 5 - - def test_utils_math(): xx = xr.DataArray(np.zeros((3, 4)), name='xx', @@ -667,24 +562,6 @@ def test_utils_math(): assert ds.xx.data.shape == (1, 3, 4) -def test_spatial_dims(): - def tt(d1, d2): - coords = {} - coords[d1] = np.arange(3) - coords[d2] = np.arange(4) - return xr.DataArray(np.zeros((3, 4)), - name='xx', - dims=(d1, d2), - coords=coords) - - assert spatial_dims(tt('y', 'x')) == ('y', 'x') - assert spatial_dims(tt('x', 'y')) == ('y', 'x') - assert spatial_dims(tt('latitude', 'longitude')) == ('latitude', 'longitude') - assert spatial_dims(tt('lat', 'lon')) == ('lat', 'lon') - assert spatial_dims(tt('a', 'b')) is None - assert spatial_dims(tt('a', 'b'), relaxed=True) == ('a', 'b') - - def test_check_write_path(tmpdir): tmpdir = Path(str(tmpdir)) some_path = tmpdir/"_should_not_exist-5125177.txt" diff --git a/tests/test_xarray_extension.py b/tests/test_xarray_extension.py index 68c37a9d2..12f856b9c 100644 --- a/tests/test_xarray_extension.py +++ b/tests/test_xarray_extension.py @@ -30,7 +30,7 @@ def test_xr_extension(odc_style_xr_dataset): xx = odc_style_xr_dataset - assert (1,) + xx.geobox.shape == xx.B10.shape + assert (1,) + xx.odc.geobox.shape == xx.B10.shape (sx, zz0, tx, zz1, sy, ty) = xx.affine[:6] assert (zz0, zz1) == (0, 0) @@ -51,8 +51,8 @@ def test_xr_geobox(): ds = mk_sample_xr_dataset(crs=epsg3857, xy=xy, resolution=resolution) - assert ds.geobox.crs == epsg3857 - assert ds.band.geobox.crs == epsg3857 + assert ds.odc.geobox.crs == epsg3857 + assert ds.band.odc.geobox.crs == epsg3857 assert ds.band.affine * (0, 0) == xy assert ds.band.affine * (1, 1) == tuple(a + b for a, b in zip(xy, rxy)) @@ -61,10 +61,10 @@ def test_xr_geobox(): assert ds.band.isel(time=0, x=0).affine is None xx = ds.band + 1000 - assert xx.geobox is not None - assert xx.geobox == ds.band.geobox + assert xx.odc.geobox is not None + assert xx.odc.geobox == ds.band.odc.geobox - assert mk_sample_xr_dataset(crs=epsg4326).geobox.crs == epsg4326 - assert mk_sample_xr_dataset(crs=epsg4326).band.geobox.crs == epsg4326 + assert mk_sample_xr_dataset(crs=epsg4326).odc.geobox.crs == epsg4326 + assert mk_sample_xr_dataset(crs=epsg4326).band.odc.geobox.crs == epsg4326 assert mk_sample_xr_dataset(crs=None).affine is not None