rasterix 0.1a3__py3-none-any.whl → 0.1a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rasterix/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1a3'
21
- __version_tuple__ = version_tuple = (0, 1, 'a3')
20
+ __version__ = version = '0.1a4'
21
+ __version_tuple__ = version_tuple = (0, 1, 'a4')
rasterix/raster_index.py CHANGED
@@ -3,59 +3,97 @@ from __future__ import annotations
3
3
  import math
4
4
  import textwrap
5
5
  from collections.abc import Hashable, Iterable, Mapping, Sequence
6
- from typing import Any, Self, TypeVar
6
+ from typing import Any, Self, TypeVar, cast
7
7
 
8
8
  import numpy as np
9
9
  import pandas as pd
10
+ import xproj
10
11
  from affine import Affine
11
- from xarray import Coordinates, DataArray, Dataset, Index, Variable
12
+ from pyproj import CRS
13
+ from xarray import Coordinates, DataArray, Dataset, Index, Variable, get_options
12
14
  from xarray.core.coordinate_transform import CoordinateTransform
13
15
 
14
16
  # TODO: import from public API once it is available
15
- from xarray.core.indexes import CoordinateTransformIndex, PandasIndex
17
+ from xarray.core.indexes import CoordinateTransformIndex
16
18
  from xarray.core.indexing import IndexSelResult, merge_sel_results
19
+ from xarray.core.types import JoinOptions
20
+ from xproj.typing import CRSAwareIndex
17
21
 
18
22
  from rasterix.odc_compat import BoundingBox, bbox_intersection, bbox_union, maybe_int, snap_grid
19
23
  from rasterix.rioxarray_compat import guess_dims
24
+ from rasterix.utils import get_affine
20
25
 
21
26
  T_Xarray = TypeVar("T_Xarray", "DataArray", "Dataset")
22
27
 
23
28
  __all__ = ["assign_index", "RasterIndex"]
24
29
 
25
30
 
26
- def assign_index(obj: T_Xarray, *, x_dim: str | None = None, y_dim: str | None = None) -> T_Xarray:
31
+ # X/Y axis order conventions used for public and internal attributes
32
+ XAXIS = 0
33
+ YAXIS = 1
34
+
35
+
36
+ def assign_index(
37
+ obj: T_Xarray, *, x_dim: str | None = None, y_dim: str | None = None, crs: bool = True
38
+ ) -> T_Xarray:
27
39
  """Assign a RasterIndex to an Xarray DataArray or Dataset.
28
40
 
41
+ By default, the affine transform is guessed by first looking for a ``GeoTransform`` attribute
42
+ on a CF "grid mapping" variable (commonly ``"spatial_ref"``). If not present, then the affine is determined from 1D coordinate
43
+ variables named ``x_dim`` and ``y_dim`` provided to this function.
44
+
29
45
  Parameters
30
46
  ----------
31
47
  obj : xarray.DataArray or xarray.Dataset
32
- The object to assign the index to. Must have a rio accessor with a transform.
48
+ The object to assign the index to.
33
49
  x_dim : str, optional
34
50
  Name of the x dimension. If None, will be automatically detected.
35
51
  y_dim : str, optional
36
52
  Name of the y dimension. If None, will be automatically detected.
53
+ crs: bool, optional
54
+ Auto-detect CRS using xproj?
37
55
 
38
56
  Returns
39
57
  -------
40
58
  xarray.DataArray or xarray.Dataset
41
59
  The input object with RasterIndex coordinates assigned.
42
60
 
61
+ Notes
62
+ -----
63
+ The "grid mapping" variable is determined following the CF conventions:
64
+
65
+ - If a DataArray is provided, we look for an attribute named ``"grid_mapping"``.
66
+ - For a Dataset, we pull the first detected ``"grid_mapping"`` attribute when iterating over data variables.
67
+
68
+ The value of this attribute is a variable name containing projection information (commonly ``"spatial_ref"``).
69
+ We then look for a ``"GeoTransform"`` attribute on this variable (following GDAL convention).
70
+
71
+ References
72
+ ----------
73
+ - `CF conventions document <http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#grid-mappings-and-projections>`_.
74
+ - `GDAL docs on GeoTransform <https://gdal.org/en/stable/tutorials/geotransforms_tut.html>`_.
75
+
43
76
  Examples
44
77
  --------
45
78
  >>> import xarray as xr
46
- >>> import rioxarray # Required for rio accessor
79
+ >>> import rioxarray # Required for reading TIFF
47
80
  >>> da = xr.open_dataset("path/to/raster.tif", engine="rasterio")
48
81
  >>> indexed_da = assign_index(da)
49
82
  """
50
- import rioxarray # noqa
51
-
52
83
  if x_dim is None or y_dim is None:
53
84
  guessed_x, guessed_y = guess_dims(obj)
54
85
  x_dim = x_dim or guessed_x
55
86
  y_dim = y_dim or guessed_y
56
87
 
88
+ affine = get_affine(obj, x_dim=x_dim, y_dim=y_dim, clear_transform=True)
89
+
57
90
  index = RasterIndex.from_transform(
58
- obj.rio.transform(), obj.sizes[x_dim], obj.sizes[y_dim], x_dim=x_dim, y_dim=y_dim
91
+ affine,
92
+ width=obj.sizes[x_dim],
93
+ height=obj.sizes[y_dim],
94
+ x_dim=x_dim,
95
+ y_dim=y_dim,
96
+ crs=obj.proj.crs if crs else None,
59
97
  )
60
98
  coords = Coordinates.from_xindex(index)
61
99
  return obj.assign_coords(coords)
@@ -91,8 +129,8 @@ class AffineTransform(CoordinateTransform):
91
129
  self.affine = affine
92
130
 
93
131
  # array dimensions in reverse order (y = rows, x = cols)
94
- self.xy_dims = self.dims[0], self.dims[1]
95
- self.dims = self.dims[1], self.dims[0]
132
+ self.xy_dims = self.dims[XAXIS], self.dims[YAXIS]
133
+ self.dims = self.dims[YAXIS], self.dims[XAXIS]
96
134
 
97
135
  def forward(self, dim_positions):
98
136
  positions = tuple(dim_positions[dim] for dim in self.xy_dims)
@@ -114,13 +152,17 @@ class AffineTransform(CoordinateTransform):
114
152
 
115
153
  return results
116
154
 
117
- def equals(self, other, *, exclude):
155
+ def equals(self, other: CoordinateTransform, *, exclude: frozenset[Hashable] | None = None) -> bool:
118
156
  if exclude is not None:
119
157
  raise NotImplementedError
120
158
  if not isinstance(other, AffineTransform):
121
159
  return False
122
160
  return self.affine == other.affine and self.dim_size == other.dim_size
123
161
 
162
+ def __repr__(self) -> str:
163
+ params = ", ".join(f"{pn}={getattr(self.affine, pn):.4g}" for pn in "abcdef")
164
+ return f"{type(self).__name__}({params})"
165
+
124
166
 
125
167
  class AxisAffineTransform(CoordinateTransform):
126
168
  """Axis-independent wrapper of an affine 2D transform with no skew/rotation."""
@@ -169,7 +211,7 @@ class AxisAffineTransform(CoordinateTransform):
169
211
 
170
212
  return {self.dim: positions}
171
213
 
172
- def equals(self, other, *, exclude):
214
+ def equals(self, other: CoordinateTransform, *, exclude: frozenset[Hashable] | None = None) -> bool:
173
215
  if not isinstance(other, AxisAffineTransform):
174
216
  return False
175
217
  if exclude is not None:
@@ -188,14 +230,10 @@ class AxisAffineTransform(CoordinateTransform):
188
230
  return self.forward({self.dim: np.arange(self.size)})
189
231
 
190
232
  def slice(self, slice: slice) -> AxisAffineTransform:
191
- start = max(slice.start or 0, 0)
192
- stop = min(slice.stop or self.size, self.size)
193
- step = slice.step or 1
194
-
195
- # TODO: support reverse transform (i.e., start > stop)?
196
- assert start < stop
197
-
198
- size = (stop - start) // step
233
+ newrange = range(self.size)[slice]
234
+ start = newrange.start
235
+ step = newrange.step or 1
236
+ size = len(newrange)
199
237
  scale = float(step)
200
238
 
201
239
  if self.is_xaxis:
@@ -212,6 +250,10 @@ class AxisAffineTransform(CoordinateTransform):
212
250
  dtype=self.dtype,
213
251
  )
214
252
 
253
+ def __repr__(self) -> str:
254
+ params = ", ".join(f"{pn}={getattr(self.affine, pn):.4g}" for pn in "abcdef")
255
+ return f"{type(self).__name__}({params}, axis={'X' if self.is_xaxis else 'Y'}, dim={self.dim!r})"
256
+
215
257
 
216
258
  class AxisAffineTransformIndex(CoordinateTransformIndex):
217
259
  """Axis-independent Xarray Index for an affine 2D transform with no
@@ -225,9 +267,6 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
225
267
  - Data slicing computes a new affine transform and returns a new
226
268
  `AxisAffineTransformIndex` object
227
269
 
228
- - Otherwise data selection creates and returns a new Xarray
229
- `PandasIndex` object for non-scalar indexers
230
-
231
270
  - The index can be converted to a `pandas.Index` object (useful for Xarray
232
271
  operations that don't work with Xarray indexes yet).
233
272
 
@@ -244,7 +283,7 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
244
283
 
245
284
  def isel( # type: ignore[override]
246
285
  self, indexers: Mapping[Any, int | slice | np.ndarray | Variable]
247
- ) -> AxisAffineTransformIndex | PandasIndex | None:
286
+ ) -> AxisAffineTransformIndex | None:
248
287
  idxer = indexers[self.dim]
249
288
 
250
289
  # generate a new index with updated transform if a slice is given
@@ -256,14 +295,17 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
256
295
  # no index for scalar value
257
296
  elif np.ndim(idxer) == 0:
258
297
  return None
259
- # otherwise return a PandasIndex with values computed by forward transformation
298
+ # otherwise drop the index
299
+ # (TODO: return a PandasIndex with values computed by forward transformation when it
300
+ # will be possible to auto-convert RasterIndex to PandasIndex for x and/or y axis)
260
301
  else:
261
- values = self.axis_transform.forward({self.dim: idxer})[self.axis_transform.coord_name]
262
- if isinstance(idxer, Variable):
263
- new_dim = idxer.dims[0]
264
- else:
265
- new_dim = self.dim
266
- return PandasIndex(values, new_dim, coord_dtype=values.dtype)
302
+ return None
303
+ # values = self.axis_transform.forward({self.dim: idxer})[self.axis_transform.coord_name]
304
+ # if isinstance(idxer, Variable):
305
+ # new_dim = idxer.dims[0]
306
+ # else:
307
+ # new_dim = self.dim
308
+ # return PandasIndex(values, new_dim, coord_dtype=values.dtype)
267
309
 
268
310
  def sel(self, labels, method=None, tolerance=None):
269
311
  coord_name = self.axis_transform.coord_name
@@ -311,22 +353,10 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
311
353
  return pd.Index(values[self.dim])
312
354
 
313
355
 
314
- # The types of Xarray indexes that may be wrapped by RasterIndex
315
- WrappedIndex = AxisAffineTransformIndex | PandasIndex | CoordinateTransformIndex
316
- WrappedIndexCoords = Hashable | tuple[Hashable, Hashable]
317
-
318
-
319
- def _filter_dim_indexers(index: WrappedIndex, indexers: Mapping) -> Mapping:
320
- if isinstance(index, CoordinateTransformIndex):
321
- dims = index.transform.dims
322
- else:
323
- # PandasIndex
324
- dims = (str(index.dim),)
356
+ WrappedIndex = tuple[AxisAffineTransformIndex, AxisAffineTransformIndex] | CoordinateTransformIndex
325
357
 
326
- return {dim: indexers[dim] for dim in dims if dim in indexers}
327
358
 
328
-
329
- class RasterIndex(Index):
359
+ class RasterIndex(Index, xproj.ProjIndexMixin):
330
360
  """Xarray index for raster coordinate indexing and spatial operations.
331
361
 
332
362
  RasterIndex provides spatial indexing capabilities for raster data by wrapping
@@ -341,11 +371,13 @@ class RasterIndex(Index):
341
371
  - **Rectilinear grids**: Uses separate 1D indexes for independent x/y axes,
342
372
  enabling more efficient slicing operations.
343
373
 
344
- Parameters
345
- ----------
346
- indexes : Mapping[WrappedIndexCoords, WrappedIndex]
347
- Dictionary mapping coordinate names to their corresponding index objects.
348
- Keys are either single coordinate names or tuples for coupled coordinates.
374
+ RasterIndex is CRS-aware, i.e., it has a ``crs`` property that is used for
375
+ checking equality or compatibility with other RasterIndex instances. CRS is
376
+ optional.
377
+
378
+ Do not use :py:meth:`~rasterix.RasterIndex.__init__` directly. Instead use
379
+ :py:meth:`~rasterix.RasterIndex.from_transform` or
380
+ :py:func:`~rasterix.assign_index`.
349
381
 
350
382
  Attributes
351
383
  ----------
@@ -376,38 +408,95 @@ class RasterIndex(Index):
376
408
  For rectilinear grids without rotation, RasterIndex creates separate 1D indexes
377
409
  for x and y coordinates, which enables efficient slicing operations. For grids
378
410
  with rotation or skew, it uses a coupled 2D transform.
411
+
379
412
  """
380
413
 
381
- _wrapped_indexes: dict[WrappedIndexCoords, WrappedIndex]
382
- _shape: dict[str, int]
414
+ _index: WrappedIndex
415
+ _axis_independent: bool
416
+ _crs: CRS | None
417
+ _xy_shape: tuple[int, int]
418
+ _xy_dims: tuple[str, str]
419
+ _xy_coord_names: tuple[Hashable, Hashable]
420
+
421
+ def __init__(self, index: WrappedIndex, crs: CRS | Any | None = None):
422
+ if isinstance(index, CoordinateTransformIndex) and isinstance(index.transform, AffineTransform):
423
+ self._axis_independent = False
424
+ xtransform = cast(AffineTransform, index.transform)
425
+ dim_size = xtransform.dim_size
426
+ xy_dims = xtransform.xy_dims
427
+ self._xy_shape = (dim_size[xy_dims[XAXIS]], dim_size[xy_dims[YAXIS]])
428
+ self._xy_dims = xtransform.xy_dims
429
+ self._xy_coord_names = (xtransform.coord_names[XAXIS], xtransform.coord_names[YAXIS])
430
+ elif (
431
+ isinstance(index, tuple)
432
+ and len(index) == 2
433
+ and isinstance(index[XAXIS], AxisAffineTransformIndex)
434
+ and isinstance(index[YAXIS], AxisAffineTransformIndex)
435
+ ):
436
+ self._axis_independent = True
437
+ self._xy_shape = (index[XAXIS].axis_transform.size, index[YAXIS].axis_transform.size)
438
+ self._xy_dims = (index[XAXIS].axis_transform.dim, index[YAXIS].axis_transform.dim)
439
+ self._xy_coord_names = (
440
+ index[XAXIS].axis_transform.coord_name,
441
+ index[YAXIS].axis_transform.coord_name,
442
+ )
443
+ else:
444
+ raise ValueError(f"Could not create RasterIndex. Received invalid index {index!r}")
383
445
 
384
- def __init__(self, indexes: Mapping[WrappedIndexCoords, WrappedIndex]):
385
- idx_keys = list(indexes)
386
- idx_vals = list(indexes.values())
446
+ self._index = index
387
447
 
388
- # either one or the other configuration (dependent vs. independent x/y axes)
389
- axis_dependent = (
390
- len(indexes) == 1
391
- and isinstance(idx_keys[0], tuple)
392
- and isinstance(idx_vals[0], CoordinateTransformIndex)
393
- )
394
- axis_independent = len(indexes) in (1, 2) and all(
395
- isinstance(idx, AxisAffineTransformIndex | PandasIndex) for idx in idx_vals
396
- )
397
- assert axis_dependent ^ axis_independent
398
-
399
- self._wrapped_indexes = dict(indexes)
400
- if axis_independent:
401
- self._shape = {
402
- "x": self._wrapped_indexes["x"].axis_transform.size,
403
- "y": self._wrapped_indexes["y"].axis_transform.size,
404
- }
448
+ if crs is not None:
449
+ crs = CRS.from_user_input(crs)
450
+ self._crs = crs
451
+
452
+ @property
453
+ def _wrapped_indexes(self) -> tuple[CoordinateTransformIndex | AxisAffineTransformIndex, ...]:
454
+ """Returns the wrapped index objects as a tuple."""
455
+ if not isinstance(self._index, tuple):
456
+ return (self._index,)
405
457
  else:
406
- self._shape = next(iter(self._wrapped_indexes.values())).dim_size
458
+ return self._index
459
+
460
+ @property
461
+ def _xy_indexes(self) -> tuple[AxisAffineTransformIndex, AxisAffineTransformIndex]:
462
+ assert self._axis_independent
463
+ return cast(tuple[AxisAffineTransformIndex, AxisAffineTransformIndex], self._index)
464
+
465
+ @property
466
+ def _xxyy_index(self) -> CoordinateTransformIndex:
467
+ assert not self._axis_independent
468
+ return cast(CoordinateTransformIndex, self._index)
469
+
470
+ @property
471
+ def xy_shape(self) -> tuple[int, int]:
472
+ """Return the dimension size of the X and Y axis, respectively."""
473
+ return self._xy_shape
474
+
475
+ @property
476
+ def xy_dims(self) -> tuple[str, str]:
477
+ """Return the dimension name of the X and Y axis, respectively."""
478
+ return self._xy_dims
479
+
480
+ @property
481
+ def xy_coord_names(self) -> tuple[Hashable, Hashable]:
482
+ """Return the name of the coordinate variables representing labels on
483
+ the X and Y axis, respectively.
484
+
485
+ """
486
+ return self._xy_coord_names
407
487
 
408
488
  @classmethod
409
489
  def from_transform(
410
- cls, affine: Affine, width: int, height: int, x_dim: str = "x", y_dim: str = "y"
490
+ cls,
491
+ affine: Affine,
492
+ *,
493
+ width: int,
494
+ height: int,
495
+ x_dim: str = "x",
496
+ y_dim: str = "y",
497
+ x_coord_name: str = "xc",
498
+ y_coord_name: str = "yc",
499
+ crs: CRS | Any | None = None,
411
500
  ) -> RasterIndex:
412
501
  """Create a RasterIndex from an affine transform and raster dimensions.
413
502
 
@@ -420,10 +509,17 @@ class RasterIndex(Index):
420
509
  Number of pixels in the x direction.
421
510
  height : int
422
511
  Number of pixels in the y direction.
423
- x_dim : str, default "x"
512
+ x_dim : str, optional
424
513
  Name for the x dimension.
425
- y_dim : str, default "y"
514
+ y_dim : str, optional
426
515
  Name for the y dimension.
516
+ x_coord_name : str, optional
517
+ Name for the x dimension. For non-rectilinear transforms only.
518
+ y_coord_name : str, optional
519
+ Name for the y dimension. For non-rectilinear transforms only.
520
+ crs : :class:`pyproj.crs.CRS` or any, optional
521
+ The coordinate reference system. Any value accepted by
522
+ :meth:`pyproj.crs.CRS.from_user_input`.
427
523
 
428
524
  Returns
429
525
  -------
@@ -443,24 +539,38 @@ class RasterIndex(Index):
443
539
  >>> from affine import Affine
444
540
  >>> transform = Affine(1.0, 0.0, 0.0, 0.0, -1.0, 100.0)
445
541
  >>> index = RasterIndex.from_transform(transform, width=100, height=100)
542
+
543
+ Create a non-rectilinear index:
544
+
545
+ >>> index = RasterIndex.from_transform(
546
+ ... Affine.rotation(45), width=100, height=100, x_coord_name="x1", y_coord_name="x2"
547
+ ... )
446
548
  """
447
- indexes: dict[WrappedIndexCoords, AxisAffineTransformIndex | CoordinateTransformIndex]
549
+ index: WrappedIndex
448
550
 
449
551
  # pixel centered coordinates
450
552
  affine = affine * Affine.translation(0.5, 0.5)
451
553
 
452
554
  if affine.is_rectilinear and affine.b == affine.d == 0:
453
- x_transform = AxisAffineTransform(affine, width, "x", x_dim, is_xaxis=True)
454
- y_transform = AxisAffineTransform(affine, height, "y", y_dim, is_xaxis=False)
455
- indexes = {
456
- "x": AxisAffineTransformIndex(x_transform),
457
- "y": AxisAffineTransformIndex(y_transform),
458
- }
555
+ x_transform = AxisAffineTransform(affine, width, x_dim, x_dim, is_xaxis=True)
556
+ y_transform = AxisAffineTransform(affine, height, y_dim, y_dim, is_xaxis=False)
557
+ index = (
558
+ AxisAffineTransformIndex(x_transform),
559
+ AxisAffineTransformIndex(y_transform),
560
+ )
459
561
  else:
460
- xy_transform = AffineTransform(affine, width, height, x_dim=x_dim, y_dim=y_dim)
461
- indexes = {("x", "y"): CoordinateTransformIndex(xy_transform)}
562
+ xy_transform = AffineTransform(
563
+ affine,
564
+ width,
565
+ height,
566
+ x_dim=x_dim,
567
+ y_dim=y_dim,
568
+ x_coord_name=x_coord_name,
569
+ y_coord_name=y_coord_name,
570
+ )
571
+ index = CoordinateTransformIndex(xy_transform)
462
572
 
463
- return cls(indexes)
573
+ return cls(index, crs=crs)
464
574
 
465
575
  @classmethod
466
576
  def from_variables(
@@ -475,92 +585,116 @@ class RasterIndex(Index):
475
585
  def create_variables(self, variables: Mapping[Any, Variable] | None = None) -> dict[Hashable, Variable]:
476
586
  new_variables: dict[Hashable, Variable] = {}
477
587
 
478
- for index in self._wrapped_indexes.values():
588
+ for index in self._wrapped_indexes:
479
589
  new_variables.update(index.create_variables())
480
590
 
591
+ if self.crs is not None:
592
+ xname, yname = self.xy_coord_names
593
+ xattrs, yattrs = self.crs.cs_to_cf()
594
+ if "axis" in xattrs and "axis" in yattrs:
595
+ # The axis order is defined by the projection
596
+ # So we have to figure which is which.
597
+ # This is an ugly hack that works for common cases.
598
+ if xattrs["axis"] == "Y":
599
+ xattrs, yattrs = yattrs, xattrs
600
+ new_variables[xname].attrs = xattrs
601
+ new_variables[yname].attrs = yattrs
602
+
481
603
  return new_variables
482
604
 
605
+ @property
606
+ def crs(self) -> CRS | None:
607
+ """Returns the coordinate reference system (CRS) of the index as a
608
+ :class:`pyproj.crs.CRS` object, or ``None`` if CRS is undefined.
609
+ """
610
+ return self._crs
611
+
612
+ def _proj_set_crs(self: RasterIndex, spatial_ref: Hashable, crs: CRS) -> RasterIndex:
613
+ # Returns a raster index shallow copy with a replaced CRS
614
+ # (XProj integration via xproj.ProjIndexMixin)
615
+ # Note: XProj already handles the case of overriding any existing CRS
616
+ return RasterIndex(self._wrapped_indexes, crs=crs)
617
+
483
618
  def isel(self, indexers: Mapping[Any, int | slice | np.ndarray | Variable]) -> RasterIndex | None:
484
- new_indexes: dict[WrappedIndexCoords, WrappedIndex] = {}
485
-
486
- for coord_names, index in self._wrapped_indexes.items():
487
- index_indexers = _filter_dim_indexers(index, indexers)
488
- if not index_indexers:
489
- # no selection to perform: simply propagate the index
490
- # TODO: uncomment when https://github.com/pydata/xarray/issues/10063 is fixed
491
- # new_indexes[coord_names] = index
492
- ...
493
- else:
494
- new_index = index.isel(index_indexers)
495
- if new_index is not None:
496
- new_indexes[coord_names] = new_index
497
-
498
- if new_indexes:
499
- # TODO: if there's only a single PandasIndex can we just return it?
500
- # (maybe better to keep it wrapped if we plan to later make RasterIndex CRS-aware)
501
- return RasterIndex(new_indexes)
502
- else:
619
+ if not self._axis_independent:
620
+ # preserve RasterIndex is not supported in the case of coupled x/y 2D coordinates
503
621
  return None
622
+ else:
623
+ new_indexes = []
624
+
625
+ for index in self._xy_indexes:
626
+ dim = index.axis_transform.dim
627
+
628
+ if dim not in indexers:
629
+ # simply propagate the index
630
+ new_indexes.append(index)
631
+ else:
632
+ new_index = index.isel({dim: indexers[dim]})
633
+ if new_index is not None:
634
+ new_indexes.append(new_index)
635
+
636
+ # TODO: if/when supported in Xarray, return PandasIndex instances for either the
637
+ # x or the y axis (or both) instead of returning None (drop the index)
638
+ if len(new_indexes) == 2:
639
+ return RasterIndex(tuple(new_indexes), crs=self.crs)
640
+ else:
641
+ return None
504
642
 
505
643
  def sel(self, labels: dict[Any, Any], method=None, tolerance=None) -> IndexSelResult:
506
- results = []
644
+ if not self._axis_independent:
645
+ return self._xxyy_index.sel(labels, method=method, tolerance=tolerance)
646
+ else:
647
+ results: list[IndexSelResult] = []
507
648
 
508
- for coord_names, index in self._wrapped_indexes.items():
509
- if not isinstance(coord_names, tuple):
510
- coord_names = (coord_names,)
511
- index_labels = {k: v for k, v in labels.items() if k in coord_names}
512
- if index_labels:
513
- results.append(index.sel(index_labels, method=method, tolerance=tolerance))
649
+ for index in self._xy_indexes:
650
+ coord_name = index.axis_transform.coord_name
651
+ if coord_name in labels:
652
+ res = index.sel({coord_name: labels[coord_name]}, method=method, tolerance=tolerance)
653
+ results.append(res)
514
654
 
515
- return merge_sel_results(results)
655
+ return merge_sel_results(results)
516
656
 
517
657
  def equals(self, other: Index, *, exclude=None) -> bool:
518
658
  if exclude is None:
519
659
  exclude = {}
660
+
520
661
  if not isinstance(other, RasterIndex):
521
662
  return False
522
- if set(self._wrapped_indexes) != set(other._wrapped_indexes):
663
+ if not self._proj_crs_equals(cast(CRSAwareIndex, other), allow_none=True):
664
+ return False
665
+ if self._axis_independent != other._axis_independent:
523
666
  return False
524
667
 
525
- return all(
526
- index.equals(other._wrapped_indexes[k]) # type: ignore[arg-type]
527
- for k, index in self._wrapped_indexes.items()
528
- if k not in exclude
529
- )
530
-
531
- def to_pandas_index(self) -> pd.Index:
532
- # conversion is possible only if this raster index encapsulates
533
- # exactly one AxisAffineTransformIndex or a PandasIndex associated
534
- # to either the x or y axis (1-dimensional) coordinate.
535
- if len(self._wrapped_indexes) == 1:
536
- index = next(iter(self._wrapped_indexes.values()))
537
- if isinstance(index, AxisAffineTransformIndex | PandasIndex):
538
- return index.to_pandas_index()
539
-
540
- raise ValueError("Cannot convert RasterIndex to pandas.Index")
541
-
542
- def __repr__(self):
543
- items: list[str] = []
544
-
545
- for coord_names, index in self._wrapped_indexes.items():
546
- items += [repr(coord_names) + ":", textwrap.indent(repr(index), " ")]
547
-
548
- return "RasterIndex\n" + "\n".join(items)
668
+ if self._axis_independent:
669
+ return all(
670
+ idx.equals(other_idx)
671
+ for idx, other_idx in zip(self._xy_indexes, other._xy_indexes)
672
+ if idx.axis_transform.dim not in exclude
673
+ )
674
+ else:
675
+ return self._xxyy_index.equals(other._xxyy_index)
549
676
 
550
677
  def transform(self) -> Affine:
551
678
  """Affine transform for top-left corners."""
552
679
  return self.center_transform() * Affine.translation(-0.5, -0.5)
553
680
 
681
+ def as_geotransform(self, *, decimals: int | None = None) -> str:
682
+ """Convert the affine transform to a string suitable for saving as the GeoTransform attribute."""
683
+ gt = self.transform().to_gdal()
684
+ if decimals is not None:
685
+ fmt = f".{decimals}f"
686
+ return " ".join(f"{num:{fmt}}" for num in gt)
687
+ else:
688
+ return " ".join(map(str, gt))
689
+
554
690
  def center_transform(self) -> Affine:
555
691
  """Affine transform for cell centers."""
556
- if len(self._wrapped_indexes) > 1:
557
- x = self._wrapped_indexes["x"].axis_transform.affine
558
- y = self._wrapped_indexes["y"].axis_transform.affine
559
- aff = Affine(x.a, x.b, x.c, y.d, y.e, y.f)
692
+ if not self._axis_independent:
693
+ return cast(AffineTransform, self._xxyy_index.transform).affine
560
694
  else:
561
- index = next(iter(self._wrapped_indexes.values()))
562
- aff = index.affine
563
- return aff
695
+ x = self._xy_indexes[XAXIS].axis_transform.affine
696
+ y = self._xy_indexes[YAXIS].axis_transform.affine
697
+ return Affine(x.a, x.b, x.c, y.d, y.e, y.f)
564
698
 
565
699
  @property
566
700
  def bbox(self) -> BoundingBox:
@@ -570,10 +704,8 @@ class RasterIndex(Index):
570
704
  -------
571
705
  BoundingBox
572
706
  """
573
- return BoundingBox.from_transform(
574
- shape=tuple(self._shape[k] for k in ("y", "x")),
575
- transform=self.transform(),
576
- )
707
+ yx_shape = (self.xy_shape[YAXIS], self.xy_shape[XAXIS])
708
+ return BoundingBox.from_transform(shape=yx_shape, transform=self.transform())
577
709
 
578
710
  @classmethod
579
711
  def concat(
@@ -581,7 +713,7 @@ class RasterIndex(Index):
581
713
  indexes: Sequence[Self],
582
714
  dim: Hashable,
583
715
  positions: Iterable[Iterable[int]] | None = None,
584
- ) -> Self:
716
+ ) -> RasterIndex:
585
717
  if len(indexes) == 1:
586
718
  return next(iter(indexes))
587
719
 
@@ -593,7 +725,7 @@ class RasterIndex(Index):
593
725
  new_bbox = bbox_union(as_compatible_bboxes(*indexes, concat_dim=dim))
594
726
  return indexes[0]._new_with_bbox(new_bbox)
595
727
 
596
- def _new_with_bbox(self, bbox: BoundingBox) -> Self:
728
+ def _new_with_bbox(self, bbox: BoundingBox) -> RasterIndex:
597
729
  affine = self.transform()
598
730
  new_affine, Nx, Ny = bbox_to_affine(bbox, rx=affine.a, ry=affine.e)
599
731
  # TODO: set xdim, ydim explicitly
@@ -601,8 +733,14 @@ class RasterIndex(Index):
601
733
  assert new_index.bbox == bbox
602
734
  return new_index
603
735
 
604
- def join(self, other: RasterIndex, how) -> RasterIndex:
605
- if set(self._wrapped_indexes.keys()) != set(other._wrapped_indexes.keys()):
736
+ def join(self, other: RasterIndex, how: JoinOptions = "inner") -> RasterIndex:
737
+ if not self._proj_crs_equals(cast(CRSAwareIndex, other), allow_none=True):
738
+ raise ValueError(
739
+ "raster indexes on objects to align do not have the same CRS\n"
740
+ f"first index:\n{self!r}\n\nsecond index:\n{other!r}"
741
+ )
742
+
743
+ if len(self._wrapped_indexes) != len(other._wrapped_indexes):
606
744
  # TODO: better error message
607
745
  raise ValueError(
608
746
  "Alignment is only supported between RasterIndexes, when both contain compatible transforms."
@@ -619,6 +757,7 @@ class RasterIndex(Index):
619
757
  return self._new_with_bbox(new_bbox)
620
758
 
621
759
  def reindex_like(self, other: Self, method=None, tolerance=None) -> dict[Hashable, Any]:
760
+ x_dim, y_dim = self.xy_dims
622
761
  affine = self.transform()
623
762
  ours, theirs = as_compatible_bboxes(self, other, concat_dim=None)
624
763
  inter = bbox_intersection([ours, theirs])
@@ -631,14 +770,35 @@ class RasterIndex(Index):
631
770
  tol: float = 0.01
632
771
 
633
772
  indexers = {}
634
- indexers["x"] = get_indexer(
635
- theirs.left, ours.left, inter.left, inter.right, spacing=dx, tol=tol, size=other._shape["x"]
773
+ indexers[x_dim] = get_indexer(
774
+ theirs.left, ours.left, inter.left, inter.right, spacing=dx, tol=tol, size=other.xy_shape[XAXIS]
636
775
  )
637
- indexers["y"] = get_indexer(
638
- theirs.top, ours.top, inter.top, inter.bottom, spacing=dy, tol=tol, size=other._shape["y"]
776
+ indexers[y_dim] = get_indexer(
777
+ theirs.top, ours.top, inter.top, inter.bottom, spacing=dy, tol=tol, size=other.xy_shape[YAXIS]
639
778
  )
640
779
  return indexers
641
780
 
781
+ def _repr_inline_(self, max_width: int) -> str:
782
+ # TODO: remove when fixed in Xarray (https://github.com/pydata/xarray/pull/10415)
783
+ if max_width is None:
784
+ max_width = get_options()["display_width"]
785
+
786
+ srs = xproj.format_crs(self.crs, max_width=max_width)
787
+ return f"{self.__class__.__name__} (crs={srs})"
788
+
789
+ def __repr__(self) -> str:
790
+ srs = xproj.format_crs(self.crs)
791
+
792
+ def index_repr(idx) -> str:
793
+ return textwrap.indent(f"{type(idx).__name__}({idx.transform!r})", " ")
794
+
795
+ if self._axis_independent:
796
+ idx_repr = "\n".join(index_repr(idx) for idx in self._xy_indexes)
797
+ else:
798
+ idx_repr = index_repr(self._xxyy_index)
799
+
800
+ return f"RasterIndex(crs={srs})\n{idx_repr}"
801
+
642
802
 
643
803
  def get_indexer(off, our_off, start, stop, spacing, tol, size) -> np.ndarray:
644
804
  istart = math.ceil(maybe_int((start - off) / spacing, tol))
@@ -656,7 +816,7 @@ def get_indexer(off, our_off, start, stop, spacing, tol, size) -> np.ndarray:
656
816
  return idxr
657
817
 
658
818
 
659
- def bbox_to_affine(bbox: BoundingBox, rx, ry) -> Affine:
819
+ def bbox_to_affine(bbox: BoundingBox, rx, ry) -> tuple[Affine, int, int]:
660
820
  # Fraction of a pixel that can be ignored, defaults to 1/100. Bounding box of the output
661
821
  # geobox is allowed to be smaller than supplied bounding box by that amount.
662
822
  # FIXME: translate user-provided `tolerance` to `tol`
@@ -675,10 +835,10 @@ def as_compatible_bboxes(*indexes: RasterIndex, concat_dim: Hashable | None) ->
675
835
  _assert_transforms_are_compatible(*transforms)
676
836
 
677
837
  expected_off_x = (transforms[0].c,) + tuple(
678
- t.c + i._shape["x"] * t.a for i, t in zip(indexes[:-1], transforms[:-1])
838
+ t.c + i.xy_shape[XAXIS] * t.a for i, t in zip(indexes[:-1], transforms[:-1])
679
839
  )
680
840
  expected_off_y = (transforms[0].f,) + tuple(
681
- t.f + i._shape["y"] * t.e for i, t in zip(indexes[:-1], transforms[:-1])
841
+ t.f + i.xy_shape[YAXIS] * t.e for i, t in zip(indexes[:-1], transforms[:-1])
682
842
  )
683
843
 
684
844
  off_x = tuple(t.c for t in transforms)
@@ -688,14 +848,17 @@ def as_compatible_bboxes(*indexes: RasterIndex, concat_dim: Hashable | None) ->
688
848
  if all(o == off_x[0] for o in off_x[1:]) and all(o == off_y[0] for o in off_y[1:]):
689
849
  raise ValueError("Attempting to concatenate arrays with same transform along X or Y.")
690
850
 
691
- if concat_dim == "x":
851
+ # note: Xarray alignment already ensures that the indexes dimensions are compatible.
852
+ x_dim, y_dim = indexes[0].xy_dims
853
+
854
+ if concat_dim == x_dim:
692
855
  if any(off_y[0] != o for o in off_y[1:]):
693
856
  raise ValueError("offsets must be identical in X when concatenating along Y")
694
857
  if any(a != b for a, b in zip(off_x, expected_off_x)):
695
858
  raise ValueError(
696
859
  f"X offsets are incompatible. Provided offsets {off_x}, expected offsets: {expected_off_x}"
697
860
  )
698
- elif concat_dim == "y":
861
+ elif concat_dim == y_dim:
699
862
  if any(off_x[0] != o for o in off_x[1:]):
700
863
  raise ValueError("offsets must be identical in X when concatenating along Y")
701
864
 
@@ -15,7 +15,8 @@ from rasterio.features import MergeAlg
15
15
  from rasterio.features import geometry_mask as geometry_mask_rio
16
16
  from rasterio.features import rasterize as rasterize_rio
17
17
 
18
- from .utils import XAXIS, YAXIS, clip_to_bbox, get_affine, is_in_memory, prepare_for_dask
18
+ from ..utils import get_affine
19
+ from .utils import XAXIS, YAXIS, clip_to_bbox, is_in_memory, prepare_for_dask
19
20
 
20
21
  F = TypeVar("F", bound=Callable[..., Any])
21
22
 
@@ -161,7 +162,7 @@ def rasterize(
161
162
  obj = clip_to_bbox(obj, geometries, xdim=xdim, ydim=ydim)
162
163
 
163
164
  rasterize_kwargs = dict(
164
- all_touched=all_touched, merge_alg=merge_alg, affine=get_affine(obj, xdim=xdim, ydim=ydim), env=env
165
+ all_touched=all_touched, merge_alg=merge_alg, affine=get_affine(obj, x_dim=xdim, y_dim=ydim), env=env
165
166
  )
166
167
  # FIXME: box.crs == geometries.crs
167
168
 
@@ -325,7 +326,7 @@ def geometry_mask(
325
326
  obj = clip_to_bbox(obj, geometries, xdim=xdim, ydim=ydim)
326
327
 
327
328
  geometry_mask_kwargs = dict(
328
- all_touched=all_touched, affine=get_affine(obj, xdim=xdim, ydim=ydim), env=env
329
+ all_touched=all_touched, affine=get_affine(obj, x_dim=xdim, y_dim=ydim), env=env
329
330
  )
330
331
 
331
332
  if is_in_memory(obj=obj, geometries=geometries):
rasterix/utils.py ADDED
@@ -0,0 +1,67 @@
1
+ import xarray as xr
2
+ from affine import Affine
3
+
4
+
5
+ def get_grid_mapping_var(obj: xr.Dataset | xr.DataArray) -> xr.DataArray | None:
6
+ grid_mapping_var = None
7
+ if isinstance(obj, xr.DataArray):
8
+ if maybe := obj.attrs.get("grid_mapping", None):
9
+ if maybe in obj.coords:
10
+ grid_mapping_var = maybe
11
+ else:
12
+ # for datasets, grab the first one for simplicity
13
+ for var in obj.data_vars.values():
14
+ if maybe := var.attrs.get("grid_mapping"):
15
+ if maybe in obj.coords:
16
+ # make sure it exists and is not an out-of-date attribute
17
+ grid_mapping_var = maybe
18
+ break
19
+ if grid_mapping_var is None and "spatial_ref" in obj.coords:
20
+ # hardcode this
21
+ grid_mapping_var = "spatial_ref"
22
+ if grid_mapping_var is not None:
23
+ return obj[grid_mapping_var]
24
+ return None
25
+
26
+
27
+ def get_affine(
28
+ obj: xr.Dataset | xr.DataArray, *, x_dim="x", y_dim="y", clear_transform: bool = False
29
+ ) -> Affine:
30
+ """
31
+ Grabs an affine transform from an Xarray object.
32
+
33
+ This method will first look for the ``"GeoTransform"`` attribute on a variable named
34
+ ``"spatial_ref"``. If not, it will auto-guess the transform from the provided ``x_dim``,
35
+ and ``y_dim``.
36
+
37
+ Parameters
38
+ ----------
39
+ obj: xr.DataArray or xr.Dataset
40
+ x_dim: str, optional
41
+ Name of the X dimension coordinate variable.
42
+ y_dim: str, optional
43
+ Name of the Y dimension coordinate variable.
44
+ clear_transform: bool
45
+ Whether to delete the ``GeoTransform`` attribute if detected.
46
+
47
+ Returns
48
+ -------
49
+ affine: Affine
50
+ """
51
+ grid_mapping_var = get_grid_mapping_var(obj)
52
+ if grid_mapping_var is not None and (transform := grid_mapping_var.attrs.get("GeoTransform")):
53
+ if clear_transform:
54
+ del grid_mapping_var.attrs["GeoTransform"]
55
+ return Affine.from_gdal(*map(float, transform.split(" ")))
56
+ else:
57
+ x = obj.coords[x_dim]
58
+ y = obj.coords[y_dim]
59
+ if x.ndim != 1:
60
+ raise ValueError(f"Coordinate variable {x_dim=!r} must be 1D.")
61
+ if y.ndim != 1:
62
+ raise ValueError(f"Coordinate variable {y_dim=!r} must be 1D.")
63
+ dx = (x[1] - x[0]).item()
64
+ dy = (y[1] - y[0]).item()
65
+ return Affine.translation(
66
+ x[0].item() - dx / 2, (y[0] if dy < 0 else y[-1]).item() - dy / 2
67
+ ) * Affine.scale(dx, dy)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rasterix
3
- Version: 0.1a3
3
+ Version: 0.1a4
4
4
  Summary: Raster extensions for Xarray
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -17,6 +17,7 @@ Requires-Dist: affine
17
17
  Requires-Dist: numpy>=2
18
18
  Requires-Dist: pandas>=2
19
19
  Requires-Dist: xarray>=2025
20
+ Requires-Dist: xproj>=0.2.0
20
21
  Provides-Extra: dask
21
22
  Requires-Dist: dask-geopandas; extra == 'dask'
22
23
  Provides-Extra: docs
@@ -52,57 +53,30 @@ Requires-Dist: hypothesis; extra == 'test'
52
53
  Requires-Dist: netcdf4; extra == 'test'
53
54
  Requires-Dist: pooch; extra == 'test'
54
55
  Requires-Dist: rasterio; extra == 'test'
55
- Requires-Dist: rioxarray; extra == 'test'
56
56
  Requires-Dist: sparse; extra == 'test'
57
57
  Description-Content-Type: text/markdown
58
58
 
59
59
  # rasterix: Raster tricks for Xarray
60
60
 
61
- [![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/dcherian/rasterix/test.yml?branch=main&logo=github&style=flat)](https://github.com/dcherian/rasterix/actions)
61
+ [![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/xarray-contrib/rasterix/test.yml?branch=main&logo=github&style=flat)](https://github.com/xarray-contrib/rasterix/actions)
62
62
  [![Documentation Status](https://readthedocs.org/projects/rasterix/badge/?version=latest)](https://rasterix.readthedocs.io/en/latest/?badge=latest)
63
63
  [![PyPI](https://img.shields.io/pypi/v/rasterix.svg?style=flat)](https://pypi.org/project/rasterix/)
64
64
  [![Conda-forge](https://img.shields.io/conda/vn/conda-forge/rasterix.svg?style=flat)](https://anaconda.org/conda-forge/rasterix)
65
65
 
66
- <img src="rasterix.png" width="300">
66
+ <img src="_static/rasterix.png" width="300">
67
67
 
68
68
  This WIP project contains tools to make it easier to analyze raster data with Xarray.
69
-
70
- The intent is to provide reusable building blocks for the many sub-ecosystems around: e.g. rioxarray, odc-geo, etc.
71
-
72
- ## Contents
73
-
74
69
  It currently has two pieces.
75
70
 
76
- ### 1. RasterIndex
77
-
78
- See `src/ rasterix/raster_index.py` and `notebooks/raster_index.ipynb` for a brief demo.
79
-
80
- ### 2. Dask-aware rasterization wrappers
71
+ 1. `RasterIndex` for indexing using the affine transform recorded in GeoTIFFs.
72
+ 1. Dask-aware rasterization wrappers around `exactextract`, `rasterio.features.rasterize`, and `rasterio.features.geometry_mask`.
81
73
 
82
- See `src/rasterix/rasterize.py` for dask-aware wrappers around [`exactextract`](https://github.com/dcherian/rasterix/blob/ec3f51e60e25aa312e6f48c4b22f91bec70413ed/rasterize.py#L165), [`rasterio.features.rasterize`](https://github.com/dcherian/rasterix/blob/ec3f51e60e25aa312e6f48c4b22f91bec70413ed/rasterize.py#L307), and [`rasterio.features.geometry_mask`](https://github.com/dcherian/rasterix/blob/ec3f51e60e25aa312e6f48c4b22f91bec70413ed/rasterize.py#L472).
83
-
84
- This code is likely to move elsewhere!
74
+ Our intent is to provide reusable building blocks for the many sub-ecosystems around: e.g. `rioxarray`, `odc.geo`, etc.
85
75
 
86
76
  ## Installing
87
77
 
88
- ### PyPI
89
-
90
78
  `rasterix` alpha releases are available on pypi
91
79
 
92
80
  ```
93
81
  pip install rasterix
94
82
  ```
95
-
96
- ## Developing
97
-
98
- 1. Clone the repo
99
- ```
100
- git remote add upstream git@github.com:dcherian/rasterix.git
101
- cd rasterix
102
- ```
103
- 1. [Install hatch](https://hatch.pypa.io/1.12/install/)
104
- 1. Run the tests
105
- ```
106
- hatch env run --env test.py3.13 run-pytest # Run the tests without coverage reports
107
- hatch env run --env test.py3.13 run-coverage-html # Run the tests with an html coverage report
108
- ```
@@ -0,0 +1,14 @@
1
+ rasterix/__init__.py,sha256=iSC54A4b-7zgxtKV0CUok0O2-ApGtzVA-6hWG3a5AAA,283
2
+ rasterix/_version.py,sha256=Rlam12WbDAQ2TzSYxzdnw4SLFERCr6sX8OCMwKNxEX4,514
3
+ rasterix/odc_compat.py,sha256=MxjctCH0zV6VSSQymatJreHYUnUo3qkB1oIKbOxEK4A,19099
4
+ rasterix/raster_index.py,sha256=uwEDjjmnESWpyqsJwAnZvpWrjnKDeg0m1qNNH9mYTWY,32808
5
+ rasterix/rioxarray_compat.py,sha256=o32UBkWBpNhmC35ar0Ozn2QszpWdPvdDDXif3Mq2ZKg,12428
6
+ rasterix/utils.py,sha256=y2N-3d9-D8tBKjeZXv4vcjY_fs_dbbx5SfBBkQibLNs,2441
7
+ rasterix/rasterize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ rasterix/rasterize/exact.py,sha256=UPB9zXpj7ZgkDy-x4vcNwY3J8DTrOTkRcIBMkhPQ04s,10558
9
+ rasterix/rasterize/rasterio.py,sha256=gtdQxM9CiosalYk3rLKGkp0UmhZVO5parAVsR7IINoE,12883
10
+ rasterix/rasterize/utils.py,sha256=iIuyTdigpFnts_-bUfas6FMsgix5lMqMQTxQaEBlyXM,3521
11
+ rasterix-0.1a4.dist-info/METADATA,sha256=K5l2PDTNLvmdA6TVGvy2tg5-YRevdmhV5mYX_Os3Rbw,3402
12
+ rasterix-0.1a4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ rasterix-0.1a4.dist-info/licenses/LICENSE,sha256=QFnsASMx8_yBNbrS7GVOhJ5CglGsLMj83Rn61uWyMs8,10265
14
+ rasterix-0.1a4.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- rasterix/__init__.py,sha256=iSC54A4b-7zgxtKV0CUok0O2-ApGtzVA-6hWG3a5AAA,283
2
- rasterix/_version.py,sha256=hJd92hgvVD0aLSzXDQZBYe_xFmMQly9wq_1h2CuGX_g,514
3
- rasterix/odc_compat.py,sha256=MxjctCH0zV6VSSQymatJreHYUnUo3qkB1oIKbOxEK4A,19099
4
- rasterix/raster_index.py,sha256=yCq08-BvgEWe5ZbtknkMsXqKkZVqGyw85Otcx3_atf4,26130
5
- rasterix/rioxarray_compat.py,sha256=o32UBkWBpNhmC35ar0Ozn2QszpWdPvdDDXif3Mq2ZKg,12428
6
- rasterix/rasterize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- rasterix/rasterize/exact.py,sha256=UPB9zXpj7ZgkDy-x4vcNwY3J8DTrOTkRcIBMkhPQ04s,10558
8
- rasterix/rasterize/rasterio.py,sha256=TdnDMGfsN3npP_LPEPXH2LV25AJEkmEuTOpke5GN8_8,12860
9
- rasterix/rasterize/utils.py,sha256=iIuyTdigpFnts_-bUfas6FMsgix5lMqMQTxQaEBlyXM,3521
10
- rasterix-0.1a3.dist-info/METADATA,sha256=05YXAANNx2f50SqN_bXO_N7gb1YR29S3GIMEmi1mEXQ,4262
11
- rasterix-0.1a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- rasterix-0.1a3.dist-info/licenses/LICENSE,sha256=QFnsASMx8_yBNbrS7GVOhJ5CglGsLMj83Rn61uWyMs8,10265
13
- rasterix-0.1a3.dist-info/RECORD,,