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 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.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
- if label.start is None:
316
- label = slice(0, label.stop, label.step)
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 auto-guess the transform from the provided ``x_dim``,
35
- and ``y_dim``.
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 ``GeoTransform`` attribute if detected.
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
- 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)
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.1a4
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,