yirgacheffe 1.9.2__tar.gz → 1.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (62) hide show
  1. yirgacheffe-1.9.4/PKG-INFO +149 -0
  2. yirgacheffe-1.9.4/README.md +102 -0
  3. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/pyproject.toml +1 -1
  4. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_datatypes.py +1 -1
  5. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_intersection.py +5 -5
  6. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_openers.py +2 -2
  7. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_operators.py +40 -14
  8. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_parallel_operators.py +3 -3
  9. yirgacheffe-1.9.4/tests/test_reduce.py +23 -0
  10. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_rescaling.py +2 -0
  11. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_union.py +7 -7
  12. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/__init__.py +5 -0
  13. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/_core.py +6 -6
  14. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/_operators.py +94 -27
  15. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/operators.py +1 -1
  16. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/rounding.py +2 -1
  17. yirgacheffe-1.9.4/yirgacheffe.egg-info/PKG-INFO +149 -0
  18. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe.egg-info/SOURCES.txt +1 -0
  19. yirgacheffe-1.9.2/PKG-INFO +0 -600
  20. yirgacheffe-1.9.2/README.md +0 -553
  21. yirgacheffe-1.9.2/yirgacheffe.egg-info/PKG-INFO +0 -600
  22. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/LICENSE +0 -0
  23. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/MANIFEST.in +0 -0
  24. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/setup.cfg +0 -0
  25. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_area.py +0 -0
  26. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_auto_windowing.py +0 -0
  27. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_constants.py +0 -0
  28. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_group.py +0 -0
  29. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_h3layer.py +0 -0
  30. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_multiband.py +0 -0
  31. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_nodata.py +0 -0
  32. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_optimisation.py +0 -0
  33. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_pickle.py +0 -0
  34. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_pixel_coord.py +0 -0
  35. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_projection.py +0 -0
  36. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_raster.py +0 -0
  37. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_rounding.py +0 -0
  38. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_save_with_window.py +0 -0
  39. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_sum_with_window.py +0 -0
  40. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_uniform_area_layer.py +0 -0
  41. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_vectors.py +0 -0
  42. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/tests/test_window.py +0 -0
  43. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/_backends/__init__.py +0 -0
  44. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/_backends/enumeration.py +0 -0
  45. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/_backends/mlx.py +0 -0
  46. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/_backends/numpy.py +0 -0
  47. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/constants.py +0 -0
  48. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/__init__.py +0 -0
  49. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/area.py +0 -0
  50. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/base.py +0 -0
  51. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/constant.py +0 -0
  52. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/group.py +0 -0
  53. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/h3layer.py +0 -0
  54. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/rasters.py +0 -0
  55. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/rescaled.py +0 -0
  56. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/layers/vectors.py +0 -0
  57. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/py.typed +0 -0
  58. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe/window.py +0 -0
  59. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  60. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe.egg-info/entry_points.txt +0 -0
  61. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe.egg-info/requires.txt +0 -0
  62. {yirgacheffe-1.9.2 → yirgacheffe-1.9.4}/yirgacheffe.egg-info/top_level.txt +0 -0
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: yirgacheffe
3
+ Version: 1.9.4
4
+ Summary: Abstraction of gdal datasets for doing basic math operations
5
+ Author-email: Michael Dales <mwd24@cam.ac.uk>
6
+ License-Expression: ISC
7
+ Project-URL: Homepage, https://yirgacheffe.org/
8
+ Project-URL: Repository, https://github.com/quantifyearth/yirgacheffe.git
9
+ Project-URL: Issues, https://github.com/quantifyearth/yirgacheffe/issues
10
+ Project-URL: Changelog, https://yirgacheffe.org/latest/changelog/
11
+ Keywords: gdal,gis,geospatial,declarative
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: GIS
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy<3.0,>=1.24
23
+ Requires-Dist: gdal[numpy]<4.0,>=3.8
24
+ Requires-Dist: scikit-image<1.0,>=0.20
25
+ Requires-Dist: torch
26
+ Requires-Dist: dill
27
+ Requires-Dist: deprecation
28
+ Requires-Dist: tomli
29
+ Requires-Dist: h3
30
+ Requires-Dist: pyproj
31
+ Provides-Extra: mlx
32
+ Requires-Dist: mlx; extra == "mlx"
33
+ Provides-Extra: matplotlib
34
+ Requires-Dist: matplotlib; extra == "matplotlib"
35
+ Provides-Extra: dev
36
+ Requires-Dist: mypy; extra == "dev"
37
+ Requires-Dist: pylint; extra == "dev"
38
+ Requires-Dist: pytest; extra == "dev"
39
+ Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: build; extra == "dev"
41
+ Requires-Dist: twine; extra == "dev"
42
+ Requires-Dist: mkdocs-material; extra == "dev"
43
+ Requires-Dist: mkdocstrings-python; extra == "dev"
44
+ Requires-Dist: mike; extra == "dev"
45
+ Requires-Dist: mkdocs-gen-files; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
49
+
50
+ [![CI](https://github.com/quantifyearth/yirgacheffe/actions/workflows/pull-request.yml/badge.svg?branch=main)](https://github.com/quantifyearth/yirgacheffe/actions)
51
+ [![Documentation](https://img.shields.io/badge/docs-yirgacheffe.org-blue)](https://yirgacheffe.org)
52
+ [![PyPI version](https://img.shields.io/pypi/v/yirgacheffe)](https://pypi.org/project/yirgacheffe/)
53
+
54
+
55
+ ## Overview
56
+
57
+ Yirgacheffe is a declarative geospatial library, allowing you to operate on both raster and polygon geospatial datasets without having to do all the tedious book keeping around layer alignment or dealing with hardware concerns around memory or parallelism. you can load into memory safely.
58
+
59
+ Example common use-cases:
60
+
61
+ * Do the datasets overlap? Yirgacheffe will let you define either the intersection or the union of a set of different datasets, scaling up or down the area as required.
62
+ * Rasterisation of vector layers: if you have a vector dataset then you can add that to your computation and yirgaceffe will rasterize it on demand, so you never need to store more data in memory than necessary.
63
+ * Do the raster layers get big and take up large amounts of memory? Yirgacheffe will let you do simple numerical operations with layers directly and then worry about the memory management behind the scenes for you.
64
+ * Parallelisation of operations over many CPU cores.
65
+ * Built in support for optionally using GPUs via [MLX](https://ml-explore.github.io/mlx/build/html/index.html) support.
66
+
67
+ ## Installation
68
+
69
+ Yirgacheffe is available via pypi, so can be installed with pip for example:
70
+
71
+ ```SystemShell
72
+ $ pip install yirgacheffe
73
+ ```
74
+
75
+ ## Documentation
76
+
77
+ The documentation can be found on [yirgacheffe.org](https://yirgacheffe.org/)
78
+
79
+ ## Simple examples:
80
+
81
+ Here is how to do cloud removal from [Sentinel-2 data](https://browser.dataspace.copernicus.eu/?zoom=14&lat=6.15468&lng=38.20581&themeId=DEFAULT-THEME&visualizationUrl=U2FsdGVkX1944lrmeTJcaSsnoxNMp4oucN1AjklGUANHd2cRZWyXnepHvzpaOWzMhH8SrWQo%2BqrOvOnu6f9FeCMrS%2FDZmvjzID%2FoE1tbOCEHK8ohPXjFqYojeR9%2B82ri&datasetId=S2_L2A_CDAS&fromTime=2025-09-09T00%3A00%3A00.000Z&toTime=2025-09-09T23%3A59%3A59.999Z&layerId=1_TRUE_COLOR&demSource3D=%22MAPZEN%22&cloudCoverage=30&dateMode=SINGLE), using the [Scene Classification Layer](https://custom-scripts.sentinel-hub.com/custom-scripts/sentinel-2/scene-classification/) data:
82
+
83
+ ```python
84
+ import yirgaceffe as yg
85
+
86
+ with (
87
+ yg.read_raster("T37NCG_20250909T073609_B06_20m.jp2") as vre2,
88
+ yg.read_raster("T37NCG_20250909T073609_SCL_20m.jp2") as scl,
89
+ ):
90
+ is_cloud = (scl == 8) | (scl == 9) | (scl == 10) # various cloud types
91
+ is_shadow = (scl == 3)
92
+ is_bad = is_cloud | is_shadow
93
+
94
+ masked_vre2 = yg.where(is_bad, float("nan"), vre2)
95
+ masked_vre2.to_geotiff("vre2_cleaned.tif")
96
+ ```
97
+
98
+ or a species' [Area of Habitat](https://www.sciencedirect.com/science/article/pii/S0169534719301892) calculation:
99
+
100
+ ```python
101
+ import yirgaceffe as yg
102
+
103
+ with (
104
+ yg.read_raster("habitats.tif") as habitat_map,
105
+ yg.read_raster('elevation.tif') as elevation_map,
106
+ yg.read_shape('species123.geojson') as range_map,
107
+ ):
108
+ refined_habitat = habitat_map.isin([...species habitat codes...])
109
+ refined_elevation = (elevation_map >= species_min) & (elevation_map <= species_max)
110
+ aoh = refined_habitat * refined_elevation * range_polygon * area_per_pixel_map
111
+ print(f'Area of habitat: {aoh.sum()}')
112
+ ```
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
+
145
+ ## Thanks
146
+
147
+ Thanks to discussion and feedback from my colleagues, particularly Alison Eyres, Patrick Ferris, Amelia Holcomb, and Anil Madhavapeddy.
148
+
149
+ Inspired by the work of Daniele Baisero in his AoH library.
@@ -0,0 +1,102 @@
1
+ # Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
2
+
3
+ [![CI](https://github.com/quantifyearth/yirgacheffe/actions/workflows/pull-request.yml/badge.svg?branch=main)](https://github.com/quantifyearth/yirgacheffe/actions)
4
+ [![Documentation](https://img.shields.io/badge/docs-yirgacheffe.org-blue)](https://yirgacheffe.org)
5
+ [![PyPI version](https://img.shields.io/pypi/v/yirgacheffe)](https://pypi.org/project/yirgacheffe/)
6
+
7
+
8
+ ## Overview
9
+
10
+ Yirgacheffe is a declarative geospatial library, allowing you to operate on both raster and polygon geospatial datasets without having to do all the tedious book keeping around layer alignment or dealing with hardware concerns around memory or parallelism. you can load into memory safely.
11
+
12
+ Example common use-cases:
13
+
14
+ * Do the datasets overlap? Yirgacheffe will let you define either the intersection or the union of a set of different datasets, scaling up or down the area as required.
15
+ * Rasterisation of vector layers: if you have a vector dataset then you can add that to your computation and yirgaceffe will rasterize it on demand, so you never need to store more data in memory than necessary.
16
+ * Do the raster layers get big and take up large amounts of memory? Yirgacheffe will let you do simple numerical operations with layers directly and then worry about the memory management behind the scenes for you.
17
+ * Parallelisation of operations over many CPU cores.
18
+ * Built in support for optionally using GPUs via [MLX](https://ml-explore.github.io/mlx/build/html/index.html) support.
19
+
20
+ ## Installation
21
+
22
+ Yirgacheffe is available via pypi, so can be installed with pip for example:
23
+
24
+ ```SystemShell
25
+ $ pip install yirgacheffe
26
+ ```
27
+
28
+ ## Documentation
29
+
30
+ The documentation can be found on [yirgacheffe.org](https://yirgacheffe.org/)
31
+
32
+ ## Simple examples:
33
+
34
+ Here is how to do cloud removal from [Sentinel-2 data](https://browser.dataspace.copernicus.eu/?zoom=14&lat=6.15468&lng=38.20581&themeId=DEFAULT-THEME&visualizationUrl=U2FsdGVkX1944lrmeTJcaSsnoxNMp4oucN1AjklGUANHd2cRZWyXnepHvzpaOWzMhH8SrWQo%2BqrOvOnu6f9FeCMrS%2FDZmvjzID%2FoE1tbOCEHK8ohPXjFqYojeR9%2B82ri&datasetId=S2_L2A_CDAS&fromTime=2025-09-09T00%3A00%3A00.000Z&toTime=2025-09-09T23%3A59%3A59.999Z&layerId=1_TRUE_COLOR&demSource3D=%22MAPZEN%22&cloudCoverage=30&dateMode=SINGLE), using the [Scene Classification Layer](https://custom-scripts.sentinel-hub.com/custom-scripts/sentinel-2/scene-classification/) data:
35
+
36
+ ```python
37
+ import yirgaceffe as yg
38
+
39
+ with (
40
+ yg.read_raster("T37NCG_20250909T073609_B06_20m.jp2") as vre2,
41
+ yg.read_raster("T37NCG_20250909T073609_SCL_20m.jp2") as scl,
42
+ ):
43
+ is_cloud = (scl == 8) | (scl == 9) | (scl == 10) # various cloud types
44
+ is_shadow = (scl == 3)
45
+ is_bad = is_cloud | is_shadow
46
+
47
+ masked_vre2 = yg.where(is_bad, float("nan"), vre2)
48
+ masked_vre2.to_geotiff("vre2_cleaned.tif")
49
+ ```
50
+
51
+ or a species' [Area of Habitat](https://www.sciencedirect.com/science/article/pii/S0169534719301892) calculation:
52
+
53
+ ```python
54
+ import yirgaceffe as yg
55
+
56
+ with (
57
+ yg.read_raster("habitats.tif") as habitat_map,
58
+ yg.read_raster('elevation.tif') as elevation_map,
59
+ yg.read_shape('species123.geojson') as range_map,
60
+ ):
61
+ refined_habitat = habitat_map.isin([...species habitat codes...])
62
+ refined_elevation = (elevation_map >= species_min) & (elevation_map <= species_max)
63
+ aoh = refined_habitat * refined_elevation * range_polygon * area_per_pixel_map
64
+ print(f'Area of habitat: {aoh.sum()}')
65
+ ```
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
+
98
+ ## Thanks
99
+
100
+ Thanks to discussion and feedback from my colleagues, particularly Alison Eyres, Patrick Ferris, Amelia Holcomb, and Anil Madhavapeddy.
101
+
102
+ Inspired by the work of Daniele Baisero in his AoH library.
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "yirgacheffe"
9
- version = "1.9.2"
9
+ version = "1.9.4"
10
10
  description = "Abstraction of gdal datasets for doing basic math operations"
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Michael Dales", email = "mwd24@cam.ac.uk" }]
@@ -62,5 +62,5 @@ def test_float_to_int() -> None:
62
62
  comp.save(result)
63
63
 
64
64
  expected = backend.promote(np.array([[1, 2, 3, 4], [5, 6, 7, 8]]))
65
- actual = backend.demote_array(result.read_array(0, 0, 4, 2))
65
+ actual = result.read_array(0, 0, 4, 2)
66
66
  assert (expected == actual).all()
@@ -67,7 +67,7 @@ def test_find_intersection_with_vector_unbound() -> None:
67
67
  path = Path(tempdir) / "test.gpkg"
68
68
  area = Area(left=58, top=74, right=180, bottom=42)
69
69
  make_vectors_with_id(42, {area}, path)
70
- assert path.exists
70
+ assert path.exists()
71
71
 
72
72
  raster = RasterLayer(gdal_dataset_of_region(Area(left=-180.05, top=90.09, right=180.05, bottom=-90.09), 0.13))
73
73
  vector = VectorLayer.layer_from_file(path, None, None, None)
@@ -86,10 +86,10 @@ def test_find_intersection_with_vector_bound() -> None:
86
86
  path = Path(tempdir) / "test.gpkg"
87
87
  area = Area(left=58, top=74, right=180, bottom=42)
88
88
  make_vectors_with_id(42, {area}, path)
89
- assert path.exists
89
+ assert path.exists()
90
90
 
91
91
  raster = RasterLayer(gdal_dataset_of_region(Area(left=-180.05, top=90.09, right=180.05, bottom=-90.09), 0.13))
92
- vector = VectorLayer.layer_from_file(path, None, raster.map_projection.scale, raster.map_projection.name)
92
+ vector = VectorLayer.layer_from_file_like(path, raster)
93
93
  assert vector.area != area
94
94
 
95
95
  layers = [raster, vector]
@@ -104,10 +104,10 @@ def test_find_intersection_with_vector_awkward_rounding() -> None:
104
104
  path = Path(tempdir) / "test.gpkg"
105
105
  area = Area(left=-90, top=45, right=90, bottom=-45)
106
106
  make_vectors_with_id(42, {area}, path)
107
- assert path.exists
107
+ assert path.exists()
108
108
 
109
109
  raster = RasterLayer(gdal_dataset_of_region(Area(left=-180, top=90, right=180, bottom=-90), 18.0))
110
- vector = VectorLayer.layer_from_file(path, None, raster.map_projection.scale, raster.map_projection.name)
110
+ vector = VectorLayer.layer_from_file_like(path, raster)
111
111
 
112
112
  rounded_area = Area(left=-90, top=54, right=90, bottom=-54)
113
113
  assert vector.area == rounded_area
@@ -38,7 +38,7 @@ def test_open_raster_file_as_path() -> None:
38
38
  area = Area(-10, 10, 10, -10)
39
39
  dataset = gdal_dataset_of_region(area, 0.02, filename=path)
40
40
  dataset.Close()
41
- assert path.exists
41
+ assert path.exists()
42
42
 
43
43
  with yg.read_raster(path) as layer:
44
44
  assert layer.area == area
@@ -156,7 +156,7 @@ def test_open_shape_like() -> None:
156
156
  assert os.path.exists(path)
157
157
 
158
158
  with yg.read_raster(path) as raster_layer:
159
- path = os.path.join(tempdir, "test.gpkg")
159
+ path = Path(tempdir) / "test.gpkg"
160
160
  area = Area(-10.0, 10.0, 10.0, 0.0)
161
161
  make_vectors_with_id(42, {area}, path)
162
162
 
@@ -78,18 +78,41 @@ def test_add_byte_layers_with_callback(skip, expected_steps) -> None:
78
78
  assert layer1.datatype == DataType.Byte
79
79
  assert layer2.datatype == DataType.Byte
80
80
 
81
- callback_possitions = []
81
+ callback_positions: list[float] = []
82
82
 
83
83
  comp = layer1 + layer2
84
84
  comp.ystep = skip
85
- comp.save(result, callback=callback_possitions.append)
85
+ comp.save(result, callback=callback_positions.append)
86
86
 
87
87
  expected = data1 + data2
88
88
  actual = result.read_array(0, 0, 4, 2)
89
89
 
90
90
  assert (expected == actual).all()
91
91
 
92
- assert callback_possitions == expected_steps
92
+ assert callback_positions == expected_steps
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
93
116
 
94
117
  def test_sub_byte_layers() -> None:
95
118
  data1 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]])
@@ -150,7 +173,7 @@ def test_mult_float_layers() -> None:
150
173
  comp = layer1 * layer2
151
174
  comp.save(result)
152
175
 
153
- expected = backend.promote(data1) * backend.promote(data2)
176
+ expected = backend.demote_array(backend.promote(data1) * backend.promote(data2))
154
177
  backend.eval_op(expected)
155
178
 
156
179
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -168,7 +191,7 @@ def test_div_float_layers() -> None:
168
191
  comp = layer1 / layer2
169
192
  comp.save(result)
170
193
 
171
- expected = backend.promote(data1) / backend.promote(data2)
194
+ expected = backend.demote_array(backend.promote(data1) / backend.promote(data2))
172
195
  backend.eval_op(expected)
173
196
 
174
197
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -186,7 +209,7 @@ def test_floor_div_float_layers() -> None:
186
209
  comp = layer1 // layer2
187
210
  comp.save(result)
188
211
 
189
- expected = backend.promote(data1) // backend.promote(data2)
212
+ expected = backend.demote_array(backend.promote(data1) // backend.promote(data2))
190
213
  backend.eval_op(expected)
191
214
 
192
215
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -204,7 +227,7 @@ def test_remainder_float_layers() -> None:
204
227
  comp = layer1 % layer2
205
228
  comp.save(result)
206
229
 
207
- expected = backend.promote(data1) % backend.promote(data2)
230
+ expected = backend.demote_array(backend.promote(data1) % backend.promote(data2))
208
231
  backend.eval_op(expected)
209
232
 
210
233
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -264,7 +287,7 @@ def test_div_float_layer_by_const() -> None:
264
287
  comp = layer1 / 2.5
265
288
  comp.save(result)
266
289
 
267
- expected = backend.promote(data1) / 2.5
290
+ expected = backend.demote_array(backend.promote(data1) / 2.5)
268
291
  backend.eval_op(expected)
269
292
 
270
293
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -279,7 +302,7 @@ def test_floordiv_float_layer_by_const() -> None:
279
302
  comp = layer1 // 2.5
280
303
  comp.save(result)
281
304
 
282
- expected = backend.promote(data1) // 2.5
305
+ expected = backend.demote_array(backend.promote(data1) // 2.5)
283
306
  backend.eval_op(expected)
284
307
 
285
308
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -294,7 +317,7 @@ def test_remainder_float_layer_by_const() -> None:
294
317
  comp = layer1 % 2.5
295
318
  comp.save(result)
296
319
 
297
- expected = backend.promote(data1) % 2.5
320
+ expected = backend.demote_array(backend.promote(data1) % 2.5)
298
321
  backend.eval_op(expected)
299
322
 
300
323
  actual = backend.demote_array(result.read_array(0, 0, 4, 2))
@@ -1529,10 +1552,13 @@ def test_to_geotiff_parallel_thread_and_sum(monkeypatch, parallelism) -> None:
1529
1552
  with yirgacheffe.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)
@@ -1546,7 +1572,7 @@ def test_raster_and_vector() -> None:
1546
1572
  path = Path(tempdir) / "test.gpkg"
1547
1573
  area = Area(-5.0, 5.0, 5.0, -5.0)
1548
1574
  make_vectors_with_id(42, {area}, path)
1549
- assert path.exists
1575
+ assert path.exists()
1550
1576
 
1551
1577
  vector = VectorLayer.layer_from_file(path, None, PixelScale(1.0, -1.0), yirgacheffe.WGS_84_PROJECTION)
1552
1578
 
@@ -1562,7 +1588,7 @@ def test_raster_and_vector_mixed_projection() -> None:
1562
1588
  path = Path(tempdir) / "test.gpkg"
1563
1589
  area = Area(-5.0, 5.0, 5.0, -5.0)
1564
1590
  make_vectors_with_id(42, {area}, path)
1565
- assert path.exists
1591
+ assert path.exists()
1566
1592
 
1567
1593
  vector = VectorLayer.layer_from_file(path, None, PixelScale(1.0, -1.0), yirgacheffe.WGS_84_PROJECTION)
1568
1594
 
@@ -1577,7 +1603,7 @@ def test_raster_and_vector_no_scale_on_vector() -> None:
1577
1603
  path = Path(tempdir) / "test.gpkg"
1578
1604
  area = Area(-5.0, 5.0, 5.0, -5.0)
1579
1605
  make_vectors_with_id(42, {area}, path)
1580
- assert path.exists
1606
+ assert path.exists()
1581
1607
 
1582
1608
  vector = VectorLayer.layer_from_file(path, None, None, None)
1583
1609
 
@@ -182,18 +182,18 @@ def test_parallel_with_different_skip(monkeypatch, skip, expected_steps) -> None
182
182
  layer2 = RasterLayer.layer_from_file(path2)
183
183
  result = RasterLayer.empty_raster_layer_like(layer1)
184
184
 
185
- callback_possitions = []
185
+ callback_positions: list[float] = []
186
186
 
187
187
  comp = layer1 + layer2
188
188
  comp.ystep = skip
189
- comp.parallel_save(result, callback=callback_possitions.append)
189
+ comp.parallel_save(result, callback=callback_positions.append)
190
190
 
191
191
  expected = data1 + data2
192
192
  actual = result.read_array(0, 0, 4, 4)
193
193
 
194
194
  assert (expected == actual).all()
195
195
 
196
- assert callback_possitions == expected_steps
196
+ assert callback_positions == expected_steps
197
197
 
198
198
  @pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
199
199
  def test_parallel_equality(monkeypatch) -> None:
@@ -0,0 +1,23 @@
1
+ from functools import reduce
2
+ import operator
3
+
4
+ import numpy as np
5
+
6
+ from yirgacheffe.layers import RasterLayer
7
+ from tests.helpers import gdal_dataset_with_data
8
+
9
+ def test_add_similar_layers() -> None:
10
+ data = [
11
+ np.array([[1, 2, 3, 4], [5, 6, 7, 8]]),
12
+ np.array([[10, 20, 30, 40], [50, 60, 70, 80]]),
13
+ np.array([[100, 200, 300, 400], [500, 600, 700, 800]]),
14
+ ]
15
+
16
+ layers = [RasterLayer(gdal_dataset_with_data((0,0), 1.0, x)) for x in data]
17
+
18
+ summed_layers = reduce(operator.add, layers)
19
+ actual = summed_layers.read_array(0, 0, 4, 2)
20
+
21
+ expected = reduce(operator.add, data)
22
+
23
+ assert (expected == actual).all()
@@ -170,6 +170,7 @@ def test_rescaled_up_in_operation() -> None:
170
170
  data1[4:8,0:4] = 1
171
171
  dataset1 = gdal_dataset_with_data((0, 0), 1.0, data1)
172
172
  raster1 = RasterLayer(dataset1)
173
+ assert raster1.map_projection
173
174
 
174
175
  data2 = np.zeros((4, 4))
175
176
  data2[0:2,0:2] = 1
@@ -198,6 +199,7 @@ def test_rescaled_down_in_operation() -> None:
198
199
  data2[2:4,2:4] = 1
199
200
  dataset2 = gdal_dataset_with_data((0, 0), 2.0, data2)
200
201
  raster2 = RasterLayer(dataset2)
202
+ assert raster2.map_projection
201
203
 
202
204
  rescaled = RescaledRasterLayer(raster1, raster2.map_projection)
203
205
 
@@ -73,7 +73,7 @@ def test_find_union_with_vector_unbound() -> None:
73
73
  path = Path(tempdir) / "test.gpkg"
74
74
  area = Area(left=58, top=74, right=180, bottom=42)
75
75
  make_vectors_with_id(42, {area}, path)
76
- assert path.exists
76
+ assert path.exists()
77
77
 
78
78
  raster = RasterLayer(gdal_dataset_of_region(Area(left=59.93, top=70.07, right=170.04, bottom=44.98), 0.13))
79
79
  vector = VectorLayer.layer_from_file(path, None, None, None)
@@ -93,10 +93,10 @@ def test_find_union_with_vector_bound() -> None:
93
93
  path = Path(tempdir) / "test.gpkg"
94
94
  area = Area(left=58, top=74, right=180, bottom=42)
95
95
  make_vectors_with_id(42, {area}, path)
96
- assert path.exists
96
+ assert path.exists()
97
97
 
98
98
  raster = RasterLayer(gdal_dataset_of_region(Area(left=59.93, top=70.07, right=170.04, bottom=44.98), 0.13))
99
- vector = VectorLayer.layer_from_file(path, None, raster.map_projection.scale, raster.map_projection.name)
99
+ vector = VectorLayer.layer_from_file_like(path, raster)
100
100
  assert vector.area != area
101
101
 
102
102
  layers = [raster, vector]
@@ -158,10 +158,10 @@ def test_set_union_superset(left_padding: int, right_padding: int, top_padding:
158
158
  superset = Area(-1 - left_padding, 1 + top_padding, 1 + right_padding, -1 - bottom_padding)
159
159
  layer.set_window_for_union(superset)
160
160
  assert layer.window == Window(
161
- (0 - left_padding) / pixel_density,
162
- (0 - top_padding) / pixel_density,
163
- (2 + left_padding + right_padding) / pixel_density,
164
- (2 + top_padding + bottom_padding) / pixel_density,
161
+ round((0 - left_padding) / pixel_density),
162
+ round((0 - top_padding) / pixel_density),
163
+ round((2 + left_padding + right_padding) / pixel_density),
164
+ round((2 + top_padding + bottom_padding) / pixel_density),
165
165
  )
166
166
 
167
167
  origin_after_pixel = layer.read_array(
@@ -12,9 +12,14 @@ except ModuleNotFoundError:
12
12
  pyproject_data = tomllib.load(f)
13
13
  __version__ = pyproject_data["project"]["version"]
14
14
 
15
+ from .layers import YirgacheffeLayer
15
16
  from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster
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
 
21
+ from ._operators import where, minimum, maximum, clip, log, log2, log10, exp, exp2, nan_to_num, isin, \
22
+ floor, ceil # pylint: disable=W0611
23
+ from ._operators import abs, round # pylint: disable=W0611,W0622
24
+
20
25
  gdal.UseExceptions()
@@ -16,7 +16,7 @@ def read_raster(
16
16
  filename: Path | str,
17
17
  band: int = 1,
18
18
  ignore_nodata: bool = False,
19
- ) -> RasterLayer:
19
+ ) -> YirgacheffeLayer:
20
20
  """Open a raster file (e.g., GeoTIFF).
21
21
 
22
22
  Args:
@@ -38,7 +38,7 @@ def read_narrow_raster(
38
38
  filename: Path | str,
39
39
  band: int = 1,
40
40
  ignore_nodata: bool = False,
41
- ) -> RasterLayer:
41
+ ) -> YirgacheffeLayer:
42
42
  """Open a 1 pixel wide raster file as a global raster.
43
43
 
44
44
  This exists for the special use case where an area per pixel raster would have the same value per horizontal row
@@ -58,7 +58,7 @@ def read_narrow_raster(
58
58
  def read_rasters(
59
59
  filenames : Sequence[Path | str],
60
60
  tiled: bool=False
61
- ) -> GroupLayer:
61
+ ) -> YirgacheffeLayer:
62
62
  """Open a set of raster files (e.g., GeoTIFFs) as a single layer.
63
63
 
64
64
  Args:
@@ -86,7 +86,7 @@ def read_shape(
86
86
  where_filter: str | None = None,
87
87
  datatype: DataType | None = None,
88
88
  burn_value: int | float | str = 1,
89
- ) -> VectorLayer:
89
+ ) -> YirgacheffeLayer:
90
90
  """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
91
91
 
92
92
  Args:
@@ -124,7 +124,7 @@ def read_shape_like(
124
124
  where_filter: str | None = None,
125
125
  datatype: DataType | None = None,
126
126
  burn_value: int | float | str = 1,
127
- ) -> VectorLayer:
127
+ ) -> YirgacheffeLayer:
128
128
  """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
129
129
 
130
130
  Args:
@@ -146,7 +146,7 @@ def read_shape_like(
146
146
  burn_value,
147
147
  )
148
148
 
149
- def constant(value: int | float) -> ConstantLayer:
149
+ def constant(value: int | float) -> YirgacheffeLayer:
150
150
  """Generate a layer that has the same value in all pixels regardless of scale, projection, and area.
151
151
 
152
152
  Generally this should not be necessary unless you must have the constant as the first term in an