yirgacheffe 1.7.2__tar.gz → 1.7.4__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.

Potentially problematic release.


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

Files changed (56) hide show
  1. {yirgacheffe-1.7.2/yirgacheffe.egg-info → yirgacheffe-1.7.4}/PKG-INFO +4 -1
  2. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/pyproject.toml +4 -1
  3. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_intersection.py +36 -3
  4. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_nodata.py +28 -0
  5. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_operators.py +12 -0
  6. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_union.py +35 -2
  7. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/_backends/enumeration.py +1 -0
  8. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/_backends/mlx.py +2 -0
  9. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/_backends/numpy.py +2 -0
  10. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/base.py +11 -9
  11. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/group.py +4 -3
  12. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/operators.py +10 -1
  13. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4/yirgacheffe.egg-info}/PKG-INFO +4 -1
  14. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/LICENSE +0 -0
  15. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/MANIFEST.in +0 -0
  16. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/README.md +0 -0
  17. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/setup.cfg +0 -0
  18. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_area.py +0 -0
  19. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_auto_windowing.py +0 -0
  20. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_base.py +0 -0
  21. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_constants.py +0 -0
  22. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_datatypes.py +0 -0
  23. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_group.py +0 -0
  24. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_h3layer.py +0 -0
  25. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_multiband.py +0 -0
  26. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_openers.py +0 -0
  27. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_optimisation.py +0 -0
  28. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_parallel_operators.py +0 -0
  29. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_pickle.py +0 -0
  30. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_projection.py +0 -0
  31. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_raster.py +0 -0
  32. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_rescaling.py +0 -0
  33. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_rounding.py +0 -0
  34. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_save_with_window.py +0 -0
  35. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_sum_with_window.py +0 -0
  36. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_uniform_area_layer.py +0 -0
  37. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_vectors.py +0 -0
  38. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/tests/test_window.py +0 -0
  39. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/__init__.py +0 -0
  40. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/_backends/__init__.py +0 -0
  41. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/_core.py +0 -0
  42. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/constants.py +0 -0
  43. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/__init__.py +0 -0
  44. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/area.py +0 -0
  45. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/constant.py +0 -0
  46. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/h3layer.py +0 -0
  47. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/rasters.py +0 -0
  48. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/rescaled.py +0 -0
  49. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/layers/vectors.py +0 -0
  50. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/rounding.py +0 -0
  51. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe/window.py +0 -0
  52. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe.egg-info/SOURCES.txt +0 -0
  53. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  54. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe.egg-info/entry_points.txt +0 -0
  55. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe.egg-info/requires.txt +0 -0
  56. {yirgacheffe-1.7.2 → yirgacheffe-1.7.4}/yirgacheffe.egg-info/top_level.txt +0 -0
@@ -1,10 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.7.2
3
+ Version: 1.7.4
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
7
7
  Project-URL: Homepage, https://github.com/quantifyearth/yirgacheffe
8
+ Project-URL: Repository, https://github.com/quantifyearth/yirgacheffe.git
9
+ Project-URL: Issues, https://github.com/quantifyearth/yirgacheffe/issues
10
+ Project-URL: Changelog, https://github.com/quantifyearth/yirgacheffe/blob/main/CHANGES.md
8
11
  Keywords: gdal,numpy,math
9
12
  Requires-Python: >=3.10
10
13
  Description-Content-Type: text/markdown
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "yirgacheffe"
9
- version = "1.7.2"
9
+ version = "1.7.4"
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" }]
@@ -28,6 +28,9 @@ dev = ["mypy", "pylint", "pytest", "h3", "pytest-cov", "mlx"]
28
28
 
29
29
  [project.urls]
30
30
  Homepage = "https://github.com/quantifyearth/yirgacheffe"
31
+ Repository = "https://github.com/quantifyearth/yirgacheffe.git"
32
+ Issues = "https://github.com/quantifyearth/yirgacheffe/issues"
33
+ Changelog = "https://github.com/quantifyearth/yirgacheffe/blob/main/CHANGES.md"
31
34
 
32
35
  [project.scripts]
33
36
  realpython = "reader.__main__:main"
@@ -1,9 +1,12 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+
1
4
  import pytest
2
5
  from osgeo import gdal
3
6
 
4
- from tests.helpers import gdal_dataset_of_region, gdal_empty_dataset_of_region
7
+ from tests.helpers import gdal_dataset_of_region, gdal_empty_dataset_of_region, make_vectors_with_id
5
8
  from yirgacheffe.window import Area, MapProjection, Window
6
- from yirgacheffe.layers import RasterLayer, ConstantLayer, H3CellLayer
9
+ from yirgacheffe.layers import RasterLayer, ConstantLayer, H3CellLayer, VectorLayer
7
10
  from yirgacheffe import WGS_84_PROJECTION
8
11
 
9
12
 
@@ -56,6 +59,36 @@ def test_find_intersection_with_constant() -> None:
56
59
  intersection = RasterLayer.find_intersection(layers)
57
60
  assert intersection == layers[0].area
58
61
 
62
+ def test_find_intersection_with_vector_unbound() -> None:
63
+ with tempfile.TemporaryDirectory() as tempdir:
64
+ path = Path(tempdir) / "test.gpkg"
65
+ area = Area(left=58, top=74, right=180, bottom=42)
66
+ make_vectors_with_id(42, {area}, path)
67
+ assert path.exists
68
+
69
+ raster = RasterLayer(gdal_dataset_of_region(Area(left=-180.05, top=90.09, right=180.05, bottom=-90.09), 0.13))
70
+ vector = VectorLayer.layer_from_file(path, None, None, None)
71
+ assert vector.area == area
72
+
73
+ layers = [raster, vector]
74
+ intersection = RasterLayer.find_intersection(layers)
75
+ assert intersection == vector.area
76
+
77
+ def test_find_intersection_with_vector_bound() -> None:
78
+ with tempfile.TemporaryDirectory() as tempdir:
79
+ path = Path(tempdir) / "test.gpkg"
80
+ area = Area(left=58, top=74, right=180, bottom=42)
81
+ make_vectors_with_id(42, {area}, path)
82
+ assert path.exists
83
+
84
+ raster = RasterLayer(gdal_dataset_of_region(Area(left=-180.05, top=90.09, right=180.05, bottom=-90.09), 0.13))
85
+ vector = VectorLayer.layer_from_file(path, None, raster.map_projection.scale, raster.map_projection.name)
86
+ assert vector.area != area
87
+
88
+ layers = [raster, vector]
89
+ intersection = RasterLayer.find_intersection(layers)
90
+ assert intersection == vector.area
91
+
59
92
  def test_find_intersection_different_pixel_pitch() -> None:
60
93
  layers = [
61
94
  RasterLayer(gdal_dataset_of_region(Area(-10, 10, 10, -10), 0.02)),
@@ -143,7 +176,7 @@ def test_find_intersection_nearly_same() -> None:
143
176
  assert layers[0].window.xsize == other.window.xsize
144
177
  assert layers[0].window.ysize == other.window.ysize
145
178
 
146
- def test_intersection_stability():
179
+ def test_intersection_stability() -> None:
147
180
  # This test uses h3 tiles as a lazy way to get some bounded regions,
148
181
  # but the bug this test exercises is not h3 specific. This was another case of
149
182
  # a rounding error that causes set_window_for_* methods to wobble depending on how far
@@ -62,3 +62,31 @@ def test_group_layer_with_nodata_values_ignore_nodata() -> None:
62
62
  with GroupLayer([layer1, layer2]) as group:
63
63
  actual = group.read_array(0, 0, 4, 2)
64
64
  assert np.array_equal(data1, actual, equal_nan=True)
65
+
66
+ def test_group_layer_with_nodata_read_from_empty_area() -> None:
67
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 5.0, 5.0, 5.0]])
68
+ dataset1 = gdal_dataset_with_data((0.0, 10.0), 1.0, data1)
69
+ dataset1.GetRasterBand(1).SetNoDataValue(5.0)
70
+
71
+ data2 = np.array([[1.0, 1.0, 1.0, 1.0], [5.0, 6.0, 7.0, 8.0]])
72
+ dataset2 = gdal_dataset_with_data((0.0, -8.0), 1.0, data2)
73
+ dataset2.GetRasterBand(1).SetNoDataValue(1.0)
74
+
75
+ with RasterLayer(dataset1) as layer1:
76
+ with RasterLayer(dataset2) as layer2:
77
+ with GroupLayer([layer1, layer2]) as group:
78
+
79
+ assert group.window.xsize == 4
80
+ assert group.window.ysize == 20
81
+
82
+ actual = group.read_array(0, 0, 4, 2)
83
+ expected = np.array([[1.0, 2.0, 3.0, 4.0], [0.0, 0.0, 0.0, 0.0]])
84
+ assert np.array_equal(expected, actual, equal_nan=True)
85
+
86
+ actual = group.read_array(0, 10, 4, 2)
87
+ expected = np.array([[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]])
88
+ assert np.array_equal(expected, actual, equal_nan=True)
89
+
90
+ actual = group.read_array(0, 18, 4, 2)
91
+ expected = np.array([[0.0, 0.0, 0.0, 0.0], [5.0, 6.0, 7.0, 8.0]])
92
+ assert np.array_equal(expected, actual, equal_nan=True)
@@ -1567,3 +1567,15 @@ def test_raster_and_vector_no_scale_on_vector() -> None:
1567
1567
  calc = raster * vector
1568
1568
  assert calc.sum() > 0.0
1569
1569
  assert calc.sum() < raster.sum()
1570
+
1571
+ def test_isnan() -> None:
1572
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
1573
+ dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
1574
+ dataset.GetRasterBand(1).SetNoDataValue(5.0)
1575
+ with RasterLayer(dataset) as layer:
1576
+ calc = layer.isnan()
1577
+ with RasterLayer.empty_raster_layer_like(calc) as result:
1578
+ calc.save(result)
1579
+ actual = result.read_array(0, 0, 4, 2)
1580
+ expected = data1 == 5.0
1581
+ assert (expected == actual).all()
@@ -1,8 +1,11 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+
1
4
  import pytest
2
5
 
3
- from tests.helpers import gdal_dataset_of_region
6
+ from tests.helpers import gdal_dataset_of_region, make_vectors_with_id
4
7
  from yirgacheffe.window import Area, Window
5
- from yirgacheffe.layers import ConstantLayer, RasterLayer
8
+ from yirgacheffe.layers import ConstantLayer, RasterLayer, VectorLayer
6
9
 
7
10
 
8
11
  def test_find_union_empty_list() -> None:
@@ -62,6 +65,36 @@ def test_find_union_different_pixel_pitch() -> None:
62
65
  with pytest.raises(ValueError):
63
66
  _ = RasterLayer.find_union(layers)
64
67
 
68
+ def test_find_union_with_vector_unbound() -> None:
69
+ with tempfile.TemporaryDirectory() as tempdir:
70
+ path = Path(tempdir) / "test.gpkg"
71
+ area = Area(left=58, top=74, right=180, bottom=42)
72
+ make_vectors_with_id(42, {area}, path)
73
+ assert path.exists
74
+
75
+ raster = RasterLayer(gdal_dataset_of_region(Area(left=59.93, top=70.07, right=170.04, bottom=44.98), 0.13))
76
+ vector = VectorLayer.layer_from_file(path, None, None, None)
77
+ assert vector.area == area
78
+
79
+ layers = [raster, vector]
80
+ union = RasterLayer.find_union(layers)
81
+ assert union == vector.area
82
+
83
+ def test_find_union_with_vector_bound() -> None:
84
+ with tempfile.TemporaryDirectory() as tempdir:
85
+ path = Path(tempdir) / "test.gpkg"
86
+ area = Area(left=58, top=74, right=180, bottom=42)
87
+ make_vectors_with_id(42, {area}, path)
88
+ assert path.exists
89
+
90
+ raster = RasterLayer(gdal_dataset_of_region(Area(left=59.93, top=70.07, right=170.04, bottom=44.98), 0.13))
91
+ vector = VectorLayer.layer_from_file(path, None, raster.map_projection.scale, raster.map_projection.name)
92
+ assert vector.area != area
93
+
94
+ layers = [raster, vector]
95
+ union = RasterLayer.find_union(layers)
96
+ assert union == vector.area
97
+
65
98
  @pytest.mark.parametrize("scale", [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09])
66
99
  def test_set_union_self(scale) -> None:
67
100
  layer = RasterLayer(gdal_dataset_of_region(Area(-10, 10, 10, -10), scale))
@@ -38,6 +38,7 @@ class operators(Enum):
38
38
  FLOOR = 33
39
39
  ROUND = 34
40
40
  CEIL = 35
41
+ ISNAN = 36
41
42
 
42
43
  class dtype(Enum):
43
44
  Float32 = gdal.GDT_Float32
@@ -40,6 +40,7 @@ maximum = mx.maximum
40
40
  minimum = mx.minimum
41
41
  zeros = mx.zeros
42
42
  pad = mx.pad
43
+ isnan = mx.isnan
43
44
  isscalar = np.isscalar
44
45
  full = mx.full
45
46
  allclose = mx.allclose
@@ -218,4 +219,5 @@ operator_map : Dict[op,Callable] = {
218
219
  op.FLOOR: mx.floor,
219
220
  op.ROUND: mx.round,
220
221
  op.CEIL: mx.ceil,
222
+ op.ISNAN: mx.isnan,
221
223
  }
@@ -43,6 +43,7 @@ minimum = np.minimum
43
43
  zeros = np.zeros
44
44
  pad = np.pad
45
45
  sum_op = lambda a: np.sum(a.astype(np.float64))
46
+ isnan = np.isnan
46
47
  isscalar = np.isscalar
47
48
  full = np.full
48
49
  allclose = np.allclose
@@ -156,4 +157,5 @@ operator_map : Dict[op,Callable] = {
156
157
  op.FLOOR: np.floor,
157
158
  op.ROUND: np.round,
158
159
  op.CEIL: np.ceil,
160
+ op.ISNAN: np.isnan,
159
161
  }
@@ -91,7 +91,7 @@ class YirgacheffeLayer(LayerMathMixin):
91
91
  else:
92
92
  return self._underlying_area
93
93
 
94
- def _get_operation_area(self, projection: Optional[MapProjection]) -> Area:
94
+ def _get_operation_area(self, projection: Optional[MapProjection]=None) -> Area:
95
95
  if self._projection is not None and projection is not None and self._projection != projection:
96
96
  raise ValueError("Calculation projection does not match layer projection")
97
97
  return self.area
@@ -119,11 +119,12 @@ class YirgacheffeLayer(LayerMathMixin):
119
119
  if not all(projections[0] == x for x in projections[1:]):
120
120
  raise ValueError("Not all layers are at the same projectin or pixel scale")
121
121
 
122
+ layer_areas = [x._get_operation_area() for x in layers]
122
123
  intersection = Area(
123
- left=max(x._underlying_area.left for x in layers),
124
- top=min(x._underlying_area.top for x in layers),
125
- right=min(x._underlying_area.right for x in layers),
126
- bottom=max(x._underlying_area.bottom for x in layers)
124
+ left=max(x.left for x in layer_areas),
125
+ top=min(x.top for x in layer_areas),
126
+ right=min(x.right for x in layer_areas),
127
+ bottom=max(x.bottom for x in layer_areas)
127
128
  )
128
129
  if (intersection.left >= intersection.right) or (intersection.bottom >= intersection.top):
129
130
  raise ValueError('No intersection possible')
@@ -142,11 +143,12 @@ class YirgacheffeLayer(LayerMathMixin):
142
143
  if not all(projections[0] == x for x in projections[1:]):
143
144
  raise ValueError("Not all layers are at the same projectin or pixel scale")
144
145
 
146
+ layer_areas = [x._get_operation_area() for x in layers]
145
147
  return Area(
146
- left=min(x._underlying_area.left for x in layers),
147
- top=max(x._underlying_area.top for x in layers),
148
- right=max(x._underlying_area.right for x in layers),
149
- bottom=min(x._underlying_area.bottom for x in layers)
148
+ left=min(x.left for x in layer_areas),
149
+ top=max(x.top for x in layer_areas),
150
+ right=max(x.right for x in layer_areas),
151
+ bottom=min(x.bottom for x in layer_areas)
150
152
  )
151
153
 
152
154
  @property
@@ -114,8 +114,9 @@ class GroupLayer(YirgacheffeLayer):
114
114
  if (xsize <= 0) or (ysize <= 0):
115
115
  raise ValueError("Request dimensions must be positive and non-zero")
116
116
 
117
- scale = self.pixel_scale
118
- assert scale is not None
117
+ map_projection = self.map_projection
118
+ assert map_projection is not None
119
+ scale = map_projection.scale
119
120
 
120
121
  target_window = Window(
121
122
  window.xoff + xoffset,
@@ -151,7 +152,7 @@ class GroupLayer(YirgacheffeLayer):
151
152
  intersection.ysize
152
153
  )
153
154
  if layer.nodata is not None:
154
- data[data.isnan()] = 0.0
155
+ data = backend.where(backend.isnan(data), 0.0, data)
155
156
  return data
156
157
 
157
158
  result = np.zeros((ysize, xsize), dtype=float)
@@ -139,6 +139,13 @@ class LayerMathMixin:
139
139
  test_elements=test_elements,
140
140
  )
141
141
 
142
+ def isnan(self):
143
+ return LayerOperation(
144
+ self,
145
+ op.ISNAN,
146
+ window_op=WindowOperation.NONE,
147
+ )
148
+
142
149
  def abs(self):
143
150
  return LayerOperation(
144
151
  self,
@@ -659,7 +666,9 @@ class LayerOperation(LayerMathMixin):
659
666
 
660
667
  if (computation_window.xsize != destination_window.xsize) \
661
668
  or (computation_window.ysize != destination_window.ysize):
662
- raise ValueError("Destination raster window size does not match input raster window size.")
669
+ raise ValueError((f"Destination raster window size does not match input raster window size: "
670
+ f"{(destination_window.xsize, destination_window.ysize)} vs "
671
+ f"{(computation_window.xsize, computation_window.ysize)}"))
663
672
 
664
673
  total = 0.0
665
674
 
@@ -1,10 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.7.2
3
+ Version: 1.7.4
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
7
7
  Project-URL: Homepage, https://github.com/quantifyearth/yirgacheffe
8
+ Project-URL: Repository, https://github.com/quantifyearth/yirgacheffe.git
9
+ Project-URL: Issues, https://github.com/quantifyearth/yirgacheffe/issues
10
+ Project-URL: Changelog, https://github.com/quantifyearth/yirgacheffe/blob/main/CHANGES.md
8
11
  Keywords: gdal,numpy,math
9
12
  Requires-Python: >=3.10
10
13
  Description-Content-Type: text/markdown
File without changes
File without changes
File without changes
File without changes