rasterix 0.1a4__py3-none-any.whl → 0.1.1__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 +16 -3
- rasterix/lib.py +106 -0
- rasterix/raster_index.py +166 -5
- rasterix/strategies.py +165 -0
- rasterix/utils.py +56 -15
- {rasterix-0.1a4.dist-info → rasterix-0.1.1.dist-info}/METADATA +1 -10
- rasterix-0.1.1.dist-info/RECORD +16 -0
- {rasterix-0.1a4.dist-info → rasterix-0.1.1.dist-info}/WHEEL +1 -1
- rasterix-0.1a4.dist-info/RECORD +0 -14
- {rasterix-0.1a4.dist-info → rasterix-0.1.1.dist-info}/licenses/LICENSE +0 -0
rasterix/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
rasterix/lib.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Shared library utilities for rasterix."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from affine import Affine
|
|
6
|
+
|
|
7
|
+
# Define TRACE level (lower than DEBUG)
|
|
8
|
+
TRACE = 5
|
|
9
|
+
logging.addLevelName(TRACE, "TRACE")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TraceLogger(logging.Logger):
|
|
13
|
+
"""Logger with trace level support."""
|
|
14
|
+
|
|
15
|
+
def trace(self, message, *args, **kwargs):
|
|
16
|
+
"""Log a message with severity 'TRACE'."""
|
|
17
|
+
if self.isEnabledFor(TRACE):
|
|
18
|
+
self._log(TRACE, message, args, **kwargs)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Set the custom logger class
|
|
22
|
+
logging.setLoggerClass(TraceLogger)
|
|
23
|
+
|
|
24
|
+
# Create logger for the rasterix package
|
|
25
|
+
logger = logging.getLogger("rasterix")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def affine_from_tiepoint_and_scale(
|
|
29
|
+
tiepoint: list[float] | tuple[float, ...],
|
|
30
|
+
scale: list[float] | tuple[float, ...],
|
|
31
|
+
) -> Affine:
|
|
32
|
+
"""Create an Affine transform from GeoTIFF tiepoint and pixel scale.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
tiepoint : list or tuple
|
|
37
|
+
GeoTIFF model tiepoint in format [I, J, K, X, Y, Z]
|
|
38
|
+
where (I, J, K) are pixel coords and (X, Y, Z) are world coords.
|
|
39
|
+
scale : list or tuple
|
|
40
|
+
GeoTIFF model pixel scale in format [ScaleX, ScaleY, ScaleZ].
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
Affine
|
|
45
|
+
Affine transformation matrix.
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
AssertionError
|
|
50
|
+
If ScaleZ is not 0 (only 2D rasters are supported).
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> tiepoint = [0.0, 0.0, 0.0, 323400.0, 4265400.0, 0.0]
|
|
55
|
+
>>> scale = [30.0, 30.0, 0.0]
|
|
56
|
+
>>> affine = affine_from_tiepoint_and_scale(tiepoint, scale)
|
|
57
|
+
"""
|
|
58
|
+
if len(tiepoint) < 6:
|
|
59
|
+
raise ValueError(f"tiepoint must have at least 6 elements, got {len(tiepoint)}")
|
|
60
|
+
if len(scale) < 3:
|
|
61
|
+
raise ValueError(f"scale must have at least 3 elements, got {len(scale)}")
|
|
62
|
+
|
|
63
|
+
i, j, k, x, y, z = tiepoint[:6]
|
|
64
|
+
scale_x, scale_y, scale_z = scale[:3]
|
|
65
|
+
|
|
66
|
+
# We only support 2D rasters
|
|
67
|
+
assert scale_z == 0, f"Z pixel scale must be 0 for 2D rasters, got {scale_z}"
|
|
68
|
+
|
|
69
|
+
# The tiepoint gives us the world coordinates at pixel (I, J)
|
|
70
|
+
# Affine transform: x_world = c + i * a, y_world = f + j * e
|
|
71
|
+
# So: c = x - i * scale_x, f = y - j * scale_y
|
|
72
|
+
c = x - i * scale_x
|
|
73
|
+
f = y - j * scale_y
|
|
74
|
+
|
|
75
|
+
return Affine.translation(c, f) * Affine.scale(scale_x, scale_y)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def affine_from_stac_proj_metadata(metadata: dict) -> Affine | None:
|
|
79
|
+
"""Extract Affine transform from STAC projection metadata.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
metadata : dict
|
|
84
|
+
Dictionary containing STAC metadata. Should contain a 'proj:transform' key.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
Affine or None
|
|
89
|
+
Affine transformation matrix if 'proj:transform' is found, None otherwise.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
>>> metadata = {"proj:transform": [30.0, 0.0, 323400.0, 0.0, 30.0, 4268400.0]}
|
|
94
|
+
>>> affine = affine_from_stac_proj_metadata(metadata)
|
|
95
|
+
"""
|
|
96
|
+
if "proj:transform" not in metadata:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
transform = metadata["proj:transform"]
|
|
100
|
+
# proj:transform is a 3x3 matrix in row-major order, but typically only 6 elements
|
|
101
|
+
# [a, b, c, d, e, f, 0, 0, 1] where the affine is constructed from first 6 elements
|
|
102
|
+
if len(transform) < 6:
|
|
103
|
+
raise ValueError(f"proj:transform must have at least 6 elements, got {len(transform)}")
|
|
104
|
+
|
|
105
|
+
a, b, c, d, e, f = transform[:6]
|
|
106
|
+
return Affine(a, b, c, d, e, f)
|
rasterix/raster_index.py
CHANGED
|
@@ -14,7 +14,7 @@ from xarray import Coordinates, DataArray, Dataset, Index, Variable, get_options
|
|
|
14
14
|
from xarray.core.coordinate_transform import CoordinateTransform
|
|
15
15
|
|
|
16
16
|
# TODO: import from public API once it is available
|
|
17
|
-
from xarray.core.indexes import CoordinateTransformIndex
|
|
17
|
+
from xarray.core.indexes import CoordinateTransformIndex, PandasIndex
|
|
18
18
|
from xarray.core.indexing import IndexSelResult, merge_sel_results
|
|
19
19
|
from xarray.core.types import JoinOptions
|
|
20
20
|
from xproj.typing import CRSAwareIndex
|
|
@@ -310,17 +310,20 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
310
310
|
def sel(self, labels, method=None, tolerance=None):
|
|
311
311
|
coord_name = self.axis_transform.coord_name
|
|
312
312
|
label = labels[coord_name]
|
|
313
|
-
|
|
313
|
+
transform = self.axis_transform
|
|
314
314
|
if isinstance(label, slice):
|
|
315
|
-
|
|
316
|
-
label
|
|
315
|
+
label = slice(
|
|
316
|
+
label.start or transform.forward({coord_name: 0})[coord_name],
|
|
317
|
+
label.stop or transform.forward({coord_name: transform.size})[coord_name],
|
|
318
|
+
label.step,
|
|
319
|
+
)
|
|
317
320
|
if label.step is None:
|
|
318
321
|
# continuous interval slice indexing (preserves the index)
|
|
319
322
|
pos = self.transform.reverse({coord_name: np.array([label.start, label.stop])})
|
|
320
323
|
# np.round rounds to even, this way we round upwards
|
|
321
324
|
pos = np.floor(pos[self.dim] + 0.5).astype("int")
|
|
322
325
|
new_start = max(pos[0], 0)
|
|
323
|
-
new_stop = min(pos[1], self.axis_transform.size)
|
|
326
|
+
new_stop = min(pos[1] + 1, self.axis_transform.size)
|
|
324
327
|
return IndexSelResult({self.dim: slice(new_start, new_stop)})
|
|
325
328
|
else:
|
|
326
329
|
# otherwise convert to basic (array) indexing
|
|
@@ -572,6 +575,147 @@ class RasterIndex(Index, xproj.ProjIndexMixin):
|
|
|
572
575
|
|
|
573
576
|
return cls(index, crs=crs)
|
|
574
577
|
|
|
578
|
+
@classmethod
|
|
579
|
+
def from_tiepoint_and_scale(
|
|
580
|
+
cls,
|
|
581
|
+
*,
|
|
582
|
+
tiepoint: list[float] | tuple[float, ...],
|
|
583
|
+
scale: list[float] | tuple[float, ...],
|
|
584
|
+
width: int,
|
|
585
|
+
height: int,
|
|
586
|
+
x_dim: str = "x",
|
|
587
|
+
y_dim: str = "y",
|
|
588
|
+
x_coord_name: str = "xc",
|
|
589
|
+
y_coord_name: str = "yc",
|
|
590
|
+
crs: CRS | Any | None = None,
|
|
591
|
+
) -> RasterIndex:
|
|
592
|
+
"""Create a RasterIndex from GeoTIFF tiepoint and pixel scale metadata.
|
|
593
|
+
|
|
594
|
+
Parameters
|
|
595
|
+
----------
|
|
596
|
+
tiepoint : list or tuple
|
|
597
|
+
GeoTIFF model tiepoint in format [I, J, K, X, Y, Z]
|
|
598
|
+
where (I, J, K) are pixel coords and (X, Y, Z) are world coords.
|
|
599
|
+
scale : list or tuple
|
|
600
|
+
GeoTIFF model pixel scale in format [ScaleX, ScaleY, ScaleZ].
|
|
601
|
+
width : int
|
|
602
|
+
Number of pixels in the x direction.
|
|
603
|
+
height : int
|
|
604
|
+
Number of pixels in the y direction.
|
|
605
|
+
x_dim : str, optional
|
|
606
|
+
Name for the x dimension.
|
|
607
|
+
y_dim : str, optional
|
|
608
|
+
Name for the y dimension.
|
|
609
|
+
x_coord_name : str, optional
|
|
610
|
+
Name for the x coordinate. For non-rectilinear transforms only.
|
|
611
|
+
y_coord_name : str, optional
|
|
612
|
+
Name for the y coordinate. For non-rectilinear transforms only.
|
|
613
|
+
crs : :class:`pyproj.crs.CRS` or any, optional
|
|
614
|
+
The coordinate reference system. Any value accepted by
|
|
615
|
+
:meth:`pyproj.crs.CRS.from_user_input`.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
RasterIndex
|
|
620
|
+
A new RasterIndex object with appropriate internal structure.
|
|
621
|
+
|
|
622
|
+
Raises
|
|
623
|
+
------
|
|
624
|
+
AssertionError
|
|
625
|
+
If ScaleZ is not 0 (only 2D rasters are supported).
|
|
626
|
+
|
|
627
|
+
Examples
|
|
628
|
+
--------
|
|
629
|
+
Create an index from GeoTIFF metadata:
|
|
630
|
+
|
|
631
|
+
>>> tiepoint = [0.0, 0.0, 0.0, 323400.0, 4265400.0, 0.0]
|
|
632
|
+
>>> scale = [30.0, 30.0, 0.0]
|
|
633
|
+
>>> index = RasterIndex.from_tiepoint_and_scale(tiepoint=tiepoint, scale=scale, width=100, height=100)
|
|
634
|
+
"""
|
|
635
|
+
from rasterix.lib import affine_from_tiepoint_and_scale
|
|
636
|
+
|
|
637
|
+
affine = affine_from_tiepoint_and_scale(tiepoint, scale)
|
|
638
|
+
return cls.from_transform(
|
|
639
|
+
affine,
|
|
640
|
+
width=width,
|
|
641
|
+
height=height,
|
|
642
|
+
x_dim=x_dim,
|
|
643
|
+
y_dim=y_dim,
|
|
644
|
+
x_coord_name=x_coord_name,
|
|
645
|
+
y_coord_name=y_coord_name,
|
|
646
|
+
crs=crs,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
@classmethod
|
|
650
|
+
def from_stac_proj_metadata(
|
|
651
|
+
cls,
|
|
652
|
+
metadata: dict,
|
|
653
|
+
*,
|
|
654
|
+
width: int,
|
|
655
|
+
height: int,
|
|
656
|
+
x_dim: str = "x",
|
|
657
|
+
y_dim: str = "y",
|
|
658
|
+
x_coord_name: str = "xc",
|
|
659
|
+
y_coord_name: str = "yc",
|
|
660
|
+
crs: CRS | Any | None = None,
|
|
661
|
+
) -> RasterIndex:
|
|
662
|
+
"""Create a RasterIndex from STAC projection metadata.
|
|
663
|
+
|
|
664
|
+
Parameters
|
|
665
|
+
----------
|
|
666
|
+
metadata : dict
|
|
667
|
+
Dictionary containing STAC metadata. Must contain a 'proj:transform' key
|
|
668
|
+
with the affine transformation as a flat array.
|
|
669
|
+
width : int
|
|
670
|
+
Number of pixels in the x direction.
|
|
671
|
+
height : int
|
|
672
|
+
Number of pixels in the y direction.
|
|
673
|
+
x_dim : str, optional
|
|
674
|
+
Name for the x dimension.
|
|
675
|
+
y_dim : str, optional
|
|
676
|
+
Name for the y dimension.
|
|
677
|
+
x_coord_name : str, optional
|
|
678
|
+
Name for the x coordinate. For non-rectilinear transforms only.
|
|
679
|
+
y_coord_name : str, optional
|
|
680
|
+
Name for the y coordinate. For non-rectilinear transforms only.
|
|
681
|
+
crs : :class:`pyproj.crs.CRS` or any, optional
|
|
682
|
+
The coordinate reference system. Any value accepted by
|
|
683
|
+
:meth:`pyproj.crs.CRS.from_user_input`.
|
|
684
|
+
|
|
685
|
+
Returns
|
|
686
|
+
-------
|
|
687
|
+
RasterIndex
|
|
688
|
+
A new RasterIndex object with appropriate internal structure.
|
|
689
|
+
|
|
690
|
+
Raises
|
|
691
|
+
------
|
|
692
|
+
ValueError
|
|
693
|
+
If 'proj:transform' is not found in metadata or has invalid format.
|
|
694
|
+
|
|
695
|
+
Examples
|
|
696
|
+
--------
|
|
697
|
+
Create an index from STAC metadata:
|
|
698
|
+
|
|
699
|
+
>>> metadata = {"proj:transform": [30.0, 0.0, 323400.0, 0.0, 30.0, 4268400.0]}
|
|
700
|
+
>>> index = RasterIndex.from_stac_proj_metadata(metadata, width=100, height=100)
|
|
701
|
+
"""
|
|
702
|
+
from rasterix.lib import affine_from_stac_proj_metadata
|
|
703
|
+
|
|
704
|
+
affine = affine_from_stac_proj_metadata(metadata)
|
|
705
|
+
if affine is None:
|
|
706
|
+
raise ValueError("metadata must contain 'proj:transform' key")
|
|
707
|
+
|
|
708
|
+
return cls.from_transform(
|
|
709
|
+
affine,
|
|
710
|
+
width=width,
|
|
711
|
+
height=height,
|
|
712
|
+
x_dim=x_dim,
|
|
713
|
+
y_dim=y_dim,
|
|
714
|
+
x_coord_name=x_coord_name,
|
|
715
|
+
y_coord_name=y_coord_name,
|
|
716
|
+
crs=crs,
|
|
717
|
+
)
|
|
718
|
+
|
|
575
719
|
@classmethod
|
|
576
720
|
def from_variables(
|
|
577
721
|
cls,
|
|
@@ -696,6 +840,23 @@ class RasterIndex(Index, xproj.ProjIndexMixin):
|
|
|
696
840
|
y = self._xy_indexes[YAXIS].axis_transform.affine
|
|
697
841
|
return Affine(x.a, x.b, x.c, y.d, y.e, y.f)
|
|
698
842
|
|
|
843
|
+
def _as_pandas_index(self) -> dict[str, PandasIndex]:
|
|
844
|
+
"""Convert RasterIndex to equivalent PandasIndex objects.
|
|
845
|
+
|
|
846
|
+
Returns a dict mapping dimension names to PandasIndex instances.
|
|
847
|
+
For internal/testing use.
|
|
848
|
+
"""
|
|
849
|
+
result = {}
|
|
850
|
+
if self._axis_independent:
|
|
851
|
+
for idx in self._xy_indexes:
|
|
852
|
+
dim = idx.axis_transform.dim
|
|
853
|
+
values = idx.to_pandas_index()
|
|
854
|
+
result[dim] = PandasIndex(values, dim)
|
|
855
|
+
else:
|
|
856
|
+
raise NotImplementedError("_as_pandas_index not supported for non-rectilinear grids")
|
|
857
|
+
|
|
858
|
+
return result
|
|
859
|
+
|
|
699
860
|
@property
|
|
700
861
|
def bbox(self) -> BoundingBox:
|
|
701
862
|
"""Bounding Box for index.
|
rasterix/strategies.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Hypothesis strategies for generating label-based indexers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Hashable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from hypothesis import note
|
|
10
|
+
from hypothesis import strategies as st
|
|
11
|
+
from xarray.core.indexes import Indexes
|
|
12
|
+
from xarray.testing.strategies import (
|
|
13
|
+
basic_indexers,
|
|
14
|
+
outer_array_indexers,
|
|
15
|
+
vectorized_indexers,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def pos_to_label_indexer(idx: pd.Index, idxr: int | slice | np.ndarray, *, use_scalar: bool = True) -> Any:
|
|
20
|
+
"""Convert a positional indexer to a label-based indexer.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
idx : pd.Index
|
|
25
|
+
The pandas Index to use for label lookup.
|
|
26
|
+
idxr : int | slice | np.ndarray
|
|
27
|
+
The positional indexer (integer, slice, or array of integers).
|
|
28
|
+
use_scalar : bool, optional
|
|
29
|
+
If True, attempt to convert scalar values to Python scalars. Default is True.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
Any
|
|
34
|
+
The label-based indexer (scalar, slice, or array of labels).
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(idxr, slice):
|
|
37
|
+
return slice(
|
|
38
|
+
None if idxr.start is None else idx[idxr.start],
|
|
39
|
+
# FIXME: This will never go past the label range
|
|
40
|
+
None if idxr.stop is None else idx[min(idxr.stop, idx.size - 1)],
|
|
41
|
+
)
|
|
42
|
+
elif isinstance(idxr, np.ndarray):
|
|
43
|
+
# Convert array of position indices to array of label values
|
|
44
|
+
return idx[idxr].values
|
|
45
|
+
else:
|
|
46
|
+
val = idx[idxr]
|
|
47
|
+
if use_scalar:
|
|
48
|
+
try:
|
|
49
|
+
# pass python scalars occasionally
|
|
50
|
+
val = val.item()
|
|
51
|
+
except Exception:
|
|
52
|
+
note(f"casting {val!r} to item() failed")
|
|
53
|
+
pass
|
|
54
|
+
return val
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@st.composite
|
|
58
|
+
def basic_label_indexers(draw, /, *, indexes: Indexes) -> dict[Hashable, float | slice]:
|
|
59
|
+
"""Generate label-based indexers by converting position indexers to labels.
|
|
60
|
+
|
|
61
|
+
This works in label space by using the coordinate Index values.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
draw : callable
|
|
66
|
+
The Hypothesis draw function (automatically provided by @st.composite).
|
|
67
|
+
indexes : Indexes
|
|
68
|
+
Dictionary mapping dimension names to their associated indexes
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
dict[Hashable, float | slice]
|
|
73
|
+
Label-based indexers as a dict with keys from sizes.keys().
|
|
74
|
+
Values are either float (for scalar labels) or slice (for label ranges).
|
|
75
|
+
"""
|
|
76
|
+
idxs = indexes.get_unique()
|
|
77
|
+
assert all(isinstance(idx, xr.indexes.PandasIndex) for idx in idxs)
|
|
78
|
+
|
|
79
|
+
# FIXME: this should be indexes.sizes!
|
|
80
|
+
sizes = indexes.dims
|
|
81
|
+
|
|
82
|
+
pos_indexer = draw(basic_indexers(sizes=sizes))
|
|
83
|
+
pdindexes = indexes.to_pandas_indexes()
|
|
84
|
+
|
|
85
|
+
label_indexer = {
|
|
86
|
+
dim: pos_to_label_indexer(pdindexes[dim], idx, use_scalar=draw(st.booleans()))
|
|
87
|
+
for dim, idx in pos_indexer.items()
|
|
88
|
+
}
|
|
89
|
+
return label_indexer
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@st.composite
|
|
93
|
+
def outer_array_label_indexers(draw, /, *, indexes: Indexes) -> dict[Hashable, np.ndarray]:
|
|
94
|
+
"""Generate label-based outer array indexers by converting position indexers to labels.
|
|
95
|
+
|
|
96
|
+
This works in label space by using the coordinate Index values.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
draw : callable
|
|
101
|
+
The Hypothesis draw function (automatically provided by @st.composite).
|
|
102
|
+
indexes : Indexes
|
|
103
|
+
Dictionary mapping dimension names to their associated indexes
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
dict[Hashable, np.ndarray]
|
|
108
|
+
Label-based indexers as a dict with keys from indexes.
|
|
109
|
+
Values are numpy arrays of label values for each dimension.
|
|
110
|
+
"""
|
|
111
|
+
idxs = indexes.get_unique()
|
|
112
|
+
assert all(isinstance(idx, xr.indexes.PandasIndex) for idx in idxs)
|
|
113
|
+
|
|
114
|
+
# FIXME: this should be indexes.sizes!
|
|
115
|
+
sizes = indexes.dims
|
|
116
|
+
|
|
117
|
+
pos_indexer = draw(outer_array_indexers(sizes=sizes))
|
|
118
|
+
pdindexes = indexes.to_pandas_indexes()
|
|
119
|
+
|
|
120
|
+
label_indexer = {
|
|
121
|
+
dim: pos_to_label_indexer(pdindexes[dim], idx, use_scalar=False) for dim, idx in pos_indexer.items()
|
|
122
|
+
}
|
|
123
|
+
return label_indexer
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@st.composite
|
|
127
|
+
def vectorized_label_indexers(draw, /, *, indexes: Indexes, **kwargs) -> dict[Hashable, xr.DataArray]:
|
|
128
|
+
"""Generate label-based vectorized indexers by converting position indexers to labels.
|
|
129
|
+
|
|
130
|
+
This works in label space by using the coordinate Index values.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
draw : callable
|
|
135
|
+
The Hypothesis draw function (automatically provided by @st.composite).
|
|
136
|
+
indexes : Indexes
|
|
137
|
+
Dictionary mapping dimension names to their associated indexes
|
|
138
|
+
**kwargs : dict
|
|
139
|
+
Additional keyword arguments to pass to vectorized_indexers
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
dict[Hashable, xr.DataArray]
|
|
144
|
+
Label-based indexers as a dict with keys from indexes.
|
|
145
|
+
Values are DataArrays of label values for each dimension.
|
|
146
|
+
"""
|
|
147
|
+
idxs = indexes.get_unique()
|
|
148
|
+
assert all(isinstance(idx, xr.indexes.PandasIndex) for idx in idxs)
|
|
149
|
+
|
|
150
|
+
# FIXME: this should be indexes.sizes!
|
|
151
|
+
sizes = indexes.dims
|
|
152
|
+
|
|
153
|
+
pos_indexer = draw(vectorized_indexers(sizes=sizes, **kwargs))
|
|
154
|
+
pdindexes = indexes.to_pandas_indexes()
|
|
155
|
+
|
|
156
|
+
label_indexer = {}
|
|
157
|
+
for dim, idx_array in pos_indexer.items():
|
|
158
|
+
# Convert each position in the array to its corresponding label
|
|
159
|
+
# Flatten, index, then reshape back to original shape
|
|
160
|
+
flat_indices = idx_array.values.ravel()
|
|
161
|
+
flat_labels = pdindexes[dim][flat_indices].values
|
|
162
|
+
label_values = flat_labels.reshape(idx_array.shape)
|
|
163
|
+
label_indexer[dim] = xr.DataArray(label_values, dims=idx_array.dims)
|
|
164
|
+
|
|
165
|
+
return label_indexer
|
rasterix/utils.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import xarray as xr
|
|
2
2
|
from affine import Affine
|
|
3
3
|
|
|
4
|
+
from rasterix.lib import affine_from_stac_proj_metadata, affine_from_tiepoint_and_scale, logger
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def get_grid_mapping_var(obj: xr.Dataset | xr.DataArray) -> xr.DataArray | None:
|
|
6
8
|
grid_mapping_var = None
|
|
@@ -31,8 +33,9 @@ def get_affine(
|
|
|
31
33
|
Grabs an affine transform from an Xarray object.
|
|
32
34
|
|
|
33
35
|
This method will first look for the ``"GeoTransform"`` attribute on a variable named
|
|
34
|
-
``"spatial_ref"``. If not, it will
|
|
35
|
-
and ``
|
|
36
|
+
``"spatial_ref"``. If not, it will look for STAC ``proj:transform`` attribute, then
|
|
37
|
+
GeoTIFF metadata (``model_tiepoint`` and ``model_pixel_scale``). Finally, it will
|
|
38
|
+
auto-guess the transform from the provided ``x_dim`` and ``y_dim``.
|
|
36
39
|
|
|
37
40
|
Parameters
|
|
38
41
|
----------
|
|
@@ -42,7 +45,7 @@ def get_affine(
|
|
|
42
45
|
y_dim: str, optional
|
|
43
46
|
Name of the Y dimension coordinate variable.
|
|
44
47
|
clear_transform: bool
|
|
45
|
-
Whether to delete the
|
|
48
|
+
Whether to delete the transform attributes if detected.
|
|
46
49
|
|
|
47
50
|
Returns
|
|
48
51
|
-------
|
|
@@ -50,18 +53,56 @@ def get_affine(
|
|
|
50
53
|
"""
|
|
51
54
|
grid_mapping_var = get_grid_mapping_var(obj)
|
|
52
55
|
if grid_mapping_var is not None and (transform := grid_mapping_var.attrs.get("GeoTransform")):
|
|
56
|
+
logger.trace("Creating affine from GeoTransform attribute")
|
|
53
57
|
if clear_transform:
|
|
54
58
|
del grid_mapping_var.attrs["GeoTransform"]
|
|
55
59
|
return Affine.from_gdal(*map(float, transform.split(" ")))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
|
|
61
|
+
# Check for STAC and GeoTIFF metadata in DataArray attrs
|
|
62
|
+
attrs = obj.attrs if isinstance(obj, xr.DataArray) else {}
|
|
63
|
+
|
|
64
|
+
# Try to extract affine from STAC proj:transform
|
|
65
|
+
if affine := affine_from_stac_proj_metadata(attrs):
|
|
66
|
+
logger.trace("Creating affine from STAC proj:transform attribute")
|
|
67
|
+
if clear_transform:
|
|
68
|
+
del attrs["proj:transform"]
|
|
69
|
+
return affine
|
|
70
|
+
|
|
71
|
+
# Try to extract affine from GeoTIFF model_tiepoint and model_pixel_scale
|
|
72
|
+
if "model_tiepoint" in attrs and "model_pixel_scale" in attrs:
|
|
73
|
+
logger.trace("Creating affine from GeoTIFF model_tiepoint and model_pixel_scale attributes")
|
|
74
|
+
affine = affine_from_tiepoint_and_scale(attrs["model_tiepoint"], attrs["model_pixel_scale"])
|
|
75
|
+
|
|
76
|
+
# Clean up GeoTIFF metadata attributes after using them
|
|
77
|
+
if clear_transform:
|
|
78
|
+
del attrs["model_tiepoint"]
|
|
79
|
+
del attrs["model_pixel_scale"]
|
|
80
|
+
|
|
81
|
+
return affine
|
|
82
|
+
|
|
83
|
+
# Fall back to computing from coordinate arrays
|
|
84
|
+
logger.trace(f"Creating affine from coordinate arrays {x_dim=!r} and {y_dim=!r}")
|
|
85
|
+
if x_dim not in obj.coords or y_dim not in obj.coords:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Cannot create affine transform: dimensions {x_dim=!r} and {y_dim=!r} "
|
|
88
|
+
f"do not have explicit coordinate values and no transform metadata found."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
x = obj.coords[x_dim]
|
|
92
|
+
y = obj.coords[y_dim]
|
|
93
|
+
if x.ndim != 1:
|
|
94
|
+
raise ValueError(f"Coordinate variable {x_dim=!r} must be 1D.")
|
|
95
|
+
if y.ndim != 1:
|
|
96
|
+
raise ValueError(f"Coordinate variable {y_dim=!r} must be 1D.")
|
|
97
|
+
|
|
98
|
+
# Check that coordinates have actual values (not just dimension placeholders)
|
|
99
|
+
if len(x) == 0 or len(y) == 0:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Cannot create affine transform from empty coordinate arrays for {x_dim=!r} and {y_dim=!r}."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
dx = (x[1] - x[0]).item()
|
|
105
|
+
dy = (y[1] - y[0]).item()
|
|
106
|
+
return Affine.translation(
|
|
107
|
+
x[0].item() - dx / 2, (y[0] if dy < 0 else y[-1]).item() - dy / 2
|
|
108
|
+
) * Affine.scale(dx, dy)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rasterix
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Raster extensions for Xarray
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -45,15 +45,6 @@ Requires-Dist: exactextract; extra == 'exactextract'
|
|
|
45
45
|
Requires-Dist: sparse; extra == 'exactextract'
|
|
46
46
|
Provides-Extra: rasterize
|
|
47
47
|
Requires-Dist: rasterio; extra == 'rasterize'
|
|
48
|
-
Provides-Extra: test
|
|
49
|
-
Requires-Dist: dask-geopandas; extra == 'test'
|
|
50
|
-
Requires-Dist: exactextract; extra == 'test'
|
|
51
|
-
Requires-Dist: geodatasets; extra == 'test'
|
|
52
|
-
Requires-Dist: hypothesis; extra == 'test'
|
|
53
|
-
Requires-Dist: netcdf4; extra == 'test'
|
|
54
|
-
Requires-Dist: pooch; extra == 'test'
|
|
55
|
-
Requires-Dist: rasterio; extra == 'test'
|
|
56
|
-
Requires-Dist: sparse; extra == 'test'
|
|
57
48
|
Description-Content-Type: text/markdown
|
|
58
49
|
|
|
59
50
|
# rasterix: Raster tricks for Xarray
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
rasterix/__init__.py,sha256=iSC54A4b-7zgxtKV0CUok0O2-ApGtzVA-6hWG3a5AAA,283
|
|
2
|
+
rasterix/_version.py,sha256=m8HxkqoKGw_wAJtc4ZokpJKNLXqp4zwnNhbnfDtro7w,704
|
|
3
|
+
rasterix/lib.py,sha256=nuX3DFdx8smK8jc41Y6xQ2BaGkMdIBTIpOhk__YkTJw,3236
|
|
4
|
+
rasterix/odc_compat.py,sha256=MxjctCH0zV6VSSQymatJreHYUnUo3qkB1oIKbOxEK4A,19099
|
|
5
|
+
rasterix/raster_index.py,sha256=UhpfqGwKPLSo5-OhnTu43rkwVXi2TB2QyXC2r2VZnVs,38333
|
|
6
|
+
rasterix/rioxarray_compat.py,sha256=o32UBkWBpNhmC35ar0Ozn2QszpWdPvdDDXif3Mq2ZKg,12428
|
|
7
|
+
rasterix/strategies.py,sha256=gCc-BWU3oJsaSQalkbad1T8HYBk91079wlkEhwc2mzc,5525
|
|
8
|
+
rasterix/utils.py,sha256=PMcfOfutDiH5aE2XKl486fZR4qMAU7h6AzUNwMmEAFQ,4295
|
|
9
|
+
rasterix/rasterize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
rasterix/rasterize/exact.py,sha256=UPB9zXpj7ZgkDy-x4vcNwY3J8DTrOTkRcIBMkhPQ04s,10558
|
|
11
|
+
rasterix/rasterize/rasterio.py,sha256=gtdQxM9CiosalYk3rLKGkp0UmhZVO5parAVsR7IINoE,12883
|
|
12
|
+
rasterix/rasterize/utils.py,sha256=iIuyTdigpFnts_-bUfas6FMsgix5lMqMQTxQaEBlyXM,3521
|
|
13
|
+
rasterix-0.1.1.dist-info/METADATA,sha256=cqTlelh4_8aeAXuDnkozCtsWXBEsXsSLxSO0eqfIfWs,3044
|
|
14
|
+
rasterix-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
rasterix-0.1.1.dist-info/licenses/LICENSE,sha256=QFnsASMx8_yBNbrS7GVOhJ5CglGsLMj83Rn61uWyMs8,10265
|
|
16
|
+
rasterix-0.1.1.dist-info/RECORD,,
|
rasterix-0.1a4.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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,,
|
|
File without changes
|