yirgacheffe 1.7.5__tar.gz → 1.7.7__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.7.5/yirgacheffe.egg-info → yirgacheffe-1.7.7}/PKG-INFO +21 -5
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/README.md +20 -4
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/pyproject.toml +1 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_auto_windowing.py +80 -16
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_openers.py +54 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_operators.py +2 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_parallel_operators.py +35 -2
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_pickle.py +3 -4
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_raster.py +5 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_vectors.py +3 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/__init__.py +1 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_core.py +50 -1
- yirgacheffe-1.7.5/yirgacheffe/operators.py → yirgacheffe-1.7.7/yirgacheffe/_operators.py +89 -118
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/base.py +2 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/constant.py +1 -2
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/group.py +1 -2
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/h3layer.py +1 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/rasters.py +7 -3
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/rescaled.py +1 -1
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/vectors.py +9 -1
- yirgacheffe-1.7.7/yirgacheffe/operators.py +7 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/window.py +2 -2
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7/yirgacheffe.egg-info}/PKG-INFO +21 -5
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/SOURCES.txt +1 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/LICENSE +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/MANIFEST.in +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/setup.cfg +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_area.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_base.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_constants.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_datatypes.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_group.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_h3layer.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_intersection.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_multiband.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_nodata.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_optimisation.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_projection.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_rescaling.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_rounding.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_save_with_window.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_sum_with_window.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_uniform_area_layer.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_union.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_window.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/__init__.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/enumeration.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/mlx.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/numpy.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/constants.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/__init__.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/area.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/rounding.py +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/dependency_links.txt +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/entry_points.txt +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/requires.txt +0 -0
- {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/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.7.
|
|
3
|
+
Version: 1.7.7
|
|
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
|
|
@@ -262,17 +262,33 @@ Notes:
|
|
|
262
262
|
* You can have missing tiles, and these will be filled in with zeros.
|
|
263
263
|
* 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.
|
|
264
264
|
|
|
265
|
-
###
|
|
265
|
+
### Constants
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
At times it is useful to have a fixed constant in an expression. Typically, similar to numpy, if an expression involving layers has a constant in, Yirgacheffe will apply that to all pixels in the equation without need for further elaboration:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
with yg.read_raster("some_data.tif") as layer:
|
|
271
|
+
doubled_layer = layer * 2.0
|
|
272
|
+
...
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
This can be useful in tasks where you have an optional layer in your code. For example, here the code optionally loads an area-per-pixel layer, which if not present can just be substituted with a 1.0:
|
|
268
276
|
|
|
269
277
|
```python
|
|
270
278
|
try:
|
|
271
|
-
area_layer =
|
|
279
|
+
area_layer = yg.read_raster('myarea.tiff')
|
|
272
280
|
except FileDoesNotExist:
|
|
273
|
-
area_layer =
|
|
281
|
+
area_layer = 1.0
|
|
274
282
|
```
|
|
275
283
|
|
|
284
|
+
However, as with numpy, Python can not make the correct inference if the constant value is the first term in the equation. In that case you need to explicitly wrap the value with `constant` to help Python understand what is happening:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
with yg.read_raster("some_data.tif") as layer:
|
|
288
|
+
result = yg.constant(1.0) / layer
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
|
|
276
292
|
### H3CellLayer
|
|
277
293
|
|
|
278
294
|
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.
|
|
@@ -232,17 +232,33 @@ Notes:
|
|
|
232
232
|
* You can have missing tiles, and these will be filled in with zeros.
|
|
233
233
|
* 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.
|
|
234
234
|
|
|
235
|
-
###
|
|
235
|
+
### Constants
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
At times it is useful to have a fixed constant in an expression. Typically, similar to numpy, if an expression involving layers has a constant in, Yirgacheffe will apply that to all pixels in the equation without need for further elaboration:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
with yg.read_raster("some_data.tif") as layer:
|
|
241
|
+
doubled_layer = layer * 2.0
|
|
242
|
+
...
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
This can be useful in tasks where you have an optional layer in your code. For example, here the code optionally loads an area-per-pixel layer, which if not present can just be substituted with a 1.0:
|
|
238
246
|
|
|
239
247
|
```python
|
|
240
248
|
try:
|
|
241
|
-
area_layer =
|
|
249
|
+
area_layer = yg.read_raster('myarea.tiff')
|
|
242
250
|
except FileDoesNotExist:
|
|
243
|
-
area_layer =
|
|
251
|
+
area_layer = 1.0
|
|
244
252
|
```
|
|
245
253
|
|
|
254
|
+
However, as with numpy, Python can not make the correct inference if the constant value is the first term in the equation. In that case you need to explicitly wrap the value with `constant` to help Python understand what is happening:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
with yg.read_raster("some_data.tif") as layer:
|
|
258
|
+
result = yg.constant(1.0) / layer
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
|
|
246
262
|
### H3CellLayer
|
|
247
263
|
|
|
248
264
|
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.
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "yirgacheffe"
|
|
9
|
-
version = "1.7.
|
|
9
|
+
version = "1.7.7"
|
|
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" }]
|
|
@@ -201,27 +201,91 @@ def test_constant_layer_result_lhs_multiply() -> None:
|
|
|
201
201
|
|
|
202
202
|
def test_vector_layers_add() -> None:
|
|
203
203
|
data1 = np.array([[1, 2], [3, 4]])
|
|
204
|
-
|
|
204
|
+
with RasterLayer(gdal_dataset_with_data((0.0, 0.0), 1.1, data1)) as raster_layer:
|
|
205
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
206
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
207
|
+
areas = {
|
|
208
|
+
(Area(-10.0, 10.0, 0.0, 0.0), 42),
|
|
209
|
+
(Area(0.0, 0.0, 10, -10), 43)
|
|
210
|
+
}
|
|
211
|
+
make_vectors_with_mutlile_ids(areas, path)
|
|
212
|
+
|
|
213
|
+
burn_value = 2
|
|
214
|
+
with VectorLayer.layer_from_file(
|
|
215
|
+
path,
|
|
216
|
+
None,
|
|
217
|
+
raster_layer.map_projection.scale,
|
|
218
|
+
raster_layer.map_projection.name,
|
|
219
|
+
burn_value=burn_value
|
|
220
|
+
) as vector_layer:
|
|
221
|
+
layer2_total = vector_layer.sum()
|
|
222
|
+
assert layer2_total == ((vector_layer.window.xsize * vector_layer.window.ysize) / 2) * burn_value
|
|
223
|
+
|
|
224
|
+
calc = raster_layer + vector_layer
|
|
225
|
+
|
|
226
|
+
assert calc.area == vector_layer.area
|
|
227
|
+
|
|
228
|
+
total = calc.sum()
|
|
229
|
+
assert total == layer2_total + np.sum(data1)
|
|
230
|
+
|
|
231
|
+
with RasterLayer.empty_raster_layer_like(calc) as result:
|
|
232
|
+
calc.save(result)
|
|
233
|
+
total = result.sum()
|
|
234
|
+
assert total == layer2_total + np.sum(data1)
|
|
235
|
+
|
|
236
|
+
def test_vector_layers_add_unbound_rhs() -> None:
|
|
237
|
+
data1 = np.array([[1, 2], [3, 4]])
|
|
238
|
+
with RasterLayer(gdal_dataset_with_data((0.0, 0.0), 1.1, data1)) as raster_layer:
|
|
239
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
240
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
241
|
+
areas = {
|
|
242
|
+
(Area(-10.0, 10.0, 0.0, 0.0), 42),
|
|
243
|
+
(Area(0.0, 0.0, 10, -10), 43)
|
|
244
|
+
}
|
|
245
|
+
make_vectors_with_mutlile_ids(areas, path)
|
|
205
246
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
(Area(-10.0, 10.0, 0.0, 0.0), 42),
|
|
210
|
-
(Area(0.0, 0.0, 10, -10), 43)
|
|
211
|
-
}
|
|
212
|
-
make_vectors_with_mutlile_ids(areas, path)
|
|
247
|
+
burn_value = 2
|
|
248
|
+
with VectorLayer.layer_from_file(path, None, None, None, burn_value=burn_value) as vector_layer:
|
|
249
|
+
calc = raster_layer + vector_layer
|
|
213
250
|
|
|
214
|
-
|
|
215
|
-
layer2 = VectorLayer.layer_from_file(path, None, layer1.pixel_scale, layer1.projection, burn_value=burn_value)
|
|
216
|
-
layer2_total = layer2.sum()
|
|
217
|
-
assert layer2_total == ((layer2.window.xsize * layer2.window.ysize) / 2) * burn_value
|
|
251
|
+
layer2_total = ((calc.window.xsize * calc.window.ysize) / 2) * burn_value
|
|
218
252
|
|
|
219
|
-
|
|
253
|
+
assert calc.area != vector_layer.area
|
|
220
254
|
|
|
221
|
-
|
|
255
|
+
total = calc.sum()
|
|
256
|
+
assert total == layer2_total + np.sum(data1)
|
|
222
257
|
|
|
223
|
-
|
|
224
|
-
|
|
258
|
+
with RasterLayer.empty_raster_layer_like(calc) as result:
|
|
259
|
+
calc.save(result)
|
|
260
|
+
total = result.sum()
|
|
261
|
+
assert total == layer2_total + np.sum(data1)
|
|
262
|
+
|
|
263
|
+
def test_vector_layers_add_unbound_lhs() -> None:
|
|
264
|
+
data1 = np.array([[1, 2], [3, 4]])
|
|
265
|
+
with RasterLayer(gdal_dataset_with_data((0.0, 0.0), 1.1, data1)) as raster_layer:
|
|
266
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
267
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
268
|
+
areas = {
|
|
269
|
+
(Area(-10.0, 10.0, 0.0, 0.0), 42),
|
|
270
|
+
(Area(0.0, 0.0, 10, -10), 43)
|
|
271
|
+
}
|
|
272
|
+
make_vectors_with_mutlile_ids(areas, path)
|
|
273
|
+
|
|
274
|
+
burn_value = 2
|
|
275
|
+
with VectorLayer.layer_from_file(path, None, None, None, burn_value=burn_value) as vector_layer:
|
|
276
|
+
calc = vector_layer + raster_layer
|
|
277
|
+
|
|
278
|
+
layer2_total = ((calc.window.xsize * calc.window.ysize) / 2) * burn_value
|
|
279
|
+
|
|
280
|
+
assert calc.area != vector_layer.area
|
|
281
|
+
|
|
282
|
+
total = calc.sum()
|
|
283
|
+
assert total == layer2_total + np.sum(data1)
|
|
284
|
+
|
|
285
|
+
with RasterLayer.empty_raster_layer_like(calc) as result:
|
|
286
|
+
calc.save(result)
|
|
287
|
+
total = result.sum()
|
|
288
|
+
assert total == layer2_total + np.sum(data1)
|
|
225
289
|
|
|
226
290
|
def test_vector_layers_multiply() -> None:
|
|
227
291
|
data1 = np.array([[1, 2], [3, 4]])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
+
from math import ceil, floor
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
@@ -7,8 +8,9 @@ import pytest
|
|
|
7
8
|
|
|
8
9
|
import yirgacheffe as yg
|
|
9
10
|
from yirgacheffe import WGS_84_PROJECTION
|
|
10
|
-
from yirgacheffe.layers import InvalidRasterBand
|
|
11
|
+
from yirgacheffe.layers import InvalidRasterBand, RasterLayer
|
|
11
12
|
from yirgacheffe.window import Area, MapProjection, Window
|
|
13
|
+
from yirgacheffe.operators import DataType
|
|
12
14
|
from tests.helpers import gdal_dataset_of_region, gdal_multiband_dataset_with_data, \
|
|
13
15
|
make_vectors_with_id, make_vectors_with_mutlile_ids
|
|
14
16
|
|
|
@@ -213,3 +215,54 @@ def test_open_two_raster_by_glob(tiled):
|
|
|
213
215
|
with yg.read_raster(path1) as raster1:
|
|
214
216
|
with yg.read_raster(path2) as raster2:
|
|
215
217
|
assert group.sum() == raster1.sum() + raster2.sum()
|
|
218
|
+
|
|
219
|
+
def test_open_uniform_area_layer() -> None:
|
|
220
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
221
|
+
path = os.path.join(tempdir, "test.tif")
|
|
222
|
+
pixel_scale = 0.5
|
|
223
|
+
area = Area(
|
|
224
|
+
floor(-180 / pixel_scale) * pixel_scale,
|
|
225
|
+
ceil(90 / pixel_scale) * pixel_scale,
|
|
226
|
+
(floor(-180 / pixel_scale) * pixel_scale) + pixel_scale,
|
|
227
|
+
floor(-90 / pixel_scale) * pixel_scale
|
|
228
|
+
)
|
|
229
|
+
dataset = gdal_dataset_of_region(area, pixel_scale, filename=path)
|
|
230
|
+
assert dataset.RasterXSize == 1
|
|
231
|
+
assert dataset.RasterYSize == ceil(180 / pixel_scale)
|
|
232
|
+
dataset.Close()
|
|
233
|
+
|
|
234
|
+
with yg.read_narrow_raster(path) as layer:
|
|
235
|
+
assert layer.map_projection is not None
|
|
236
|
+
assert layer.map_projection.scale == (pixel_scale, -pixel_scale)
|
|
237
|
+
assert layer.area == Area(
|
|
238
|
+
floor(-180 / pixel_scale) * pixel_scale,
|
|
239
|
+
ceil(90 / pixel_scale) * pixel_scale,
|
|
240
|
+
ceil(180 / pixel_scale) * pixel_scale,
|
|
241
|
+
floor(-90 / pixel_scale) * pixel_scale
|
|
242
|
+
)
|
|
243
|
+
assert layer.window == Window(
|
|
244
|
+
0,
|
|
245
|
+
0,
|
|
246
|
+
ceil((layer.area.right - layer.area.left) / pixel_scale),
|
|
247
|
+
ceil((layer.area.top - layer.area.bottom) / pixel_scale)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def test_incorrect_tiff_for_uniform_area() -> None:
|
|
251
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
252
|
+
path = Path(tempdir) / "test.tif"
|
|
253
|
+
area = Area(-10, 10, 10, -10)
|
|
254
|
+
gdal_dataset_of_region(area, 1.0, filename=path)
|
|
255
|
+
assert path.exists()
|
|
256
|
+
with pytest.raises(ValueError):
|
|
257
|
+
_ = yg.read_narrow_raster(path)
|
|
258
|
+
|
|
259
|
+
def test_constant() -> None:
|
|
260
|
+
with yg.constant(42.0) as layer:
|
|
261
|
+
area = Area(left=-1.0, right=1.0, top=1.0, bottom=-1.0)
|
|
262
|
+
projection = MapProjection(WGS_84_PROJECTION, 0.1, -0.1)
|
|
263
|
+
with RasterLayer.empty_raster_layer(area, projection.scale, DataType.Float32) as result:
|
|
264
|
+
layer.save(result)
|
|
265
|
+
|
|
266
|
+
expected = np.full((20, 20), 42.0)
|
|
267
|
+
actual = result.read_array(0, 0, 20, 20)
|
|
268
|
+
assert (expected == actual).all()
|
|
@@ -10,7 +10,8 @@ import torch
|
|
|
10
10
|
import yirgacheffe
|
|
11
11
|
from yirgacheffe.window import Area, PixelScale
|
|
12
12
|
from yirgacheffe.layers import ConstantLayer, RasterLayer, VectorLayer
|
|
13
|
-
from yirgacheffe.operators import
|
|
13
|
+
from yirgacheffe.operators import DataType
|
|
14
|
+
from yirgacheffe._operators import LayerOperation
|
|
14
15
|
from yirgacheffe._backends import backend
|
|
15
16
|
from tests.helpers import gdal_dataset_with_data, gdal_dataset_of_region, make_vectors_with_id
|
|
16
17
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import resource
|
|
2
3
|
import tempfile
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
@@ -8,7 +9,7 @@ import torch
|
|
|
8
9
|
import yirgacheffe
|
|
9
10
|
from tests.helpers import gdal_dataset_with_data
|
|
10
11
|
from yirgacheffe.layers import RasterLayer
|
|
11
|
-
from yirgacheffe.
|
|
12
|
+
from yirgacheffe._operators import LayerOperation
|
|
12
13
|
|
|
13
14
|
# These tests are marked skip for MLX, because there seems to be a problem with
|
|
14
15
|
# calling mx.eval in the tests for parallel save on Linux (which is what we use
|
|
@@ -42,7 +43,6 @@ def test_add_byte_layers_with_one_thread_uses_regular_save(monkeypatch) -> None:
|
|
|
42
43
|
with pytest.raises(TypeError):
|
|
43
44
|
comp.parallel_save(result)
|
|
44
45
|
|
|
45
|
-
|
|
46
46
|
@pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
|
|
47
47
|
def test_add_byte_layers(monkeypatch) -> None:
|
|
48
48
|
with monkeypatch.context() as m:
|
|
@@ -71,6 +71,39 @@ def test_add_byte_layers(monkeypatch) -> None:
|
|
|
71
71
|
|
|
72
72
|
assert (expected == actual).all()
|
|
73
73
|
|
|
74
|
+
@pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
|
|
75
|
+
def test_rlimit_nofiles(monkeypatch) -> None:
|
|
76
|
+
with monkeypatch.context() as m:
|
|
77
|
+
m.setattr(yirgacheffe.constants, "YSTEP", 1)
|
|
78
|
+
m.setattr(LayerOperation, "save", None)
|
|
79
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
80
|
+
path1 = os.path.join(tempdir, "test1.tif")
|
|
81
|
+
data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
|
|
82
|
+
dataset1 = gdal_dataset_with_data((0.0, 0.0), 0.02, data1, filename=path1)
|
|
83
|
+
dataset1.Close()
|
|
84
|
+
|
|
85
|
+
rlimit_log = []
|
|
86
|
+
def callback_rlimit_recorder(_progress: float) -> None:
|
|
87
|
+
rlimit_log.append(resource.getrlimit(resource.RLIMIT_NOFILE))
|
|
88
|
+
|
|
89
|
+
with RasterLayer.layer_from_file(path1) as layer:
|
|
90
|
+
with RasterLayer.empty_raster_layer_like(layer) as result:
|
|
91
|
+
|
|
92
|
+
before_current_fd_limit, before_max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
93
|
+
|
|
94
|
+
comp = layer * 2
|
|
95
|
+
comp.parallel_save(result, callback=callback_rlimit_recorder)
|
|
96
|
+
|
|
97
|
+
after_current_fd_limit, after_max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
98
|
+
|
|
99
|
+
assert after_current_fd_limit == before_current_fd_limit
|
|
100
|
+
assert after_max_fd_limit == before_max_fd_limit
|
|
101
|
+
|
|
102
|
+
assert len(rlimit_log) > 0
|
|
103
|
+
for recorded_current_fd_limit, recorded_max_fd_limit in rlimit_log:
|
|
104
|
+
assert recorded_current_fd_limit == before_max_fd_limit
|
|
105
|
+
assert recorded_max_fd_limit == before_max_fd_limit
|
|
106
|
+
|
|
74
107
|
@pytest.mark.skipif(yirgacheffe._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
|
|
75
108
|
def test_add_byte_layers_and_sum(monkeypatch) -> None:
|
|
76
109
|
with monkeypatch.context() as m:
|
|
@@ -30,10 +30,9 @@ def test_pickle_raster_layer() -> None:
|
|
|
30
30
|
|
|
31
31
|
def test_pickle_raster_mem_layer_fails() -> None:
|
|
32
32
|
area = Area(-10, 10, 10, -10)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_ = pickle.dumps(layer)
|
|
33
|
+
with RasterLayer(gdal_dataset_of_region(area, 0.02)) as layer:
|
|
34
|
+
with pytest.raises(ValueError):
|
|
35
|
+
_ = pickle.dumps(layer)
|
|
37
36
|
|
|
38
37
|
def test_pickle_dyanamic_vector_layer() -> None:
|
|
39
38
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
@@ -91,6 +91,7 @@ def test_empty_layer_from_raster():
|
|
|
91
91
|
empty = RasterLayer.empty_raster_layer_like(source)
|
|
92
92
|
assert empty.pixel_scale == source.pixel_scale
|
|
93
93
|
assert empty.projection == source.projection
|
|
94
|
+
assert empty.map_projection == source.map_projection
|
|
94
95
|
assert empty.window == source.window
|
|
95
96
|
assert empty.datatype == source.datatype
|
|
96
97
|
assert empty.geo_transform == source.geo_transform
|
|
@@ -102,6 +103,7 @@ def test_empty_layer_from_raster_with_no_data_value(nodata):
|
|
|
102
103
|
empty = RasterLayer.empty_raster_layer_like(source, nodata=nodata)
|
|
103
104
|
assert empty.pixel_scale == source.pixel_scale
|
|
104
105
|
assert empty.projection == source.projection
|
|
106
|
+
assert empty.map_projection == source.map_projection
|
|
105
107
|
assert empty.window == source.window
|
|
106
108
|
assert empty.datatype == source.datatype
|
|
107
109
|
assert empty.geo_transform == source.geo_transform
|
|
@@ -113,6 +115,7 @@ def test_empty_layer_from_raster_with_new_smaller_area():
|
|
|
113
115
|
empty = RasterLayer.empty_raster_layer_like(source, area=smaller_area)
|
|
114
116
|
assert empty.pixel_scale == source.pixel_scale
|
|
115
117
|
assert empty.projection == source.projection
|
|
118
|
+
assert empty.map_projection == source.map_projection
|
|
116
119
|
assert empty.window == Window(0, 0, 100, 100)
|
|
117
120
|
assert empty.datatype == source.datatype
|
|
118
121
|
assert empty.geo_transform == (-1.0, 0.02, 0.0, 1.0, 0.0, -0.02)
|
|
@@ -123,6 +126,7 @@ def test_empty_layer_from_raster_new_datatype():
|
|
|
123
126
|
empty = RasterLayer.empty_raster_layer_like(source, datatype=gdal.GDT_Float64)
|
|
124
127
|
assert empty.pixel_scale == source.pixel_scale
|
|
125
128
|
assert empty.projection == source.projection
|
|
129
|
+
assert empty.map_projection == source.map_projection
|
|
126
130
|
assert empty.window == source.window
|
|
127
131
|
assert empty.datatype == DataType.Float64
|
|
128
132
|
|
|
@@ -136,6 +140,7 @@ def test_empty_layer_from_raster_with_window():
|
|
|
136
140
|
empty = RasterLayer.empty_raster_layer_like(source)
|
|
137
141
|
assert empty.pixel_scale == source.pixel_scale
|
|
138
142
|
assert empty.projection == source.projection
|
|
143
|
+
assert empty.map_projection == source.map_projection
|
|
139
144
|
assert empty.window.xoff == 0
|
|
140
145
|
assert empty.window.yoff == 0
|
|
141
146
|
assert empty.window.xsize == source.window.xsize
|
|
@@ -30,6 +30,7 @@ def test_basic_dynamic_vector_layer() -> None:
|
|
|
30
30
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
31
31
|
assert layer.window == Window(0, 0, 20, 10)
|
|
32
32
|
assert layer.projection == WGS_84_PROJECTION
|
|
33
|
+
assert layer.map_projection.name == WGS_84_PROJECTION
|
|
33
34
|
|
|
34
35
|
# The astype here is to catch escaping MLX types...
|
|
35
36
|
res = layer.read_array(0, 0, 20, 20).astype(int)
|
|
@@ -46,6 +47,7 @@ def test_rastered_vector_layer() -> None:
|
|
|
46
47
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
47
48
|
assert layer.window == Window(0, 0, 20, 10)
|
|
48
49
|
assert layer.projection == WGS_84_PROJECTION
|
|
50
|
+
assert layer.map_projection.name == WGS_84_PROJECTION
|
|
49
51
|
|
|
50
52
|
def test_basic_dynamic_vector_layer_no_filter_match() -> None:
|
|
51
53
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
@@ -100,6 +102,7 @@ def test_empty_layer_from_vector():
|
|
|
100
102
|
empty = RasterLayer.empty_raster_layer_like(source)
|
|
101
103
|
assert empty.pixel_scale == source.pixel_scale
|
|
102
104
|
assert empty.projection == source.projection
|
|
105
|
+
assert empty.map_projection == source.map_projection
|
|
103
106
|
assert empty.window == source.window
|
|
104
107
|
assert empty.area == source.area
|
|
105
108
|
|
|
@@ -12,7 +12,7 @@ except ModuleNotFoundError:
|
|
|
12
12
|
pyproject_data = tomllib.load(f)
|
|
13
13
|
__version__ = pyproject_data["project"]["version"]
|
|
14
14
|
|
|
15
|
-
from ._core import read_raster, read_rasters, read_shape, read_shape_like
|
|
15
|
+
from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster
|
|
16
16
|
from .constants import WGS_84_PROJECTION
|
|
17
17
|
|
|
18
18
|
gdal.UseExceptions()
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Optional, Sequence, Tuple, Union
|
|
3
3
|
|
|
4
|
+
from .layers.area import UniformAreaLayer
|
|
4
5
|
from .layers.base import YirgacheffeLayer
|
|
6
|
+
from .layers.constant import ConstantLayer
|
|
5
7
|
from .layers.group import GroupLayer, TiledGroupLayer
|
|
6
8
|
from .layers.rasters import RasterLayer
|
|
7
9
|
from .layers.vectors import VectorLayer
|
|
8
10
|
from .window import MapProjection
|
|
9
|
-
from .
|
|
11
|
+
from ._backends.enumeration import dtype as DataType
|
|
10
12
|
|
|
11
13
|
def read_raster(
|
|
12
14
|
filename: Union[Path,str],
|
|
@@ -31,6 +33,33 @@ def read_raster(
|
|
|
31
33
|
"""
|
|
32
34
|
return RasterLayer.layer_from_file(filename, band, ignore_nodata)
|
|
33
35
|
|
|
36
|
+
def read_narrow_raster(
|
|
37
|
+
filename: Union[Path,str],
|
|
38
|
+
band: int = 1,
|
|
39
|
+
ignore_nodata: bool = False,
|
|
40
|
+
) -> RasterLayer:
|
|
41
|
+
"""Open a 1 pixel wide raster file as a global raster.
|
|
42
|
+
|
|
43
|
+
This exists for the special use case where an area per pixel raster would have the same value per horizontal row
|
|
44
|
+
(e.g., a WGS84 map projection). For that case you can use this to load a raster that is 1 pixel wide and have
|
|
45
|
+
it automatically expanded to act like a global raster in calculations.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
filename : Path
|
|
50
|
+
Path of raster file to open.
|
|
51
|
+
band : int, default=1
|
|
52
|
+
For multi-band rasters, which band to use (defaults to first if not specified)
|
|
53
|
+
ignore_nodata : bool, default=False
|
|
54
|
+
If the GeoTIFF has a NODATA value, don't subsitute that value for NaN
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
RasterLayer
|
|
59
|
+
Returns an layer representing the raster data.
|
|
60
|
+
"""
|
|
61
|
+
return UniformAreaLayer.layer_from_file(filename, band, ignore_nodata)
|
|
62
|
+
|
|
34
63
|
def read_rasters(
|
|
35
64
|
filenames : Sequence[Union[Path,str]],
|
|
36
65
|
tiled: bool=False
|
|
@@ -132,3 +161,23 @@ def read_shape_like(
|
|
|
132
161
|
datatype,
|
|
133
162
|
burn_value,
|
|
134
163
|
)
|
|
164
|
+
|
|
165
|
+
def constant(value: Union[int,float]) -> ConstantLayer:
|
|
166
|
+
"""Generate a layer that has the same value in all pixels regardless of scale, projection, and area.
|
|
167
|
+
|
|
168
|
+
Generally this should not be necessary unless you must have the constant as the first term in an
|
|
169
|
+
expression, as Yirgacheffe will automatically convert numbers into constant layers. However if the
|
|
170
|
+
constant is the first term in the expression it must be wrapped by this call otherwise Python will
|
|
171
|
+
not know that it should be part of the Yirgacheffe expression.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
value : int or float
|
|
176
|
+
The value to be in each pixel of the expression term.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
ConstantLayer
|
|
181
|
+
Returns a constant layer of the provided value.
|
|
182
|
+
"""
|
|
183
|
+
return ConstantLayer(value)
|
|
@@ -2,10 +2,12 @@ import logging
|
|
|
2
2
|
import math
|
|
3
3
|
import multiprocessing
|
|
4
4
|
import os
|
|
5
|
+
import resource
|
|
5
6
|
import sys
|
|
6
7
|
import tempfile
|
|
7
8
|
import time
|
|
8
9
|
import types
|
|
10
|
+
from contextlib import ExitStack
|
|
9
11
|
from enum import Enum
|
|
10
12
|
from multiprocessing import Semaphore, Process
|
|
11
13
|
from multiprocessing.managers import SharedMemoryManager
|
|
@@ -387,50 +389,9 @@ class LayerOperation(LayerMathMixin):
|
|
|
387
389
|
|
|
388
390
|
@property
|
|
389
391
|
def area(self) -> Area:
|
|
390
|
-
|
|
391
|
-
lhs_area = self.lhs.area
|
|
392
|
-
try:
|
|
393
|
-
rhs_area = self.rhs.area
|
|
394
|
-
except AttributeError:
|
|
395
|
-
rhs_area = None
|
|
396
|
-
try:
|
|
397
|
-
other_area = self.other.area
|
|
398
|
-
except AttributeError:
|
|
399
|
-
other_area = None
|
|
400
|
-
|
|
401
|
-
all_areas = [x for x in [lhs_area, rhs_area, other_area] if (x is not None) and (not x.is_world)]
|
|
402
|
-
|
|
403
|
-
match self.window_op:
|
|
404
|
-
case WindowOperation.NONE:
|
|
405
|
-
return all_areas[0]
|
|
406
|
-
case WindowOperation.LEFT:
|
|
407
|
-
return lhs_area
|
|
408
|
-
case WindowOperation.RIGHT:
|
|
409
|
-
assert rhs_area is not None
|
|
410
|
-
return rhs_area
|
|
411
|
-
case WindowOperation.INTERSECTION:
|
|
412
|
-
intersection = Area(
|
|
413
|
-
left=max(x.left for x in all_areas),
|
|
414
|
-
top=min(x.top for x in all_areas),
|
|
415
|
-
right=min(x.right for x in all_areas),
|
|
416
|
-
bottom=max(x.bottom for x in all_areas)
|
|
417
|
-
)
|
|
418
|
-
if (intersection.left >= intersection.right) or (intersection.bottom >= intersection.top):
|
|
419
|
-
raise ValueError('No intersection possible')
|
|
420
|
-
return intersection
|
|
421
|
-
case WindowOperation.UNION:
|
|
422
|
-
return Area(
|
|
423
|
-
left=min(x.left for x in all_areas),
|
|
424
|
-
top=max(x.top for x in all_areas),
|
|
425
|
-
right=max(x.right for x in all_areas),
|
|
426
|
-
bottom=min(x.bottom for x in all_areas)
|
|
427
|
-
)
|
|
428
|
-
case _:
|
|
429
|
-
assert False, "Should not be reached"
|
|
392
|
+
return self._get_operation_area(self.map_projection)
|
|
430
393
|
|
|
431
394
|
def _get_operation_area(self, projection: Optional[MapProjection]) -> Area:
|
|
432
|
-
|
|
433
|
-
# The type().__name__ here is to avoid a circular import dependancy
|
|
434
395
|
lhs_area = self.lhs._get_operation_area(projection)
|
|
435
396
|
try:
|
|
436
397
|
rhs_area = self.rhs._get_operation_area(projection)
|
|
@@ -462,12 +423,13 @@ class LayerOperation(LayerMathMixin):
|
|
|
462
423
|
raise ValueError('No intersection possible')
|
|
463
424
|
return intersection
|
|
464
425
|
case WindowOperation.UNION:
|
|
465
|
-
|
|
426
|
+
union = Area(
|
|
466
427
|
left=min(x.left for x in all_areas),
|
|
467
428
|
top=max(x.top for x in all_areas),
|
|
468
429
|
right=max(x.right for x in all_areas),
|
|
469
430
|
bottom=min(x.bottom for x in all_areas)
|
|
470
431
|
)
|
|
432
|
+
return union
|
|
471
433
|
case _:
|
|
472
434
|
assert False, "Should not be reached"
|
|
473
435
|
|
|
@@ -809,83 +771,92 @@ class LayerOperation(LayerMathMixin):
|
|
|
809
771
|
|
|
810
772
|
total = 0.0
|
|
811
773
|
|
|
812
|
-
with
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
mem_sem_cast
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if yoffset+self.ystep > computation_window.ysize
|
|
832
|
-
else self.ystep)
|
|
833
|
-
source_queue.put((
|
|
834
|
-
yoffset,
|
|
835
|
-
step
|
|
836
|
-
))
|
|
837
|
-
for _ in range(worker_count):
|
|
838
|
-
source_queue.put(None)
|
|
839
|
-
|
|
840
|
-
if callback:
|
|
841
|
-
callback(0.0)
|
|
842
|
-
|
|
843
|
-
workers = [Process(target=self._parallel_worker, args=(
|
|
844
|
-
i,
|
|
845
|
-
mem_sem_cast[i][0],
|
|
846
|
-
mem_sem_cast[i][1],
|
|
847
|
-
np_dtype,
|
|
848
|
-
computation_window.xsize,
|
|
849
|
-
source_queue,
|
|
850
|
-
result_queue,
|
|
851
|
-
computation_window
|
|
852
|
-
)) for i in range(worker_count)]
|
|
853
|
-
for worker in workers:
|
|
854
|
-
worker.start()
|
|
855
|
-
|
|
856
|
-
sentinal_count = len(workers)
|
|
857
|
-
retired_blocks = 0
|
|
858
|
-
while sentinal_count > 0:
|
|
859
|
-
res = result_queue.get()
|
|
860
|
-
if res is None:
|
|
861
|
-
sentinal_count -= 1
|
|
862
|
-
continue
|
|
863
|
-
index, yoffset, step = res
|
|
864
|
-
_, sem, arr = mem_sem_cast[index]
|
|
865
|
-
if band:
|
|
866
|
-
band.WriteArray(
|
|
867
|
-
arr[0:step],
|
|
868
|
-
destination_window.xoff,
|
|
869
|
-
yoffset + destination_window.yoff,
|
|
774
|
+
with ExitStack() as stack:
|
|
775
|
+
# If we get this far, then we're going to do the multiprocessing path. In general we've had
|
|
776
|
+
# a lot of issues with limits on open file descriptors using multiprocessing on bigger machines
|
|
777
|
+
# with hundreds of cores, and so to avoid blowing up in a way that is confusing to non-compsci
|
|
778
|
+
# types, we just set the soft ulimit as high as we can
|
|
779
|
+
previous_fd_limit, max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
780
|
+
resource.setrlimit(resource.RLIMIT_NOFILE, (max_fd_limit, max_fd_limit))
|
|
781
|
+
stack.callback(resource.setrlimit, resource.RLIMIT_NOFILE, (previous_fd_limit, max_fd_limit))
|
|
782
|
+
|
|
783
|
+
with multiprocessing.Manager() as manager:
|
|
784
|
+
with SharedMemoryManager() as smm:
|
|
785
|
+
|
|
786
|
+
mem_sem_cast = []
|
|
787
|
+
for _ in range(worker_count):
|
|
788
|
+
shared_buf = smm.SharedMemory(size=np_dtype.itemsize * self.ystep * computation_window.xsize)
|
|
789
|
+
cast_buf : npt.NDArray = np.ndarray(
|
|
790
|
+
(self.ystep, computation_window.xsize),
|
|
791
|
+
dtype=np_dtype,
|
|
792
|
+
buffer=shared_buf.buf
|
|
870
793
|
)
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
794
|
+
cast_buf[:] = np.zeros((self.ystep, computation_window.xsize), np_dtype)
|
|
795
|
+
mem_sem_cast.append((shared_buf, Semaphore(), cast_buf))
|
|
796
|
+
|
|
797
|
+
source_queue = manager.Queue()
|
|
798
|
+
result_queue = manager.Queue()
|
|
799
|
+
|
|
800
|
+
for yoffset in range(0, computation_window.ysize, self.ystep):
|
|
801
|
+
step = ((computation_window.ysize - yoffset)
|
|
802
|
+
if yoffset+self.ystep > computation_window.ysize
|
|
803
|
+
else self.ystep)
|
|
804
|
+
source_queue.put((
|
|
805
|
+
yoffset,
|
|
806
|
+
step
|
|
807
|
+
))
|
|
808
|
+
for _ in range(worker_count):
|
|
809
|
+
source_queue.put(None)
|
|
810
|
+
|
|
875
811
|
if callback:
|
|
876
|
-
callback(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
812
|
+
callback(0.0)
|
|
813
|
+
|
|
814
|
+
workers = [Process(target=self._parallel_worker, args=(
|
|
815
|
+
i,
|
|
816
|
+
mem_sem_cast[i][0],
|
|
817
|
+
mem_sem_cast[i][1],
|
|
818
|
+
np_dtype,
|
|
819
|
+
computation_window.xsize,
|
|
820
|
+
source_queue,
|
|
821
|
+
result_queue,
|
|
822
|
+
computation_window
|
|
823
|
+
)) for i in range(worker_count)]
|
|
824
|
+
for worker in workers:
|
|
825
|
+
worker.start()
|
|
826
|
+
|
|
827
|
+
sentinal_count = len(workers)
|
|
828
|
+
retired_blocks = 0
|
|
829
|
+
while sentinal_count > 0:
|
|
830
|
+
res = result_queue.get()
|
|
831
|
+
if res is None:
|
|
832
|
+
sentinal_count -= 1
|
|
833
|
+
continue
|
|
834
|
+
index, yoffset, step = res
|
|
835
|
+
_, sem, arr = mem_sem_cast[index]
|
|
836
|
+
if band:
|
|
837
|
+
band.WriteArray(
|
|
838
|
+
arr[0:step],
|
|
839
|
+
destination_window.xoff,
|
|
840
|
+
yoffset + destination_window.yoff,
|
|
841
|
+
)
|
|
842
|
+
if and_sum:
|
|
843
|
+
total += np.sum(np.array(arr[0:step]).astype(np.float64))
|
|
844
|
+
sem.release()
|
|
845
|
+
retired_blocks += 1
|
|
846
|
+
if callback:
|
|
847
|
+
callback(retired_blocks / work_blocks)
|
|
848
|
+
|
|
849
|
+
processes = workers
|
|
850
|
+
while processes:
|
|
851
|
+
candidates = [x for x in processes if not x.is_alive()]
|
|
852
|
+
for candidate in candidates:
|
|
853
|
+
candidate.join()
|
|
854
|
+
if candidate.exitcode:
|
|
855
|
+
for victim in processes:
|
|
856
|
+
victim.kill()
|
|
857
|
+
sys.exit(candidate.exitcode)
|
|
858
|
+
processes.remove(candidate)
|
|
859
|
+
time.sleep(0.01)
|
|
889
860
|
|
|
890
861
|
return total if and_sum else None
|
|
891
862
|
|
|
@@ -4,10 +4,11 @@ from typing import Any, Optional, Sequence, Tuple
|
|
|
4
4
|
import deprecation
|
|
5
5
|
|
|
6
6
|
from .. import __version__
|
|
7
|
-
from ..
|
|
7
|
+
from .._operators import LayerMathMixin
|
|
8
8
|
from ..rounding import almost_equal, round_up_pixels, round_down_pixels
|
|
9
9
|
from ..window import Area, MapProjection, PixelScale, Window
|
|
10
10
|
from .._backends import backend
|
|
11
|
+
from .._backends.enumeration import dtype as DataType
|
|
11
12
|
|
|
12
13
|
class YirgacheffeLayer(LayerMathMixin):
|
|
13
14
|
"""The common base class for the different layer types. Most still inherit from RasterLayer as deep down
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from typing import Any, Union
|
|
2
2
|
|
|
3
|
-
from ..operators import DataType
|
|
4
3
|
from ..window import Area, MapProjection, PixelScale, Window
|
|
5
4
|
from .base import YirgacheffeLayer
|
|
6
5
|
from .._backends import backend
|
|
7
|
-
|
|
6
|
+
from .._backends.enumeration import dtype as DataType
|
|
8
7
|
|
|
9
8
|
class ConstantLayer(YirgacheffeLayer):
|
|
10
9
|
"""This is a layer that will return the identity value - can be used when an input layer is
|
|
@@ -6,13 +6,12 @@ from typing import Any, List, Optional, Sequence, Union
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from numpy import ma
|
|
8
8
|
|
|
9
|
-
from ..operators import DataType
|
|
10
9
|
from ..rounding import round_down_pixels
|
|
11
10
|
from ..window import Area, Window
|
|
12
11
|
from .base import YirgacheffeLayer
|
|
13
12
|
from .rasters import RasterLayer
|
|
14
13
|
from .._backends import backend
|
|
15
|
-
|
|
14
|
+
from .._backends.enumeration import dtype as DataType
|
|
16
15
|
|
|
17
16
|
class GroupLayerEmpty(ValueError):
|
|
18
17
|
def __init__(self, msg):
|
|
@@ -3,12 +3,12 @@ from typing import Any, Tuple
|
|
|
3
3
|
|
|
4
4
|
import h3
|
|
5
5
|
import numpy as np
|
|
6
|
-
from yirgacheffe.operators import DataType
|
|
7
6
|
|
|
8
7
|
from ..rounding import round_up_pixels
|
|
9
8
|
from ..window import Area, MapProjection, Window
|
|
10
9
|
from .base import YirgacheffeLayer
|
|
11
10
|
from .._backends import backend
|
|
11
|
+
from .._backends.enumeration import dtype as DataType
|
|
12
12
|
|
|
13
13
|
class H3CellLayer(YirgacheffeLayer):
|
|
14
14
|
|
|
@@ -10,8 +10,8 @@ from ..constants import WGS_84_PROJECTION
|
|
|
10
10
|
from ..window import Area, MapProjection, PixelScale, Window
|
|
11
11
|
from ..rounding import round_up_pixels
|
|
12
12
|
from .base import YirgacheffeLayer
|
|
13
|
-
from ..operators import DataType
|
|
14
13
|
from .._backends import backend
|
|
14
|
+
from .._backends.enumeration import dtype as DataType
|
|
15
15
|
|
|
16
16
|
class InvalidRasterBand(Exception):
|
|
17
17
|
def __init__ (self, band):
|
|
@@ -97,8 +97,6 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
97
97
|
threads: Optional[int]=None,
|
|
98
98
|
bands: int=1
|
|
99
99
|
) -> RasterLayer:
|
|
100
|
-
width = layer.window.xsize
|
|
101
|
-
height = layer.window.ysize
|
|
102
100
|
if area is None:
|
|
103
101
|
area = layer.area
|
|
104
102
|
assert area is not None
|
|
@@ -117,6 +115,12 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
117
115
|
area.left, projection.xstep, 0.0, area.top, 0.0, projection.ystep
|
|
118
116
|
)
|
|
119
117
|
|
|
118
|
+
if area is None:
|
|
119
|
+
og_width = layer.window.xsize
|
|
120
|
+
og_height = layer.window.ysize
|
|
121
|
+
assert (og_width == width) and (og_height == height), \
|
|
122
|
+
f"original size ({og_width}, {og_height}) != estimated ({width}, {height})"
|
|
123
|
+
|
|
120
124
|
if datatype is None:
|
|
121
125
|
datatype_arg = layer.datatype
|
|
122
126
|
elif isinstance(datatype, int):
|
|
@@ -4,11 +4,11 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, Optional, Union
|
|
5
5
|
|
|
6
6
|
from skimage import transform
|
|
7
|
-
from yirgacheffe.operators import DataType
|
|
8
7
|
|
|
9
8
|
from ..window import MapProjection, PixelScale, Window
|
|
10
9
|
from .rasters import RasterLayer, YirgacheffeLayer
|
|
11
10
|
from .._backends import backend
|
|
11
|
+
from .._backends.enumeration import dtype as DataType
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class RescaledRasterLayer(YirgacheffeLayer):
|
|
@@ -4,13 +4,15 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, Optional, Tuple, Union
|
|
5
5
|
from typing_extensions import NotRequired
|
|
6
6
|
|
|
7
|
+
import deprecation
|
|
7
8
|
from osgeo import gdal, ogr
|
|
8
9
|
|
|
9
|
-
from ..
|
|
10
|
+
from .. import __version__
|
|
10
11
|
from ..window import Area, MapProjection, PixelScale
|
|
11
12
|
from .base import YirgacheffeLayer
|
|
12
13
|
from .rasters import RasterLayer
|
|
13
14
|
from .._backends import backend
|
|
15
|
+
from .._backends.enumeration import dtype as DataType
|
|
14
16
|
|
|
15
17
|
def _validate_burn_value(burn_value: Any, layer: ogr.Layer) -> DataType: # pylint: disable=R0911
|
|
16
18
|
if isinstance(burn_value, str):
|
|
@@ -60,6 +62,12 @@ class RasteredVectorLayer(RasterLayer):
|
|
|
60
62
|
VectorLayer."""
|
|
61
63
|
|
|
62
64
|
@classmethod
|
|
65
|
+
@deprecation.deprecated(
|
|
66
|
+
deprecated_in="1.7",
|
|
67
|
+
removed_in="2.0",
|
|
68
|
+
current_version=__version__,
|
|
69
|
+
details="Use `VectorLayer` instead."
|
|
70
|
+
)
|
|
63
71
|
def layer_from_file( # type: ignore[override] # pylint: disable=W0221
|
|
64
72
|
cls,
|
|
65
73
|
filename: Union[Path,str],
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Eventually all this should be moved to the top level in 2.0, but for backwards compatibility in 1.x needs
|
|
2
|
+
# to remain here
|
|
3
|
+
|
|
4
|
+
from ._operators import where, minumum, maximum, clip, log, log2, log10, exp, exp2, nan_to_num, isin, \
|
|
5
|
+
floor, ceil # pylint: disable=W0611
|
|
6
|
+
from ._operators import abs, round # pylint: disable=W0611,W0622
|
|
7
|
+
from ._backends.enumeration import dtype as DataType # pylint: disable=W0611
|
|
@@ -174,7 +174,7 @@ class Window:
|
|
|
174
174
|
Y axis offset
|
|
175
175
|
xsize : int
|
|
176
176
|
Width of data in pixels
|
|
177
|
-
|
|
177
|
+
ysize : float
|
|
178
178
|
Height of data in pixels
|
|
179
179
|
|
|
180
180
|
Attributes
|
|
@@ -185,7 +185,7 @@ class Window:
|
|
|
185
185
|
Y axis offset
|
|
186
186
|
xsize : int
|
|
187
187
|
Width of data in pixels
|
|
188
|
-
|
|
188
|
+
ysize : float
|
|
189
189
|
Height of data in pixels
|
|
190
190
|
"""
|
|
191
191
|
xoff: int
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.7
|
|
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
|
|
@@ -262,17 +262,33 @@ Notes:
|
|
|
262
262
|
* You can have missing tiles, and these will be filled in with zeros.
|
|
263
263
|
* 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.
|
|
264
264
|
|
|
265
|
-
###
|
|
265
|
+
### Constants
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
At times it is useful to have a fixed constant in an expression. Typically, similar to numpy, if an expression involving layers has a constant in, Yirgacheffe will apply that to all pixels in the equation without need for further elaboration:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
with yg.read_raster("some_data.tif") as layer:
|
|
271
|
+
doubled_layer = layer * 2.0
|
|
272
|
+
...
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
This can be useful in tasks where you have an optional layer in your code. For example, here the code optionally loads an area-per-pixel layer, which if not present can just be substituted with a 1.0:
|
|
268
276
|
|
|
269
277
|
```python
|
|
270
278
|
try:
|
|
271
|
-
area_layer =
|
|
279
|
+
area_layer = yg.read_raster('myarea.tiff')
|
|
272
280
|
except FileDoesNotExist:
|
|
273
|
-
area_layer =
|
|
281
|
+
area_layer = 1.0
|
|
274
282
|
```
|
|
275
283
|
|
|
284
|
+
However, as with numpy, Python can not make the correct inference if the constant value is the first term in the equation. In that case you need to explicitly wrap the value with `constant` to help Python understand what is happening:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
with yg.read_raster("some_data.tif") as layer:
|
|
288
|
+
result = yg.constant(1.0) / layer
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
|
|
276
292
|
### H3CellLayer
|
|
277
293
|
|
|
278
294
|
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.
|
|
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
|