yirgacheffe 1.4.0__py3-none-any.whl → 1.5.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.

Potentially problematic release.


This version of yirgacheffe might be problematic. Click here for more details.

yirgacheffe/__init__.py CHANGED
@@ -5,10 +5,7 @@ try:
5
5
  except ModuleNotFoundError:
6
6
  __version__ = "unknown"
7
7
 
8
- gdal.UseExceptions()
8
+ from ._core import read_raster, read_rasters, read_shape, read_shape_like
9
+ from .constants import WGS_84_PROJECTION
9
10
 
10
- # I don't really want this here, but it's just too useful having it exposed
11
- WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
12
- 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
13
- 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
14
- 'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
11
+ gdal.UseExceptions()
@@ -1,3 +1,4 @@
1
+ from typing import Callable, Dict
1
2
 
2
3
  import numpy as np
3
4
  import mlx.core as mx # type: ignore
@@ -182,7 +183,7 @@ def backend_to_dtype(val):
182
183
  def astype_op(data, datatype):
183
184
  return data.astype(dtype_to_backed(datatype))
184
185
 
185
- operator_map = {
186
+ operator_map : Dict[op,Callable] = {
186
187
  op.ADD: mx.array.__add__,
187
188
  op.SUB: mx.array.__sub__,
188
189
  op.MUL: mul_op,
@@ -1,3 +1,4 @@
1
+ from typing import Callable, Dict
1
2
 
2
3
  import numpy as np
3
4
  import torch
@@ -120,7 +121,7 @@ def backend_to_dtype(val):
120
121
  def astype_op(data, datatype):
121
122
  return data.astype(dtype_to_backed(datatype))
122
123
 
123
- operator_map = {
124
+ operator_map : Dict[op,Callable] = {
124
125
  op.ADD: np.ndarray.__add__,
125
126
  op.SUB: np.ndarray.__sub__,
126
127
  op.MUL: np.ndarray.__mul__,
yirgacheffe/_core.py ADDED
@@ -0,0 +1,133 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional, Tuple, Union
3
+
4
+ from .layers.base import YirgacheffeLayer
5
+ from .layers.group import GroupLayer, TiledGroupLayer
6
+ from .layers.rasters import RasterLayer
7
+ from .layers.vectors import VectorLayer
8
+ from .window import PixelScale
9
+ from .operators import DataType
10
+
11
+ def read_raster(
12
+ filename: Union[Path,str],
13
+ band: int = 1
14
+ ) -> RasterLayer:
15
+ """Open a raster file (e.g., GeoTIFF).
16
+
17
+ Parameters
18
+ ----------
19
+ filename : Path
20
+ Path of raster file to open.
21
+ band : int, default=1
22
+ For multi-band rasters, which band to use (defaults to first if not specified).
23
+
24
+ Returns
25
+ -------
26
+ RasterLayer
27
+ Returns an layer representing the raster data.
28
+ """
29
+ return RasterLayer.layer_from_file(filename, band)
30
+
31
+ def read_rasters(
32
+ filenames : Union[List[Path],List[str]],
33
+ tiled: bool=False
34
+ ) -> GroupLayer:
35
+ """Open a set of raster files (e.g., GeoTIFFs) as a single layer.
36
+
37
+ Parameters
38
+ ----------
39
+ filenames : List[Path]
40
+ List of paths of raster files to open.
41
+ tiled : bool, default=False
42
+ If you know that the rasters for a regular tileset, then setting this flag allows
43
+ Yirgacheffe to perform certain optimisations that significantly improve performance for
44
+ this use case.
45
+
46
+ Returns
47
+ -------
48
+ GroupLayer
49
+ Returns an layer representing the raster data.
50
+ """
51
+ if not tiled:
52
+ return GroupLayer.layer_from_files(filenames)
53
+ else:
54
+ return TiledGroupLayer.layer_from_files(filenames)
55
+
56
+ def read_shape(
57
+ filename: Union[Path,str],
58
+ scale: Union[PixelScale, Tuple[float,float]],
59
+ projection: str,
60
+ where_filter: Optional[str] = None,
61
+ datatype: Optional[DataType] = None,
62
+ burn_value: Union[int,float,str] = 1,
63
+ ) -> VectorLayer:
64
+ """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
65
+
66
+ Parameters
67
+ ----------
68
+ filename : Path
69
+ Path of raster file to open.
70
+ scale: PixelScale or tuple of float
71
+ The dimensions of each pixel.
72
+ projection: str
73
+ The map projection to use
74
+ where_filter : str, optional
75
+ For use with files with many entries (e.g., GPKG), applies this filter to the data.
76
+ datatype: DataType, default=DataType.Byte
77
+ Specify the data type of the raster data generated.
78
+ burn_value: int or float or str, default=1
79
+ The value of each pixel in the polygon.
80
+
81
+ Returns
82
+ -------
83
+ VectorLayer
84
+ Returns an layer representing the vector data.
85
+ """
86
+
87
+ if not isinstance(scale, PixelScale):
88
+ scale = PixelScale(scale[0], scale[1])
89
+
90
+ return VectorLayer.layer_from_file(
91
+ filename,
92
+ where_filter,
93
+ scale,
94
+ projection,
95
+ datatype,
96
+ burn_value
97
+ )
98
+
99
+ def read_shape_like(
100
+ filename: Union[Path,str],
101
+ like: YirgacheffeLayer,
102
+ where_filter: Optional[str] = None,
103
+ datatype: Optional[DataType] = None,
104
+ burn_value: Union[int,float,str] = 1,
105
+ ) -> VectorLayer:
106
+ """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
107
+
108
+ Parameters
109
+ ----------
110
+ filename : Path
111
+ Path of raster file to open.
112
+ like: YirgacheffeLayer
113
+ Another layer that has a projection and pixel scale set. This layer will
114
+ use the same projection and pixel scale as that one.
115
+ where_filter : str, optional
116
+ For use with files with many entries (e.g., GPKG), applies this filter to the data.
117
+ datatype: DataType, default=DataType.Byte
118
+ Specify the data type of the raster data generated.
119
+ burn_value: int or float or str, default=1
120
+ The value of each pixel in the polygon.
121
+
122
+ Returns
123
+ -------
124
+ VectorLayer
125
+ Returns an layer representing the vector data.
126
+ """
127
+ return VectorLayer.layer_from_file_like(
128
+ filename,
129
+ like,
130
+ where_filter,
131
+ datatype,
132
+ burn_value,
133
+ )
yirgacheffe/constants.py CHANGED
@@ -1,2 +1,8 @@
1
1
  YSTEP = 512
2
2
  MINIMUM_CHUNKS_PER_THREAD = 1
3
+
4
+ # I don't really want this here, but it's just too useful having it exposed
5
+ WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
6
+ 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
7
+ 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
8
+ 'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
@@ -1,5 +1,5 @@
1
-
2
- from typing import Any, List, Optional, Tuple
1
+ from __future__ import annotations
2
+ from typing import Any, Optional, Sequence, Tuple
3
3
 
4
4
  from ..operators import DataType, LayerMathMixin
5
5
  from ..rounding import almost_equal, are_pixel_scales_equal_enough, round_up_pixels, round_down_pixels
@@ -67,7 +67,7 @@ class YirgacheffeLayer(LayerMathMixin):
67
67
  return self._window
68
68
 
69
69
  @staticmethod
70
- def find_intersection(layers: List) -> Area:
70
+ def find_intersection(layers: Sequence[YirgacheffeLayer]) -> Area:
71
71
  if not layers:
72
72
  raise ValueError("Expected list of layers")
73
73
 
@@ -87,7 +87,7 @@ class YirgacheffeLayer(LayerMathMixin):
87
87
  return intersection
88
88
 
89
89
  @staticmethod
90
- def find_union(layers: List) -> Area:
90
+ def find_union(layers: Sequence[YirgacheffeLayer]) -> Area:
91
91
  if not layers:
92
92
  raise ValueError("Expected list of layers")
93
93
 
@@ -183,7 +183,7 @@ class YirgacheffeLayer(LayerMathMixin):
183
183
  self._window = new_window
184
184
  self._active_area = new_area
185
185
 
186
- def reset_window(self):
186
+ def reset_window(self) -> None:
187
187
  self._active_area = self._underlying_area
188
188
  if self._pixel_scale:
189
189
  abs_xstep, abs_ystep = abs(self._pixel_scale.xstep), abs(self._pixel_scale.ystep)
@@ -194,7 +194,7 @@ class YirgacheffeLayer(LayerMathMixin):
194
194
  ysize=round_up_pixels((self.area.bottom - self.area.top) / self._pixel_scale.ystep, abs_ystep),
195
195
  )
196
196
 
197
- def offset_window_by_pixels(self, offset: int):
197
+ def offset_window_by_pixels(self, offset: int) -> None:
198
198
  """Grows (if pixels is positive) or shrinks (if pixels is negative) the window for the layer."""
199
199
  if offset == 0:
200
200
  return
@@ -4,19 +4,14 @@ from ..operators import DataType
4
4
  from ..window import Area, PixelScale, Window
5
5
  from .base import YirgacheffeLayer
6
6
  from .._backends import backend
7
- from .. import WGS_84_PROJECTION
7
+ from ..constants import WGS_84_PROJECTION
8
8
 
9
9
 
10
10
  class ConstantLayer(YirgacheffeLayer):
11
11
  """This is a layer that will return the identity value - can be used when an input layer is
12
12
  missing (e.g., area) without having the calculation full of branches."""
13
13
  def __init__(self, value: Union[int,float]): # pylint: disable=W0231
14
- area = Area(
15
- left = -180.0,
16
- top = 90.0,
17
- right = 180.0,
18
- bottom = -90.0
19
- )
14
+ area = Area.world()
20
15
  super().__init__(area, None, WGS_84_PROJECTION)
21
16
  self.value = float(value)
22
17
 
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import copy
3
- import glob
4
- import os
5
- from typing import Any, List, Optional
3
+ from pathlib import Path
4
+ from typing import Any, List, Optional, Union
6
5
 
7
6
  import numpy as np
8
7
  from yirgacheffe.operators import DataType
@@ -23,19 +22,25 @@ class GroupLayer(YirgacheffeLayer):
23
22
  @classmethod
24
23
  def layer_from_directory(
25
24
  cls,
26
- directory_path: str,
25
+ directory_path: Union[Path,str],
27
26
  name: Optional[str] = None,
28
27
  matching: str = "*.tif"
29
28
  ) -> GroupLayer:
30
29
  if directory_path is None:
31
30
  raise ValueError("Directory path is None")
32
- files = [os.path.join(directory_path, x) for x in glob.glob(matching, root_dir=directory_path)]
31
+ if isinstance(directory_path, str):
32
+ directory_path = Path(directory_path)
33
+ files = list(directory_path.glob(matching))
33
34
  if len(files) < 1:
34
35
  raise GroupLayerEmpty(directory_path)
35
36
  return cls.layer_from_files(files, name)
36
37
 
37
38
  @classmethod
38
- def layer_from_files(cls, filenames: List[str], name: Optional[str] = None) -> GroupLayer:
39
+ def layer_from_files(
40
+ cls,
41
+ filenames: Union[List[Path],List[str]],
42
+ name: Optional[str] = None
43
+ ) -> GroupLayer:
39
44
  if filenames is None:
40
45
  raise ValueError("filenames argument is None")
41
46
  if len(filenames) < 1:
@@ -43,7 +48,11 @@ class GroupLayer(YirgacheffeLayer):
43
48
  rasters: List[YirgacheffeLayer] = [RasterLayer.layer_from_file(x) for x in filenames]
44
49
  return cls(rasters, name)
45
50
 
46
- def __init__(self, layers: List[YirgacheffeLayer], name: Optional[str] = None) -> None:
51
+ def __init__(
52
+ self,
53
+ layers: List[YirgacheffeLayer],
54
+ name: Optional[str] = None
55
+ ) -> None:
47
56
  if not layers:
48
57
  raise GroupLayerEmpty("Expected one or more layers")
49
58
  if not are_pixel_scales_equal_enough([x.pixel_scale for x in layers]):
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
  import math
3
- import os
3
+ from pathlib import Path
4
4
  from typing import Any, Optional, Tuple, Union
5
5
 
6
6
  import numpy as np
7
7
  from osgeo import gdal
8
8
 
9
- from .. import WGS_84_PROJECTION
9
+ from ..constants import WGS_84_PROJECTION
10
10
  from ..window import Area, PixelScale, Window
11
11
  from ..rounding import round_up_pixels
12
12
  from .base import YirgacheffeLayer
@@ -88,7 +88,7 @@ class RasterLayer(YirgacheffeLayer):
88
88
  @staticmethod
89
89
  def empty_raster_layer_like(
90
90
  layer: Any,
91
- filename: Optional[str]=None,
91
+ filename: Optional[Union[Path,str]]=None,
92
92
  area: Optional[Area]=None,
93
93
  datatype: Optional[Union[int, DataType]]=None,
94
94
  compress: bool=True,
@@ -214,7 +214,7 @@ class RasterLayer(YirgacheffeLayer):
214
214
  return RasterLayer(dataset)
215
215
 
216
216
  @classmethod
217
- def layer_from_file(cls, filename: str, band: int = 1) -> RasterLayer:
217
+ def layer_from_file(cls, filename: Union[Path,str], band: int = 1) -> RasterLayer:
218
218
  try:
219
219
  dataset = gdal.Open(filename, gdal.GA_ReadOnly)
220
220
  except RuntimeError as exc:
@@ -224,7 +224,7 @@ class RasterLayer(YirgacheffeLayer):
224
224
  _ = dataset.GetRasterBand(band)
225
225
  except RuntimeError as exc:
226
226
  raise InvalidRasterBand(band) from exc
227
- return cls(dataset, filename, band)
227
+ return cls(dataset, str(filename), band)
228
228
 
229
229
  def __init__(self, dataset: gdal.Dataset, name: Optional[str] = None, band: int = 1):
230
230
  if not dataset:
@@ -252,7 +252,7 @@ class RasterLayer(YirgacheffeLayer):
252
252
  assert self.window == Window(0, 0, dataset.RasterXSize, dataset.RasterYSize)
253
253
 
254
254
  self._dataset = dataset
255
- self._dataset_path = dataset.GetDescription()
255
+ self._dataset_path = Path(dataset.GetDescription())
256
256
  self._band = band
257
257
  self._raster_xsize = dataset.RasterXSize
258
258
  self._raster_ysize = dataset.RasterYSize
@@ -275,7 +275,7 @@ class RasterLayer(YirgacheffeLayer):
275
275
 
276
276
  def __getstate__(self) -> object:
277
277
  # Only support pickling on file backed layers (ideally read only ones...)
278
- if not os.path.isfile(self._dataset_path):
278
+ if not self._dataset_path.exists():
279
279
  raise ValueError("Can not pickle layer that is not file backed.")
280
280
  odict = self.__dict__.copy()
281
281
  self._park()
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
  from math import floor, ceil
3
- from typing import Any, Optional
3
+ from pathlib import Path
4
+ from typing import Any, Optional, Union
4
5
 
5
6
  from skimage import transform
6
7
  from yirgacheffe.operators import DataType
@@ -17,7 +18,7 @@ class RescaledRasterLayer(YirgacheffeLayer):
17
18
  @classmethod
18
19
  def layer_from_file(
19
20
  cls,
20
- filename: str,
21
+ filename: Union[Path,str],
21
22
  pixel_scale: PixelScale,
22
23
  band: int = 1,
23
24
  nearest_neighbour: bool = True,
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
- import os
3
2
  from math import ceil, floor
3
+ from pathlib import Path
4
4
  from typing import Any, Optional, Tuple, Union
5
5
  from typing_extensions import NotRequired
6
6
 
@@ -62,7 +62,7 @@ class RasteredVectorLayer(RasterLayer):
62
62
  @classmethod
63
63
  def layer_from_file( # type: ignore[override] # pylint: disable=W0221
64
64
  cls,
65
- filename: str,
65
+ filename: Union[Path,str],
66
66
  where_filter: Optional[str],
67
67
  scale: PixelScale,
68
68
  projection: str,
@@ -172,7 +172,7 @@ class VectorLayer(YirgacheffeLayer):
172
172
  @classmethod
173
173
  def layer_from_file_like(
174
174
  cls,
175
- filename: str,
175
+ filename: Union[Path,str],
176
176
  other_layer: YirgacheffeLayer,
177
177
  where_filter: Optional[str]=None,
178
178
  datatype: Optional[Union[int, DataType]] = None,
@@ -198,7 +198,7 @@ class VectorLayer(YirgacheffeLayer):
198
198
  layer,
199
199
  other_layer.pixel_scale,
200
200
  other_layer.projection,
201
- name=filename,
201
+ name=str(filename),
202
202
  datatype=datatype if datatype is not None else other_layer.datatype,
203
203
  burn_value=burn_value,
204
204
  anchor=(other_layer.area.left, other_layer.area.top),
@@ -208,14 +208,14 @@ class VectorLayer(YirgacheffeLayer):
208
208
  # a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
209
209
  # the original object being around
210
210
  vector_layer._original = vectors
211
- vector_layer._dataset_path = filename
211
+ vector_layer._dataset_path = filename if isinstance(filename, Path) else Path(filename)
212
212
  vector_layer._filter = where_filter
213
213
  return vector_layer
214
214
 
215
215
  @classmethod
216
216
  def layer_from_file(
217
217
  cls,
218
- filename: str,
218
+ filename: Union[Path,str],
219
219
  where_filter: Optional[str],
220
220
  scale: PixelScale,
221
221
  projection: str,
@@ -223,9 +223,11 @@ class VectorLayer(YirgacheffeLayer):
223
223
  burn_value: Union[int,float,str] = 1,
224
224
  anchor: Tuple[float,float] = (0.0, 0.0)
225
225
  ) -> VectorLayer:
226
- vectors = ogr.Open(filename)
227
- if vectors is None:
228
- raise FileNotFoundError(filename)
226
+ try:
227
+ vectors = ogr.Open(filename)
228
+ except RuntimeError as exc:
229
+ # With exceptions on GDAL now returns the wrong (IMHO) exception
230
+ raise FileNotFoundError(filename) from exc
229
231
  layer = vectors.GetLayer()
230
232
  if where_filter is not None:
231
233
  layer.SetAttributeFilter(where_filter)
@@ -243,7 +245,7 @@ class VectorLayer(YirgacheffeLayer):
243
245
  layer,
244
246
  scale,
245
247
  projection,
246
- name=filename,
248
+ name=str(filename),
247
249
  datatype=datatype_arg,
248
250
  burn_value=burn_value,
249
251
  anchor=anchor
@@ -253,7 +255,7 @@ class VectorLayer(YirgacheffeLayer):
253
255
  # a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
254
256
  # the original object being around
255
257
  vector_layer._original = vectors
256
- vector_layer._dataset_path = filename
258
+ vector_layer._dataset_path = filename if isinstance(filename, Path) else Path(filename)
257
259
  vector_layer._filter = where_filter
258
260
  return vector_layer
259
261
 
@@ -282,7 +284,7 @@ class VectorLayer(YirgacheffeLayer):
282
284
  self.burn_value = burn_value
283
285
 
284
286
  self._original = None
285
- self._dataset_path: Optional[str] = None
287
+ self._dataset_path: Optional[Path] = None
286
288
  self._filter: Optional[str] = None
287
289
 
288
290
  # work out region for mask
@@ -323,7 +325,7 @@ class VectorLayer(YirgacheffeLayer):
323
325
 
324
326
  def __getstate__(self) -> object:
325
327
  # Only support pickling on file backed layers (ideally read only ones...)
326
- if self._dataset_path is None or not os.path.isfile(self._dataset_path):
328
+ if self._dataset_path is None or not self._dataset_path.exists():
327
329
  raise ValueError("Can not pickle layer that is not file backed.")
328
330
  odict = self.__dict__.copy()
329
331
  del odict['_original']
yirgacheffe/operators.py CHANGED
@@ -1,15 +1,19 @@
1
1
  import logging
2
2
  import math
3
3
  import multiprocessing
4
+ import os
4
5
  import sys
6
+ import tempfile
5
7
  import time
6
8
  import types
7
9
  from enum import Enum
8
10
  from multiprocessing import Semaphore, Process
9
11
  from multiprocessing.managers import SharedMemoryManager
10
- from typing import Optional
12
+ from pathlib import Path
13
+ from typing import Callable, Dict, Optional, Union
11
14
 
12
15
  import numpy as np
16
+ import numpy.typing as npt
13
17
  from osgeo import gdal
14
18
  from dill import dumps, loads # type: ignore
15
19
 
@@ -40,6 +44,9 @@ class LayerConstant:
40
44
  def _eval(self, _area, _index, _step, _target_window):
41
45
  return self.val
42
46
 
47
+ @property
48
+ def area(self):
49
+ return Area.world()
43
50
 
44
51
  class LayerMathMixin:
45
52
 
@@ -346,25 +353,19 @@ class LayerOperation(LayerMathMixin):
346
353
  self.__dict__.update(state)
347
354
 
348
355
  @property
349
- def area(self) -> Optional[Area]:
356
+ def area(self) -> Area:
350
357
  # The type().__name__ here is to avoid a circular import dependancy
351
- lhs_area = self.lhs.area if not type(self.lhs).__name__ == "ConstantLayer" else None
358
+ lhs_area = self.lhs.area
352
359
  try:
353
- rhs_area = self.rhs.area if not type(self.rhs).__name__ == "ConstantLayer" else None # type: ignore[return-value] # pylint: disable=C0301
360
+ rhs_area = self.rhs.area
354
361
  except AttributeError:
355
362
  rhs_area = None
356
363
  try:
357
- other_area = self.other.area if not type(self.other).__name__ == "ConstantLayer" else None # type: ignore[return-value] # pylint: disable=C0301
364
+ other_area = self.other.area
358
365
  except AttributeError:
359
366
  other_area = None
360
367
 
361
- all_areas = []
362
- if lhs_area is not None:
363
- all_areas.append(lhs_area)
364
- if rhs_area is not None:
365
- all_areas.append(rhs_area)
366
- if other_area is not None:
367
- all_areas.append(other_area)
368
+ all_areas = [x for x in [lhs_area, rhs_area, other_area] if (x is not None) and (not x.is_world)]
368
369
 
369
370
  match self.window_op:
370
371
  case WindowOperation.NONE:
@@ -453,7 +454,7 @@ class LayerOperation(LayerMathMixin):
453
454
  return lhs_data
454
455
 
455
456
  try:
456
- operator = backend.operator_map[self.operator]
457
+ operator: Callable = backend.operator_map[self.operator]
457
458
  except KeyError:
458
459
  # Handles things like `numpy_apply` where a custom operator is provided
459
460
  operator = self.operator
@@ -511,7 +512,7 @@ class LayerOperation(LayerMathMixin):
511
512
  res = chunk_max
512
513
  return res
513
514
 
514
- def save(self, destination_layer, and_sum=False, callback=None, band=1):
515
+ def save(self, destination_layer, and_sum=False, callback=None, band=1) -> Optional[float]:
515
516
  """
516
517
  Calling save will write the output of the operation to the provied layer.
517
518
  If you provide sum as true it will additionall compute the sum and return that.
@@ -608,7 +609,14 @@ class LayerOperation(LayerMathMixin):
608
609
  except AttributeError:
609
610
  pass
610
611
 
611
- def _parallel_save(self, destination_layer, and_sum=False, callback=None, parallelism=None, band=1):
612
+ def _parallel_save(
613
+ self,
614
+ destination_layer,
615
+ and_sum=False,
616
+ callback=None,
617
+ parallelism=None,
618
+ band=1
619
+ ) -> Optional[float]:
612
620
  assert (destination_layer is not None) or and_sum
613
621
  try:
614
622
  computation_window = self.window
@@ -648,7 +656,7 @@ class LayerOperation(LayerMathMixin):
648
656
  or (computation_window.ysize != destination_window.ysize):
649
657
  raise ValueError("Destination raster window size does not match input raster window size.")
650
658
 
651
- np_dtype = {
659
+ np_type_map : Dict[int, np.dtype] = {
652
660
  gdal.GDT_Byte: np.dtype('byte'),
653
661
  gdal.GDT_Float32: np.dtype('float32'),
654
662
  gdal.GDT_Float64: np.dtype('float64'),
@@ -659,7 +667,8 @@ class LayerOperation(LayerMathMixin):
659
667
  gdal.GDT_UInt16: np.dtype('uint16'),
660
668
  gdal.GDT_UInt32: np.dtype('uint32'),
661
669
  gdal.GDT_UInt64: np.dtype('uint64'),
662
- }[band.DataType]
670
+ }
671
+ np_dtype = np_type_map[band.DataType]
663
672
  else:
664
673
  band = None
665
674
  np_dtype = np.dtype('float64')
@@ -674,9 +683,13 @@ class LayerOperation(LayerMathMixin):
674
683
  with SharedMemoryManager() as smm:
675
684
 
676
685
  mem_sem_cast = []
677
- for i in range(worker_count):
686
+ for _ in range(worker_count):
678
687
  shared_buf = smm.SharedMemory(size=np_dtype.itemsize * self.ystep * computation_window.xsize)
679
- cast_buf = np.ndarray((self.ystep, computation_window.xsize), dtype=np_dtype, buffer=shared_buf.buf)
688
+ cast_buf : npt.NDArray = np.ndarray(
689
+ (self.ystep, computation_window.xsize),
690
+ dtype=np_dtype,
691
+ buffer=shared_buf.buf
692
+ )
680
693
  cast_buf[:] = np.zeros((self.ystep, computation_window.xsize), np_dtype)
681
694
  mem_sem_cast.append((shared_buf, Semaphore(), cast_buf))
682
695
 
@@ -746,7 +759,14 @@ class LayerOperation(LayerMathMixin):
746
759
 
747
760
  return total if and_sum else None
748
761
 
749
- def parallel_save(self, destination_layer, and_sum=False, callback=None, parallelism=None, band=1):
762
+ def parallel_save(
763
+ self,
764
+ destination_layer,
765
+ and_sum=False,
766
+ callback=None,
767
+ parallelism=None,
768
+ band=1
769
+ ) -> Optional[float]:
750
770
  if destination_layer is None:
751
771
  raise ValueError("Layer is required")
752
772
  return self._parallel_save(destination_layer, and_sum, callback, parallelism, band)
@@ -754,6 +774,50 @@ class LayerOperation(LayerMathMixin):
754
774
  def parallel_sum(self, callback=None, parallelism=None, band=1):
755
775
  return self._parallel_save(None, True, callback, parallelism, band)
756
776
 
777
+ def to_geotiff(
778
+ self,
779
+ filename: Union[Path,str],
780
+ and_sum: bool = False,
781
+ parallelism:Optional[int]=None
782
+ ) -> Optional[float]:
783
+ """Saves a calculation to a raster file, optionally also returning the sum of pixels.
784
+
785
+ Parameters
786
+ ----------
787
+ filename : Path
788
+ Path of the raster to save the result to.
789
+ and_sum : bool, default=False
790
+ If true then the function will also calculate the sum of the raster as it goes and return that value.
791
+ parallelism : int, optional, default=None
792
+ If passed, attempt to use multiple CPU cores up to the number provided.
793
+
794
+ Returns
795
+ -------
796
+ float, optional
797
+ Either returns None, or the sum of the pixels in the resulting raster if `and_sum` was specified.
798
+ """
799
+
800
+ # We want to write to a tempfile before we move the result into place, but we can't use
801
+ # the actual $TMPDIR as that might be on a different device, and so we use a file next to where
802
+ # the final file will be, so we just need to rename the file at the end, not move it.
803
+ if isinstance(filename, str):
804
+ filename = Path(filename)
805
+ target_dir = filename.parent
806
+
807
+ with tempfile.NamedTemporaryFile(dir=target_dir, delete=False) as tempory_file:
808
+ # Local import due to circular dependancy
809
+ from yirgacheffe.layers.rasters import RasterLayer # type: ignore # pylint: disable=C0415
810
+ with RasterLayer.empty_raster_layer_like(self, filename=tempory_file.name) as layer:
811
+ if parallelism is None:
812
+ result = self.save(layer, and_sum=and_sum)
813
+ else:
814
+ result = self.parallel_save(layer, and_sum=and_sum, parallelism=parallelism)
815
+
816
+ os.makedirs(target_dir, exist_ok=True)
817
+ os.rename(src=tempory_file.name, dst=filename)
818
+
819
+ return result
820
+
757
821
  class ShaderStyleOperation(LayerOperation):
758
822
 
759
823
  def _eval(self, area, index, step, target_window=None):
yirgacheffe/window.py CHANGED
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import math
2
3
  import sys
3
4
  from collections import namedtuple
@@ -37,6 +38,17 @@ class Area:
37
38
  right: float
38
39
  bottom: float
39
40
 
41
+ @staticmethod
42
+ def world() -> Area:
43
+ """Creates an area that covers the entire planet.
44
+
45
+ Returns
46
+ -------
47
+ Area
48
+ An area where the extents are nan, but is_world returns true.
49
+ """
50
+ return Area(float("nan"), float("nan"), float("nan"), float("nan"))
51
+
40
52
  def __hash__(self):
41
53
  return (self.left, self.top, self.right, self.bottom).__hash__()
42
54
 
@@ -69,6 +81,17 @@ class Area:
69
81
  bottom=self.bottom - offset
70
82
  )
71
83
 
84
+ @property
85
+ def is_world(self) -> bool:
86
+ """Returns true if this is a global area, independant of projection.
87
+
88
+ Returns
89
+ -------
90
+ bool
91
+ True is the Area was created with `world` otherwise False.
92
+ """
93
+ return math.isnan(self.left)
94
+
72
95
  def overlaps(self, other) -> bool:
73
96
  """Check if this area overlaps with another area.
74
97
 
@@ -82,6 +105,10 @@ class Area:
82
105
  bool
83
106
  True if the two areas intersect, otherwise false.
84
107
  """
108
+
109
+ if self.is_world or other.is_world:
110
+ return True
111
+
85
112
  return (
86
113
  (self.left <= other.left <= self.right) or
87
114
  (self.left <= other.right <= self.right) or
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Abstraction of gdal datasets for doing basic math operations
5
5
  Author-email: Michael Dales <mwd24@cam.ac.uk>
6
6
  License-Expression: ISC
@@ -13,6 +13,7 @@ Requires-Dist: numpy
13
13
  Requires-Dist: gdal[numpy]
14
14
  Requires-Dist: scikit-image
15
15
  Requires-Dist: torch
16
+ Requires-Dist: dill
16
17
  Provides-Extra: dev
17
18
  Requires-Dist: mypy; extra == "dev"
18
19
  Requires-Dist: pylint; extra == "dev"
@@ -52,12 +53,12 @@ The motivation for Yirgacheffe layers is to make working with gdal data slightly
52
53
  For example, if we wanted to do a simple [Area of Habitat](https://github.com/quantifyearth/aoh-calculator/) calculation, whereby we find the pixels where a species resides by combining its range polygon, its habitat preferences, and its elevation preferences, the code would be like this:
53
54
 
54
55
  ```python
55
- from yirgacheffe.layer import RasterLayer, VectorLayer
56
+ import yirgaceffe as yg
56
57
 
57
- habitat_map = RasterLayer.layer_from_file("habitats.tif")
58
- elevation_map = RasterLayer.layer_from_file('elevation.tif')
59
- range_polygon = VectorLayer.layer_from_file('species123.geojson', raster_like=habitat_map)
60
- area_per_pixel_map = RasterLayer.layer_from_file('area_per_pixel.tif')
58
+ habitat_map = yg.read_raster("habitats.tif")
59
+ elevation_map = yg.read_raster('elevation.tif')
60
+ range_polygon = yg.read_shape_like('species123.geojson', like=habitat_map)
61
+ area_per_pixel_map = yg.read_raster('area_per_pixel.tif')
61
62
 
62
63
  refined_habitat = habitat_map.isin([...species habitat codes...])
63
64
  refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
@@ -70,8 +71,8 @@ print(f'area for species 123: {aoh.sum()}')
70
71
  Similarly, you could save the result to a new raster layer:
71
72
 
72
73
  ```python
73
- with RasterLayer.empty_raster_layer_like(aoh, filename="result.tif") as result:
74
- aoh.save(result)
74
+ ...
75
+ aoh.to_geotiff("result.tif")
75
76
  ```
76
77
 
77
78
  Yirgacheffe will automatically infer if you want to do an intersection of maps or a union of the maps based on the operators you use (see below for a full table). You can explicitly override that if you want.
@@ -109,27 +110,28 @@ If you have set either the intersection window or union window on a layer and yo
109
110
  If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
110
111
 
111
112
 
112
- ### Todo but not supported
113
-
114
- Yirgacheffe is work in progress, so things planned but not supported currently:
115
-
116
- * Dynamic pixel scale adjustment - all raster layers must be provided at the same pixel scale currently *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
117
- * A fold operation
118
- * CUDA/Metal support via CUPY/MLX
119
- * Dispatching work across multiple CPUs *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
120
-
121
-
122
-
123
113
  ## Layer types
124
114
 
115
+ Note that as part of the move to the next major release, 2.0, we are adding simpler ways to create layers. Not all of those have been implemented yet, which is why this section has some inconsistencies. However, given many of the common cases are already covered, we present the new 2.0 style methods (`read_raster` and similar) here so you can write cleaner code today rather than making people wait for the final 2.0 release.
116
+
125
117
  ### RasterLayer
126
118
 
127
119
  This is your basic GDAL raster layer, which you load from a geotiff.
128
120
 
129
121
  ```python
122
+ from yirgaceffe.layers import RasterLayer
123
+
130
124
  with RasterLayer.layer_from_file('test1.tif') as layer:
131
- data = layer.read_array(0, 0, 10, 10)
132
- ...
125
+ total = layer.sum()
126
+ ```
127
+
128
+ The new 2.0 way of doing this is:
129
+
130
+ ```python
131
+ import yirgacheffe as yg
132
+
133
+ with yg.read_raster('test.tif') as layer:
134
+ total = layer.sum()
133
135
  ```
134
136
 
135
137
  You can also create empty layers ready for you to store results, either by taking the dimensions from an existing layer. In both these cases you can either provide a filename to which the data will be written, or if you do not provide a filename then the layer will only exist in memory - this will be more efficient if the layer is being used for intermediary results.
@@ -166,51 +168,30 @@ This layer will load vector data and rasterize it on demand as part of a calcula
166
168
  Because it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
167
169
 
168
170
  ```python
169
- with VectorLayer.layer_from_file('range.gpkg', 'id_no == 42', layer1.pixel_scale, layer1.projection) as layer:
170
- ...
171
- ```
172
-
173
- This class was formerly called `DynamicVectorRangeLayer`, a name now deprecated.
174
-
171
+ from yirgaceffe import WGS_84_PROJECTION
172
+ from yirgaceffe.window import PixelScale
173
+ from yirgaceffe.layers import VectorLayer
175
174
 
176
- ### UniformAreaLayer
177
-
178
- In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
179
-
180
- ```python
181
- with UniformAreaLayer('area.tiff') as layer:
182
- ....
175
+ with VectorLayer.layer_from_file('range.gpkg', PixelScale(0.001, -0.001), WGS_84_PROJECTION) as layer:
176
+ ...
183
177
  ```
184
178
 
185
- Note that loading this data can still be very slow, due to how image compression works. So if you plan to use area.tiff more than once, we recommend use save an optimised version - this will do the slow uncompression once and then save a minimal file to speed up future processing:
179
+ The new 2.0 way of doing this is:
186
180
 
187
181
  ```python
188
- if not os.path.exists('yirgacheffe_area.tiff'):
189
- UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
190
- area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
191
- ```
192
-
193
-
194
- ### ConstantLayer
182
+ import yirgacheffe as yg
195
183
 
196
- This is there to simplify code when you have some optional layers. Rather than littering your code with checks, you can just use a constant layer, which can be included in calculations and will just return an fixed value as if it wasn't there. Useful with 0.0 or 1.0 for sum or multiplication null layers.
197
-
198
- ```python
199
- try:
200
- area_layer = UniformAreaLayer('myarea.tiff')
201
- except FileDoesNotExist:
202
- area_layer = ConstantLayer(0.0)
184
+ with yg.read_shape('range.gpkg', (0.001, -0.001), WGS_84_PROJECTION) as layer:
185
+ ...
203
186
  ```
204
187
 
188
+ It is more common that when a shape file is loaded that its pixel size and projection will want to be made to match that of an existing raster (as per the opening area of habitat example). For that there is the following convenience method:
205
189
 
206
- ### H3CellLayer
207
-
208
- If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
209
-
210
- Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
211
190
 
212
191
  ```python
213
- hex_cell_layer = H3CellLayer('88972eac11fffff', layer1.pixel_scale, layer1.projection)
192
+ with yg.read_raster("test.tif") as raster_layer:
193
+ with yg.read_shape_like('range.gpkg', raster_layer) as shape_layer:
194
+ ...
214
195
  ```
215
196
 
216
197
  ### GroupLayer
@@ -239,6 +220,15 @@ with GroupLayer.layer_from_directory('.') as all_tiles:
239
220
  ...
240
221
  ```
241
222
 
223
+ The new 2.0 way of doing this is:
224
+
225
+ ```python
226
+ import yirgacheffe as yg
227
+
228
+ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
229
+ ...
230
+ ```
231
+
242
232
  ### TiledGroupLayer
243
233
 
244
234
  This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
@@ -249,11 +239,59 @@ tile2 = RasterLayer.layer_from_file('tile_N20_E10.tif')
249
239
  all_tiles = TiledGroupLayer([tile1, tile2])
250
240
  ```
251
241
 
242
+ The new 2.0 way of doing this is:
243
+
244
+ ```python
245
+ import yirgacheffe as yg
246
+
247
+ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif'], tiled=True) as all_tiles:
248
+ ...
249
+ ```
250
+
252
251
  Notes:
253
252
 
254
253
  * You can have missing tiles, and these will be filled in with zeros.
255
254
  * You can have tiles that overlap, so long as they still conform to the rule that all tiles are the same size and on a grid.
256
255
 
256
+ ### ConstantLayer
257
+
258
+ This is there to simplify code when you have some optional layers. Rather than littering your code with checks, you can just use a constant layer, which can be included in calculations and will just return an fixed value as if it wasn't there. Useful with 0.0 or 1.0 for sum or multiplication null layers.
259
+
260
+ ```python
261
+ try:
262
+ area_layer = RasterLayer.layer_from_file('myarea.tiff')
263
+ except FileDoesNotExist:
264
+ area_layer = ConstantLayer(0.0)
265
+ ```
266
+
267
+ ### H3CellLayer
268
+
269
+ If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
270
+
271
+ Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
272
+
273
+ ```python
274
+ hex_cell_layer = H3CellLayer('88972eac11fffff', layer1.pixel_scale, layer1.projection)
275
+ ```
276
+
277
+
278
+ ### UniformAreaLayer
279
+
280
+ In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
281
+
282
+ ```python
283
+ with UniformAreaLayer('area.tiff') as layer:
284
+ ....
285
+ ```
286
+
287
+ Note that loading this data can still be very slow, due to how image compression works. So if you plan to use area.tiff more than once, we recommend use save an optimised version - this will do the slow uncompression once and then save a minimal file to speed up future processing:
288
+
289
+ ```python
290
+ if not os.path.exists('yirgacheffe_area.tiff'):
291
+ UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
292
+ area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
293
+ ```
294
+
257
295
 
258
296
  ## Supported operations on layers
259
297
 
@@ -280,6 +318,24 @@ with RasterLayer.layer_from_file('test1.tif') as layer1:
280
318
  calc.save(result)
281
319
  ```
282
320
 
321
+
322
+ The new 2.0 way of doing these are:
323
+
324
+ ```python
325
+ with yg.read_raster('test1.tif') as layer1:
326
+ with yg.read_raster('test2.tif') as layer2:
327
+ result = layer1 + layer2
328
+ result.to_geotiff("result.tif")
329
+ ```
330
+
331
+ or
332
+
333
+ ```python
334
+ with yg.read_raster('test1.tif') as layer1:
335
+ result = layer1 * 42.0
336
+ result.to_geotiff("result.tif")
337
+ ```
338
+
283
339
  ### Boolean testing
284
340
 
285
341
  Testing for equality, less than, less than or equal, greater than, and greater than or equal are supported on layers, along with logical or and logical and, as per this example, where `elevation_upper` and `elevation_lower` are scalar values:
@@ -0,0 +1,25 @@
1
+ yirgacheffe/__init__.py,sha256=n0v998xPOMMS6hdAQ16UNzmJUdnN5612HRtdudwjYa4,302
2
+ yirgacheffe/_core.py,sha256=6g_gqTBj4XdJ8TOiMPos5asLOWwSeKqIhpvYP49RgtI,3916
3
+ yirgacheffe/constants.py,sha256=uCWJwec3-ND-zVxYbsk1sdHKANl3ToNCTPg7MZb0j2g,434
4
+ yirgacheffe/operators.py,sha256=cEgnngjj_fZOg9BileC7WTp6VP9TnmKibR84pDK7A8I,31362
5
+ yirgacheffe/rounding.py,sha256=ggBG4lMyLMtHLW3dBxr3gBCcF2qhRrY5etZiFGlIoqA,2258
6
+ yirgacheffe/window.py,sha256=kR8sHQ6lcixpndOWC18wLRgbgS8y00tq4Fkh8PLESvM,8209
7
+ yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
8
+ yirgacheffe/_backends/enumeration.py,sha256=pADawllxpW_hW-IVVvZpHWIKzvEMs9aaqfkZRD1zjnY,1003
9
+ yirgacheffe/_backends/mlx.py,sha256=2vOTMqHbQbeqt81Eq_8hxWDXZHaPsDpbXkALRVGEnnw,6130
10
+ yirgacheffe/_backends/numpy.py,sha256=cYO628s4-5K_-Bp3CrnHegzYSZfkt2QC8iE9oOOMtvA,4069
11
+ yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
12
+ yirgacheffe/layers/area.py,sha256=yIRXzeeLi3MMyuh4LG_VgZrKNWe5xwZgDGdgaoYRpP0,3805
13
+ yirgacheffe/layers/base.py,sha256=1B3s4cyC-DBeM357U9ds4mqwE23iIiD8x5F4aVEfc_c,12057
14
+ yirgacheffe/layers/constant.py,sha256=c3ugo58jkzImCiEqpzJoGLuBKJPGHRni1kxnKKcZs2E,1328
15
+ yirgacheffe/layers/group.py,sha256=5TICz6bQREzah1MdcEttYbTIOk0zX-KCqyuuVdaz7HU,15472
16
+ yirgacheffe/layers/h3layer.py,sha256=kWDqs6fIkHLR7gDL2E6F5y9-XxT6ws4M3Tj_7qF090U,9863
17
+ yirgacheffe/layers/rasters.py,sha256=nJKgvDpPEbj_7dG55SLGFBqPGMXjkR2oGWIRne0b558,12682
18
+ yirgacheffe/layers/rescaled.py,sha256=W-AhFuTIYAJVgMvcLHAZdrFwUxuJuVnAQWv8d4x1mDM,3048
19
+ yirgacheffe/layers/vectors.py,sha256=Y7zQkbNWYY9iNyIxfE0iN3BeWJt8yPgfSOcWbR1JDKM,15781
20
+ yirgacheffe-1.5.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
21
+ yirgacheffe-1.5.0.dist-info/METADATA,sha256=zjsKlcKx50LdA4D2eIdHw17E4Zl4nH9ldVSS6eS1TPc,21842
22
+ yirgacheffe-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ yirgacheffe-1.5.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
24
+ yirgacheffe-1.5.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
25
+ yirgacheffe-1.5.0.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- yirgacheffe/__init__.py,sha256=nrVZPE_4DgReqmqEOfUaXJiRtxjOt-OJSIVi51Z5k98,587
2
- yirgacheffe/constants.py,sha256=WccPcISG1FqL_Kw1tI72BJGjHy6RvEcGEx_I9RK776U,42
3
- yirgacheffe/operators.py,sha256=eeDlIGFrspHqQBWdcXVjbP2mBEDsPxhXZucWo74geTc,29343
4
- yirgacheffe/rounding.py,sha256=ggBG4lMyLMtHLW3dBxr3gBCcF2qhRrY5etZiFGlIoqA,2258
5
- yirgacheffe/window.py,sha256=PVh9EIg1PMcUENjC2yahmrolGWaJP4OeM44uKqQ6I0U,7504
6
- yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
7
- yirgacheffe/_backends/enumeration.py,sha256=pADawllxpW_hW-IVVvZpHWIKzvEMs9aaqfkZRD1zjnY,1003
8
- yirgacheffe/_backends/mlx.py,sha256=oIWJULZ9NBPTqydnCTQxVwbfmBjQPOsKvY9Rhr6_wl8,6076
9
- yirgacheffe/_backends/numpy.py,sha256=qQYvff1oHIuGUimV04rcFyPxnwEf0acvVd3ijeom4T4,4015
10
- yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
11
- yirgacheffe/layers/area.py,sha256=yIRXzeeLi3MMyuh4LG_VgZrKNWe5xwZgDGdgaoYRpP0,3805
12
- yirgacheffe/layers/base.py,sha256=l2spGsDsv2DaUqkK-nuJCC97fw0kVWaLntvvLqZgJS4,11959
13
- yirgacheffe/layers/constant.py,sha256=LWQuGGuUkJirLoQomZHs44oksmMKcvNGM8pOuRk9vHo,1427
14
- yirgacheffe/layers/group.py,sha256=Fys5BvYmWRQMc95YE8Gy6bxxZRqvHUU7gILFE_X-CpY,15330
15
- yirgacheffe/layers/h3layer.py,sha256=kWDqs6fIkHLR7gDL2E6F5y9-XxT6ws4M3Tj_7qF090U,9863
16
- yirgacheffe/layers/rasters.py,sha256=I9bRuqb0W8EEvdSq3rAeKNmezIRK-yajFaHtDFR81sw,12630
17
- yirgacheffe/layers/rescaled.py,sha256=v3ZXo3aNNDP4b8O6QnQHgrhBu0GslTgiZQmy6O8vb8k,3004
18
- yirgacheffe/layers/vectors.py,sha256=3QWjg0E9deZyLIBp-BewgDGY90Ufqd6OcUA7jzJXJPo,15515
19
- yirgacheffe-1.4.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
20
- yirgacheffe-1.4.0.dist-info/METADATA,sha256=XamBMRPMRWZvzTu4yXqyhYIdlQkJV8mb5qCIAnt06Ls,20496
21
- yirgacheffe-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- yirgacheffe-1.4.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
23
- yirgacheffe-1.4.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
24
- yirgacheffe-1.4.0.dist-info/RECORD,,