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.
- {yirgacheffe-1.9.3/yirgacheffe.egg-info → yirgacheffe-1.9.5}/PKG-INFO +33 -2
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/README.md +32 -1
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/pyproject.toml +1 -1
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_datatypes.py +46 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_multiband.py +7 -5
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_openers.py +26 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_operators.py +51 -24
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_reduce.py +4 -3
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/__init__.py +3 -2
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/enumeration.py +30 -1
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_core.py +56 -7
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_operators.py +93 -27
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/group.py +1 -1
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/operators.py +1 -1
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/window.py +9 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5/yirgacheffe.egg-info}/PKG-INFO +33 -2
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/LICENSE +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/MANIFEST.in +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/setup.cfg +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_area.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_auto_windowing.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_constants.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_group.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_h3layer.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_intersection.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_nodata.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_optimisation.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_parallel_operators.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_pickle.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_pixel_coord.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_projection.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_raster.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_rescaling.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_rounding.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_save_with_window.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_sum_with_window.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_uniform_area_layer.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_union.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_vectors.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/tests/test_window.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/__init__.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/mlx.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/_backends/numpy.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/constants.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/__init__.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/area.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/base.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/constant.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/h3layer.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/rasters.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/rescaled.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/layers/vectors.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/py.typed +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe/rounding.py +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/SOURCES.txt +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/dependency_links.txt +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/entry_points.txt +0 -0
- {yirgacheffe-1.9.3 → yirgacheffe-1.9.5}/yirgacheffe.egg-info/requires.txt +0 -0
- {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
|
+
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)
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
1552
|
+
with yg.read_raster(src_filename) as layer1:
|
|
1530
1553
|
filename = os.path.join(tempdir, "test.tif")
|
|
1531
1554
|
calc = layer1 * 2
|
|
1532
|
-
|
|
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),
|
|
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),
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1673
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 .
|
|
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,
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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) ->
|
|
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
|
|
398
|
-
cond,
|
|
399
|
-
op.WHERE,
|
|
400
|
-
rhs=a,
|
|
401
|
-
other=b
|
|
402
|
-
)
|
|
403
|
+
return where(cond, a, b)
|
|
403
404
|
|
|
404
405
|
@staticmethod
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
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,
|
|
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
|
+
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)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|