rasterix 0.1a4__py3-none-any.whl → 0.2.0__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/__init__.py +2 -1
- rasterix/_version.py +16 -3
- rasterix/lib.py +180 -0
- rasterix/odc_compat.py +23 -0
- rasterix/options.py +75 -0
- rasterix/raster_index.py +288 -18
- rasterix/rasterize/__init__.py +4 -0
- rasterix/rasterize/core.py +503 -0
- rasterix/rasterize/exact.py +279 -16
- rasterix/rasterize/rasterio.py +11 -307
- rasterix/rasterize/rusterize.py +240 -0
- rasterix/rasterize/utils.py +0 -15
- rasterix/strategies.py +165 -0
- rasterix/utils.py +128 -15
- {rasterix-0.1a4.dist-info → rasterix-0.2.0.dist-info}/METADATA +7 -12
- rasterix-0.2.0.dist-info/RECORD +19 -0
- {rasterix-0.1a4.dist-info → rasterix-0.2.0.dist-info}/WHEEL +1 -1
- rasterix-0.1a4.dist-info/RECORD +0 -14
- {rasterix-0.1a4.dist-info → rasterix-0.2.0.dist-info}/licenses/LICENSE +0 -0
rasterix/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .options import get_options, set_options
|
|
1
2
|
from .raster_index import RasterIndex, assign_index
|
|
2
3
|
|
|
3
4
|
|
|
@@ -12,4 +13,4 @@ def _get_version():
|
|
|
12
13
|
|
|
13
14
|
__version__ = _get_version()
|
|
14
15
|
|
|
15
|
-
__all__ = ["RasterIndex", "assign_index"]
|
|
16
|
+
__all__ = ["RasterIndex", "assign_index", "set_options", "get_options"]
|
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,
|
|
31
|
+
__version__ = version = '0.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
rasterix/lib.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Shared library utilities for rasterix."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import NotRequired, TypedDict
|
|
5
|
+
|
|
6
|
+
from affine import Affine
|
|
7
|
+
|
|
8
|
+
# https://github.com/zarr-conventions/spatial
|
|
9
|
+
_ZARR_SPATIAL_CONVENTION_UUID = "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Define TRACE level (lower than DEBUG)
|
|
13
|
+
TRACE = 5
|
|
14
|
+
logging.addLevelName(TRACE, "TRACE")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TraceLogger(logging.Logger):
|
|
18
|
+
"""Logger with trace level support."""
|
|
19
|
+
|
|
20
|
+
def trace(self, message, *args, **kwargs):
|
|
21
|
+
"""Log a message with severity 'TRACE'."""
|
|
22
|
+
if self.isEnabledFor(TRACE):
|
|
23
|
+
self._log(TRACE, message, args, **kwargs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Set the custom logger class
|
|
27
|
+
logging.setLoggerClass(TraceLogger)
|
|
28
|
+
|
|
29
|
+
# Create logger for the rasterix package
|
|
30
|
+
logger = logging.getLogger("rasterix")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def affine_from_tiepoint_and_scale(
|
|
34
|
+
tiepoint: list[float] | tuple[float, ...],
|
|
35
|
+
scale: list[float] | tuple[float, ...],
|
|
36
|
+
) -> Affine:
|
|
37
|
+
"""Create an Affine transform from GeoTIFF tiepoint and pixel scale.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
tiepoint : list or tuple
|
|
42
|
+
GeoTIFF model tiepoint in format [I, J, K, X, Y, Z]
|
|
43
|
+
where (I, J, K) are pixel coords and (X, Y, Z) are world coords.
|
|
44
|
+
scale : list or tuple
|
|
45
|
+
GeoTIFF model pixel scale in format [ScaleX, ScaleY, ScaleZ].
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
Affine
|
|
50
|
+
Affine transformation matrix.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
AssertionError
|
|
55
|
+
If ScaleZ is not 0 (only 2D rasters are supported).
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
>>> tiepoint = [0.0, 0.0, 0.0, 323400.0, 4265400.0, 0.0]
|
|
60
|
+
>>> scale = [30.0, 30.0, 0.0]
|
|
61
|
+
>>> affine = affine_from_tiepoint_and_scale(tiepoint, scale)
|
|
62
|
+
"""
|
|
63
|
+
if len(tiepoint) < 6:
|
|
64
|
+
raise ValueError(f"tiepoint must have at least 6 elements, got {len(tiepoint)}")
|
|
65
|
+
if len(scale) < 3:
|
|
66
|
+
raise ValueError(f"scale must have at least 3 elements, got {len(scale)}")
|
|
67
|
+
|
|
68
|
+
i, j, k, x, y, z = tiepoint[:6]
|
|
69
|
+
scale_x, scale_y, scale_z = scale[:3]
|
|
70
|
+
|
|
71
|
+
# We only support 2D rasters
|
|
72
|
+
assert scale_z == 0, f"Z pixel scale must be 0 for 2D rasters, got {scale_z}"
|
|
73
|
+
|
|
74
|
+
# The tiepoint gives us the world coordinates at pixel (I, J)
|
|
75
|
+
# Affine transform: x_world = c + i * a, y_world = f + j * e
|
|
76
|
+
# So: c = x - i * scale_x, f = y - j * scale_y
|
|
77
|
+
c = x - i * scale_x
|
|
78
|
+
f = y - j * scale_y
|
|
79
|
+
|
|
80
|
+
return Affine.translation(c, f) * Affine.scale(scale_x, scale_y)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def affine_from_stac_proj_metadata(metadata: dict) -> Affine | None:
|
|
84
|
+
"""Extract Affine transform from STAC projection metadata.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
metadata : dict
|
|
89
|
+
Dictionary containing STAC metadata. Should contain a 'proj:transform' key.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
Affine or None
|
|
94
|
+
Affine transformation matrix if 'proj:transform' is found, None otherwise.
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
>>> metadata = {"proj:transform": [30.0, 0.0, 323400.0, 0.0, 30.0, 4268400.0]}
|
|
99
|
+
>>> affine = affine_from_stac_proj_metadata(metadata)
|
|
100
|
+
"""
|
|
101
|
+
if "proj:transform" not in metadata:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
transform = metadata["proj:transform"]
|
|
105
|
+
# proj:transform is a 3x3 matrix in row-major order, but typically only 6 elements
|
|
106
|
+
# [a, b, c, d, e, f, 0, 0, 1] where the affine is constructed from first 6 elements
|
|
107
|
+
if len(transform) < 6:
|
|
108
|
+
raise ValueError(f"proj:transform must have at least 6 elements, got {len(transform)}")
|
|
109
|
+
|
|
110
|
+
a, b, c, d, e, f = transform[:6]
|
|
111
|
+
return Affine(a, b, c, d, e, f)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_ZarrConventionRegistration = TypedDict("_ZarrConventionRegistration", {"spatial:": str})
|
|
115
|
+
|
|
116
|
+
_ZarrSpatialMetadata = TypedDict(
|
|
117
|
+
"_ZarrSpatialMetadata",
|
|
118
|
+
{
|
|
119
|
+
"zarr_conventions": NotRequired[list[_ZarrConventionRegistration | dict]],
|
|
120
|
+
"spatial:transform": NotRequired[list[float]],
|
|
121
|
+
"spatial:transform_type": NotRequired[str],
|
|
122
|
+
"spatial:registration": NotRequired[str],
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _has_spatial_zarr_convention(metadata: _ZarrSpatialMetadata) -> bool:
|
|
128
|
+
zarr_conventions = metadata.get("zarr_conventions")
|
|
129
|
+
if not zarr_conventions:
|
|
130
|
+
return False
|
|
131
|
+
for entry in zarr_conventions:
|
|
132
|
+
if isinstance(entry, dict) and (
|
|
133
|
+
entry.get("uuid") == _ZARR_SPATIAL_CONVENTION_UUID or entry.get("name") == "spatial:"
|
|
134
|
+
):
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def affine_from_spatial_zarr_convention(metadata: dict) -> Affine | None:
|
|
140
|
+
"""Extract Affine transform from Zarr spatial convention metadata.
|
|
141
|
+
|
|
142
|
+
See https://github.com/zarr-conventions/spatial for the full specification.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
metadata : dict
|
|
147
|
+
Dictionary containing Zarr spatial convention metadata.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
Affine or None
|
|
152
|
+
Affine transformation matrix if minimal Zarr spatial metadata is found, None otherwise.
|
|
153
|
+
|
|
154
|
+
Examples
|
|
155
|
+
--------
|
|
156
|
+
>>> ds: xr.Dataset = ...
|
|
157
|
+
>>> affine = affine_from_spatial_zarr_convention(ds.attrs)
|
|
158
|
+
"""
|
|
159
|
+
possibly_spatial_metadata: _ZarrSpatialMetadata = metadata # type: ignore[assignment]
|
|
160
|
+
|
|
161
|
+
if _has_spatial_zarr_convention(possibly_spatial_metadata):
|
|
162
|
+
if transform := possibly_spatial_metadata.get("spatial:transform"):
|
|
163
|
+
if len(transform) < 6:
|
|
164
|
+
raise ValueError(f"spatial:transform must have at least 6 elements, got {len(transform)}")
|
|
165
|
+
|
|
166
|
+
transform_type = possibly_spatial_metadata.get("spatial:transform_type", "affine")
|
|
167
|
+
if transform_type != "affine":
|
|
168
|
+
raise NotImplementedError(
|
|
169
|
+
f"Unsupported spatial:transform_type {transform_type!r}; only 'affine' is supported."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
registration = possibly_spatial_metadata.get("spatial:registration", "pixel")
|
|
173
|
+
if registration != "pixel":
|
|
174
|
+
raise NotImplementedError(
|
|
175
|
+
f"Unsupported spatial:registration {registration!r}; only 'pixel' is supported."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return Affine(*map(float, transform[:6]))
|
|
179
|
+
|
|
180
|
+
return None
|
rasterix/odc_compat.py
CHANGED
|
@@ -340,6 +340,29 @@ class BoundingBox(Sequence[float]):
|
|
|
340
340
|
def __hash__(self) -> int:
|
|
341
341
|
return hash(self._box)
|
|
342
342
|
|
|
343
|
+
def isclose(self, other: "BoundingBox", rtol: float = 1e-12, atol: float = 0.0) -> bool:
|
|
344
|
+
"""
|
|
345
|
+
Check if two bounding boxes are approximately equal.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
other : BoundingBox
|
|
350
|
+
The bounding box to compare against.
|
|
351
|
+
rtol : float, default 1e-12
|
|
352
|
+
Relative tolerance for comparison.
|
|
353
|
+
atol : float, default 0.0
|
|
354
|
+
Absolute tolerance for comparison.
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
bool
|
|
359
|
+
True if all four bounds (left, bottom, right, top) are close
|
|
360
|
+
within the specified tolerances.
|
|
361
|
+
"""
|
|
362
|
+
if not isinstance(other, BoundingBox):
|
|
363
|
+
return False
|
|
364
|
+
return all(math.isclose(a, b, rel_tol=rtol, abs_tol=atol) for a, b in zip(self._box, other._box))
|
|
365
|
+
|
|
343
366
|
def __len__(self) -> int:
|
|
344
367
|
return 4
|
|
345
368
|
|
rasterix/options.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Options for rasterix with context manager support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
OPTIONS: dict[str, Any] = {
|
|
9
|
+
"transform_rtol": 1e-12,
|
|
10
|
+
"transform_atol": 0.0,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _validate_tolerance(value: Any) -> bool:
|
|
15
|
+
"""Validate that value is a non-negative float."""
|
|
16
|
+
return isinstance(value, int | float) and value >= 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_VALIDATORS = {
|
|
20
|
+
"transform_rtol": _validate_tolerance,
|
|
21
|
+
"transform_atol": _validate_tolerance,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def set_options(**kwargs):
|
|
27
|
+
"""
|
|
28
|
+
Set options for rasterix in a controlled context.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
transform_rtol : float, default: 1e-12
|
|
33
|
+
Relative tolerance for comparing affine transform parameters
|
|
34
|
+
during alignment and concatenation operations. This small default
|
|
35
|
+
handles typical floating-point representation noise.
|
|
36
|
+
transform_atol : float, default: 0.0
|
|
37
|
+
Absolute tolerance for comparing affine transform parameters.
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
Use as a context manager:
|
|
42
|
+
|
|
43
|
+
>>> import rasterix
|
|
44
|
+
>>> import xarray as xr
|
|
45
|
+
>>> with rasterix.set_options(transform_rtol=1e-9):
|
|
46
|
+
... result = xr.concat([ds1, ds2], dim="x")
|
|
47
|
+
"""
|
|
48
|
+
old = {}
|
|
49
|
+
for k, v in kwargs.items():
|
|
50
|
+
if k not in OPTIONS:
|
|
51
|
+
raise ValueError(f"argument name {k!r} is not in the set of valid options {set(OPTIONS)!r}")
|
|
52
|
+
if k in _VALIDATORS and not _VALIDATORS[k](v):
|
|
53
|
+
raise ValueError(f"option {k!r} given an invalid value: {v!r}. Expected a non-negative number.")
|
|
54
|
+
old[k] = OPTIONS[k]
|
|
55
|
+
OPTIONS.update(kwargs)
|
|
56
|
+
try:
|
|
57
|
+
yield
|
|
58
|
+
finally:
|
|
59
|
+
OPTIONS.update(old)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_options() -> dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Get current options for rasterix.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
dict
|
|
69
|
+
Dictionary of current option values.
|
|
70
|
+
|
|
71
|
+
See Also
|
|
72
|
+
--------
|
|
73
|
+
set_options
|
|
74
|
+
"""
|
|
75
|
+
return OPTIONS.copy()
|