yirgacheffe 1.9.3__tar.gz → 1.9.5__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 (59) hide show
  1. {yirgacheffe-1.9.3/yirgacheffe.egg-info → yirgacheffe-1.9.5}/PKG-INFO +33 -2
  2. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/README.md +32 -1
  3. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/pyproject.toml +1 -1
  4. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_datatypes.py +46 -0
  5. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_multiband.py +7 -5
  6. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_openers.py +26 -0
  7. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_operators.py +51 -24
  8. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_reduce.py +4 -3
  9. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/__init__.py +3 -2
  10. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/enumeration.py +30 -1
  11. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_core.py +56 -7
  12. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_operators.py +93 -27
  13. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/group.py +1 -1
  14. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/operators.py +1 -1
  15. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/window.py +9 -0
  16. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5/yirgacheffe.egg-info}/PKG-INFO +33 -2
  17. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/LICENSE +0 -0
  18. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/MANIFEST.in +0 -0
  19. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/setup.cfg +0 -0
  20. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_area.py +0 -0
  21. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_auto_windowing.py +0 -0
  22. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_constants.py +0 -0
  23. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_group.py +0 -0
  24. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_h3layer.py +0 -0
  25. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_intersection.py +0 -0
  26. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_nodata.py +0 -0
  27. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_optimisation.py +0 -0
  28. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_parallel_operators.py +0 -0
  29. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_pickle.py +0 -0
  30. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_pixel_coord.py +0 -0
  31. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_projection.py +0 -0
  32. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_raster.py +0 -0
  33. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_rescaling.py +0 -0
  34. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_rounding.py +0 -0
  35. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_save_with_window.py +0 -0
  36. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_sum_with_window.py +0 -0
  37. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_uniform_area_layer.py +0 -0
  38. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_union.py +0 -0
  39. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_vectors.py +0 -0
  40. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_window.py +0 -0
  41. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/__init__.py +0 -0
  42. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/mlx.py +0 -0
  43. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/numpy.py +0 -0
  44. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/constants.py +0 -0
  45. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/__init__.py +0 -0
  46. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/area.py +0 -0
  47. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/base.py +0 -0
  48. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/constant.py +0 -0
  49. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/h3layer.py +0 -0
  50. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/rasters.py +0 -0
  51. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/rescaled.py +0 -0
  52. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/vectors.py +0 -0
  53. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/py.typed +0 -0
  54. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/rounding.py +0 -0
  55. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/SOURCES.txt +0 -0
  56. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  57. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/entry_points.txt +0 -0
  58. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/requires.txt +0 -0
  59. {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/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.9.3
3
+ Version: 1.9.5
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
@@ -106,11 +106,42 @@ with (
106
106
  yg.read_shape('species123.geojson') as range_map,
107
107
  ):
108
108
  refined_habitat = habitat_map.isin([...species habitat codes...])
109
- refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
109
+ refined_elevation = (elevation_map >= species_min) & (elevation_map <= species_max)
110
110
  aoh = refined_habitat * refined_elevation * range_polygon * area_per_pixel_map
111
111
  print(f'Area of habitat: {aoh.sum()}')
112
112
  ```
113
113
 
114
+ ## Citation
115
+
116
+ If you use Yirgacheffe in your research, please cite our paper:
117
+
118
+ > Michael Winston Dales, Alison Eyres, Patrick Ferris, Francesca A. Ridley, Simon Tarr, and Anil Madhavapeddy. 2025. Yirgacheffe: A Declarative Approach to Geospatial Data. In *Proceedings of the 2nd ACM SIGPLAN International Workshop on Programming for the Planet* (PROPL '25). Association for Computing Machinery, New York, NY, USA, 47–54. https://doi.org/10.1145/3759536.3763806
119
+
120
+ <details>
121
+ <summary>BibTeX</summary>
122
+
123
+ ```bibtex
124
+ @inproceedings{10.1145/3759536.3763806,
125
+ author = {Dales, Michael Winston and Eyres, Alison and Ferris, Patrick and Ridley, Francesca A. and Tarr, Simon and Madhavapeddy, Anil},
126
+ title = {Yirgacheffe: A Declarative Approach to Geospatial Data},
127
+ year = {2025},
128
+ isbn = {9798400721618},
129
+ publisher = {Association for Computing Machinery},
130
+ address = {New York, NY, USA},
131
+ url = {https://doi.org/10.1145/3759536.3763806},
132
+ doi = {10.1145/3759536.3763806},
133
+ abstract = {We present Yirgacheffe, a declarative geospatial library that allows spatial algorithms to be implemented concisely, supports parallel execution, and avoids common errors by automatically handling data (large geospatial rasters) and resources (cores, memory, GPUs). Our primary user domain comprises ecologists, where a typical problem involves cleaning messy occurrence data, overlaying it over tiled rasters, combining layers, and deriving actionable insights from the results. We describe the successes of this approach towards driving key pipelines related to global biodiversity and describe the capability gaps that remain, hoping to motivate more research into geospatial domain-specific languages.},
134
+ booktitle = {Proceedings of the 2nd ACM SIGPLAN International Workshop on Programming for the Planet},
135
+ pages = {47–54},
136
+ numpages = {8},
137
+ keywords = {Biodiversity, Declarative, Geospatial, Python},
138
+ location = {Singapore, Singapore},
139
+ series = {PROPL '25}
140
+ }
141
+ ```
142
+
143
+ </details>
144
+
114
145
  ## Thanks
115
146
 
116
147
  Thanks to discussion and feedback from my colleagues, particularly Alison Eyres, Patrick Ferris, Amelia Holcomb, and Anil Madhavapeddy.
@@ -59,11 +59,42 @@ with (
59
59
  yg.read_shape('species123.geojson') as range_map,
60
60
  ):
61
61
  refined_habitat = habitat_map.isin([...species habitat codes...])
62
- refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
62
+ refined_elevation = (elevation_map >= species_min) & (elevation_map <= species_max)
63
63
  aoh = refined_habitat * refined_elevation * range_polygon * area_per_pixel_map
64
64
  print(f'Area of habitat: {aoh.sum()}')
65
65
  ```
66
66
 
67
+ ## Citation
68
+
69
+ If you use Yirgacheffe in your research, please cite our paper:
70
+
71
+ > Michael Winston Dales, Alison Eyres, Patrick Ferris, Francesca A. Ridley, Simon Tarr, and Anil Madhavapeddy. 2025. Yirgacheffe: A Declarative Approach to Geospatial Data. In *Proceedings of the 2nd ACM SIGPLAN International Workshop on Programming for the Planet* (PROPL '25). Association for Computing Machinery, New York, NY, USA, 47–54. https://doi.org/10.1145/3759536.3763806
72
+
73
+ <details>
74
+ <summary>BibTeX</summary>
75
+
76
+ ```bibtex
77
+ @inproceedings{10.1145/3759536.3763806,
78
+ author = {Dales, Michael Winston and Eyres, Alison and Ferris, Patrick and Ridley, Francesca A. and Tarr, Simon and Madhavapeddy, Anil},
79
+ title = {Yirgacheffe: A Declarative Approach to Geospatial Data},
80
+ year = {2025},
81
+ isbn = {9798400721618},
82
+ publisher = {Association for Computing Machinery},
83
+ address = {New York, NY, USA},
84
+ url = {https://doi.org/10.1145/3759536.3763806},
85
+ doi = {10.1145/3759536.3763806},
86
+ abstract = {We present Yirgacheffe, a declarative geospatial library that allows spatial algorithms to be implemented concisely, supports parallel execution, and avoids common errors by automatically handling data (large geospatial rasters) and resources (cores, memory, GPUs). Our primary user domain comprises ecologists, where a typical problem involves cleaning messy occurrence data, overlaying it over tiled rasters, combining layers, and deriving actionable insights from the results. We describe the successes of this approach towards driving key pipelines related to global biodiversity and describe the capability gaps that remain, hoping to motivate more research into geospatial domain-specific languages.},
87
+ booktitle = {Proceedings of the 2nd ACM SIGPLAN International Workshop on Programming for the Planet},
88
+ pages = {47–54},
89
+ numpages = {8},
90
+ keywords = {Biodiversity, Declarative, Geospatial, Python},
91
+ location = {Singapore, Singapore},
92
+ series = {PROPL '25}
93
+ }
94
+ ```
95
+
96
+ </details>
97
+
67
98
  ## Thanks
68
99
 
69
100
  Thanks to discussion and feedback from my colleagues, particularly Alison Eyres, Patrick Ferris, Amelia Holcomb, and Anil Madhavapeddy.
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "yirgacheffe"
9
- version = "1.9.3"
9
+ version = "1.9.5"
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" }]
@@ -64,3 +64,49 @@ def test_float_to_int() -> None:
64
64
  expected = backend.promote(np.array([[1, 2, 3, 4], [5, 6, 7, 8]]))
65
65
  actual = result.read_array(0, 0, 4, 2)
66
66
  assert (expected == actual).all()
67
+
68
+ @pytest.mark.parametrize("array,expected_type", [
69
+ (
70
+ np.ones((2, 2)).astype(np.int8),
71
+ DataType.Int8,
72
+ ),
73
+ (
74
+ np.ones((2, 2)).astype(np.int16),
75
+ DataType.Int16,
76
+ ),
77
+ (
78
+ np.ones((2, 2)).astype(np.int32),
79
+ DataType.Int32,
80
+ ),
81
+ (
82
+ np.ones((2, 2)).astype(np.int64),
83
+ DataType.Int64,
84
+ ),
85
+ (
86
+ np.ones((2, 2)).astype(np.uint8),
87
+ DataType.UInt8,
88
+ ),
89
+ (
90
+ np.ones((2, 2)).astype(np.uint16),
91
+ DataType.UInt16,
92
+ ),
93
+ (
94
+ np.ones((2, 2)).astype(np.uint32),
95
+ DataType.UInt32,
96
+ ),
97
+ (
98
+ np.ones((2, 2)).astype(np.uint64),
99
+ DataType.UInt64,
100
+ ),
101
+ (
102
+ np.ones((2, 2)).astype(np.float32),
103
+ DataType.Float32,
104
+ ),
105
+ (
106
+ np.ones((2, 2)).astype(np.float64),
107
+ DataType.Float64,
108
+ ),
109
+ ])
110
+ def test_of_Array(array: np.ndarray, expected_type: DataType) -> None:
111
+ ytype = DataType.of_array(array)
112
+ assert ytype == expected_type
@@ -4,7 +4,7 @@ import tempfile
4
4
  import numpy as np
5
5
  from osgeo import gdal
6
6
 
7
- from tests.helpers import gdal_dataset_with_data
7
+ import yirgacheffe as yg
8
8
  from yirgacheffe.layers import RasterLayer
9
9
  from yirgacheffe.window import Area, PixelScale
10
10
 
@@ -23,9 +23,10 @@ def test_simple_two_band_image() -> None:
23
23
  )
24
24
 
25
25
  # Create a set of rasters in turn to fill each band
26
+ projection = yg.MapProjection("epsg:4326", 1.0, -1.0)
26
27
  for i in range(bands):
27
28
  data1 = np.full((2, 2), i+1)
28
- layer1 = RasterLayer(gdal_dataset_with_data((-1.0, 1.0), 1.0, data1))
29
+ layer1 = yg.from_array(data1, (-1.0, 1.0), projection)
29
30
  layer1.save(target, band=i+1)
30
31
 
31
32
  # force things to disk
@@ -33,7 +34,7 @@ def test_simple_two_band_image() -> None:
33
34
 
34
35
  #check they do what we expect
35
36
  for i in range(bands):
36
- o = RasterLayer.layer_from_file(target_path, band=i+1)
37
+ o = yg.read_raster(target_path, band=i+1)
37
38
  assert o.sum() == (4 * (i + 1))
38
39
 
39
40
  def test_stack_tifs_with_area_match() -> None:
@@ -43,9 +44,10 @@ def test_stack_tifs_with_area_match() -> None:
43
44
  # slight alignment offset when we create them)
44
45
  bands = 4
45
46
  source_layers = []
47
+ projection = yg.MapProjection("epsg:4326", 1.0, -1.0)
46
48
  for i in range(bands):
47
49
  data1 = np.full((100, 100), i+1)
48
- layer1 = RasterLayer(gdal_dataset_with_data((-100+i, 100+i), 1, data1))
50
+ layer1 = yg.from_array(data1, (-100+i, 100+i), projection)
49
51
  source_layers.append(layer1)
50
52
 
51
53
  intersection = RasterLayer.find_intersection(source_layers)
@@ -66,5 +68,5 @@ def test_stack_tifs_with_area_match() -> None:
66
68
 
67
69
  #check they do what we expect
68
70
  for i in range(bands):
69
- o = RasterLayer.layer_from_file(target_path, band=i+1)
71
+ o = yg.read_raster(target_path, band=i+1)
70
72
  assert o.sum() == ((100 - (bands - 1)) * (100 - (bands - 1)) * (i + 1))
@@ -264,3 +264,29 @@ def test_constant() -> None:
264
264
  expected = np.full((20, 20), 42.0)
265
265
  actual = result.read_array(0, 0, 20, 20)
266
266
  assert (expected == actual).all()
267
+
268
+ def test_create_simple_float() -> None:
269
+ projection = MapProjection(WGS_84_PROJECTION, 1.0, -1.0)
270
+ data = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]])
271
+ with yg.from_array(data, (-2.0, 1.0), projection) as layer:
272
+ expected_area = Area(left=-2.0, right=2.0, top=1.0, bottom=-1.0)
273
+
274
+ assert layer.map_projection == projection
275
+ assert layer.area == expected_area
276
+ assert layer.datatype == DataType.Float64
277
+
278
+ actual = layer.read_array(0, 0, 4, 2)
279
+ assert (data == actual).all()
280
+
281
+ def test_create_simple_direct_projection() -> None:
282
+ expected_projection = MapProjection(WGS_84_PROJECTION, 1.0, -1.0)
283
+ data = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
284
+ with yg.from_array(data, (-2.0, 1.0), (WGS_84_PROJECTION, (1.0, -1.0))) as layer:
285
+ expected_area = Area(left=-2.0, right=2.0, top=1.0, bottom=-1.0)
286
+
287
+ assert layer.map_projection == expected_projection
288
+ assert layer.area == expected_area
289
+ assert layer.datatype == DataType.Int64
290
+
291
+ actual = layer.read_array(0, 0, 4, 2)
292
+ assert (data == actual).all()
@@ -7,7 +7,7 @@ import numpy as np
7
7
  import pytest
8
8
  import torch
9
9
 
10
- import yirgacheffe
10
+ import yirgacheffe as yg
11
11
  from yirgacheffe.window import Area, PixelScale
12
12
  from yirgacheffe.layers import ConstantLayer, RasterLayer, VectorLayer
13
13
  from yirgacheffe.operators import DataType
@@ -91,6 +91,29 @@ def test_add_byte_layers_with_callback(skip, expected_steps) -> None:
91
91
 
92
92
  assert callback_positions == expected_steps
93
93
 
94
+ @pytest.mark.parametrize("skip,expected_steps", [
95
+ (1, [0.0, 0.5, 1.0]),
96
+ (2, [0.0, 1.0]),
97
+ ])
98
+ def test_add_byte_layers_to_geotiff_with_callback(skip, expected_steps) -> None:
99
+ data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]]).astype(np.uint8)
100
+ data2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]]).astype(np.uint8)
101
+
102
+ with (
103
+ RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
104
+ RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
105
+ ):
106
+ callback_positions: list[float] = []
107
+
108
+ comp = layer1 + layer2
109
+ comp.ystep = skip
110
+
111
+ with tempfile.TemporaryDirectory() as tempdir:
112
+ filename = os.path.join(tempdir, "test.tif")
113
+ comp.to_geotiff(filename, callback=callback_positions.append)
114
+
115
+ assert callback_positions == expected_steps
116
+
94
117
  def test_sub_byte_layers() -> None:
95
118
  data1 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]])
96
119
  data2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
@@ -596,7 +619,7 @@ def test_direct_layer_save_and_sum() -> None:
596
619
  assert (data1 == actual_data).all()
597
620
  assert expected_sum == actual_sum
598
621
 
599
- @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
622
+ @pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
600
623
  def test_add_to_float_layer_by_np_array() -> None:
601
624
  data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]])
602
625
  layer1 = RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1))
@@ -631,7 +654,7 @@ def test_write_mulitband_raster() -> None:
631
654
 
632
655
  assert (expected == actual).all()
633
656
 
634
- @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
657
+ @pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
635
658
  def test_save_and_sum_float32(monkeypatch) -> None:
636
659
  random.seed(42)
637
660
  data = []
@@ -650,12 +673,12 @@ def test_save_and_sum_float32(monkeypatch) -> None:
650
673
 
651
674
  with monkeypatch.context() as m:
652
675
  for blocksize in range(1,11):
653
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
676
+ m.setattr(yg.constants, "YSTEP", blocksize)
654
677
  with RasterLayer.empty_raster_layer_like(layer1) as store:
655
678
  actual = layer1.save(store, and_sum=True)
656
679
  assert expected == actual
657
680
 
658
- @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
681
+ @pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
659
682
  def test_parallel_save_and_sum_float32(monkeypatch) -> None:
660
683
  random.seed(42)
661
684
  data = []
@@ -678,12 +701,12 @@ def test_parallel_save_and_sum_float32(monkeypatch) -> None:
678
701
 
679
702
  with monkeypatch.context() as m:
680
703
  for blocksize in range(1,11):
681
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
704
+ m.setattr(yg.constants, "YSTEP", blocksize)
682
705
  with RasterLayer.empty_raster_layer_like(layer1) as store:
683
706
  actual = layer1.parallel_save(store, and_sum=True)
684
707
  assert expected == actual
685
708
 
686
- @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
709
+ @pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
687
710
  def test_sum_float32(monkeypatch) -> None:
688
711
  random.seed(42)
689
712
  data = []
@@ -702,7 +725,7 @@ def test_sum_float32(monkeypatch) -> None:
702
725
 
703
726
  with monkeypatch.context() as m:
704
727
  for blocksize in range(1,11):
705
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
728
+ m.setattr(yg.constants, "YSTEP", blocksize)
706
729
  actual = layer1.sum()
707
730
  assert expected == actual
708
731
 
@@ -1488,21 +1511,21 @@ def test_to_geotiff_single_thread_and_sum() -> None:
1488
1511
  actual = result.read_array(0, 0, 4, 2)
1489
1512
  assert (expected == actual).all()
1490
1513
 
1491
- @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
1514
+ @pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
1492
1515
  @pytest.mark.parametrize("parallelism", [
1493
1516
  2,
1494
1517
  True,
1495
1518
  ])
1496
1519
  def test_to_geotiff_parallel_thread(monkeypatch, parallelism) -> None:
1497
1520
  with monkeypatch.context() as m:
1498
- m.setattr(yirgacheffe.constants, "YSTEP", 1)
1521
+ m.setattr(yg.constants, "YSTEP", 1)
1499
1522
  m.setattr(LayerOperation, "save", None)
1500
1523
  with tempfile.TemporaryDirectory() as tempdir:
1501
1524
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
1502
1525
  src_filename = os.path.join("src.tif")
1503
1526
  dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1, filename=src_filename)
1504
1527
  dataset.Close()
1505
- with yirgacheffe.read_raster(src_filename) as layer1:
1528
+ with yg.read_raster(src_filename) as layer1:
1506
1529
  calc = layer1 * 2
1507
1530
  filename = os.path.join(tempdir, "test.tif")
1508
1531
  calc.to_geotiff(filename, parallelism=parallelism)
@@ -1512,27 +1535,30 @@ def test_to_geotiff_parallel_thread(monkeypatch, parallelism) -> None:
1512
1535
  actual = result.read_array(0, 0, 4, 2)
1513
1536
  assert (expected == actual).all()
1514
1537
 
1515
- @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
1538
+ @pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
1516
1539
  @pytest.mark.parametrize("parallelism", [
1517
1540
  2,
1518
1541
  True,
1519
1542
  ])
1520
1543
  def test_to_geotiff_parallel_thread_and_sum(monkeypatch, parallelism) -> None:
1521
1544
  with monkeypatch.context() as m:
1522
- m.setattr(yirgacheffe.constants, "YSTEP", 1)
1545
+ m.setattr(yg.constants, "YSTEP", 1)
1523
1546
  m.setattr(LayerOperation, "save", None)
1524
1547
  with tempfile.TemporaryDirectory() as tempdir:
1525
1548
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
1526
1549
  src_filename = os.path.join("src.tif")
1527
1550
  dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1, filename=src_filename)
1528
1551
  dataset.Close()
1529
- with yirgacheffe.read_raster(src_filename) as layer1:
1552
+ with yg.read_raster(src_filename) as layer1:
1530
1553
  filename = os.path.join(tempdir, "test.tif")
1531
1554
  calc = layer1 * 2
1532
- actual_sum = calc.to_geotiff(filename, and_sum=True, parallelism=parallelism)
1555
+ steps: list[float] = []
1556
+ actual_sum = calc.to_geotiff(filename, and_sum=True, parallelism=parallelism, callback=steps.append)
1533
1557
 
1534
1558
  assert (data1.sum() * 2) == actual_sum
1535
1559
 
1560
+ assert steps == [0.0, 0.5, 1.0]
1561
+
1536
1562
  with RasterLayer.layer_from_file(filename) as result:
1537
1563
  expected = data1 * 2
1538
1564
  actual = result.read_array(0, 0, 4, 2)
@@ -1548,7 +1574,7 @@ def test_raster_and_vector() -> None:
1548
1574
  make_vectors_with_id(42, {area}, path)
1549
1575
  assert path.exists()
1550
1576
 
1551
- vector = VectorLayer.layer_from_file(path, None, PixelScale(1.0, -1.0), yirgacheffe.WGS_84_PROJECTION)
1577
+ vector = VectorLayer.layer_from_file(path, None, PixelScale(1.0, -1.0), yg.WGS_84_PROJECTION)
1552
1578
 
1553
1579
  calc = raster * vector
1554
1580
  assert calc.sum() > 0.0
@@ -1564,7 +1590,7 @@ def test_raster_and_vector_mixed_projection() -> None:
1564
1590
  make_vectors_with_id(42, {area}, path)
1565
1591
  assert path.exists()
1566
1592
 
1567
- vector = VectorLayer.layer_from_file(path, None, PixelScale(1.0, -1.0), yirgacheffe.WGS_84_PROJECTION)
1593
+ vector = VectorLayer.layer_from_file(path, None, PixelScale(1.0, -1.0), yg.WGS_84_PROJECTION)
1568
1594
 
1569
1595
  with pytest.raises(ValueError):
1570
1596
  _ = raster * vector
@@ -1600,7 +1626,7 @@ def test_isnan() -> None:
1600
1626
  @pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
1601
1627
  def test_add_byte_layers_read_array_all(monkeypatch, blocksize) -> None:
1602
1628
  with monkeypatch.context() as m:
1603
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
1629
+ m.setattr(yg.constants, "YSTEP", blocksize)
1604
1630
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
1605
1631
  data2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]])
1606
1632
 
@@ -1616,7 +1642,7 @@ def test_add_byte_layers_read_array_all(monkeypatch, blocksize) -> None:
1616
1642
  @pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
1617
1643
  def test_add_byte_layers_read_array_partial_horizontal(monkeypatch, blocksize) -> None:
1618
1644
  with monkeypatch.context() as m:
1619
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
1645
+ m.setattr(yg.constants, "YSTEP", blocksize)
1620
1646
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
1621
1647
  data2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]])
1622
1648
 
@@ -1632,7 +1658,7 @@ def test_add_byte_layers_read_array_partial_horizontal(monkeypatch, blocksize) -
1632
1658
  @pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
1633
1659
  def test_add_byte_layers_read_array_partial_vertical(monkeypatch, blocksize) -> None:
1634
1660
  with monkeypatch.context() as m:
1635
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
1661
+ m.setattr(yg.constants, "YSTEP", blocksize)
1636
1662
  data1 = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
1637
1663
  data2 = np.array([[10, 20], [30, 40], [50, 60], [70, 80]])
1638
1664
 
@@ -1648,7 +1674,7 @@ def test_add_byte_layers_read_array_partial_vertical(monkeypatch, blocksize) ->
1648
1674
  @pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
1649
1675
  def test_add_byte_layers_read_array_partial(monkeypatch, blocksize) -> None:
1650
1676
  with monkeypatch.context() as m:
1651
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
1677
+ m.setattr(yg.constants, "YSTEP", blocksize)
1652
1678
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
1653
1679
  data2 = np.array([[10, 10, 10, 10], [20, 20, 20, 20], [30, 30, 30, 30], [40, 40, 40, 40]])
1654
1680
 
@@ -1664,13 +1690,14 @@ def test_add_byte_layers_read_array_partial(monkeypatch, blocksize) -> None:
1664
1690
  @pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
1665
1691
  def test_add_byte_layers_read_array_superset(monkeypatch, blocksize) -> None:
1666
1692
  with monkeypatch.context() as m:
1667
- m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
1693
+ m.setattr(yg.constants, "YSTEP", blocksize)
1668
1694
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
1669
1695
  data2 = np.array([[10, 10, 10, 10], [20, 20, 20, 20], [30, 30, 30, 30], [40, 40, 40, 40]])
1670
1696
 
1697
+ projection = yg.MapProjection("epsg:4326", 0.02, -0.02)
1671
1698
  with (
1672
- RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
1673
- RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
1699
+ yg.from_array(data1, (0.0, 0.0), projection) as layer1,
1700
+ yg.from_array(data2, (0.0, 0.0), projection) as layer2,
1674
1701
  ):
1675
1702
  comp = layer1 + layer2
1676
1703
  inner_expected = data1 + data2
@@ -3,8 +3,7 @@ import operator
3
3
 
4
4
  import numpy as np
5
5
 
6
- from yirgacheffe.layers import RasterLayer
7
- from tests.helpers import gdal_dataset_with_data
6
+ import yirgacheffe as yg
8
7
 
9
8
  def test_add_similar_layers() -> None:
10
9
  data = [
@@ -13,7 +12,9 @@ def test_add_similar_layers() -> None:
13
12
  np.array([[100, 200, 300, 400], [500, 600, 700, 800]]),
14
13
  ]
15
14
 
16
- layers = [RasterLayer(gdal_dataset_with_data((0,0), 1.0, x)) for x in data]
15
+ origin = (0.0, 0.0)
16
+ map_projection = yg.MapProjection("epsg:4326", 1.0, -1.0)
17
+ layers = [yg.from_array(x, origin, map_projection) for x in data]
17
18
 
18
19
  summed_layers = reduce(operator.add, layers)
19
20
  actual = summed_layers.read_array(0, 0, 4, 2)
@@ -12,12 +12,13 @@ except ModuleNotFoundError:
12
12
  pyproject_data = tomllib.load(f)
13
13
  __version__ = pyproject_data["project"]["version"]
14
14
 
15
- from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster
15
+ from .layers import YirgacheffeLayer
16
+ from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster, from_array
16
17
  from .constants import WGS_84_PROJECTION
17
18
  from .window import Area, MapProjection, Window
18
19
  from ._backends.enumeration import dtype as DataType
19
20
 
20
- from ._operators import where, minumum, maximum, clip, log, log2, log10, exp, exp2, nan_to_num, isin, \
21
+ from ._operators import where, minimum, maximum, clip, log, log2, log10, exp, exp2, nan_to_num, isin, \
21
22
  floor, ceil # pylint: disable=W0611
22
23
  from ._operators import abs, round # pylint: disable=W0611,W0622
23
24
 
@@ -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):
@@ -77,5 +80,31 @@ class dtype(Enum):
77
80
  return self.value
78
81
 
79
82
  @classmethod
80
- def of_gdal(cls, val):
83
+ def of_gdal(cls, val: int) -> dtype:
81
84
  return cls(val)
85
+
86
+ @classmethod
87
+ def of_array(cls, val: np.ndarray) -> dtype:
88
+ match val.dtype:
89
+ case np.float32:
90
+ return dtype.Float32
91
+ case np.float64:
92
+ return dtype.Float64
93
+ case np.int8:
94
+ return dtype.Int8
95
+ case np.int16:
96
+ return dtype.Int16
97
+ case np.int32:
98
+ return dtype.Int32
99
+ case np.int64:
100
+ return dtype.Int64
101
+ case np.uint8:
102
+ return dtype.UInt8
103
+ case np.uint16:
104
+ return dtype.UInt16
105
+ case np.uint32:
106
+ return dtype.UInt32
107
+ case np.uint64:
108
+ return dtype.UInt64
109
+ case _:
110
+ raise ValueError
@@ -3,20 +3,22 @@ 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(
16
18
  filename: Path | str,
17
19
  band: int = 1,
18
20
  ignore_nodata: bool = False,
19
- ) -> RasterLayer:
21
+ ) -> YirgacheffeLayer:
20
22
  """Open a raster file (e.g., GeoTIFF).
21
23
 
22
24
  Args:
@@ -38,7 +40,7 @@ def read_narrow_raster(
38
40
  filename: Path | str,
39
41
  band: int = 1,
40
42
  ignore_nodata: bool = False,
41
- ) -> RasterLayer:
43
+ ) -> YirgacheffeLayer:
42
44
  """Open a 1 pixel wide raster file as a global raster.
43
45
 
44
46
  This exists for the special use case where an area per pixel raster would have the same value per horizontal row
@@ -58,7 +60,7 @@ def read_narrow_raster(
58
60
  def read_rasters(
59
61
  filenames : Sequence[Path | str],
60
62
  tiled: bool=False
61
- ) -> GroupLayer:
63
+ ) -> YirgacheffeLayer:
62
64
  """Open a set of raster files (e.g., GeoTIFFs) as a single layer.
63
65
 
64
66
  Args:
@@ -86,7 +88,7 @@ def read_shape(
86
88
  where_filter: str | None = None,
87
89
  datatype: DataType | None = None,
88
90
  burn_value: int | float | str = 1,
89
- ) -> VectorLayer:
91
+ ) -> YirgacheffeLayer:
90
92
  """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
91
93
 
92
94
  Args:
@@ -124,7 +126,7 @@ def read_shape_like(
124
126
  where_filter: str | None = None,
125
127
  datatype: DataType | None = None,
126
128
  burn_value: int | float | str = 1,
127
- ) -> VectorLayer:
129
+ ) -> YirgacheffeLayer:
128
130
  """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
129
131
 
130
132
  Args:
@@ -146,7 +148,7 @@ def read_shape_like(
146
148
  burn_value,
147
149
  )
148
150
 
149
- def constant(value: int | float) -> ConstantLayer:
151
+ def constant(value: int | float) -> YirgacheffeLayer:
150
152
  """Generate a layer that has the same value in all pixels regardless of scale, projection, and area.
151
153
 
152
154
  Generally this should not be necessary unless you must have the constant as the first term in an
@@ -161,3 +163,50 @@ def constant(value: int | float) -> ConstantLayer:
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
@@ -10,12 +10,12 @@ import sys
10
10
  import tempfile
11
11
  import time
12
12
  import types
13
+ from collections.abc import Callable
13
14
  from contextlib import ExitStack
14
15
  from enum import Enum
15
16
  from multiprocessing import Semaphore, Process
16
17
  from multiprocessing.managers import SharedMemoryManager
17
18
  from pathlib import Path
18
- from typing import Callable
19
19
 
20
20
  import deprecation
21
21
  import numpy as np
@@ -393,31 +393,34 @@ class LayerMathMixin:
393
393
  class LayerOperation(LayerMathMixin):
394
394
 
395
395
  @staticmethod
396
+ @deprecation.deprecated(
397
+ deprecated_in="1.10",
398
+ removed_in="2.0",
399
+ current_version=__version__,
400
+ details="Use from top level module instead."
401
+ )
396
402
  def where(cond, a, b):
397
- return LayerOperation(
398
- cond,
399
- op.WHERE,
400
- rhs=a,
401
- other=b
402
- )
403
+ return where(cond, a, b)
403
404
 
404
405
  @staticmethod
405
- def maximum(a, b):
406
- return LayerOperation(
407
- a,
408
- op.MAXIMUM,
409
- b,
410
- window_op=WindowOperation.UNION,
411
- )
406
+ @deprecation.deprecated(
407
+ deprecated_in="1.10",
408
+ removed_in="2.0",
409
+ current_version=__version__,
410
+ details="Use from top level module instead."
411
+ )
412
+ def minimum(a, b):
413
+ return minimum(a, b)
412
414
 
413
415
  @staticmethod
414
- def minimum(a, b):
415
- return LayerOperation(
416
- a,
417
- op.MINIMUM,
418
- rhs=b,
419
- window_op=WindowOperation.UNION,
420
- )
416
+ @deprecation.deprecated(
417
+ deprecated_in="1.10",
418
+ removed_in="2.0",
419
+ current_version=__version__,
420
+ details="Use from top level module instead."
421
+ )
422
+ def maximum(a, b):
423
+ return maximum(a, b)
421
424
 
422
425
  def __init__(
423
426
  self,
@@ -999,7 +1002,8 @@ class LayerOperation(LayerMathMixin):
999
1002
  self,
1000
1003
  filename: Path | str,
1001
1004
  and_sum: bool = False,
1002
- parallelism: int | bool | None = None
1005
+ parallelism: int | bool | None = None,
1006
+ callback: Callable[[float], None] | None = None,
1003
1007
  ) -> float | None:
1004
1008
  """Saves a calculation to a raster file, optionally also returning the sum of pixels.
1005
1009
 
@@ -1009,6 +1013,8 @@ class LayerOperation(LayerMathMixin):
1009
1013
  that value.
1010
1014
  parallelism: If passed, attempt to use multiple CPU cores up to the number provided, or if set to True,
1011
1015
  yirgacheffe will pick a sensible value.
1016
+ callback: If passed, this callback will be called periodically with a progress update for the saving,
1017
+ with a value between 0.0 and 1.0.
1012
1018
 
1013
1019
  Returns:
1014
1020
  Either returns None, or the sum of the pixels in the resulting raster if `and_sum` was specified.
@@ -1026,12 +1032,12 @@ class LayerOperation(LayerMathMixin):
1026
1032
  from yirgacheffe.layers.rasters import RasterLayer # type: ignore # pylint: disable=C0415
1027
1033
  with RasterLayer.empty_raster_layer_like(self, filename=tempory_file.name) as layer:
1028
1034
  if parallelism is None:
1029
- result = self.save(layer, and_sum=and_sum)
1035
+ result = self.save(layer, and_sum=and_sum, callback=callback)
1030
1036
  else:
1031
1037
  if isinstance(parallelism, bool):
1032
1038
  # Parallel save treats None as "work it out"
1033
1039
  parallelism = None
1034
- result = self.parallel_save(layer, and_sum=and_sum, parallelism=parallelism)
1040
+ result = self.parallel_save(layer, and_sum=and_sum, callback=callback, parallelism=parallelism)
1035
1041
 
1036
1042
  os.makedirs(target_dir, exist_ok=True)
1037
1043
  os.rename(src=tempory_file.name, dst=filename)
@@ -1121,10 +1127,70 @@ class ShaderStyleOperation(LayerOperation):
1121
1127
 
1122
1128
  return result
1123
1129
 
1130
+ def where(cond, a, b):
1131
+ """Return elements chosen from `a` or `b` depending on `cond`.
1132
+
1133
+ Behaves like numpy.where(condition, x, y), returning a layer operation
1134
+ where elements from `a` are selected where `cond` is True, and elements
1135
+ from `b` are selected where `cond` is False.
1136
+
1137
+ Args:
1138
+ cond: Layer or constant used as condition. Where True, yield `a`, otherwise yield `b`.
1139
+ a: Layer or constant with values from which to choose where `cond` is True.
1140
+ b: Layer or constant with values from which to choose where `cond` is False.
1141
+
1142
+ Returns:
1143
+ New layer representing the conditional selection.
1144
+ """
1145
+ return LayerOperation(
1146
+ cond,
1147
+ op.WHERE,
1148
+ rhs=a,
1149
+ other=b
1150
+ )
1151
+
1152
+ def maximum(a, b):
1153
+ """Element-wise maximum of layer elements.
1154
+
1155
+ Behaves like numpy.maximum(x1, x2), comparing two layers element-by-element
1156
+ and returning a new layer with the maximum values.
1157
+
1158
+ Args:
1159
+ a: First layer or constant to compare.
1160
+ b: Second layer or constant to compare.
1161
+
1162
+ Returns:
1163
+ New layer representing the element-wise maximum of the inputs.
1164
+ """
1165
+ return LayerOperation(
1166
+ a,
1167
+ op.MAXIMUM,
1168
+ b,
1169
+ window_op=WindowOperation.UNION,
1170
+ )
1171
+
1172
+ def minimum(a, b):
1173
+ """Element-wise minimum of layer elements.
1174
+
1175
+ Behaves like numpy.minimum(x1, x2), comparing two layers element-by-element
1176
+ and returning a new layer with the minimum values.
1177
+
1178
+ Args:
1179
+ a: First layer or constant to compare.
1180
+ b: Second layer or constant to compare.
1181
+
1182
+ Returns:
1183
+ New layer representing the element-wise minimum of the inputs.
1184
+ """
1185
+ return LayerOperation(
1186
+ a,
1187
+ op.MINIMUM,
1188
+ rhs=b,
1189
+ window_op=WindowOperation.UNION,
1190
+ )
1191
+
1192
+
1124
1193
  # We provide these module level accessors as it's often nicer to write `log(x/y)` rather than `(x/y).log()`
1125
- where = LayerOperation.where
1126
- minumum = LayerOperation.minimum
1127
- maximum = LayerOperation.maximum
1128
1194
  clip = LayerOperation.clip
1129
1195
  log = LayerOperation.log
1130
1196
  log2 = LayerOperation.log2
@@ -81,7 +81,7 @@ class GroupLayer(YirgacheffeLayer):
81
81
 
82
82
  @property
83
83
  def datatype(self) -> DataType:
84
- return DataType.of_gdal(self.layers[0].datatype)
84
+ return self.layers[0].datatype
85
85
 
86
86
  def set_window_for_intersection(self, new_area: Area) -> None:
87
87
  super().set_window_for_intersection(new_area)
@@ -1,7 +1,7 @@
1
1
  # Eventually all this should be moved to the top level in 2.0, but for backwards compatibility in 1.x needs
2
2
  # to remain here
3
3
 
4
- from ._operators import where, minumum, maximum, clip, log, log2, log10, exp, exp2, nan_to_num, isin, \
4
+ from ._operators import where, minimum, maximum, clip, log, log2, log10, exp, exp2, nan_to_num, isin, \
5
5
  floor, ceil # pylint: disable=W0611
6
6
  from ._operators import abs, round # pylint: disable=W0611,W0622
7
7
  from ._backends.enumeration import dtype as DataType # pylint: disable=W0611
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.9.3
3
+ Version: 1.9.5
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
@@ -106,11 +106,42 @@ with (
106
106
  yg.read_shape('species123.geojson') as range_map,
107
107
  ):
108
108
  refined_habitat = habitat_map.isin([...species habitat codes...])
109
- refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
109
+ refined_elevation = (elevation_map >= species_min) & (elevation_map <= species_max)
110
110
  aoh = refined_habitat * refined_elevation * range_polygon * area_per_pixel_map
111
111
  print(f'Area of habitat: {aoh.sum()}')
112
112
  ```
113
113
 
114
+ ## Citation
115
+
116
+ If you use Yirgacheffe in your research, please cite our paper:
117
+
118
+ > Michael Winston Dales, Alison Eyres, Patrick Ferris, Francesca A. Ridley, Simon Tarr, and Anil Madhavapeddy. 2025. Yirgacheffe: A Declarative Approach to Geospatial Data. In *Proceedings of the 2nd ACM SIGPLAN International Workshop on Programming for the Planet* (PROPL '25). Association for Computing Machinery, New York, NY, USA, 47–54. https://doi.org/10.1145/3759536.3763806
119
+
120
+ <details>
121
+ <summary>BibTeX</summary>
122
+
123
+ ```bibtex
124
+ @inproceedings{10.1145/3759536.3763806,
125
+ author = {Dales, Michael Winston and Eyres, Alison and Ferris, Patrick and Ridley, Francesca A. and Tarr, Simon and Madhavapeddy, Anil},
126
+ title = {Yirgacheffe: A Declarative Approach to Geospatial Data},
127
+ year = {2025},
128
+ isbn = {9798400721618},
129
+ publisher = {Association for Computing Machinery},
130
+ address = {New York, NY, USA},
131
+ url = {https://doi.org/10.1145/3759536.3763806},
132
+ doi = {10.1145/3759536.3763806},
133
+ abstract = {We present Yirgacheffe, a declarative geospatial library that allows spatial algorithms to be implemented concisely, supports parallel execution, and avoids common errors by automatically handling data (large geospatial rasters) and resources (cores, memory, GPUs). Our primary user domain comprises ecologists, where a typical problem involves cleaning messy occurrence data, overlaying it over tiled rasters, combining layers, and deriving actionable insights from the results. We describe the successes of this approach towards driving key pipelines related to global biodiversity and describe the capability gaps that remain, hoping to motivate more research into geospatial domain-specific languages.},
134
+ booktitle = {Proceedings of the 2nd ACM SIGPLAN International Workshop on Programming for the Planet},
135
+ pages = {47–54},
136
+ numpages = {8},
137
+ keywords = {Biodiversity, Declarative, Geospatial, Python},
138
+ location = {Singapore, Singapore},
139
+ series = {PROPL '25}
140
+ }
141
+ ```
142
+
143
+ </details>
144
+
114
145
  ## Thanks
115
146
 
116
147
  Thanks to discussion and feedback from my colleagues, particularly Alison Eyres, Patrick Ferris, Amelia Holcomb, and Anil Madhavapeddy.
File without changes
File without changes
File without changes