yirgacheffe 1.9.4__py3-none-any.whl → 1.10.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
@@ -13,7 +13,7 @@ except ModuleNotFoundError:
13
13
  __version__ = pyproject_data["project"]["version"]
14
14
 
15
15
  from .layers import YirgacheffeLayer
16
- from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster
16
+ from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster, from_array
17
17
  from .constants import WGS_84_PROJECTION
18
18
  from .window import Area, MapProjection, Window
19
19
  from ._backends.enumeration import dtype as DataType
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
 
5
+ import numpy as np
3
6
  from osgeo import gdal
4
7
 
5
8
  class operators(Enum):
@@ -39,6 +42,13 @@ class operators(Enum):
39
42
  ROUND = 34
40
43
  CEIL = 35
41
44
  ISNAN = 36
45
+ RADD = 37
46
+ RSUB = 38
47
+ RMUL = 39
48
+ RTRUEDIV = 40
49
+ RFLOORDIV = 41
50
+ RREMAINDER = 42
51
+ RPOW = 43
42
52
 
43
53
  class dtype(Enum):
44
54
  """Represents the type of data returned by a layer.
@@ -77,5 +87,31 @@ class dtype(Enum):
77
87
  return self.value
78
88
 
79
89
  @classmethod
80
- def of_gdal(cls, val):
90
+ def of_gdal(cls, val: int) -> dtype:
81
91
  return cls(val)
92
+
93
+ @classmethod
94
+ def of_array(cls, val: np.ndarray) -> dtype:
95
+ match val.dtype:
96
+ case np.float32:
97
+ return dtype.Float32
98
+ case np.float64:
99
+ return dtype.Float64
100
+ case np.int8:
101
+ return dtype.Int8
102
+ case np.int16:
103
+ return dtype.Int16
104
+ case np.int32:
105
+ return dtype.Int32
106
+ case np.int64:
107
+ return dtype.Int64
108
+ case np.uint8:
109
+ return dtype.UInt8
110
+ case np.uint16:
111
+ return dtype.UInt16
112
+ case np.uint32:
113
+ return dtype.UInt32
114
+ case np.uint64:
115
+ return dtype.UInt64
116
+ case _:
117
+ raise ValueError
@@ -222,4 +222,11 @@ operator_map: dict[op, Callable] = {
222
222
  op.ROUND: mx.round,
223
223
  op.CEIL: mx.ceil,
224
224
  op.ISNAN: mx.isnan,
225
+ op.RADD: mx.array.__radd__,
226
+ op.RSUB: mx.array.__rsub__,
227
+ op.RMUL: mx.array.__rmul__,
228
+ op.RTRUEDIV: mx.array.__rtruediv__,
229
+ op.RFLOORDIV: mx.array.__rfloordiv__,
230
+ op.RREMAINDER: mx.array.__rmod__,
231
+ op.RPOW: mx.array.__rpow__,
225
232
  }
@@ -160,4 +160,15 @@ operator_map: dict[op, Callable] = {
160
160
  op.ROUND: np.round,
161
161
  op.CEIL: np.ceil,
162
162
  op.ISNAN: np.isnan,
163
+ # Pylint doesn't recognise the reverse operators, despite them
164
+ # clearly existing on np.ndarray, so we have to disable the checks
165
+ # pylint: disable=no-member
166
+ op.RADD: np.ndarray.__radd__,
167
+ op.RSUB: np.ndarray.__rsub__,
168
+ op.RMUL: np.ndarray.__rmul__,
169
+ op.RTRUEDIV: np.ndarray.__rtruediv__,
170
+ op.RFLOORDIV: np.ndarray.__rfloordiv__,
171
+ op.RREMAINDER: np.ndarray.__rmod__,
172
+ op.RPOW: np.ndarray.__rpow__,
173
+ # pylint: enable=no-member
163
174
  }
yirgacheffe/_core.py CHANGED
@@ -3,13 +3,15 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Sequence
5
5
 
6
+ import numpy as np
7
+
6
8
  from .layers.area import UniformAreaLayer
7
9
  from .layers.base import YirgacheffeLayer
8
10
  from .layers.constant import ConstantLayer
9
11
  from .layers.group import GroupLayer, TiledGroupLayer
10
12
  from .layers.rasters import RasterLayer
11
13
  from .layers.vectors import VectorLayer
12
- from .window import MapProjection
14
+ from .window import Area, MapProjection
13
15
  from ._backends.enumeration import dtype as DataType
14
16
 
15
17
  def read_raster(
@@ -161,3 +163,50 @@ def constant(value: int | float) -> YirgacheffeLayer:
161
163
  A constant layer of the provided value.
162
164
  """
163
165
  return ConstantLayer(value)
166
+
167
+ def from_array(
168
+ values: np.ndarray,
169
+ origin: tuple[float, float],
170
+ projection: MapProjection | tuple[str, tuple[float, float]],
171
+ ) -> YirgacheffeLayer:
172
+ """Creates an in-memory layer from a numerical array.
173
+
174
+ Args:
175
+ values: a 2D array of data values, with Y on the first dimension, X on
176
+ the second dimension.
177
+ origin: the position of the top left pixel in the geospatial space
178
+ projection: the map projection and pixel scale to use.
179
+
180
+ Returns:
181
+ A geospatial layer that uses the provided data for its values.
182
+ """
183
+
184
+ if projection is None:
185
+ raise ValueError("Projection must not be none")
186
+
187
+ if not isinstance(projection, MapProjection):
188
+ projection_name, scale_tuple = projection
189
+ projection = MapProjection(projection_name, scale_tuple[0], scale_tuple[1])
190
+
191
+ dims = values.shape
192
+
193
+ area = Area(
194
+ left=origin[0],
195
+ top=origin[1],
196
+ right=origin[0] + (projection.xstep * dims[1]),
197
+ bottom=origin[1] + (projection.ystep * dims[0])
198
+ )
199
+
200
+ layer = RasterLayer.empty_raster_layer(
201
+ area,
202
+ scale=projection.scale,
203
+ datatype=DataType.of_array(values),
204
+ filename=None,
205
+ projection=projection.name,
206
+ )
207
+ assert layer._dataset
208
+ assert layer._dataset.RasterXSize == dims[1]
209
+ assert layer._dataset.RasterYSize == dims[0]
210
+ layer._dataset.GetRasterBand(1).WriteArray(values, 0, 0)
211
+
212
+ return layer
@@ -13,9 +13,11 @@ import types
13
13
  from collections.abc import Callable
14
14
  from contextlib import ExitStack
15
15
  from enum import Enum
16
- from multiprocessing import Semaphore, Process
16
+ from multiprocessing import Process, Semaphore
17
+ from multiprocessing.synchronize import Semaphore as SemaphoreType
17
18
  from multiprocessing.managers import SharedMemoryManager
18
19
  from pathlib import Path
20
+ from typing import Any
19
21
 
20
22
  import deprecation
21
23
  import numpy as np
@@ -24,14 +26,15 @@ from osgeo import gdal
24
26
  from dill import dumps, loads # type: ignore
25
27
  from pyproj import Transformer
26
28
 
27
- from . import constants, __version__
28
- from .rounding import round_up_pixels, round_down_pixels
29
- from .window import Area, PixelScale, MapProjection, Window
30
- from ._backends import backend
31
- from ._backends.enumeration import operators as op
32
- from ._backends.enumeration import dtype as DataType
33
- from ._backends.numpy import dtype_to_backend as dtype_to_numpy
34
- from ._backends.numpy import backend_to_dtype as numpy_to_dtype
29
+ from .. import constants, __version__
30
+ from ..rounding import round_up_pixels, round_down_pixels
31
+ from ..window import Area, PixelScale, MapProjection, Window
32
+ from .._backends import backend
33
+ from .._backends.enumeration import operators as op
34
+ from .._backends.enumeration import dtype as DataType
35
+ from .._backends.numpy import dtype_to_backend as dtype_to_numpy
36
+ from .._backends.numpy import backend_to_dtype as numpy_to_dtype
37
+ from .cse import CSECacheTable
35
38
 
36
39
  logger = logging.getLogger(__name__)
37
40
  logger.setLevel(logging.WARNING)
@@ -50,9 +53,13 @@ class LayerConstant:
50
53
  def __str__(self) -> str:
51
54
  return str(self.val)
52
55
 
53
- def _eval(self, _area, _projection, _index, _step, _target_window):
56
+ def _eval(self, _cse_cache, _area, _projection, _index, _step, _target_window):
54
57
  return self.val
55
58
 
59
+ @property
60
+ def _cse_hash(self) -> int | None:
61
+ return hash(self.val)
62
+
56
63
  @property
57
64
  def datatype(self) -> DataType:
58
65
  numpy_type = np.result_type(self.val)
@@ -70,24 +77,45 @@ class LayerMathMixin:
70
77
  def __add__(self, other):
71
78
  return LayerOperation(self, op.ADD, other, window_op=WindowOperation.UNION)
72
79
 
80
+ def __radd__(self, other):
81
+ return LayerOperation(self, op.RADD, other, window_op=WindowOperation.UNION)
82
+
73
83
  def __sub__(self, other):
74
84
  return LayerOperation(self, op.SUB, other, window_op=WindowOperation.UNION)
75
85
 
86
+ def __rsub__(self, other):
87
+ return LayerOperation(self, op.RSUB, other, window_op=WindowOperation.UNION)
88
+
76
89
  def __mul__(self, other):
77
90
  return LayerOperation(self, op.MUL, other, window_op=WindowOperation.INTERSECTION)
78
91
 
92
+ def __rmul__(self, other):
93
+ return LayerOperation(self, op.RMUL, other, window_op=WindowOperation.INTERSECTION)
94
+
79
95
  def __truediv__(self, other):
80
96
  return LayerOperation(self, op.TRUEDIV, other, window_op=WindowOperation.INTERSECTION)
81
97
 
98
+ def __rtruediv__(self, other):
99
+ return LayerOperation(self, op.RTRUEDIV, other, window_op=WindowOperation.INTERSECTION)
100
+
82
101
  def __floordiv__(self, other):
83
102
  return LayerOperation(self, op.FLOORDIV, other, window_op=WindowOperation.INTERSECTION)
84
103
 
104
+ def __rfloordiv__(self, other):
105
+ return LayerOperation(self, op.RFLOORDIV, other, window_op=WindowOperation.INTERSECTION)
106
+
85
107
  def __mod__(self, other):
86
108
  return LayerOperation(self, op.REMAINDER, other, window_op=WindowOperation.INTERSECTION)
87
109
 
110
+ def __rmod__(self, other):
111
+ return LayerOperation(self, op.RREMAINDER, other, window_op=WindowOperation.INTERSECTION)
112
+
88
113
  def __pow__(self, other):
89
114
  return LayerOperation(self, op.POW, other, window_op=WindowOperation.UNION)
90
115
 
116
+ def __rpow__(self, other):
117
+ return LayerOperation(self, op.RPOW, other, window_op=WindowOperation.UNION)
118
+
91
119
  def __eq__(self, other):
92
120
  return LayerOperation(self, op.EQ, other, window_op=WindowOperation.INTERSECTION)
93
121
 
@@ -114,17 +142,22 @@ class LayerMathMixin:
114
142
 
115
143
  def _eval(
116
144
  self,
117
- area,
118
- projection,
119
- index,
120
- step,
121
- target_window=None
145
+ cse_cache: CSECacheTable,
146
+ area: Area,
147
+ projection: MapProjection,
148
+ index: int,
149
+ step: int,
150
+ target_window: Window,
122
151
  ):
152
+ cache_data = cse_cache.get_data(self._cse_hash, target_window)
153
+ if cache_data is not None:
154
+ return cache_data
155
+
123
156
  try:
124
157
  window = self.window if target_window is None else target_window
125
- return self._read_array_for_area(area, projection, 0, index, window.xsize, step)
158
+ result = self._read_array_for_area(area, projection, 0, index, window.xsize, step)
126
159
  except AttributeError:
127
- return self._read_array_for_area(
160
+ result = self._read_array_for_area(
128
161
  area,
129
162
  projection,
130
163
  0,
@@ -133,6 +166,9 @@ class LayerMathMixin:
133
166
  step
134
167
  )
135
168
 
169
+ cse_cache.set_data(self._cse_hash, target_window, result)
170
+ return result
171
+
136
172
  def nan_to_num(self, nan=0, posinf=None, neginf=None):
137
173
  return LayerOperation(
138
174
  self,
@@ -149,7 +185,7 @@ class LayerMathMixin:
149
185
  self,
150
186
  op.ISIN,
151
187
  window_op=WindowOperation.NONE,
152
- test_elements=test_elements,
188
+ test_elements=tuple(test_elements),
153
189
  )
154
190
 
155
191
  def isnan(self):
@@ -252,10 +288,10 @@ class LayerMathMixin:
252
288
  weights=weights.astype(np.float32),
253
289
  )
254
290
 
255
- def numpy_apply(self, func, other=None):
291
+ def numpy_apply(self, func: Callable, other=None):
256
292
  return LayerOperation(self, func, other)
257
293
 
258
- def shader_apply(self, func, other=None):
294
+ def shader_apply(self, func: Callable, other=None):
259
295
  return ShaderStyleOperation(self, func, other)
260
296
 
261
297
  def save(self, destination_layer, and_sum=False, callback=None, band=1):
@@ -347,9 +383,16 @@ class LayerMathMixin:
347
383
  def area(self) -> Area:
348
384
  raise NotImplementedError("Must be overridden by subclass")
349
385
 
386
+ @property
387
+ def _cse_hash(self) -> int | None:
388
+ raise NotImplementedError("Must be overridden by subclass")
389
+
350
390
  def read_array(self, _x, _y, _w, _h):
351
391
  raise NotImplementedError("Must be overridden by subclass")
352
392
 
393
+ def _read_array_for_area(self, _target_area, _target_projection, _x, _y, _w, _h):
394
+ raise NotImplementedError("Must be overridden by subclass")
395
+
353
396
  def show(self, ax=None, max_pixels: int | None =1000, **kwargs):
354
397
  """Display data using matplotlib.
355
398
 
@@ -424,12 +467,12 @@ class LayerOperation(LayerMathMixin):
424
467
 
425
468
  def __init__(
426
469
  self,
427
- lhs,
428
- operator=None,
429
- rhs=None,
430
- other=None,
431
- window_op=WindowOperation.NONE,
432
- buffer_padding=0,
470
+ lhs: Any,
471
+ operator: op | Callable | None = None,
472
+ rhs: Any = None,
473
+ other: Any = None,
474
+ window_op: WindowOperation = WindowOperation.NONE,
475
+ buffer_padding: int = 0,
433
476
  **kwargs
434
477
  ):
435
478
  self.ystep = constants.YSTEP
@@ -446,7 +489,7 @@ class LayerOperation(LayerMathMixin):
446
489
 
447
490
  if rhs is not None:
448
491
  if backend.isscalar(rhs):
449
- self.rhs = LayerConstant(rhs)
492
+ self.rhs: Any = LayerConstant(rhs)
450
493
  elif isinstance(rhs, (backend.array_t)):
451
494
  if rhs.shape == ():
452
495
  self.rhs = LayerConstant(rhs.item())
@@ -461,7 +504,7 @@ class LayerOperation(LayerMathMixin):
461
504
 
462
505
  if other is not None:
463
506
  if backend.isscalar(other):
464
- self.other = LayerConstant(other)
507
+ self.other: Any = LayerConstant(other)
465
508
  elif isinstance(other, (backend.array_t)):
466
509
  if other.shape == ():
467
510
  self.rhs = LayerConstant(other.item())
@@ -474,6 +517,13 @@ class LayerOperation(LayerMathMixin):
474
517
  else:
475
518
  self.other = None
476
519
 
520
+ # this is expensive, so don't do it all the time
521
+ self._cse_hash_cache = self._calc_cse_hash()
522
+
523
+ @property
524
+ def _cse_hash(self) -> int | None:
525
+ return self._cse_hash_cache
526
+
477
527
  def __str__(self) -> str:
478
528
  try:
479
529
  return f"({self.lhs} {self.operator} {self.rhs})"
@@ -483,9 +533,6 @@ class LayerOperation(LayerMathMixin):
483
533
  except AttributeError:
484
534
  return str(self.lhs)
485
535
 
486
- def __len__(self) -> int:
487
- return len(self.lhs)
488
-
489
536
  def __getstate__(self) -> object:
490
537
  odict = self.__dict__.copy()
491
538
  if isinstance(self.operator, types.LambdaType):
@@ -499,6 +546,35 @@ class LayerOperation(LayerMathMixin):
499
546
  del state['operator_dill']
500
547
  self.__dict__.update(state)
501
548
 
549
+ @property
550
+ def _children(self) -> list:
551
+ return [x for x in [self.lhs, self.rhs, self.other] if x is not None]
552
+
553
+ def _calc_cse_hash(self) -> int | None:
554
+ # If we can't hash any of the child nodes then we can't hash this node, as if we can't store
555
+ # their results in the cache, we can't know this result is stable
556
+ child_hashes = [x._cse_hash for x in self._children]
557
+ if any(x is None for x in child_hashes):
558
+ return None
559
+
560
+ # This really should be recursive
561
+ def _make_hashable(value):
562
+ if isinstance(value, (list, tuple, set)):
563
+ return tuple(value)
564
+ if isinstance(value, np.ndarray):
565
+ return id(value)
566
+ else:
567
+ return value
568
+ frozen_kwargs = tuple(sorted((k, _make_hashable(v)) for (k, v) in self.kwargs.items()))
569
+
570
+ terms = [self.operator, self.window_op, frozen_kwargs, self.buffer_padding] + child_hashes
571
+
572
+ try:
573
+ return hash(tuple(terms))
574
+ except TypeError:
575
+ # This is assumed to be because kwargs contains something unhashable
576
+ return None
577
+
502
578
  @property
503
579
  def area(self) -> Area:
504
580
  return self._get_operation_area(self.map_projection)
@@ -546,7 +622,7 @@ class LayerOperation(LayerMathMixin):
546
622
  )
547
623
  return union
548
624
  case _:
549
- assert False, "Should not be reached"
625
+ raise RuntimeError("Should not be reached")
550
626
 
551
627
  @property
552
628
  @deprecation.deprecated(
@@ -632,15 +708,35 @@ class LayerOperation(LayerMathMixin):
632
708
  pass
633
709
  return projection
634
710
 
711
+ def pretty_print(self, prefix="", is_last=True):
712
+ kwargs_str = ", ".join(f"{k}={v}" for k, v in self.kwargs.items())
713
+ label = f"{self.operator}({kwargs_str})" if kwargs_str else self.operator
714
+ label = f"{label} - {self._cse_hash}"
715
+
716
+ connector = "└── " if is_last else "├── "
717
+ print(f"{prefix}{connector}{label}")
718
+
719
+ extension = " " if is_last else "│ "
720
+ new_prefix = prefix + extension
721
+
722
+ children = self._children
723
+ for i, child in enumerate(children):
724
+ child_is_last = i == len(children) - 1
725
+ if hasattr(child, 'pretty_print'):
726
+ child.pretty_print(new_prefix, child_is_last)
727
+ else:
728
+ child_connector = "└── " if child_is_last else "├── "
729
+ print(f"{new_prefix}{child_connector}{repr(child)}")
730
+
635
731
  def _eval(
636
732
  self,
733
+ cse_cache: CSECacheTable,
637
734
  area: Area,
638
735
  projection: MapProjection,
639
736
  index: int,
640
737
  step: int,
641
- target_window: Window | None = None
738
+ target_window: Window,
642
739
  ):
643
-
644
740
  if self.buffer_padding:
645
741
  if target_window:
646
742
  target_window = target_window.grow(self.buffer_padding)
@@ -648,30 +744,36 @@ class LayerOperation(LayerMathMixin):
648
744
  # The index doesn't need updating because we updated area/window
649
745
  step += (2 * self.buffer_padding)
650
746
 
651
- lhs_data = self.lhs._eval(area, projection, index, step, target_window)
652
-
653
- if self.operator is None:
654
- return lhs_data
655
-
656
- try:
657
- operator: Callable = backend.operator_map[self.operator]
658
- except KeyError:
659
- # Handles things like `numpy_apply` where a custom operator is provided
660
- operator = self.operator
747
+ cache_data = cse_cache.get_data(self._cse_hash, target_window)
748
+ if cache_data is not None:
749
+ return cache_data
661
750
 
662
- if self.other is not None:
663
- assert self.rhs is not None
664
- rhs_data = self.rhs._eval(area, projection, index, step, target_window)
665
- other_data = self.other._eval(area, projection, index, step, target_window)
666
- return operator(lhs_data, rhs_data, other_data, **self.kwargs)
751
+ lhs_data = self.lhs._eval(cse_cache, area, projection, index, step, target_window)
667
752
 
668
- if self.rhs is not None:
669
- rhs_data = self.rhs._eval(area, projection, index, step, target_window)
670
- return operator(lhs_data, rhs_data, **self.kwargs)
753
+ if self.operator is None:
754
+ result = lhs_data
755
+ else:
756
+ if isinstance(self.operator, op):
757
+ operator = backend.operator_map[self.operator]
758
+ else:
759
+ # Handles things like `numpy_apply` where a custom operator is provided
760
+ operator = self.operator
761
+
762
+ if self.other is not None:
763
+ assert self.rhs is not None
764
+ rhs_data = self.rhs._eval(cse_cache, area, projection, index, step, target_window)
765
+ other_data = self.other._eval(cse_cache, area, projection, index, step, target_window)
766
+ result = operator(lhs_data, rhs_data, other_data, **self.kwargs)
767
+ elif self.rhs is not None:
768
+ rhs_data = self.rhs._eval(cse_cache, area, projection, index, step, target_window)
769
+ result = operator(lhs_data, rhs_data, **self.kwargs)
770
+ else:
771
+ result = operator(lhs_data, **self.kwargs)
671
772
 
672
- return operator(lhs_data, **self.kwargs)
773
+ cse_cache.set_data(self._cse_hash, target_window, result)
774
+ return result
673
775
 
674
- def sum(self):
776
+ def sum(self) -> float:
675
777
  # The result accumulator is float64, and for precision reasons
676
778
  # we force the sum to be done in float64 also. Otherwise we
677
779
  # see variable results depending on chunk size, as different parts
@@ -679,40 +781,77 @@ class LayerOperation(LayerMathMixin):
679
781
  res = 0.0
680
782
  computation_window = self.window
681
783
  projection = self.map_projection
784
+ if projection is None:
785
+ raise ValueError("No map projection")
786
+
787
+ cse_cache = CSECacheTable(self, computation_window)
788
+
682
789
  for yoffset in range(0, computation_window.ysize, self.ystep):
790
+ cse_cache.reset_cache()
683
791
  step=self.ystep
684
792
  if yoffset+step > computation_window.ysize:
685
793
  step = computation_window.ysize - yoffset
686
- chunk = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
794
+ chunk = self._eval(
795
+ cse_cache,
796
+ self._get_operation_area(projection),
797
+ projection,
798
+ yoffset,
799
+ step,
800
+ computation_window
801
+ )
687
802
  res += backend.sum_op(chunk)
688
803
  return res
689
804
 
690
- def min(self):
691
- res = None
805
+ def min(self) -> float:
806
+ res = float('inf')
692
807
  computation_window = self.window
693
808
  projection = self.map_projection
809
+ if projection is None:
810
+ raise ValueError("No map projection")
811
+
812
+ cse_cache = CSECacheTable(self, computation_window)
813
+
694
814
  for yoffset in range(0, computation_window.ysize, self.ystep):
815
+ cse_cache.reset_cache()
695
816
  step=self.ystep
696
817
  if yoffset+step > computation_window.ysize:
697
818
  step = computation_window.ysize - yoffset
698
- chunk = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
699
- chunk_min = backend.min_op(chunk)
700
- if (res is None) or (res > chunk_min):
701
- res = chunk_min
819
+ chunk = self._eval(
820
+ cse_cache,
821
+ self._get_operation_area(projection),
822
+ projection,
823
+ yoffset,
824
+ step,
825
+ computation_window
826
+ )
827
+ chunk_min = float(backend.min_op(chunk))
828
+ res = min(res, chunk_min)
702
829
  return res
703
830
 
704
- def max(self):
705
- res = None
831
+ def max(self) -> float:
832
+ res = float('-inf')
706
833
  computation_window = self.window
707
834
  projection = self.map_projection
835
+ if projection is None:
836
+ raise ValueError("No map projection")
837
+
838
+ cse_cache = CSECacheTable(self, computation_window)
839
+
708
840
  for yoffset in range(0, computation_window.ysize, self.ystep):
841
+ cse_cache.reset_cache()
709
842
  step=self.ystep
710
843
  if yoffset+step > computation_window.ysize:
711
844
  step = computation_window.ysize - yoffset
712
- chunk = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
713
- chunk_max = backend.max_op(chunk)
714
- if (res is None) or (chunk_max > res):
715
- res = chunk_max
845
+ chunk = self._eval(
846
+ cse_cache,
847
+ self._get_operation_area(projection),
848
+ projection,
849
+ yoffset,
850
+ step,
851
+ computation_window
852
+ )
853
+ chunk_max = float(backend.max_op(chunk))
854
+ res = max(res, chunk_max)
716
855
  return res
717
856
 
718
857
  def save(self, destination_layer, and_sum=False, callback=None, band=1) -> float | None:
@@ -757,13 +896,18 @@ class LayerOperation(LayerMathMixin):
757
896
 
758
897
  total = 0.0
759
898
 
899
+ cse_cache = CSECacheTable(self, computation_window)
900
+
760
901
  for yoffset in range(0, computation_window.ysize, self.ystep):
902
+
903
+ cse_cache.reset_cache()
904
+
761
905
  if callback:
762
906
  callback(yoffset / computation_window.ysize)
763
907
  step = self.ystep
764
908
  if yoffset + step > computation_window.ysize:
765
909
  step = computation_window.ysize - yoffset
766
- chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
910
+ chunk = self._eval(cse_cache, computation_area, projection, yoffset, step, computation_window)
767
911
  if isinstance(chunk, (float, int)):
768
912
  chunk = backend.full((step, destination_window.xsize), chunk)
769
913
  band.WriteArray(
@@ -778,11 +922,29 @@ class LayerOperation(LayerMathMixin):
778
922
 
779
923
  return total if and_sum else None
780
924
 
781
- def _parallel_worker(self, index, shared_mem, sem, np_dtype, width, input_queue, output_queue, computation_window):
782
- arr = np.ndarray((self.ystep, width), dtype=np_dtype, buffer=shared_mem.buf)
925
+ def _parallel_worker(
926
+ self,
927
+ index : int,
928
+ shared_mem,
929
+ sem : SemaphoreType,
930
+ np_dtype : type,
931
+ width : int,
932
+ input_queue : multiprocessing.Queue,
933
+ output_queue : multiprocessing.Queue,
934
+ computation_window : Window,
935
+ ):
936
+ # The hashing of python objects isn't stable across processes in general, so we have to do
937
+ # the cache build once per worker
938
+ cse_cache = CSECacheTable(self, computation_window)
939
+
940
+ arr = np.ndarray((self.ystep, width), dtype=np_dtype, buffer=shared_mem.buf) # type: ignore[var-annotated]
783
941
  projection = self.map_projection
942
+ # TODO: the `save` method does more sanity checking that parallel save!
943
+ assert projection is not None
784
944
  try:
785
945
  while True:
946
+ cse_cache.reset_cache()
947
+
786
948
  # We acquire the lock so we know we have somewhere to put the
787
949
  # result before we take work. This is because in practice
788
950
  # it seems the writing to GeoTIFF is the bottleneck, and
@@ -798,7 +960,14 @@ class LayerOperation(LayerMathMixin):
798
960
  break
799
961
  yoffset, step = task
800
962
 
801
- result = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
963
+ result = self._eval(
964
+ cse_cache,
965
+ self._get_operation_area(projection),
966
+ projection,
967
+ yoffset,
968
+ step,
969
+ computation_window,
970
+ )
802
971
  backend.eval_op(result)
803
972
 
804
973
  arr[:step] = backend.demote_array(result)
@@ -844,7 +1013,7 @@ class LayerOperation(LayerMathMixin):
844
1013
  elif and_sum:
845
1014
  return self.sum()
846
1015
  else:
847
- assert False
1016
+ raise RuntimeError("Should not be reached") # pylint: disable=W0707
848
1017
 
849
1018
  worker_count = parallelism or multiprocessing.cpu_count()
850
1019
  work_blocks = len(range(0, computation_window.ysize, self.ystep))
@@ -857,7 +1026,7 @@ class LayerOperation(LayerMathMixin):
857
1026
  elif and_sum:
858
1027
  return self.sum()
859
1028
  else:
860
- assert False
1029
+ raise RuntimeError("Should not be reached")
861
1030
 
862
1031
  if destination_layer is not None:
863
1032
  try:
@@ -942,7 +1111,7 @@ class LayerOperation(LayerMathMixin):
942
1111
  computation_window.xsize,
943
1112
  source_queue,
944
1113
  result_queue,
945
- computation_window
1114
+ computation_window,
946
1115
  )) for i in range(worker_count)]
947
1116
  for worker in workers:
948
1117
  worker.start()
@@ -1074,11 +1243,15 @@ class LayerOperation(LayerMathMixin):
1074
1243
  )
1075
1244
 
1076
1245
  chunks = []
1246
+
1247
+ cse_cache = CSECacheTable(self, computation_window)
1248
+
1077
1249
  for yoffset in range(0, height, self.ystep):
1250
+ cse_cache.reset_cache()
1078
1251
  step = self.ystep
1079
1252
  if yoffset + step > height:
1080
1253
  step = height - yoffset
1081
- chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
1254
+ chunk = self._eval(cse_cache, computation_area, projection, yoffset, step, computation_window)
1082
1255
  if isinstance(chunk, (float, int)):
1083
1256
  chunk = backend.full((step, computation_window.xsize), chunk)
1084
1257
  chunks.append(chunk)
@@ -1086,14 +1259,17 @@ class LayerOperation(LayerMathMixin):
1086
1259
 
1087
1260
  return res
1088
1261
 
1262
+ def _read_array_for_area(self, _target_area, _target_projection, _x, _y, _w, _h):
1263
+ raise RuntimeError("Should not be called")
1264
+
1089
1265
  class ShaderStyleOperation(LayerOperation):
1090
1266
 
1091
- def _eval(self, area, projection, index, step, target_window=None):
1267
+ def _eval(self, cse_cache, area, projection, index, step, target_window=None):
1092
1268
  if target_window is None:
1093
1269
  target_window = self.window
1094
- lhs_data = self.lhs._eval(area, projection, index, step, target_window)
1270
+ lhs_data = self.lhs._eval(cse_cache, area, projection, index, step, target_window)
1095
1271
  if self.rhs is not None:
1096
- rhs_data = self.rhs._eval(area, projection, index, step, target_window)
1272
+ rhs_data = self.rhs._eval(cse_cache, area, projection, index, step, target_window)
1097
1273
  else:
1098
1274
  rhs_data = None
1099
1275
 
@@ -1127,6 +1303,9 @@ class ShaderStyleOperation(LayerOperation):
1127
1303
 
1128
1304
  return result
1129
1305
 
1306
+ def _read_array_for_area(self, _target_area, _target_projection, _x, _y, _w, _h):
1307
+ raise RuntimeError("Should not be called")
1308
+
1130
1309
  def where(cond, a, b):
1131
1310
  """Return elements chosen from `a` or `b` depending on `cond`.
1132
1311
 
@@ -0,0 +1,66 @@
1
+
2
+ from .._backends import backend
3
+ from ..window import Window
4
+
5
+ class CSECacheTable:
6
+
7
+ def __init__(self, expression, window: Window) -> None:
8
+ self._table: dict[tuple[int, Window], tuple[int, backend.array_t | None]] = {}
9
+ self._populate(expression, window)
10
+
11
+ def __len__(self) -> int:
12
+ return len(self._table)
13
+
14
+ def _add(self, cse_hash: int | None, window: Window) -> int:
15
+ if cse_hash is None:
16
+ return 0
17
+
18
+ try:
19
+ count, data = self._table[(cse_hash, window)]
20
+ cache_line = (count + 1, data)
21
+ except KeyError:
22
+ cache_line = (1, None)
23
+ self._table[(cse_hash, window)] = cache_line
24
+ count, _ = cache_line
25
+ return count
26
+
27
+ def _populate(self, expression, window: Window) -> None:
28
+ used_window = window.grow(expression.buffer_padding)
29
+ count = self._add(expression._cse_hash, used_window)
30
+ if count == 1:
31
+ # We only add children the first time we see an expression, otherwise
32
+ # we will cache data covered by this node's cache line potentially
33
+ for child in expression._children:
34
+ try:
35
+ self._populate(child, used_window)
36
+ except AttributeError:
37
+ try:
38
+ self._add(child._cse_hash, used_window)
39
+ except TypeError:
40
+ pass
41
+
42
+ def set_data(self, cse_hash: int | None, window: Window, data: backend.array_t) -> None:
43
+ if cse_hash is not None:
44
+ try:
45
+ count, old_data = self._table[(cse_hash, window)]
46
+ if count < 2:
47
+ return
48
+ if old_data is not None:
49
+ raise RuntimeWarning("Failure in CSE logic, setting data that is already set")
50
+ self._table[(cse_hash, window)] = (count, data)
51
+ except KeyError:
52
+ raise RuntimeWarning("Failure in CSE logic, setting data for unknown term") # pylint: disable=W0707
53
+
54
+ def get_data(self, cse_hash: int | None, window: Window) -> backend.array_t | None:
55
+ # TODO: In theory we could release data from the table if we decremented the count each read
56
+ # but we'd also need to fixed copy of the count for reset
57
+ if cse_hash is None:
58
+ return None
59
+ try:
60
+ _count, data = self._table[(cse_hash, window)]
61
+ return data
62
+ except KeyError:
63
+ return None
64
+
65
+ def reset_cache(self):
66
+ self._table = {k: (count, None) for k, (count, _data) in self._table.items()}
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ import uuid
2
3
  from typing import Any, Sequence
3
4
 
4
5
  import deprecation
@@ -28,7 +29,7 @@ class YirgacheffeLayer(LayerMathMixin):
28
29
  self._active_area: Area | None = None
29
30
  self._projection = projection
30
31
  self._window: Window | None = None
31
- self.name = name
32
+ self.name = name if name is not None else str(uuid.uuid4())
32
33
 
33
34
  self.reset_window()
34
35
 
@@ -41,6 +42,10 @@ class YirgacheffeLayer(LayerMathMixin):
41
42
  def __exit__(self, exc_type, exc_val, exc_tb):
42
43
  self.close()
43
44
 
45
+ @property
46
+ def _cse_hash(self) -> int | None:
47
+ raise NotImplementedError("Must be overridden by subclass")
48
+
44
49
  def _park(self) -> None:
45
50
  pass
46
51
 
@@ -15,6 +15,10 @@ class ConstantLayer(YirgacheffeLayer):
15
15
  super().__init__(area, None)
16
16
  self.value = float(value)
17
17
 
18
+ @property
19
+ def _cse_hash(self) -> int | None:
20
+ return hash(self.value)
21
+
18
22
  @property
19
23
  def datatype(self) -> DataType:
20
24
  return DataType.Float64
@@ -72,6 +72,10 @@ class GroupLayer(YirgacheffeLayer):
72
72
  self._underlying_layers.reverse()
73
73
  self.layers = self._underlying_layers
74
74
 
75
+ @property
76
+ def _cse_hash(self) -> int | None:
77
+ return hash(tuple(x._cse_hash for x in self._underlying_layers))
78
+
75
79
  def _park(self) -> None:
76
80
  for layer in self.layers:
77
81
  try:
@@ -81,7 +85,7 @@ class GroupLayer(YirgacheffeLayer):
81
85
 
82
86
  @property
83
87
  def datatype(self) -> DataType:
84
- return DataType.of_gdal(self.layers[0].datatype)
88
+ return self.layers[0].datatype
85
89
 
86
90
  def set_window_for_intersection(self, new_area: Area) -> None:
87
91
  super().set_window_for_intersection(new_area)
@@ -76,6 +76,15 @@ class H3CellLayer(YirgacheffeLayer):
76
76
  (sorted_lats[1] / abs_ystep) * abs_ystep,
77
77
  )
78
78
 
79
+ @property
80
+ def _cse_hash(self) -> int | None:
81
+ return hash((
82
+ self.cell_id,
83
+ self._underlying_area,
84
+ self.map_projection,
85
+ self._active_area,
86
+ ))
87
+
79
88
  @property
80
89
  def _raster_dimensions(self) -> tuple[int, int]:
81
90
  return (self._raster_xsize, self._raster_ysize)
@@ -313,6 +313,18 @@ class RasterLayer(YirgacheffeLayer):
313
313
  except RuntimeError as exc:
314
314
  raise FileNotFoundError(f"Failed to open pickled raster {self._dataset_path}") from exc
315
315
 
316
+ @property
317
+ def _cse_hash(self) -> int | None:
318
+ return hash((
319
+ self.name,
320
+ self._underlying_area,
321
+ self.map_projection,
322
+ self.datatype,
323
+ self._active_area,
324
+ self._ignore_nodata,
325
+ self._band,
326
+ ))
327
+
316
328
  @property
317
329
  def datatype(self) -> DataType:
318
330
  if self._dataset is None:
@@ -52,6 +52,17 @@ class RescaledRasterLayer(YirgacheffeLayer):
52
52
  self._x_scale = src_projection.xstep / target_projection.xstep
53
53
  self._y_scale = src_projection.ystep / target_projection.ystep
54
54
 
55
+ @property
56
+ def _cse_hash(self) -> int | None:
57
+ return hash((
58
+ self._src._cse_hash,
59
+ self.name,
60
+ self._underlying_area,
61
+ self._nearest_neighbour,
62
+ self.map_projection,
63
+ self._active_area
64
+ ))
65
+
55
66
  def close(self):
56
67
  self._src.close()
57
68
 
@@ -303,7 +303,6 @@ class VectorLayer(YirgacheffeLayer):
303
303
  if layer is None:
304
304
  raise ValueError('No layer provided')
305
305
  self.layer = layer
306
- self.name = name
307
306
 
308
307
  if isinstance(datatype, int):
309
308
  self._datatype = DataType.of_gdal(datatype)
@@ -364,7 +363,7 @@ class VectorLayer(YirgacheffeLayer):
364
363
  bottom=floor(min(x[2] for x in envelopes)),
365
364
  )
366
365
 
367
- super().__init__(area, projection)
366
+ super().__init__(area, projection, name=name)
368
367
 
369
368
 
370
369
  def _get_operation_area(self, projection: MapProjection | None = None) -> Area:
@@ -441,6 +440,18 @@ class VectorLayer(YirgacheffeLayer):
441
440
  if self._filter is not None:
442
441
  self.layer.SetAttributeFilter(self._filter)
443
442
 
443
+ @property
444
+ def _cse_hash(self) -> int | None:
445
+ return hash((
446
+ self.name,
447
+ self._underlying_area,
448
+ self.map_projection,
449
+ self._active_area,
450
+ self._datatype,
451
+ self.burn_value,
452
+ self._filter,
453
+ ))
454
+
444
455
  @property
445
456
  def datatype(self) -> DataType:
446
457
  return self._datatype
yirgacheffe/window.py CHANGED
@@ -22,6 +22,15 @@ class MapProjection:
22
22
  name: The map projection used in WKT format.
23
23
  xstep: The number of units horizontal distance a step of one pixel makes in the map projection.
24
24
  ystep: The number of units vertical distance a step of one pixel makes in the map projection.
25
+
26
+ Examples:
27
+ Create a map projection using an EPSG code:
28
+
29
+ >>> proj_wgs84 = MapProjection("epsg:4326", 0.001, -0.001)
30
+
31
+ Create a projection using an ESRI code:
32
+
33
+ >>> proj_esri = MapProjection("esri:54030", 1000, -1000)
25
34
  """
26
35
 
27
36
  def __init__(self, projection_string: str, xstep: float, ystep: float) -> None:
@@ -32,6 +41,9 @@ class MapProjection:
32
41
  self.xstep = xstep
33
42
  self.ystep = ystep
34
43
 
44
+ def __hash__(self):
45
+ return hash((self.name, self.xstep, self.ystep))
46
+
35
47
  def __eq__(self, other) -> bool:
36
48
  if other is None:
37
49
  return True
@@ -52,7 +64,7 @@ class MapProjection:
52
64
  def scale(self) -> PixelScale:
53
65
  return PixelScale(self.xstep, self.ystep)
54
66
 
55
- @dataclass
67
+ @dataclass(frozen=True)
56
68
  class Area:
57
69
  """Class to hold a geospatial area of data in the given projection.
58
70
 
@@ -145,7 +157,7 @@ class Area:
145
157
  (other.top >= self.bottom >= other.bottom)
146
158
  )
147
159
 
148
- @dataclass
160
+ @dataclass(frozen=True)
149
161
  class Window:
150
162
  """Class to hold the pixel dimensions of data in the given projection.
151
163
 
@@ -216,7 +228,7 @@ class Window:
216
228
  """
217
229
  return Window(
218
230
  xoff=self.xoff - pixels,
219
- yoff=self.xoff - pixels,
231
+ yoff=self.yoff - pixels,
220
232
  xsize=self.xsize + (2 * pixels),
221
233
  ysize=self.ysize + (2 * pixels),
222
234
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.9.4
3
+ Version: 1.10.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
@@ -37,6 +37,7 @@ Requires-Dist: mypy; extra == "dev"
37
37
  Requires-Dist: pylint; extra == "dev"
38
38
  Requires-Dist: pytest; extra == "dev"
39
39
  Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: pytest-mock; extra == "dev"
40
41
  Requires-Dist: build; extra == "dev"
41
42
  Requires-Dist: twine; extra == "dev"
42
43
  Requires-Dist: mkdocs-material; extra == "dev"
@@ -0,0 +1,28 @@
1
+ yirgacheffe/__init__.py,sha256=Ps6W8A1TRriVNxZEF3jW1_KOLEtji4ffyoGRmQXne8g,927
2
+ yirgacheffe/_core.py,sha256=Tr6RAiRZOO3vbtiTjLoNRjjd1DkXYZOgpDqrjs7jCBw,7231
3
+ yirgacheffe/constants.py,sha256=bKUjOGNj19zwggV79lJgK7tiv51DH2-rgNOKswl2gvQ,293
4
+ yirgacheffe/operators.py,sha256=Y1KkNt79N1elR4ZplQaQngx29wdf2QFF_5la4PI3EhI,412
5
+ yirgacheffe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ yirgacheffe/rounding.py,sha256=Jzd9qlLnLpigT95GbQTByvYOo639Nfq4LBEVyvhYdoc,2289
7
+ yirgacheffe/window.py,sha256=11YqUw5GZs8qRjHSgQrvPbapTNFpx6OGwlj0qUtQc70,9863
8
+ yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
9
+ yirgacheffe/_backends/enumeration.py,sha256=xeAWNfMORHd4Ue-CryajmteD-nxjIGcrU5m9PdXgEqA,2765
10
+ yirgacheffe/_backends/mlx.py,sha256=0Kq2weyC_fo-luATcMU1fBKB3aJjAg5Bl2_A3Mejp4E,6451
11
+ yirgacheffe/_backends/numpy.py,sha256=zgZo2u9ZVW8LYRKtLzSkFz3w7Y1j3_KwvVHTZ_xqGpE,4605
12
+ yirgacheffe/_operators/__init__.py,sha256=UZEy-0FR6un3UOMdws3XzqdaaDQLiWy0IqV9hOh2baE,49977
13
+ yirgacheffe/_operators/cse.py,sha256=4x5kwAGbTe8nBZYipz53mpW-Cr81A4vK-v6YWq5SmKg,2602
14
+ yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
15
+ yirgacheffe/layers/area.py,sha256=wJcMHbLJBaXS4BeFbu5rYeKfgu3gvaE9hwQ5j6aw-y4,3976
16
+ yirgacheffe/layers/base.py,sha256=jqtlAbRSHWDMZpEOl0dVuNNYKzR90s7uEuECvXCA3Dk,13378
17
+ yirgacheffe/layers/constant.py,sha256=eheddNT3fjMDpfISRipqKzOLJxR01D1IMerGJpperls,1567
18
+ yirgacheffe/layers/group.py,sha256=R0g-PhZprweVRYBJD5hp5trM8ztMCi0yyK-3UhDPGoo,16266
19
+ yirgacheffe/layers/h3layer.py,sha256=X5lfuM-TKVly6cWn9B-VbMTijmlsi7kUgp9OTYnJzJ0,10128
20
+ yirgacheffe/layers/rasters.py,sha256=Q9MPmS9WYJ17_inpL4KVkjJBX-jizjvERPdb8Mq-r5o,13820
21
+ yirgacheffe/layers/rescaled.py,sha256=7QzHL0l73-fS3XAhkdBRb3lhYJ-HnLrpjTByVfei9Fg,3630
22
+ yirgacheffe/layers/vectors.py,sha256=KxVUgBIgNyFuACyGkcdK8eU09stVCf5VQAvBL7RfoGA,20268
23
+ yirgacheffe-1.10.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
24
+ yirgacheffe-1.10.0.dist-info/METADATA,sha256=rRgnXn19ivS7CUPT78xw9RGoh0h1Nmm80JuPuAsVG9g,7451
25
+ yirgacheffe-1.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ yirgacheffe-1.10.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
27
+ yirgacheffe-1.10.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
28
+ yirgacheffe-1.10.0.dist-info/RECORD,,
@@ -1,27 +0,0 @@
1
- yirgacheffe/__init__.py,sha256=JWQOJP_RPlBmN_N3vPGH-9FByjRQKCWUSnsh4fPNtW8,915
2
- yirgacheffe/_core.py,sha256=-X6wTjAKagcjWcH14IKp75Nf-kANO_Nsd8wKm5XEQzE,5750
3
- yirgacheffe/_operators.py,sha256=VGQ9AOOJP6cxsr_G2kxdaPaXqKi7K1csocxsudlRwVc,43440
4
- yirgacheffe/constants.py,sha256=bKUjOGNj19zwggV79lJgK7tiv51DH2-rgNOKswl2gvQ,293
5
- yirgacheffe/operators.py,sha256=Y1KkNt79N1elR4ZplQaQngx29wdf2QFF_5la4PI3EhI,412
6
- yirgacheffe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- yirgacheffe/rounding.py,sha256=Jzd9qlLnLpigT95GbQTByvYOo639Nfq4LBEVyvhYdoc,2289
8
- yirgacheffe/window.py,sha256=QuyBLOwKFI0XkEQ4Bd2hdELPbJSfHL7mt5KSi7CIHcE,9505
9
- yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
10
- yirgacheffe/_backends/enumeration.py,sha256=9bcCXz9Ssrh8Oh1iazodkx6Gm2kQBi9HQ9z9zehS4AE,1806
11
- yirgacheffe/_backends/mlx.py,sha256=U1gl1lK1mZXLEET6ylF1TNs6WJ0PBEvfSk7ppn28n8w,6203
12
- yirgacheffe/_backends/numpy.py,sha256=Gxx49JJH79GFEkKIpV6IyjCUcdtN5-qLlzRfylzKhS4,4142
13
- yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
14
- yirgacheffe/layers/area.py,sha256=wJcMHbLJBaXS4BeFbu5rYeKfgu3gvaE9hwQ5j6aw-y4,3976
15
- yirgacheffe/layers/base.py,sha256=7b4WXuvnmCv8mR0iyCIuSEolnV8D3f2vtCaYlcJCIa8,13201
16
- yirgacheffe/layers/constant.py,sha256=gtkQ98Z01CYYDgFElswtRZY4ZG3UnS5NIAoIVue5ufk,1481
17
- yirgacheffe/layers/group.py,sha256=yaqf-ra_Vh59yrWcz7-OvJ1fBnTcBXZd18AfRDN5Ymo,16157
18
- yirgacheffe/layers/h3layer.py,sha256=Rq1bFo7CApIh5NdBcV7hSj3hm-DszY79nhYsTRAvJ_g,9916
19
- yirgacheffe/layers/rasters.py,sha256=zBE9uXm6LvAQF2_XdQzcOgJQOQWGmuPflY5JNDrUf3k,13527
20
- yirgacheffe/layers/rescaled.py,sha256=gEFbXeYxX1nVn7eQYmbGww90_yc5ENmgQrD_WxXxpQE,3352
21
- yirgacheffe/layers/vectors.py,sha256=A27kuTr0C9BZhHG0-cplNEa7aSNcse37Pm9xTjEzv-c,19990
22
- yirgacheffe-1.9.4.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
23
- yirgacheffe-1.9.4.dist-info/METADATA,sha256=cUAABIwPG9_RTO0ylH25O_7usbtQneZwTUCsfk_SLIs,7407
24
- yirgacheffe-1.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- yirgacheffe-1.9.4.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
26
- yirgacheffe-1.9.4.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
27
- yirgacheffe-1.9.4.dist-info/RECORD,,