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 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__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
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.1a4'
21
- __version_tuple__ = version_tuple = (0, 1, 'a4')
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()