yirgacheffe 1.10.2__tar.gz → 1.10.3__tar.gz

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.
Files changed (62) hide show
  1. {yirgacheffe-1.10.2/yirgacheffe.egg-info → yirgacheffe-1.10.3}/PKG-INFO +1 -1
  2. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/pyproject.toml +1 -1
  3. yirgacheffe-1.10.3/tests/test_aggregations.py +68 -0
  4. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_constants.py +3 -3
  5. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_pickle.py +0 -2
  6. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_backends/mlx.py +17 -7
  7. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_operators/__init__.py +29 -19
  8. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/area.py +2 -1
  9. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3/yirgacheffe.egg-info}/PKG-INFO +1 -1
  10. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe.egg-info/SOURCES.txt +1 -0
  11. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/LICENSE +0 -0
  12. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/MANIFEST.in +0 -0
  13. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/README.md +0 -0
  14. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/setup.cfg +0 -0
  15. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_area.py +0 -0
  16. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_auto_windowing.py +0 -0
  17. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_datatypes.py +0 -0
  18. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_group.py +0 -0
  19. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_h3layer.py +0 -0
  20. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_intersection.py +0 -0
  21. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_multiband.py +0 -0
  22. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_nodata.py +0 -0
  23. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_openers.py +0 -0
  24. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_operator_hashing.py +0 -0
  25. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_operators.py +0 -0
  26. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_optimisation.py +0 -0
  27. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_parallel_operators.py +0 -0
  28. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_pixel_coord.py +0 -0
  29. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_projection.py +0 -0
  30. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_raster.py +0 -0
  31. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_reduce.py +0 -0
  32. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_rescaling.py +0 -0
  33. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_rounding.py +0 -0
  34. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_save_with_window.py +0 -0
  35. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_sum_with_window.py +0 -0
  36. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_uniform_area_layer.py +0 -0
  37. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_union.py +0 -0
  38. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_vectors.py +0 -0
  39. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/tests/test_window.py +0 -0
  40. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/__init__.py +0 -0
  41. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_backends/__init__.py +0 -0
  42. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_backends/enumeration.py +0 -0
  43. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_backends/numpy.py +0 -0
  44. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_core.py +0 -0
  45. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/_operators/cse.py +0 -0
  46. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/constants.py +0 -0
  47. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/__init__.py +0 -0
  48. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/base.py +0 -0
  49. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/constant.py +0 -0
  50. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/group.py +0 -0
  51. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/h3layer.py +0 -0
  52. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/rasters.py +0 -0
  53. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/rescaled.py +0 -0
  54. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/layers/vectors.py +0 -0
  55. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/operators.py +0 -0
  56. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/py.typed +0 -0
  57. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/rounding.py +0 -0
  58. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe/window.py +0 -0
  59. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  60. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe.egg-info/entry_points.txt +0 -0
  61. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe.egg-info/requires.txt +0 -0
  62. {yirgacheffe-1.10.2 → yirgacheffe-1.10.3}/yirgacheffe.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.10.2
3
+ Version: 1.10.3
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
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "yirgacheffe"
9
- version = "1.10.2"
9
+ version = "1.10.3"
10
10
  description = "Abstraction of gdal datasets for doing basic math operations"
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Michael Dales", email = "mwd24@cam.ac.uk" }]
@@ -0,0 +1,68 @@
1
+ import json
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ import yirgacheffe as yg
7
+ from yirgacheffe._backends import backend, BACKEND
8
+
9
+ def test_sum_result_is_scalar() -> None:
10
+ rng = np.random.default_rng(seed=42)
11
+ data = rng.integers(0, 128, size=(1000, 1000))
12
+ with yg.from_array(data, (0, 0), ("epsg:4326", (0.01, -0.01))) as layer:
13
+ total = layer.sum()
14
+ json.dumps(total)
15
+
16
+ @pytest.mark.parametrize("c,dtype,maxval", [
17
+ (int(2), np.int8, 120),
18
+ (int(2), np.uint8, 250),
19
+ (int(2), np.int16, 32000),
20
+ (int(2), np.uint16, 64000),
21
+ (int(2), np.int32, 66000),
22
+ (int(2), np.uint32, 66000),
23
+ ])
24
+ @pytest.mark.parametrize("step", [1, 2, 4, 8])
25
+ def test_sums_of_calc_int(monkeypatch, step, c, dtype: type, maxval: int) -> None:
26
+ with monkeypatch.context() as m:
27
+ m.setattr(yg.constants, "YSTEP", step)
28
+
29
+ rng = np.random.default_rng(seed=42)
30
+
31
+ data = rng.integers(0, maxval, size=(1000, 1000), dtype=dtype)
32
+ typed_data = backend.promote(data)
33
+
34
+ assert np.sum(data) == backend.sum_op(typed_data)
35
+
36
+ with yg.from_array(data, (0, 0), ("epsg:4326", (0.01, -0.01))) as layer:
37
+ assert layer.sum() == backend.sum_op(typed_data)
38
+ calc = layer * c
39
+ actual = calc.sum()
40
+ expected = backend.sum_op(typed_data * c)
41
+ assert actual == expected
42
+
43
+ @pytest.mark.parametrize("c,dtype,maxval", [
44
+ (float(2.5), np.int8, 120),
45
+ (float(2.5), np.uint8, 250),
46
+ (float(2.5), np.int16, 32000),
47
+ (float(2.5), np.uint16, 640),
48
+ (float(2.5), np.int32, 660),
49
+ (float(2.5), np.uint32, 660),
50
+ ])
51
+ @pytest.mark.parametrize("step", [1, 2, 4, 8])
52
+ def test_sums_of_calc_float_mlx(monkeypatch, step, c, dtype: type, maxval: int) -> None:
53
+ with monkeypatch.context() as m:
54
+ m.setattr(yg.constants, "YSTEP", step)
55
+
56
+ rng = np.random.default_rng(seed=42)
57
+
58
+ data = rng.integers(0, maxval, size=(100, 100), dtype=dtype)
59
+ typed_data = backend.promote(data)
60
+
61
+ with yg.from_array(data, (0, 0), ("epsg:4326", (0.01, -0.01))) as layer:
62
+ assert layer.sum() == backend.sum_op(typed_data)
63
+ calc = layer * c
64
+ actual = calc.sum()
65
+ expected = backend.sum_op(typed_data * c)
66
+ # MLX has a maximum float size of 32 bit for GPU and NUMPY has 64 bit on CPU
67
+ rel = 1e-6 if BACKEND == "MLX" else 1e-10
68
+ assert float(actual) == pytest.approx(float(expected), rel=rel)
@@ -23,9 +23,9 @@ def test_constant_parallel_save(monkeypatch) -> None:
23
23
  area = Area(left=-1.0, right=1.0, top=1.0, bottom=-1.0)
24
24
  scale = PixelScale(0.1, -0.1)
25
25
  with RasterLayer.empty_raster_layer(area, scale, DataType.Float32) as result:
26
- with ConstantLayer(42.0) as c:
27
- with monkeypatch.context() as m:
28
- m.setattr(yg.constants, "YSTEP", 1)
26
+ with monkeypatch.context() as m:
27
+ m.setattr(yg.constants, "YSTEP", 1)
28
+ with ConstantLayer(42.0) as c:
29
29
  c.parallel_save(result)
30
30
 
31
31
  expected = np.full((20, 20), 42.0)
@@ -134,8 +134,6 @@ def test_pickle_simple_calc(c) -> None:
134
134
  layer = RasterLayer(gdal_dataset_of_region(area, 0.2, filename=path))
135
135
 
136
136
  calc = layer * c
137
- assert calc.sum() != 0
138
- assert calc.sum() == layer.sum() * c
139
137
 
140
138
  p = pickle.dumps(calc)
141
139
  restore = pickle.loads(p)
@@ -14,7 +14,7 @@ float_t = mx.float32
14
14
 
15
15
  promote = mx.array
16
16
  demote_array = np.asarray
17
- demote_scalar = np.float64
17
+ demote_scalar = lambda a: a.item()
18
18
 
19
19
  eval_op = mx.eval
20
20
 
@@ -54,15 +54,25 @@ round_op = mx.round
54
54
  ceil_op = mx.ceil
55
55
 
56
56
  def sum_op(a):
57
- # There are weird issues around how MLX overflows int8, so just promote the data ahead of summing
57
+ # By default the type promotion rules for sum in MLX are not the same as with Numpy. E.g.,
58
+ #
59
+ # >>> mx.array(np.array([1, 2, 3], dtype=np.uint8)).sum()
60
+ # array(6, dtype=uint32)
61
+ # >>> np.array([1, 2, 3], dtype=np.uint8).sum()
62
+ # np.uint64(6)
63
+ #
64
+ # This has problems for Yirgacheffe as it means we get different answers depending on which
65
+ # backend is active. MLX is more conservative, which also can cause issues with Geospatial
66
+ # datasets being large and causing overflows. Thus we force promotion here to match what
67
+ # numpy does, even if this means things run a little slower.
58
68
  match a.dtype:
59
- case mx.int8:
60
- res = mx.sum(a.astype(mx.int32))
61
- case mx.uint8:
62
- res = mx.sum(a.astype(mx.uint32))
69
+ case mx.int8 | mx.int16 | mx.int32:
70
+ res = mx.sum(a.astype(mx.int64))
71
+ case mx.uint8 | mx.uint16 | mx.uint32:
72
+ res = mx.sum(a.astype(mx.uint64))
63
73
  case _:
64
74
  res = mx.sum(a)
65
- return demote_scalar(res)
75
+ return res
66
76
 
67
77
  def _is_float(x):
68
78
  if isinstance(x, float):
@@ -774,11 +774,11 @@ class LayerOperation(LayerMathMixin):
774
774
  return result
775
775
 
776
776
  def sum(self) -> float:
777
- # The result accumulator is float64, and for precision reasons
778
- # we force the sum to be done in float64 also. Otherwise we
779
- # see variable results depending on chunk size, as different parts
780
- # of the sum are done in different types.
781
- res = 0.0
777
+ # Numpy and MLX have different behaviours that make guessing
778
+ # the type of the accumulator tricky, so we go by the type of the
779
+ # first chunk returned
780
+ res = None
781
+
782
782
  computation_window = self.window
783
783
  projection = self.map_projection
784
784
  if projection is None:
@@ -799,8 +799,9 @@ class LayerOperation(LayerMathMixin):
799
799
  step,
800
800
  computation_window
801
801
  )
802
- res += backend.sum_op(chunk)
803
- return res
802
+ chunk_sum = backend.sum_op(chunk)
803
+ res = chunk_sum if res is None else res + chunk_sum
804
+ return backend.demote_scalar(res) if res is not None else 0
804
805
 
805
806
  def min(self) -> float:
806
807
  res = float('inf')
@@ -894,7 +895,7 @@ class LayerOperation(LayerMathMixin):
894
895
  f"{(destination_window.xsize, destination_window.ysize)} vs "
895
896
  f"{(computation_window.xsize, computation_window.ysize)}"))
896
897
 
897
- total = 0.0
898
+ total = None
898
899
 
899
900
  cse_cache = CSECacheTable(self, computation_window)
900
901
 
@@ -916,11 +917,12 @@ class LayerOperation(LayerMathMixin):
916
917
  yoffset + destination_window.yoff,
917
918
  )
918
919
  if and_sum:
919
- total += backend.sum_op(chunk)
920
+ chunk_sum = backend.sum_op(chunk)
921
+ total = chunk_sum if total is None else total + chunk_sum
920
922
  if callback:
921
923
  callback(1.0)
922
924
 
923
- return total if and_sum else None
925
+ return backend.demote_scalar(total) if and_sum else None
924
926
 
925
927
  def _parallel_worker(
926
928
  self,
@@ -1030,7 +1032,7 @@ class LayerOperation(LayerMathMixin):
1030
1032
 
1031
1033
  if destination_layer is not None:
1032
1034
  try:
1033
- band = destination_layer._dataset.GetRasterBand(band)
1035
+ gdal_band = destination_layer._dataset.GetRasterBand(band)
1034
1036
  except AttributeError as exc:
1035
1037
  raise ValueError("Layer must be a raster backed layer") from exc
1036
1038
 
@@ -1052,16 +1054,23 @@ class LayerOperation(LayerMathMixin):
1052
1054
  gdal.GDT_UInt32: np.dtype('uint32'),
1053
1055
  gdal.GDT_UInt64: np.dtype('uint64'),
1054
1056
  }
1055
- np_dtype = np_type_map[band.DataType]
1057
+ np_dtype = np_type_map[gdal_band.DataType]
1056
1058
  else:
1057
- band = None
1058
- np_dtype = np.dtype('float64')
1059
+ # the aggregation path
1060
+ gdal_band = None
1061
+ match self.datatype:
1062
+ case DataType.Int8 | DataType.Int16 | DataType.Int32 | DataType.Int64:
1063
+ np_dtype = np.dtype('int64')
1064
+ case DataType.UInt8 | DataType.UInt16 | DataType.UInt32 | DataType.UInt64:
1065
+ np_dtype = np.dtype('uint64')
1066
+ case _:
1067
+ np_dtype = np.dtype('float64')
1059
1068
 
1060
1069
  # The parallel save will cause a fork on linux, so we need to
1061
1070
  # remove all SWIG references
1062
1071
  self._park()
1063
1072
 
1064
- total = 0.0
1073
+ total = None
1065
1074
 
1066
1075
  with ExitStack() as stack:
1067
1076
  # If we get this far, then we're going to do the multiprocessing path. In general we've had
@@ -1125,14 +1134,15 @@ class LayerOperation(LayerMathMixin):
1125
1134
  continue
1126
1135
  index, yoffset, step = res
1127
1136
  _, sem, arr = mem_sem_cast[index]
1128
- if band:
1129
- band.WriteArray(
1137
+ if gdal_band:
1138
+ gdal_band.WriteArray(
1130
1139
  arr[0:step],
1131
1140
  destination_window.xoff,
1132
1141
  yoffset + destination_window.yoff,
1133
1142
  )
1134
1143
  if and_sum:
1135
- total += np.sum(np.array(arr[0:step]).astype(np.float64))
1144
+ chunk_sum = np.sum(np.array(arr[0:step]).astype(np.float64))
1145
+ total = chunk_sum if total is None else total + chunk_sum
1136
1146
  sem.release()
1137
1147
  retired_blocks += 1
1138
1148
  if callback:
@@ -1150,7 +1160,7 @@ class LayerOperation(LayerMathMixin):
1150
1160
  processes.remove(candidate)
1151
1161
  time.sleep(0.01)
1152
1162
 
1153
- return total if and_sum else None
1163
+ return backend.demote_scalar(total) if and_sum else None
1154
1164
 
1155
1165
  def parallel_save(
1156
1166
  self,
@@ -9,6 +9,7 @@ from osgeo import gdal
9
9
 
10
10
  from ..window import Area, Window
11
11
  from .rasters import RasterLayer
12
+ from .._backends import backend
12
13
 
13
14
  class UniformAreaLayer(RasterLayer):
14
15
  """If you have a pixel area map where all the row entries are identical, then you
@@ -98,4 +99,4 @@ class UniformAreaLayer(RasterLayer):
98
99
  if ysize <= 0:
99
100
  raise ValueError("Request dimensions must be positive and non-zero")
100
101
  offset = window.yoff + yoffset
101
- return self.databand[offset:offset + ysize]
102
+ return backend.promote(self.databand[offset:offset + ysize])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.10.2
3
+ Version: 1.10.3
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
@@ -2,6 +2,7 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
+ tests/test_aggregations.py
5
6
  tests/test_area.py
6
7
  tests/test_auto_windowing.py
7
8
  tests/test_constants.py
File without changes
File without changes
File without changes
File without changes