rasterix 0.1.1__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 +2 -2
- rasterix/lib.py +74 -0
- rasterix/odc_compat.py +23 -0
- rasterix/options.py +75 -0
- rasterix/raster_index.py +125 -16
- 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/utils.py +74 -2
- {rasterix-0.1.1.dist-info → rasterix-0.2.0.dist-info}/METADATA +7 -3
- rasterix-0.2.0.dist-info/RECORD +19 -0
- rasterix-0.1.1.dist-info/RECORD +0 -16
- {rasterix-0.1.1.dist-info → rasterix-0.2.0.dist-info}/WHEEL +0 -0
- {rasterix-0.1.1.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
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
rasterix/lib.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""Shared library utilities for rasterix."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from typing import NotRequired, TypedDict
|
|
4
5
|
|
|
5
6
|
from affine import Affine
|
|
6
7
|
|
|
8
|
+
# https://github.com/zarr-conventions/spatial
|
|
9
|
+
_ZARR_SPATIAL_CONVENTION_UUID = "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4"
|
|
10
|
+
|
|
11
|
+
|
|
7
12
|
# Define TRACE level (lower than DEBUG)
|
|
8
13
|
TRACE = 5
|
|
9
14
|
logging.addLevelName(TRACE, "TRACE")
|
|
@@ -104,3 +109,72 @@ def affine_from_stac_proj_metadata(metadata: dict) -> Affine | None:
|
|
|
104
109
|
|
|
105
110
|
a, b, c, d, e, f = transform[:6]
|
|
106
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()
|
rasterix/raster_index.py
CHANGED
|
@@ -20,8 +20,9 @@ from xarray.core.types import JoinOptions
|
|
|
20
20
|
from xproj.typing import CRSAwareIndex
|
|
21
21
|
|
|
22
22
|
from rasterix.odc_compat import BoundingBox, bbox_intersection, bbox_union, maybe_int, snap_grid
|
|
23
|
+
from rasterix.options import get_options as get_rasterix_options
|
|
23
24
|
from rasterix.rioxarray_compat import guess_dims
|
|
24
|
-
from rasterix.utils import get_affine
|
|
25
|
+
from rasterix.utils import get_affine, get_crs_from_proj_zarr_convention
|
|
25
26
|
|
|
26
27
|
T_Xarray = TypeVar("T_Xarray", "DataArray", "Dataset")
|
|
27
28
|
|
|
@@ -87,22 +88,34 @@ def assign_index(
|
|
|
87
88
|
|
|
88
89
|
affine = get_affine(obj, x_dim=x_dim, y_dim=y_dim, clear_transform=True)
|
|
89
90
|
|
|
91
|
+
detected_crs = obj.proj.crs if crs else None
|
|
92
|
+
if detected_crs is None:
|
|
93
|
+
detected_crs = get_crs_from_proj_zarr_convention(obj)
|
|
94
|
+
|
|
90
95
|
index = RasterIndex.from_transform(
|
|
91
96
|
affine,
|
|
92
97
|
width=obj.sizes[x_dim],
|
|
93
98
|
height=obj.sizes[y_dim],
|
|
94
99
|
x_dim=x_dim,
|
|
95
100
|
y_dim=y_dim,
|
|
96
|
-
crs=
|
|
101
|
+
crs=detected_crs,
|
|
97
102
|
)
|
|
98
103
|
coords = Coordinates.from_xindex(index)
|
|
99
104
|
return obj.assign_coords(coords)
|
|
100
105
|
|
|
101
106
|
|
|
107
|
+
def _isclose(a: float, b: float) -> bool:
|
|
108
|
+
"""Check if two floats are close using rasterix tolerance options."""
|
|
109
|
+
opts = get_rasterix_options()
|
|
110
|
+
return math.isclose(a, b, rel_tol=opts["transform_rtol"], abs_tol=opts["transform_atol"])
|
|
111
|
+
|
|
112
|
+
|
|
102
113
|
def _assert_transforms_are_compatible(*affines) -> None:
|
|
103
114
|
A1 = affines[0]
|
|
104
115
|
for index, A2 in enumerate(affines[1:]):
|
|
105
|
-
if
|
|
116
|
+
if not (
|
|
117
|
+
_isclose(A1.a, A2.a) and _isclose(A1.b, A2.b) and _isclose(A1.d, A2.d) and _isclose(A1.e, A2.e)
|
|
118
|
+
):
|
|
106
119
|
raise ValueError(
|
|
107
120
|
f"Transform parameters are not compatible for affine 0: {A1}, and affine {index + 1} {A2}"
|
|
108
121
|
)
|
|
@@ -219,9 +232,9 @@ class AxisAffineTransform(CoordinateTransform):
|
|
|
219
232
|
|
|
220
233
|
# only compare the affine parameters of the relevant axis
|
|
221
234
|
if self.is_xaxis:
|
|
222
|
-
affine_match = self.affine.a
|
|
235
|
+
affine_match = _isclose(self.affine.a, other.affine.a) and _isclose(self.affine.c, other.affine.c)
|
|
223
236
|
else:
|
|
224
|
-
affine_match = self.affine.e
|
|
237
|
+
affine_match = _isclose(self.affine.e, other.affine.e) and _isclose(self.affine.f, other.affine.f)
|
|
225
238
|
|
|
226
239
|
return affine_match and self.size == other.size
|
|
227
240
|
|
|
@@ -312,9 +325,12 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
312
325
|
label = labels[coord_name]
|
|
313
326
|
transform = self.axis_transform
|
|
314
327
|
if isinstance(label, slice):
|
|
328
|
+
# Use 'is None' check instead of 'or' to correctly handle 0 values
|
|
315
329
|
label = slice(
|
|
316
|
-
|
|
317
|
-
|
|
330
|
+
transform.forward({coord_name: 0})[coord_name] if label.start is None else label.start,
|
|
331
|
+
transform.forward({coord_name: transform.size})[coord_name]
|
|
332
|
+
if label.stop is None
|
|
333
|
+
else label.stop,
|
|
318
334
|
label.step,
|
|
319
335
|
)
|
|
320
336
|
if label.step is None:
|
|
@@ -322,8 +338,12 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
322
338
|
pos = self.transform.reverse({coord_name: np.array([label.start, label.stop])})
|
|
323
339
|
# np.round rounds to even, this way we round upwards
|
|
324
340
|
pos = np.floor(pos[self.dim] + 0.5).astype("int")
|
|
325
|
-
|
|
326
|
-
|
|
341
|
+
size = self.axis_transform.size
|
|
342
|
+
# Clamp both start and stop to valid range [0, size]
|
|
343
|
+
new_start = max(min(pos[0], size), 0)
|
|
344
|
+
new_stop = max(min(pos[1] + 1, size), 0)
|
|
345
|
+
# Ensure stop >= start (empty slice if selection is completely outside bounds)
|
|
346
|
+
new_stop = max(new_stop, new_start)
|
|
327
347
|
return IndexSelResult({self.dim: slice(new_start, new_stop)})
|
|
328
348
|
else:
|
|
329
349
|
# otherwise convert to basic (array) indexing
|
|
@@ -575,6 +595,89 @@ class RasterIndex(Index, xproj.ProjIndexMixin):
|
|
|
575
595
|
|
|
576
596
|
return cls(index, crs=crs)
|
|
577
597
|
|
|
598
|
+
@classmethod
|
|
599
|
+
def from_geotransform(
|
|
600
|
+
cls,
|
|
601
|
+
geotransform: Sequence[float] | str,
|
|
602
|
+
*,
|
|
603
|
+
width: int,
|
|
604
|
+
height: int,
|
|
605
|
+
x_dim: str = "x",
|
|
606
|
+
y_dim: str = "y",
|
|
607
|
+
x_coord_name: str = "xc",
|
|
608
|
+
y_coord_name: str = "yc",
|
|
609
|
+
crs: CRS | Any | None = None,
|
|
610
|
+
) -> RasterIndex:
|
|
611
|
+
"""Create a RasterIndex from a GDAL-style GeoTransform.
|
|
612
|
+
|
|
613
|
+
Parameters
|
|
614
|
+
----------
|
|
615
|
+
geotransform : sequence of float or str
|
|
616
|
+
GDAL GeoTransform as a 6-element sequence (c, a, b, f, d, e) or a
|
|
617
|
+
space-separated string of 6 numbers. The elements are:
|
|
618
|
+
- c: x-coordinate of the upper-left corner of the upper-left pixel
|
|
619
|
+
- a: pixel width (x-direction resolution)
|
|
620
|
+
- b: row rotation (typically 0)
|
|
621
|
+
- f: y-coordinate of the upper-left corner of the upper-left pixel
|
|
622
|
+
- d: column rotation (typically 0)
|
|
623
|
+
- e: pixel height (y-direction resolution, typically negative)
|
|
624
|
+
width : int
|
|
625
|
+
Number of pixels in the x direction.
|
|
626
|
+
height : int
|
|
627
|
+
Number of pixels in the y direction.
|
|
628
|
+
x_dim : str, optional
|
|
629
|
+
Name for the x dimension.
|
|
630
|
+
y_dim : str, optional
|
|
631
|
+
Name for the y dimension.
|
|
632
|
+
x_coord_name : str, optional
|
|
633
|
+
Name for the x coordinate. For non-rectilinear transforms only.
|
|
634
|
+
y_coord_name : str, optional
|
|
635
|
+
Name for the y coordinate. For non-rectilinear transforms only.
|
|
636
|
+
crs : :class:`pyproj.crs.CRS` or any, optional
|
|
637
|
+
The coordinate reference system. Any value accepted by
|
|
638
|
+
:meth:`pyproj.crs.CRS.from_user_input`.
|
|
639
|
+
|
|
640
|
+
Returns
|
|
641
|
+
-------
|
|
642
|
+
RasterIndex
|
|
643
|
+
A new RasterIndex object with appropriate internal structure.
|
|
644
|
+
|
|
645
|
+
See Also
|
|
646
|
+
--------
|
|
647
|
+
from_transform : Create from an Affine transform.
|
|
648
|
+
as_geotransform : Convert RasterIndex back to GeoTransform string.
|
|
649
|
+
|
|
650
|
+
References
|
|
651
|
+
----------
|
|
652
|
+
- `GDAL GeoTransform tutorial <https://gdal.org/en/stable/tutorials/geotransforms_tut.html>`_
|
|
653
|
+
|
|
654
|
+
Examples
|
|
655
|
+
--------
|
|
656
|
+
Create from a sequence:
|
|
657
|
+
|
|
658
|
+
>>> geotransform = (323400.0, 30.0, 0.0, 4265400.0, 0.0, -30.0)
|
|
659
|
+
>>> index = RasterIndex.from_geotransform(geotransform, width=100, height=100)
|
|
660
|
+
|
|
661
|
+
Create from a string (as stored in netCDF attributes):
|
|
662
|
+
|
|
663
|
+
>>> geotransform = "323400.0 30.0 0.0 4265400.0 0.0 -30.0"
|
|
664
|
+
>>> index = RasterIndex.from_geotransform(geotransform, width=100, height=100)
|
|
665
|
+
"""
|
|
666
|
+
if isinstance(geotransform, str):
|
|
667
|
+
geotransform = tuple(map(float, geotransform.split()))
|
|
668
|
+
|
|
669
|
+
affine = Affine.from_gdal(*geotransform)
|
|
670
|
+
return cls.from_transform(
|
|
671
|
+
affine,
|
|
672
|
+
width=width,
|
|
673
|
+
height=height,
|
|
674
|
+
x_dim=x_dim,
|
|
675
|
+
y_dim=y_dim,
|
|
676
|
+
x_coord_name=x_coord_name,
|
|
677
|
+
y_coord_name=y_coord_name,
|
|
678
|
+
crs=crs,
|
|
679
|
+
)
|
|
680
|
+
|
|
578
681
|
@classmethod
|
|
579
682
|
def from_tiepoint_and_scale(
|
|
580
683
|
cls,
|
|
@@ -891,10 +994,16 @@ class RasterIndex(Index, xproj.ProjIndexMixin):
|
|
|
891
994
|
new_affine, Nx, Ny = bbox_to_affine(bbox, rx=affine.a, ry=affine.e)
|
|
892
995
|
# TODO: set xdim, ydim explicitly
|
|
893
996
|
new_index = self.from_transform(new_affine, width=Nx, height=Ny)
|
|
894
|
-
|
|
997
|
+
opts = get_rasterix_options()
|
|
998
|
+
assert new_index.bbox.isclose(bbox, rtol=opts["transform_rtol"], atol=opts["transform_atol"])
|
|
895
999
|
return new_index
|
|
896
1000
|
|
|
897
1001
|
def join(self, other: RasterIndex, how: JoinOptions = "inner") -> RasterIndex:
|
|
1002
|
+
"""Join two RasterIndexes by computing the union or intersection of their bounding boxes.
|
|
1003
|
+
|
|
1004
|
+
Transform compatibility is checked using the tolerance configured via
|
|
1005
|
+
:py:func:`rasterix.set_options` (``transform_rtol`` and ``transform_atol``).
|
|
1006
|
+
"""
|
|
898
1007
|
if not self._proj_crs_equals(cast(CRSAwareIndex, other), allow_none=True):
|
|
899
1008
|
raise ValueError(
|
|
900
1009
|
"raster indexes on objects to align do not have the same CRS\n"
|
|
@@ -1006,24 +1115,24 @@ def as_compatible_bboxes(*indexes: RasterIndex, concat_dim: Hashable | None) ->
|
|
|
1006
1115
|
off_y = tuple(t.f for t in transforms)
|
|
1007
1116
|
|
|
1008
1117
|
if concat_dim is not None:
|
|
1009
|
-
if all(o
|
|
1118
|
+
if all(_isclose(o, off_x[0]) for o in off_x[1:]) and all(_isclose(o, off_y[0]) for o in off_y[1:]):
|
|
1010
1119
|
raise ValueError("Attempting to concatenate arrays with same transform along X or Y.")
|
|
1011
1120
|
|
|
1012
1121
|
# note: Xarray alignment already ensures that the indexes dimensions are compatible.
|
|
1013
1122
|
x_dim, y_dim = indexes[0].xy_dims
|
|
1014
1123
|
|
|
1015
1124
|
if concat_dim == x_dim:
|
|
1016
|
-
if any(off_y[0]
|
|
1017
|
-
raise ValueError("offsets must be identical in
|
|
1018
|
-
if any(a
|
|
1125
|
+
if any(not _isclose(off_y[0], o) for o in off_y[1:]):
|
|
1126
|
+
raise ValueError("offsets must be identical in Y when concatenating along X")
|
|
1127
|
+
if any(not _isclose(a, b) for a, b in zip(off_x, expected_off_x)):
|
|
1019
1128
|
raise ValueError(
|
|
1020
1129
|
f"X offsets are incompatible. Provided offsets {off_x}, expected offsets: {expected_off_x}"
|
|
1021
1130
|
)
|
|
1022
1131
|
elif concat_dim == y_dim:
|
|
1023
|
-
if any(off_x[0]
|
|
1132
|
+
if any(not _isclose(off_x[0], o) for o in off_x[1:]):
|
|
1024
1133
|
raise ValueError("offsets must be identical in X when concatenating along Y")
|
|
1025
1134
|
|
|
1026
|
-
if any(a
|
|
1135
|
+
if any(not _isclose(a, b) for a, b in zip(off_y, expected_off_y)):
|
|
1027
1136
|
raise ValueError(
|
|
1028
1137
|
f"Y offsets are incompatible. Provided offsets {off_y}, expected offsets: {expected_off_y}"
|
|
1029
1138
|
)
|
rasterix/rasterize/__init__.py
CHANGED