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.

Files changed (57) hide show
  1. {yirgacheffe-1.7.5/yirgacheffe.egg-info → yirgacheffe-1.7.7}/PKG-INFO +21 -5
  2. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/README.md +20 -4
  3. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/pyproject.toml +1 -1
  4. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_auto_windowing.py +80 -16
  5. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_openers.py +54 -1
  6. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_operators.py +2 -1
  7. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_parallel_operators.py +35 -2
  8. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_pickle.py +3 -4
  9. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_raster.py +5 -0
  10. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_vectors.py +3 -0
  11. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/__init__.py +1 -1
  12. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_core.py +50 -1
  13. yirgacheffe-1.7.5/yirgacheffe/operators.py → yirgacheffe-1.7.7/yirgacheffe/_operators.py +89 -118
  14. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/base.py +2 -1
  15. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/constant.py +1 -2
  16. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/group.py +1 -2
  17. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/h3layer.py +1 -1
  18. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/rasters.py +7 -3
  19. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/rescaled.py +1 -1
  20. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/vectors.py +9 -1
  21. yirgacheffe-1.7.7/yirgacheffe/operators.py +7 -0
  22. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/window.py +2 -2
  23. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7/yirgacheffe.egg-info}/PKG-INFO +21 -5
  24. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/SOURCES.txt +1 -0
  25. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/LICENSE +0 -0
  26. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/MANIFEST.in +0 -0
  27. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/setup.cfg +0 -0
  28. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_area.py +0 -0
  29. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_base.py +0 -0
  30. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_constants.py +0 -0
  31. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_datatypes.py +0 -0
  32. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_group.py +0 -0
  33. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_h3layer.py +0 -0
  34. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_intersection.py +0 -0
  35. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_multiband.py +0 -0
  36. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_nodata.py +0 -0
  37. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_optimisation.py +0 -0
  38. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_projection.py +0 -0
  39. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_rescaling.py +0 -0
  40. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_rounding.py +0 -0
  41. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_save_with_window.py +0 -0
  42. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_sum_with_window.py +0 -0
  43. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_uniform_area_layer.py +0 -0
  44. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_union.py +0 -0
  45. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/tests/test_window.py +0 -0
  46. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/__init__.py +0 -0
  47. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/enumeration.py +0 -0
  48. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/mlx.py +0 -0
  49. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/_backends/numpy.py +0 -0
  50. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/constants.py +0 -0
  51. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/__init__.py +0 -0
  52. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/layers/area.py +0 -0
  53. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe/rounding.py +0 -0
  54. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  55. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/entry_points.txt +0 -0
  56. {yirgacheffe-1.7.5 → yirgacheffe-1.7.7}/yirgacheffe.egg-info/requires.txt +0 -0
  57. {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.5
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
- ### ConstantLayer
265
+ ### Constants
266
266
 
267
- 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.
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 = RasterLayer.layer_from_file('myarea.tiff')
279
+ area_layer = yg.read_raster('myarea.tiff')
272
280
  except FileDoesNotExist:
273
- area_layer = ConstantLayer(0.0)
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
- ### ConstantLayer
235
+ ### Constants
236
236
 
237
- 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.
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 = RasterLayer.layer_from_file('myarea.tiff')
249
+ area_layer = yg.read_raster('myarea.tiff')
242
250
  except FileDoesNotExist:
243
- area_layer = ConstantLayer(0.0)
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.5"
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
- layer1 = RasterLayer(gdal_dataset_with_data((0.0, 0.0), 1.0, data1))
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
- with tempfile.TemporaryDirectory() as tempdir:
207
- path = os.path.join(tempdir, "test.gpkg")
208
- areas = {
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
- burn_value = 2
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
- calc = layer1 + layer2
253
+ assert calc.area != vector_layer.area
220
254
 
221
- assert calc.area == layer2.area
255
+ total = calc.sum()
256
+ assert total == layer2_total + np.sum(data1)
222
257
 
223
- total = calc.sum()
224
- assert total == layer2_total + np.sum(data1)
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 LayerOperation, DataType
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.operators import LayerOperation
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
- layer = RasterLayer(gdal_dataset_of_region(area, 0.02))
34
-
35
- with pytest.raises(ValueError):
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 .operators import DataType
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
- # The type().__name__ here is to avoid a circular import dependancy
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
- return Area(
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 multiprocessing.Manager() as manager:
813
- with SharedMemoryManager() as smm:
814
-
815
- mem_sem_cast = []
816
- for _ in range(worker_count):
817
- shared_buf = smm.SharedMemory(size=np_dtype.itemsize * self.ystep * computation_window.xsize)
818
- cast_buf : npt.NDArray = np.ndarray(
819
- (self.ystep, computation_window.xsize),
820
- dtype=np_dtype,
821
- buffer=shared_buf.buf
822
- )
823
- cast_buf[:] = np.zeros((self.ystep, computation_window.xsize), np_dtype)
824
- mem_sem_cast.append((shared_buf, Semaphore(), cast_buf))
825
-
826
- source_queue = manager.Queue()
827
- result_queue = manager.Queue()
828
-
829
- for yoffset in range(0, computation_window.ysize, self.ystep):
830
- step = ((computation_window.ysize - yoffset)
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
- if and_sum:
872
- total += np.sum(np.array(arr[0:step]).astype(np.float64))
873
- sem.release()
874
- retired_blocks += 1
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(retired_blocks / work_blocks)
877
-
878
- processes = workers
879
- while processes:
880
- candidates = [x for x in processes if not x.is_alive()]
881
- for candidate in candidates:
882
- candidate.join()
883
- if candidate.exitcode:
884
- for victim in processes:
885
- victim.kill()
886
- sys.exit(candidate.exitcode)
887
- processes.remove(candidate)
888
- time.sleep(0.01)
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 ..operators import DataType, LayerMathMixin
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 ..operators import DataType
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
- bottom : float
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
- bottom : float
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.5
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
- ### ConstantLayer
265
+ ### Constants
266
266
 
267
- 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.
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 = RasterLayer.layer_from_file('myarea.tiff')
279
+ area_layer = yg.read_raster('myarea.tiff')
272
280
  except FileDoesNotExist:
273
- area_layer = ConstantLayer(0.0)
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.
@@ -29,6 +29,7 @@ tests/test_vectors.py
29
29
  tests/test_window.py
30
30
  yirgacheffe/__init__.py
31
31
  yirgacheffe/_core.py
32
+ yirgacheffe/_operators.py
32
33
  yirgacheffe/constants.py
33
34
  yirgacheffe/operators.py
34
35
  yirgacheffe/rounding.py
File without changes
File without changes
File without changes