yirgacheffe 1.4.1__tar.gz → 1.5.0__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.
- {yirgacheffe-1.4.1/yirgacheffe.egg-info → yirgacheffe-1.5.0}/PKG-INFO +110 -55
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/README.md +109 -54
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/pyproject.toml +1 -1
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_area.py +10 -0
- yirgacheffe-1.5.0/tests/test_openers.py +165 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_operators.py +70 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_union.py +1 -1
- yirgacheffe-1.5.0/yirgacheffe/__init__.py +11 -0
- yirgacheffe-1.5.0/yirgacheffe/_core.py +133 -0
- yirgacheffe-1.5.0/yirgacheffe/constants.py +8 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/base.py +6 -6
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/constant.py +2 -7
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/group.py +16 -7
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/rasters.py +7 -7
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/rescaled.py +3 -2
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/vectors.py +15 -13
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/operators.py +83 -19
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/window.py +27 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0/yirgacheffe.egg-info}/PKG-INFO +110 -55
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe.egg-info/SOURCES.txt +2 -0
- yirgacheffe-1.4.1/yirgacheffe/__init__.py +0 -14
- yirgacheffe-1.4.1/yirgacheffe/constants.py +0 -2
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/LICENSE +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/MANIFEST.in +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/setup.cfg +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_auto_windowing.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_base.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_constants.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_datatypes.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_group.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_h3layer.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_intersection.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_multiband.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_optimisation.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_parallel_operators.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_pickle.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_raster.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_rescaling.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_rounding.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_save_with_window.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_sum_with_window.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_uniform_area_layer.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_vectors.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/tests/test_window.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/_backends/__init__.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/_backends/enumeration.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/_backends/mlx.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/_backends/numpy.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/__init__.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/area.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/layers/h3layer.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe/rounding.py +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe.egg-info/dependency_links.txt +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe.egg-info/entry_points.txt +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/yirgacheffe.egg-info/requires.txt +0 -0
- {yirgacheffe-1.4.1 → yirgacheffe-1.5.0}/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.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
|
@@ -53,12 +53,12 @@ The motivation for Yirgacheffe layers is to make working with gdal data slightly
|
|
|
53
53
|
For example, if we wanted to do a simple [Area of Habitat](https://github.com/quantifyearth/aoh-calculator/) calculation, whereby we find the pixels where a species resides by combining its range polygon, its habitat preferences, and its elevation preferences, the code would be like this:
|
|
54
54
|
|
|
55
55
|
```python
|
|
56
|
-
|
|
56
|
+
import yirgaceffe as yg
|
|
57
57
|
|
|
58
|
-
habitat_map =
|
|
59
|
-
elevation_map =
|
|
60
|
-
range_polygon =
|
|
61
|
-
area_per_pixel_map =
|
|
58
|
+
habitat_map = yg.read_raster("habitats.tif")
|
|
59
|
+
elevation_map = yg.read_raster('elevation.tif')
|
|
60
|
+
range_polygon = yg.read_shape_like('species123.geojson', like=habitat_map)
|
|
61
|
+
area_per_pixel_map = yg.read_raster('area_per_pixel.tif')
|
|
62
62
|
|
|
63
63
|
refined_habitat = habitat_map.isin([...species habitat codes...])
|
|
64
64
|
refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
|
|
@@ -71,8 +71,8 @@ print(f'area for species 123: {aoh.sum()}')
|
|
|
71
71
|
Similarly, you could save the result to a new raster layer:
|
|
72
72
|
|
|
73
73
|
```python
|
|
74
|
-
|
|
75
|
-
aoh.
|
|
74
|
+
...
|
|
75
|
+
aoh.to_geotiff("result.tif")
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Yirgacheffe will automatically infer if you want to do an intersection of maps or a union of the maps based on the operators you use (see below for a full table). You can explicitly override that if you want.
|
|
@@ -110,27 +110,28 @@ If you have set either the intersection window or union window on a layer and yo
|
|
|
110
110
|
If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
### Todo but not supported
|
|
114
|
-
|
|
115
|
-
Yirgacheffe is work in progress, so things planned but not supported currently:
|
|
116
|
-
|
|
117
|
-
* Dynamic pixel scale adjustment - all raster layers must be provided at the same pixel scale currently *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
|
|
118
|
-
* A fold operation
|
|
119
|
-
* CUDA/Metal support via CUPY/MLX
|
|
120
|
-
* Dispatching work across multiple CPUs *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
113
|
## Layer types
|
|
125
114
|
|
|
115
|
+
Note that as part of the move to the next major release, 2.0, we are adding simpler ways to create layers. Not all of those have been implemented yet, which is why this section has some inconsistencies. However, given many of the common cases are already covered, we present the new 2.0 style methods (`read_raster` and similar) here so you can write cleaner code today rather than making people wait for the final 2.0 release.
|
|
116
|
+
|
|
126
117
|
### RasterLayer
|
|
127
118
|
|
|
128
119
|
This is your basic GDAL raster layer, which you load from a geotiff.
|
|
129
120
|
|
|
130
121
|
```python
|
|
122
|
+
from yirgaceffe.layers import RasterLayer
|
|
123
|
+
|
|
131
124
|
with RasterLayer.layer_from_file('test1.tif') as layer:
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
total = layer.sum()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The new 2.0 way of doing this is:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import yirgacheffe as yg
|
|
132
|
+
|
|
133
|
+
with yg.read_raster('test.tif') as layer:
|
|
134
|
+
total = layer.sum()
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
You can also create empty layers ready for you to store results, either by taking the dimensions from an existing layer. In both these cases you can either provide a filename to which the data will be written, or if you do not provide a filename then the layer will only exist in memory - this will be more efficient if the layer is being used for intermediary results.
|
|
@@ -167,51 +168,30 @@ This layer will load vector data and rasterize it on demand as part of a calcula
|
|
|
167
168
|
Because it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
168
169
|
|
|
169
170
|
```python
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
This class was formerly called `DynamicVectorRangeLayer`, a name now deprecated.
|
|
175
|
-
|
|
171
|
+
from yirgaceffe import WGS_84_PROJECTION
|
|
172
|
+
from yirgaceffe.window import PixelScale
|
|
173
|
+
from yirgaceffe.layers import VectorLayer
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
|
|
180
|
-
|
|
181
|
-
```python
|
|
182
|
-
with UniformAreaLayer('area.tiff') as layer:
|
|
183
|
-
....
|
|
175
|
+
with VectorLayer.layer_from_file('range.gpkg', PixelScale(0.001, -0.001), WGS_84_PROJECTION) as layer:
|
|
176
|
+
...
|
|
184
177
|
```
|
|
185
178
|
|
|
186
|
-
|
|
179
|
+
The new 2.0 way of doing this is:
|
|
187
180
|
|
|
188
181
|
```python
|
|
189
|
-
|
|
190
|
-
UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
|
|
191
|
-
area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
### ConstantLayer
|
|
182
|
+
import yirgacheffe as yg
|
|
196
183
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```python
|
|
200
|
-
try:
|
|
201
|
-
area_layer = UniformAreaLayer('myarea.tiff')
|
|
202
|
-
except FileDoesNotExist:
|
|
203
|
-
area_layer = ConstantLayer(0.0)
|
|
184
|
+
with yg.read_shape('range.gpkg', (0.001, -0.001), WGS_84_PROJECTION) as layer:
|
|
185
|
+
...
|
|
204
186
|
```
|
|
205
187
|
|
|
188
|
+
It is more common that when a shape file is loaded that its pixel size and projection will want to be made to match that of an existing raster (as per the opening area of habitat example). For that there is the following convenience method:
|
|
206
189
|
|
|
207
|
-
### H3CellLayer
|
|
208
|
-
|
|
209
|
-
If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
|
|
210
|
-
|
|
211
|
-
Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
212
190
|
|
|
213
191
|
```python
|
|
214
|
-
|
|
192
|
+
with yg.read_raster("test.tif") as raster_layer:
|
|
193
|
+
with yg.read_shape_like('range.gpkg', raster_layer) as shape_layer:
|
|
194
|
+
...
|
|
215
195
|
```
|
|
216
196
|
|
|
217
197
|
### GroupLayer
|
|
@@ -240,6 +220,15 @@ with GroupLayer.layer_from_directory('.') as all_tiles:
|
|
|
240
220
|
...
|
|
241
221
|
```
|
|
242
222
|
|
|
223
|
+
The new 2.0 way of doing this is:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
import yirgacheffe as yg
|
|
227
|
+
|
|
228
|
+
with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
|
|
229
|
+
...
|
|
230
|
+
```
|
|
231
|
+
|
|
243
232
|
### TiledGroupLayer
|
|
244
233
|
|
|
245
234
|
This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
|
|
@@ -250,11 +239,59 @@ tile2 = RasterLayer.layer_from_file('tile_N20_E10.tif')
|
|
|
250
239
|
all_tiles = TiledGroupLayer([tile1, tile2])
|
|
251
240
|
```
|
|
252
241
|
|
|
242
|
+
The new 2.0 way of doing this is:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import yirgacheffe as yg
|
|
246
|
+
|
|
247
|
+
with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif'], tiled=True) as all_tiles:
|
|
248
|
+
...
|
|
249
|
+
```
|
|
250
|
+
|
|
253
251
|
Notes:
|
|
254
252
|
|
|
255
253
|
* You can have missing tiles, and these will be filled in with zeros.
|
|
256
254
|
* You can have tiles that overlap, so long as they still conform to the rule that all tiles are the same size and on a grid.
|
|
257
255
|
|
|
256
|
+
### ConstantLayer
|
|
257
|
+
|
|
258
|
+
This is there to simplify code when you have some optional layers. Rather than littering your code with checks, you can just use a constant layer, which can be included in calculations and will just return an fixed value as if it wasn't there. Useful with 0.0 or 1.0 for sum or multiplication null layers.
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
try:
|
|
262
|
+
area_layer = RasterLayer.layer_from_file('myarea.tiff')
|
|
263
|
+
except FileDoesNotExist:
|
|
264
|
+
area_layer = ConstantLayer(0.0)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### H3CellLayer
|
|
268
|
+
|
|
269
|
+
If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
|
|
270
|
+
|
|
271
|
+
Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
hex_cell_layer = H3CellLayer('88972eac11fffff', layer1.pixel_scale, layer1.projection)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
### UniformAreaLayer
|
|
279
|
+
|
|
280
|
+
In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
with UniformAreaLayer('area.tiff') as layer:
|
|
284
|
+
....
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Note that loading this data can still be very slow, due to how image compression works. So if you plan to use area.tiff more than once, we recommend use save an optimised version - this will do the slow uncompression once and then save a minimal file to speed up future processing:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
if not os.path.exists('yirgacheffe_area.tiff'):
|
|
291
|
+
UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
|
|
292
|
+
area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
|
|
293
|
+
```
|
|
294
|
+
|
|
258
295
|
|
|
259
296
|
## Supported operations on layers
|
|
260
297
|
|
|
@@ -281,6 +318,24 @@ with RasterLayer.layer_from_file('test1.tif') as layer1:
|
|
|
281
318
|
calc.save(result)
|
|
282
319
|
```
|
|
283
320
|
|
|
321
|
+
|
|
322
|
+
The new 2.0 way of doing these are:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
with yg.read_raster('test1.tif') as layer1:
|
|
326
|
+
with yg.read_raster('test2.tif') as layer2:
|
|
327
|
+
result = layer1 + layer2
|
|
328
|
+
result.to_geotiff("result.tif")
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
or
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
with yg.read_raster('test1.tif') as layer1:
|
|
335
|
+
result = layer1 * 42.0
|
|
336
|
+
result.to_geotiff("result.tif")
|
|
337
|
+
```
|
|
338
|
+
|
|
284
339
|
### Boolean testing
|
|
285
340
|
|
|
286
341
|
Testing for equality, less than, less than or equal, greater than, and greater than or equal are supported on layers, along with logical or and logical and, as per this example, where `elevation_upper` and `elevation_lower` are scalar values:
|
|
@@ -28,12 +28,12 @@ The motivation for Yirgacheffe layers is to make working with gdal data slightly
|
|
|
28
28
|
For example, if we wanted to do a simple [Area of Habitat](https://github.com/quantifyearth/aoh-calculator/) calculation, whereby we find the pixels where a species resides by combining its range polygon, its habitat preferences, and its elevation preferences, the code would be like this:
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
|
-
|
|
31
|
+
import yirgaceffe as yg
|
|
32
32
|
|
|
33
|
-
habitat_map =
|
|
34
|
-
elevation_map =
|
|
35
|
-
range_polygon =
|
|
36
|
-
area_per_pixel_map =
|
|
33
|
+
habitat_map = yg.read_raster("habitats.tif")
|
|
34
|
+
elevation_map = yg.read_raster('elevation.tif')
|
|
35
|
+
range_polygon = yg.read_shape_like('species123.geojson', like=habitat_map)
|
|
36
|
+
area_per_pixel_map = yg.read_raster('area_per_pixel.tif')
|
|
37
37
|
|
|
38
38
|
refined_habitat = habitat_map.isin([...species habitat codes...])
|
|
39
39
|
refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
|
|
@@ -46,8 +46,8 @@ print(f'area for species 123: {aoh.sum()}')
|
|
|
46
46
|
Similarly, you could save the result to a new raster layer:
|
|
47
47
|
|
|
48
48
|
```python
|
|
49
|
-
|
|
50
|
-
aoh.
|
|
49
|
+
...
|
|
50
|
+
aoh.to_geotiff("result.tif")
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Yirgacheffe will automatically infer if you want to do an intersection of maps or a union of the maps based on the operators you use (see below for a full table). You can explicitly override that if you want.
|
|
@@ -85,27 +85,28 @@ If you have set either the intersection window or union window on a layer and yo
|
|
|
85
85
|
If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
### Todo but not supported
|
|
89
|
-
|
|
90
|
-
Yirgacheffe is work in progress, so things planned but not supported currently:
|
|
91
|
-
|
|
92
|
-
* Dynamic pixel scale adjustment - all raster layers must be provided at the same pixel scale currently *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
|
|
93
|
-
* A fold operation
|
|
94
|
-
* CUDA/Metal support via CUPY/MLX
|
|
95
|
-
* Dispatching work across multiple CPUs *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
88
|
## Layer types
|
|
100
89
|
|
|
90
|
+
Note that as part of the move to the next major release, 2.0, we are adding simpler ways to create layers. Not all of those have been implemented yet, which is why this section has some inconsistencies. However, given many of the common cases are already covered, we present the new 2.0 style methods (`read_raster` and similar) here so you can write cleaner code today rather than making people wait for the final 2.0 release.
|
|
91
|
+
|
|
101
92
|
### RasterLayer
|
|
102
93
|
|
|
103
94
|
This is your basic GDAL raster layer, which you load from a geotiff.
|
|
104
95
|
|
|
105
96
|
```python
|
|
97
|
+
from yirgaceffe.layers import RasterLayer
|
|
98
|
+
|
|
106
99
|
with RasterLayer.layer_from_file('test1.tif') as layer:
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
total = layer.sum()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The new 2.0 way of doing this is:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import yirgacheffe as yg
|
|
107
|
+
|
|
108
|
+
with yg.read_raster('test.tif') as layer:
|
|
109
|
+
total = layer.sum()
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
You can also create empty layers ready for you to store results, either by taking the dimensions from an existing layer. In both these cases you can either provide a filename to which the data will be written, or if you do not provide a filename then the layer will only exist in memory - this will be more efficient if the layer is being used for intermediary results.
|
|
@@ -142,51 +143,30 @@ This layer will load vector data and rasterize it on demand as part of a calcula
|
|
|
142
143
|
Because it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
143
144
|
|
|
144
145
|
```python
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
This class was formerly called `DynamicVectorRangeLayer`, a name now deprecated.
|
|
150
|
-
|
|
146
|
+
from yirgaceffe import WGS_84_PROJECTION
|
|
147
|
+
from yirgaceffe.window import PixelScale
|
|
148
|
+
from yirgaceffe.layers import VectorLayer
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
|
|
155
|
-
|
|
156
|
-
```python
|
|
157
|
-
with UniformAreaLayer('area.tiff') as layer:
|
|
158
|
-
....
|
|
150
|
+
with VectorLayer.layer_from_file('range.gpkg', PixelScale(0.001, -0.001), WGS_84_PROJECTION) as layer:
|
|
151
|
+
...
|
|
159
152
|
```
|
|
160
153
|
|
|
161
|
-
|
|
154
|
+
The new 2.0 way of doing this is:
|
|
162
155
|
|
|
163
156
|
```python
|
|
164
|
-
|
|
165
|
-
UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
|
|
166
|
-
area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
### ConstantLayer
|
|
157
|
+
import yirgacheffe as yg
|
|
171
158
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```python
|
|
175
|
-
try:
|
|
176
|
-
area_layer = UniformAreaLayer('myarea.tiff')
|
|
177
|
-
except FileDoesNotExist:
|
|
178
|
-
area_layer = ConstantLayer(0.0)
|
|
159
|
+
with yg.read_shape('range.gpkg', (0.001, -0.001), WGS_84_PROJECTION) as layer:
|
|
160
|
+
...
|
|
179
161
|
```
|
|
180
162
|
|
|
163
|
+
It is more common that when a shape file is loaded that its pixel size and projection will want to be made to match that of an existing raster (as per the opening area of habitat example). For that there is the following convenience method:
|
|
181
164
|
|
|
182
|
-
### H3CellLayer
|
|
183
|
-
|
|
184
|
-
If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
|
|
185
|
-
|
|
186
|
-
Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
187
165
|
|
|
188
166
|
```python
|
|
189
|
-
|
|
167
|
+
with yg.read_raster("test.tif") as raster_layer:
|
|
168
|
+
with yg.read_shape_like('range.gpkg', raster_layer) as shape_layer:
|
|
169
|
+
...
|
|
190
170
|
```
|
|
191
171
|
|
|
192
172
|
### GroupLayer
|
|
@@ -215,6 +195,15 @@ with GroupLayer.layer_from_directory('.') as all_tiles:
|
|
|
215
195
|
...
|
|
216
196
|
```
|
|
217
197
|
|
|
198
|
+
The new 2.0 way of doing this is:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import yirgacheffe as yg
|
|
202
|
+
|
|
203
|
+
with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
|
|
204
|
+
...
|
|
205
|
+
```
|
|
206
|
+
|
|
218
207
|
### TiledGroupLayer
|
|
219
208
|
|
|
220
209
|
This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
|
|
@@ -225,11 +214,59 @@ tile2 = RasterLayer.layer_from_file('tile_N20_E10.tif')
|
|
|
225
214
|
all_tiles = TiledGroupLayer([tile1, tile2])
|
|
226
215
|
```
|
|
227
216
|
|
|
217
|
+
The new 2.0 way of doing this is:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
import yirgacheffe as yg
|
|
221
|
+
|
|
222
|
+
with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif'], tiled=True) as all_tiles:
|
|
223
|
+
...
|
|
224
|
+
```
|
|
225
|
+
|
|
228
226
|
Notes:
|
|
229
227
|
|
|
230
228
|
* You can have missing tiles, and these will be filled in with zeros.
|
|
231
229
|
* You can have tiles that overlap, so long as they still conform to the rule that all tiles are the same size and on a grid.
|
|
232
230
|
|
|
231
|
+
### ConstantLayer
|
|
232
|
+
|
|
233
|
+
This is there to simplify code when you have some optional layers. Rather than littering your code with checks, you can just use a constant layer, which can be included in calculations and will just return an fixed value as if it wasn't there. Useful with 0.0 or 1.0 for sum or multiplication null layers.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
try:
|
|
237
|
+
area_layer = RasterLayer.layer_from_file('myarea.tiff')
|
|
238
|
+
except FileDoesNotExist:
|
|
239
|
+
area_layer = ConstantLayer(0.0)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### H3CellLayer
|
|
243
|
+
|
|
244
|
+
If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
|
|
245
|
+
|
|
246
|
+
Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
hex_cell_layer = H3CellLayer('88972eac11fffff', layer1.pixel_scale, layer1.projection)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
### UniformAreaLayer
|
|
254
|
+
|
|
255
|
+
In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
with UniformAreaLayer('area.tiff') as layer:
|
|
259
|
+
....
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Note that loading this data can still be very slow, due to how image compression works. So if you plan to use area.tiff more than once, we recommend use save an optimised version - this will do the slow uncompression once and then save a minimal file to speed up future processing:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
if not os.path.exists('yirgacheffe_area.tiff'):
|
|
266
|
+
UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
|
|
267
|
+
area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
|
|
268
|
+
```
|
|
269
|
+
|
|
233
270
|
|
|
234
271
|
## Supported operations on layers
|
|
235
272
|
|
|
@@ -256,6 +293,24 @@ with RasterLayer.layer_from_file('test1.tif') as layer1:
|
|
|
256
293
|
calc.save(result)
|
|
257
294
|
```
|
|
258
295
|
|
|
296
|
+
|
|
297
|
+
The new 2.0 way of doing these are:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
with yg.read_raster('test1.tif') as layer1:
|
|
301
|
+
with yg.read_raster('test2.tif') as layer2:
|
|
302
|
+
result = layer1 + layer2
|
|
303
|
+
result.to_geotiff("result.tif")
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
or
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
with yg.read_raster('test1.tif') as layer1:
|
|
310
|
+
result = layer1 * 42.0
|
|
311
|
+
result.to_geotiff("result.tif")
|
|
312
|
+
```
|
|
313
|
+
|
|
259
314
|
### Boolean testing
|
|
260
315
|
|
|
261
316
|
Testing for equality, less than, less than or equal, greater than, and greater than or equal are supported on layers, along with logical or and logical and, as per this example, where `elevation_upper` and `elevation_lower` are scalar values:
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "yirgacheffe"
|
|
9
|
-
version = "1.
|
|
9
|
+
version = "1.5.0"
|
|
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" }]
|
|
@@ -19,3 +19,13 @@ def test_area_operators(lhs: Area, rhs: Area, is_equal: bool, overlaps: bool) ->
|
|
|
19
19
|
assert (lhs != rhs) == (not is_equal)
|
|
20
20
|
assert (lhs.overlaps(rhs)) == overlaps
|
|
21
21
|
assert (rhs.overlaps(lhs)) == overlaps
|
|
22
|
+
assert not lhs.is_world
|
|
23
|
+
assert not rhs.is_world
|
|
24
|
+
|
|
25
|
+
def test_global_area() -> None:
|
|
26
|
+
area = Area.world()
|
|
27
|
+
assert area.is_world
|
|
28
|
+
|
|
29
|
+
other_area = Area(-10.0, 10.0, 10.0, -10.0)
|
|
30
|
+
assert area.overlaps(other_area)
|
|
31
|
+
assert other_area.overlaps(area)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
import yirgacheffe as yg
|
|
9
|
+
from yirgacheffe import WGS_84_PROJECTION
|
|
10
|
+
from yirgacheffe.layers import InvalidRasterBand
|
|
11
|
+
from yirgacheffe.window import Area, PixelScale, Window
|
|
12
|
+
from tests.helpers import gdal_dataset_of_region, gdal_multiband_dataset_with_data, \
|
|
13
|
+
make_vectors_with_id, make_vectors_with_mutlile_ids
|
|
14
|
+
|
|
15
|
+
def test_raster_from_nonexistent_file() -> None:
|
|
16
|
+
with pytest.raises(FileNotFoundError):
|
|
17
|
+
_ = yg.read_raster("this_file_does_not_exist.tif")
|
|
18
|
+
|
|
19
|
+
def test_open_raster_file() -> None:
|
|
20
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
21
|
+
path = os.path.join(tempdir, "test.tif")
|
|
22
|
+
area = Area(-10, 10, 10, -10)
|
|
23
|
+
dataset = gdal_dataset_of_region(area, 0.02, filename=path)
|
|
24
|
+
dataset.Close()
|
|
25
|
+
assert os.path.exists(path)
|
|
26
|
+
|
|
27
|
+
with yg.read_raster(path) as layer:
|
|
28
|
+
assert layer.area == area
|
|
29
|
+
assert layer.pixel_scale == (0.02, -0.02)
|
|
30
|
+
assert layer.geo_transform == (-10, 0.02, 0.0, 10, 0.0, -0.02)
|
|
31
|
+
assert layer.window == Window(0, 0, 1000, 1000)
|
|
32
|
+
|
|
33
|
+
def test_open_raster_file_as_path() -> None:
|
|
34
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
35
|
+
path = Path(tempdir) / "test.tif"
|
|
36
|
+
area = Area(-10, 10, 10, -10)
|
|
37
|
+
dataset = gdal_dataset_of_region(area, 0.02, filename=path)
|
|
38
|
+
dataset.Close()
|
|
39
|
+
assert path.exists
|
|
40
|
+
|
|
41
|
+
with yg.read_raster(path) as layer:
|
|
42
|
+
assert layer.area == area
|
|
43
|
+
assert layer.pixel_scale == (0.02, -0.02)
|
|
44
|
+
assert layer.geo_transform == (-10, 0.02, 0.0, 10, 0.0, -0.02)
|
|
45
|
+
assert layer.window == Window(0, 0, 1000, 1000)
|
|
46
|
+
|
|
47
|
+
def test_open_multiband_raster_wrong_band() -> None:
|
|
48
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
49
|
+
path = Path(tempdir) / "test.tif"
|
|
50
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]])
|
|
51
|
+
data2 = np.array([[10.0, 20.0, 30.0, 40.0], [50.0, 60.0, 70.0, 80.0]])
|
|
52
|
+
|
|
53
|
+
datas = [data1, data2]
|
|
54
|
+
dataset = gdal_multiband_dataset_with_data((0.0, 0.0), 0.02, datas, filename=path)
|
|
55
|
+
dataset.Close()
|
|
56
|
+
|
|
57
|
+
with pytest.raises(InvalidRasterBand):
|
|
58
|
+
_ = yg.read_raster(path, 3)
|
|
59
|
+
|
|
60
|
+
def test_open_multiband_raster() -> None:
|
|
61
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
62
|
+
path = Path(tempdir) / "test.tif"
|
|
63
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]])
|
|
64
|
+
data2 = np.array([[10.0, 20.0, 30.0, 40.0], [50.0, 60.0, 70.0, 80.0]])
|
|
65
|
+
|
|
66
|
+
datas = [data1, data2]
|
|
67
|
+
dataset = gdal_multiband_dataset_with_data((0.0, 0.0), 0.02, datas, filename=path)
|
|
68
|
+
dataset.Close()
|
|
69
|
+
|
|
70
|
+
for i in range(2):
|
|
71
|
+
with yg.read_raster(path, i + 1) as layer:
|
|
72
|
+
data = datas[i]
|
|
73
|
+
actual = layer.read_array(0, 0, 4, 2)
|
|
74
|
+
assert (data == actual).all()
|
|
75
|
+
|
|
76
|
+
def test_shape_from_nonexistent_file() -> None:
|
|
77
|
+
with pytest.raises(FileNotFoundError):
|
|
78
|
+
_ = yg.read_shape("this_file_does_not_exist.gpkg", (1.0, -1.0), WGS_84_PROJECTION)
|
|
79
|
+
|
|
80
|
+
def test_open_gpkg() -> None:
|
|
81
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
82
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
83
|
+
area = Area(-10.0, 10.0, 10.0, 0.0)
|
|
84
|
+
make_vectors_with_id(42, {area}, path)
|
|
85
|
+
|
|
86
|
+
with yg.read_shape(path, PixelScale(1.0, -1.0), WGS_84_PROJECTION) as layer:
|
|
87
|
+
assert layer.area == area
|
|
88
|
+
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
89
|
+
assert layer.window == Window(0, 0, 20, 10)
|
|
90
|
+
assert layer.projection == WGS_84_PROJECTION
|
|
91
|
+
|
|
92
|
+
def test_open_gpkg_direct_scale() -> None:
|
|
93
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
94
|
+
path = Path(tempdir) / "test.gpkg"
|
|
95
|
+
area = Area(-10.0, 10.0, 10.0, 0.0)
|
|
96
|
+
make_vectors_with_id(42, {area}, path)
|
|
97
|
+
|
|
98
|
+
with yg.read_shape(path, (1.0, -1.0), WGS_84_PROJECTION) as layer:
|
|
99
|
+
assert layer.area == area
|
|
100
|
+
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
101
|
+
assert layer.window == Window(0, 0, 20, 10)
|
|
102
|
+
assert layer.projection == WGS_84_PROJECTION
|
|
103
|
+
|
|
104
|
+
def test_open_gpkg_with_filter() -> None:
|
|
105
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
106
|
+
path = Path(tempdir) / "test.gpkg"
|
|
107
|
+
areas = {
|
|
108
|
+
(Area(-10.0, 10.0, 0.0, 0.0), 42),
|
|
109
|
+
(Area(0.0, 0.0, 10, -10), 43)
|
|
110
|
+
}
|
|
111
|
+
make_vectors_with_mutlile_ids(areas, path)
|
|
112
|
+
|
|
113
|
+
with yg.read_shape(path, (1.0, -1.0), WGS_84_PROJECTION, "id_no=42") as layer:
|
|
114
|
+
assert layer.area == Area(-10.0, 10.0, 0.0, 0.0)
|
|
115
|
+
assert layer.geo_transform == (-10.0, 1.0, 0.0, 10.0, 0.0, -1.0)
|
|
116
|
+
assert layer.window == Window(0, 0, 10, 10)
|
|
117
|
+
|
|
118
|
+
# Because we picked one later, all pixels should be burned
|
|
119
|
+
total = layer.sum()
|
|
120
|
+
assert total == (layer.window.xsize * layer.window.ysize)
|
|
121
|
+
|
|
122
|
+
def test_open_shape_like() -> None:
|
|
123
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
124
|
+
path = Path(tempdir) / "test.tif"
|
|
125
|
+
area = Area(-10, 10, 10, -10)
|
|
126
|
+
dataset = gdal_dataset_of_region(area, 1.0, filename=path)
|
|
127
|
+
dataset.Close()
|
|
128
|
+
assert os.path.exists(path)
|
|
129
|
+
|
|
130
|
+
with yg.read_raster(path) as raster_layer:
|
|
131
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
132
|
+
area = Area(-10.0, 10.0, 10.0, 0.0)
|
|
133
|
+
make_vectors_with_id(42, {area}, path)
|
|
134
|
+
|
|
135
|
+
with yg.read_shape_like(path, raster_layer) as layer:
|
|
136
|
+
assert layer.area == area
|
|
137
|
+
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
138
|
+
assert layer.window == Window(0, 0, 20, 10)
|
|
139
|
+
assert layer.projection == raster_layer.projection
|
|
140
|
+
|
|
141
|
+
@pytest.mark.parametrize("tiled", [False, True])
|
|
142
|
+
def test_empty_rasters_list(tiled):
|
|
143
|
+
with pytest.raises(ValueError):
|
|
144
|
+
_ = yg.read_rasters([], tiled=tiled)
|
|
145
|
+
|
|
146
|
+
@pytest.mark.parametrize("tiled", [False, True])
|
|
147
|
+
def test_open_two_raster_areas_side_by_side(tiled):
|
|
148
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
149
|
+
path1 = Path(tempdir) / "test1.tif"
|
|
150
|
+
area1 = Area(-10, 10, 10, -10)
|
|
151
|
+
dataset1 = gdal_dataset_of_region(area1, 0.2, filename=path1)
|
|
152
|
+
dataset1.Close()
|
|
153
|
+
|
|
154
|
+
path2 = Path(tempdir) / "test2.tif"
|
|
155
|
+
area2 = Area(10, 10, 30, -10)
|
|
156
|
+
dataset2 = gdal_dataset_of_region(area2, 0.2, filename=path2)
|
|
157
|
+
dataset2.Close()
|
|
158
|
+
|
|
159
|
+
with yg.read_rasters([path1, path2], tiled=tiled) as group:
|
|
160
|
+
assert group.area == Area(-10, 10, 30, -10)
|
|
161
|
+
assert group.window == Window(0, 0, 200, 100)
|
|
162
|
+
|
|
163
|
+
with yg.read_raster(path1) as raster1:
|
|
164
|
+
with yg.read_raster(path2) as raster2:
|
|
165
|
+
assert group.sum() == raster1.sum() + raster2.sum()
|