yirgacheffe 1.7.8__tar.gz → 1.8.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.

Potentially problematic release.


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

Files changed (59) hide show
  1. {yirgacheffe-1.7.8/yirgacheffe.egg-info → yirgacheffe-1.8.0}/PKG-INFO +13 -6
  2. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/README.md +1 -1
  3. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/pyproject.toml +23 -5
  4. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_base.py +6 -7
  5. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_datatypes.py +2 -2
  6. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_operators.py +17 -1
  7. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_rounding.py +1 -2
  8. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/__init__.py +1 -0
  9. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/_backends/mlx.py +6 -4
  10. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/_backends/numpy.py +6 -4
  11. yirgacheffe-1.8.0/yirgacheffe/_core.py +163 -0
  12. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/_operators.py +41 -29
  13. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/area.py +6 -3
  14. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/base.py +20 -27
  15. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/constant.py +4 -2
  16. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/group.py +11 -11
  17. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/h3layer.py +4 -2
  18. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/rasters.py +18 -18
  19. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/rescaled.py +3 -3
  20. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/vectors.py +36 -37
  21. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/rounding.py +4 -3
  22. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/window.py +49 -96
  23. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0/yirgacheffe.egg-info}/PKG-INFO +13 -6
  24. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe.egg-info/requires.txt +9 -1
  25. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe.egg-info/top_level.txt +1 -0
  26. yirgacheffe-1.7.8/yirgacheffe/_core.py +0 -183
  27. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/LICENSE +0 -0
  28. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/MANIFEST.in +0 -0
  29. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/setup.cfg +0 -0
  30. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_area.py +0 -0
  31. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_auto_windowing.py +0 -0
  32. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_constants.py +0 -0
  33. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_group.py +0 -0
  34. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_h3layer.py +0 -0
  35. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_intersection.py +0 -0
  36. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_multiband.py +0 -0
  37. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_nodata.py +0 -0
  38. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_openers.py +0 -0
  39. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_optimisation.py +0 -0
  40. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_parallel_operators.py +0 -0
  41. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_pickle.py +0 -0
  42. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_projection.py +0 -0
  43. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_raster.py +0 -0
  44. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_rescaling.py +0 -0
  45. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_save_with_window.py +0 -0
  46. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_sum_with_window.py +0 -0
  47. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_uniform_area_layer.py +0 -0
  48. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_union.py +0 -0
  49. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_vectors.py +0 -0
  50. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/tests/test_window.py +0 -0
  51. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/_backends/__init__.py +0 -0
  52. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/_backends/enumeration.py +0 -0
  53. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/constants.py +0 -0
  54. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/layers/__init__.py +0 -0
  55. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/operators.py +0 -0
  56. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe/py.typed +0 -0
  57. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe.egg-info/SOURCES.txt +0 -0
  58. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  59. {yirgacheffe-1.7.8 → yirgacheffe-1.8.0}/yirgacheffe.egg-info/entry_points.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.7.8
3
+ Version: 1.8.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
7
- Project-URL: Homepage, https://github.com/quantifyearth/yirgacheffe
7
+ Project-URL: Homepage, https://yirgacheffe.org/
8
8
  Project-URL: Repository, https://github.com/quantifyearth/yirgacheffe.git
9
9
  Project-URL: Issues, https://github.com/quantifyearth/yirgacheffe/issues
10
- Project-URL: Changelog, https://github.com/quantifyearth/yirgacheffe/blob/main/CHANGES.md
10
+ Project-URL: Changelog, https://yirgacheffe.org/latest/changelog/
11
11
  Keywords: gdal,gis,geospatial,declarative
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Intended Audience :: Science/Research
@@ -26,16 +26,23 @@ Requires-Dist: torch
26
26
  Requires-Dist: dill
27
27
  Requires-Dist: deprecation
28
28
  Requires-Dist: tomli
29
+ Requires-Dist: h3
30
+ Provides-Extra: mlx
31
+ Requires-Dist: mlx; extra == "mlx"
29
32
  Provides-Extra: dev
30
33
  Requires-Dist: mypy; extra == "dev"
31
34
  Requires-Dist: pylint; extra == "dev"
32
35
  Requires-Dist: pytest; extra == "dev"
33
- Requires-Dist: h3; extra == "dev"
34
36
  Requires-Dist: pytest-cov; extra == "dev"
35
- Requires-Dist: mlx; extra == "dev"
37
+ Requires-Dist: build; extra == "dev"
38
+ Requires-Dist: twine; extra == "dev"
39
+ Requires-Dist: mkdocs-material; extra == "dev"
40
+ Requires-Dist: mkdocstrings-python; extra == "dev"
41
+ Requires-Dist: mike; extra == "dev"
42
+ Requires-Dist: mkdocs-gen-files; extra == "dev"
36
43
  Dynamic: license-file
37
44
 
38
- # Yirgacheffe: a gdal wrapper that does the tricky bits
45
+ # Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
39
46
 
40
47
  ## Overview
41
48
 
@@ -1,4 +1,4 @@
1
- # Yirgacheffe: a gdal wrapper that does the tricky bits
1
+ # Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
2
2
 
3
3
  ## Overview
4
4
 
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "yirgacheffe"
9
- version = "1.7.8"
9
+ version = "1.8.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" }]
@@ -28,18 +28,33 @@ dependencies = [
28
28
  "torch",
29
29
  "dill",
30
30
  "deprecation",
31
- "tomli"
31
+ "tomli",
32
+ "h3",
32
33
  ]
33
34
  requires-python = ">=3.10"
34
35
 
35
36
  [project.optional-dependencies]
36
- dev = ["mypy", "pylint", "pytest", "h3", "pytest-cov", "mlx"]
37
+ mlx = [
38
+ "mlx",
39
+ ]
40
+ dev = [
41
+ "mypy",
42
+ "pylint",
43
+ "pytest",
44
+ "pytest-cov",
45
+ "build",
46
+ "twine",
47
+ "mkdocs-material",
48
+ "mkdocstrings-python",
49
+ "mike",
50
+ "mkdocs-gen-files",
51
+ ]
37
52
 
38
53
  [project.urls]
39
- Homepage = "https://github.com/quantifyearth/yirgacheffe"
54
+ Homepage = "https://yirgacheffe.org/"
40
55
  Repository = "https://github.com/quantifyearth/yirgacheffe.git"
41
56
  Issues = "https://github.com/quantifyearth/yirgacheffe/issues"
42
- Changelog = "https://github.com/quantifyearth/yirgacheffe/blob/main/CHANGES.md"
57
+ Changelog = "https://yirgacheffe.org/latest/changelog/"
43
58
 
44
59
  [project.scripts]
45
60
  realpython = "reader.__main__:main"
@@ -58,3 +73,6 @@ ignore_missing_imports = true
58
73
  [[tool.mypy.overrides]]
59
74
  module = "deprecation.*"
60
75
  ignore_missing_imports = true
76
+
77
+ [tool.setuptools.packages.find]
78
+ exclude = ["site*", "docs*", "tests*"]
@@ -1,5 +1,4 @@
1
1
  import math
2
- from typing import Tuple
3
2
 
4
3
  import pytest
5
4
 
@@ -58,7 +57,7 @@ def test_pixel_from_latlng_unsupported_projection() -> None:
58
57
  ),
59
58
  ]
60
59
  )
61
- def test_latlng_for_pixel(area: Area, pixel: Tuple[int,int], expected: Tuple[float,float]) -> None:
60
+ def test_latlng_for_pixel(area: Area, pixel: tuple[int, int], expected: tuple[float, float]) -> None:
62
61
  layer = YirgacheffeLayer(
63
62
  area,
64
63
  MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
@@ -87,7 +86,7 @@ def test_latlng_for_pixel(area: Area, pixel: Tuple[int,int], expected: Tuple[flo
87
86
  ),
88
87
  ]
89
88
  )
90
- def test_pixel_for_latlng(area: Area, coord: Tuple[float,float], expected: Tuple[int,int]) -> None:
89
+ def test_pixel_for_latlng(area: Area, coord: tuple[float, float], expected: tuple[int, int]) -> None:
91
90
  layer = YirgacheffeLayer(
92
91
  area,
93
92
  MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
@@ -140,8 +139,8 @@ def test_pixel_for_latlng(area: Area, coord: Tuple[float,float], expected: Tuple
140
139
  def test_latlng_for_pixel_with_intersection(
141
140
  area: Area,
142
141
  window: Area,
143
- pixel: Tuple[int,int],
144
- expected: Tuple[float,float]
142
+ pixel: tuple[int, int],
143
+ expected: tuple[float, float]
145
144
  ) -> None:
146
145
  layer = YirgacheffeLayer(
147
146
  area,
@@ -178,8 +177,8 @@ def test_latlng_for_pixel_with_intersection(
178
177
  def test_pixel_for_latlng_with_intersection(
179
178
  area: Area,
180
179
  window: Area,
181
- coord: Tuple[float,float],
182
- expected: Tuple[int,int]
180
+ coord: tuple[float, float],
181
+ expected: tuple[int, int]
183
182
  ) -> None:
184
183
  layer = YirgacheffeLayer(
185
184
  area,
@@ -22,7 +22,7 @@ from tests.helpers import gdal_dataset_with_data
22
22
  ])
23
23
  def test_round_trip(gtype) -> None:
24
24
  ytype = DataType.of_gdal(gtype)
25
- backend_type = backend.dtype_to_backed(ytype)
25
+ backend_type = backend.dtype_to_backend(ytype)
26
26
  assert backend.backend_to_dtype(backend_type) == ytype
27
27
 
28
28
  @pytest.mark.parametrize("ytype", [
@@ -42,7 +42,7 @@ def test_round_trip_from_gdal(ytype) -> None:
42
42
  assert DataType.of_gdal(gtype) == ytype
43
43
 
44
44
  def test_round_trip_float64() -> None:
45
- backend_type = backend.dtype_to_backed(DataType.Float64)
45
+ backend_type = backend.dtype_to_backend(DataType.Float64)
46
46
  ytype = backend.backend_to_dtype(backend_type)
47
47
  match BACKEND:
48
48
  case "NUMPY":
@@ -1428,18 +1428,34 @@ def test_ceil_module() -> None:
1428
1428
  actual = result.read_array(0, 0, 4, 2)
1429
1429
  assert (expected == actual).all()
1430
1430
 
1431
- def test_to_geotiff_on_layer() -> None:
1431
+ def test_to_geotiff_on_int_layer() -> None:
1432
1432
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
1433
1433
  layer1 = RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1))
1434
+ assert layer1.datatype == DataType.Int64
1434
1435
 
1435
1436
  with tempfile.TemporaryDirectory() as tempdir:
1436
1437
  filename = os.path.join(tempdir, "test.tif")
1437
1438
  layer1.to_geotiff(filename)
1438
1439
 
1439
1440
  with RasterLayer.layer_from_file(filename) as result:
1441
+ assert result.datatype == DataType.Int64
1440
1442
  actual = result.read_array(0, 0, 4, 2)
1441
1443
  assert (data1 == actual).all()
1442
1444
 
1445
+ def test_to_geotiff_on_float_layer() -> None:
1446
+ data1 = np.array([[1.1, 2.1, 3.1, 4.1], [5.1, 6.1, 7.1, 8.1]])
1447
+ layer1 = RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1))
1448
+ assert layer1.datatype == DataType.Float64
1449
+
1450
+ with tempfile.TemporaryDirectory() as tempdir:
1451
+ filename = os.path.join(tempdir, "test.tif")
1452
+ layer1.to_geotiff(filename)
1453
+
1454
+ with RasterLayer.layer_from_file(filename) as result:
1455
+ assert result.datatype == DataType.Float64
1456
+ actual = result.read_array(0, 0, 4, 2)
1457
+ assert np.isclose(data1, actual).all()
1458
+
1443
1459
  def test_to_geotiff_single_thread() -> None:
1444
1460
  data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
1445
1461
  layer1 = RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1))
@@ -1,5 +1,4 @@
1
1
  import sys
2
- from typing import List
3
2
 
4
3
  import pytest
5
4
 
@@ -84,5 +83,5 @@ def test_pixel_rounding_down(pixels: float, scale: float, expected: int) -> None
84
83
  ),
85
84
  ]
86
85
  )
87
- def test_pixel_scale_comparison(pixel_scales: List[PixelScale], expected: bool) -> None:
86
+ def test_pixel_scale_comparison(pixel_scales: list[PixelScale], expected: bool) -> None:
88
87
  assert are_pixel_scales_equal_enough(pixel_scales) == expected
@@ -14,5 +14,6 @@ except ModuleNotFoundError:
14
14
 
15
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
+ from .window import Area, MapProjection, Window
17
18
 
18
19
  gdal.UseExceptions()
@@ -1,4 +1,6 @@
1
- from typing import Callable, Dict
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
2
4
 
3
5
  import numpy as np
4
6
  import mlx.core as mx # type: ignore
@@ -131,7 +133,7 @@ def conv2d_op(data, weights):
131
133
  return res[0]
132
134
 
133
135
 
134
- def dtype_to_backed(dt):
136
+ def dtype_to_backend(dt):
135
137
  match dt:
136
138
  case dtype.Float32:
137
139
  return mx.float32
@@ -182,9 +184,9 @@ def backend_to_dtype(val):
182
184
  raise ValueError
183
185
 
184
186
  def astype_op(data, datatype):
185
- return data.astype(dtype_to_backed(datatype))
187
+ return data.astype(dtype_to_backend(datatype))
186
188
 
187
- operator_map : Dict[op,Callable] = {
189
+ operator_map: dict[op, Callable] = {
188
190
  op.ADD: mx.array.__add__,
189
191
  op.SUB: mx.array.__sub__,
190
192
  op.MUL: mul_op,
@@ -1,4 +1,6 @@
1
- from typing import Callable, Dict
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
2
4
 
3
5
  import numpy as np
4
6
  import torch
@@ -67,7 +69,7 @@ def conv2d_op(data, weights):
67
69
  res = conv(preped_data)
68
70
  return res.detach().numpy()[0][0]
69
71
 
70
- def dtype_to_backed(dt):
72
+ def dtype_to_backend(dt):
71
73
  match dt:
72
74
  case dtype.Float32:
73
75
  return np.float32
@@ -120,9 +122,9 @@ def backend_to_dtype(val):
120
122
  raise ValueError
121
123
 
122
124
  def astype_op(data, datatype):
123
- return data.astype(dtype_to_backed(datatype))
125
+ return data.astype(dtype_to_backend(datatype))
124
126
 
125
- operator_map : Dict[op,Callable] = {
127
+ operator_map: dict[op, Callable] = {
126
128
  op.ADD: np.ndarray.__add__,
127
129
  op.SUB: np.ndarray.__sub__,
128
130
  op.MUL: np.ndarray.__mul__,
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Sequence
5
+
6
+ from .layers.area import UniformAreaLayer
7
+ from .layers.base import YirgacheffeLayer
8
+ from .layers.constant import ConstantLayer
9
+ from .layers.group import GroupLayer, TiledGroupLayer
10
+ from .layers.rasters import RasterLayer
11
+ from .layers.vectors import VectorLayer
12
+ from .window import MapProjection
13
+ from ._backends.enumeration import dtype as DataType
14
+
15
+ def read_raster(
16
+ filename: Path | str,
17
+ band: int = 1,
18
+ ignore_nodata: bool = False,
19
+ ) -> RasterLayer:
20
+ """Open a raster file (e.g., GeoTIFF).
21
+
22
+ Args:
23
+ filename: Path of raster file to open.
24
+ band: For multi-band rasters, which band to use (defaults to first if not specified).
25
+ ignore_nodata: If the GeoTIFF has a NODATA value, don't substitute that value for NaN.
26
+
27
+ Returns:
28
+ An layer representing the raster data.
29
+
30
+ Examples:
31
+ >>> import yirgacheffe as yg
32
+ >>> with yg.read_raster('test.tif') as layer:
33
+ ... total = layer.sum()
34
+ """
35
+ return RasterLayer.layer_from_file(filename, band, ignore_nodata)
36
+
37
+ def read_narrow_raster(
38
+ filename: Path | str,
39
+ band: int = 1,
40
+ ignore_nodata: bool = False,
41
+ ) -> RasterLayer:
42
+ """Open a 1 pixel wide raster file as a global raster.
43
+
44
+ This exists for the special use case where an area per pixel raster would have the same value per horizontal row
45
+ (e.g., a WGS84 map projection). For that case you can use this to load a raster that is 1 pixel wide and have
46
+ it automatically expanded to act like a global raster in calculations.
47
+
48
+ Args:
49
+ filename: Path of raster file to open.
50
+ band: For multi-band rasters, which band to use (defaults to first if not specified).
51
+ ignore_nodata: If the GeoTIFF has a NODATA value, don't substitute that value for NaN.
52
+
53
+ Returns:
54
+ An layer representing the raster data.
55
+ """
56
+ return UniformAreaLayer.layer_from_file(filename, band, ignore_nodata)
57
+
58
+ def read_rasters(
59
+ filenames : Sequence[Path | str],
60
+ tiled: bool=False
61
+ ) -> GroupLayer:
62
+ """Open a set of raster files (e.g., GeoTIFFs) as a single layer.
63
+
64
+ Args:
65
+ filenames: List of paths of raster files to open.
66
+ tiled: If you know that the rasters for a regular tileset, then setting this flag allows
67
+ Yirgacheffe to perform certain optimisations that significantly improve performance for
68
+ this use case.
69
+
70
+ Returns:
71
+ An layer representing the raster data.
72
+
73
+ Examples:
74
+ >>> import yirgacheffe as yg
75
+ >>> with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
76
+ ... ...
77
+ """
78
+ if not tiled:
79
+ return GroupLayer.layer_from_files(filenames)
80
+ else:
81
+ return TiledGroupLayer.layer_from_files(filenames)
82
+
83
+ def read_shape(
84
+ filename: Path | str,
85
+ projection: MapProjection | tuple[str, tuple[float, float]] | None = None,
86
+ where_filter: str | None = None,
87
+ datatype: DataType | None = None,
88
+ burn_value: int | float | str = 1,
89
+ ) -> VectorLayer:
90
+ """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
91
+
92
+ Args:
93
+ filename: Path of vector file to open.
94
+ projection: The map projection to use.
95
+ where_filter: For use with files with many entries (e.g., GPKG), applies this filter to the data.
96
+ datatype: Specify the data type of the raster data generated.
97
+ burn_value: The value of each pixel in the polygon.
98
+
99
+ Returns:
100
+ An layer representing the vector data.
101
+
102
+ Examples:
103
+ >>> import yirgacheffe as yg
104
+ >>> with yg.read_shape('range.gpkg') as layer:
105
+ ... ...
106
+ """
107
+
108
+ if projection is not None:
109
+ if not isinstance(projection, MapProjection):
110
+ projection_name, scale_tuple = projection
111
+ projection = MapProjection(projection_name, scale_tuple[0], scale_tuple[1])
112
+
113
+ return VectorLayer._future_layer_from_file(
114
+ filename,
115
+ where_filter,
116
+ projection,
117
+ datatype,
118
+ burn_value,
119
+ )
120
+
121
+ def read_shape_like(
122
+ filename: Path | str,
123
+ like: YirgacheffeLayer,
124
+ where_filter: str | None = None,
125
+ datatype: DataType | None = None,
126
+ burn_value: int | float | str = 1,
127
+ ) -> VectorLayer:
128
+ """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
129
+
130
+ Args:
131
+ filename: Path of vector file to open.
132
+ like: Another layer that has a projection and pixel scale set. This layer will
133
+ use the same projection and pixel scale as that one.
134
+ where_filter: For use with files with many entries (e.g., GPKG), applies this filter to the data.
135
+ datatype: Specify the data type of the raster data generated.
136
+ burn_value: The value of each pixel in the polygon.
137
+
138
+ Returns:
139
+ An layer representing the vector data.
140
+ """
141
+ return VectorLayer.layer_from_file_like(
142
+ filename,
143
+ like,
144
+ where_filter,
145
+ datatype,
146
+ burn_value,
147
+ )
148
+
149
+ def constant(value: int | float) -> ConstantLayer:
150
+ """Generate a layer that has the same value in all pixels regardless of scale, projection, and area.
151
+
152
+ Generally this should not be necessary unless you must have the constant as the first term in an
153
+ expression, as Yirgacheffe will automatically convert numbers into constant layers. However if the
154
+ constant is the first term in the expression it must be wrapped by this call otherwise Python will
155
+ not know that it should be part of the Yirgacheffe expression.
156
+
157
+ Args:
158
+ value: The value to be in each pixel of the expression term.
159
+
160
+ Returns:
161
+ A constant layer of the provided value.
162
+ """
163
+ return ConstantLayer(value)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import math
3
5
  import multiprocessing
@@ -12,7 +14,7 @@ from enum import Enum
12
14
  from multiprocessing import Semaphore, Process
13
15
  from multiprocessing.managers import SharedMemoryManager
14
16
  from pathlib import Path
15
- from typing import Callable, Dict, Optional, Union
17
+ from typing import Callable
16
18
 
17
19
  import deprecation
18
20
  import numpy as np
@@ -26,6 +28,8 @@ from .window import Area, PixelScale, MapProjection, Window
26
28
  from ._backends import backend
27
29
  from ._backends.enumeration import operators as op
28
30
  from ._backends.enumeration import dtype as DataType
31
+ from ._backends.numpy import dtype_to_backend as dtype_to_numpy
32
+ from ._backends.numpy import backend_to_dtype as numpy_to_dtype
29
33
 
30
34
  logger = logging.getLogger(__name__)
31
35
  logger.setLevel(logging.WARNING)
@@ -47,6 +51,11 @@ class LayerConstant:
47
51
  def _eval(self, _area, _projection, _index, _step, _target_window):
48
52
  return self.val
49
53
 
54
+ @property
55
+ def datatype(self) -> DataType:
56
+ numpy_type = np.result_type(self.val)
57
+ return numpy_to_dtype(numpy_type)
58
+
50
59
  @property
51
60
  def area(self) -> Area:
52
61
  return Area.world()
@@ -258,10 +267,10 @@ class LayerMathMixin:
258
267
 
259
268
  def to_geotiff(
260
269
  self,
261
- filename: Union[Path,str],
270
+ filename: Path | str,
262
271
  and_sum: bool = False,
263
- parallelism:Optional[Union[int,bool]]=None
264
- ) -> Optional[float]:
272
+ parallelism: int | bool | None = None
273
+ ) -> float | None:
265
274
  return LayerOperation(self).to_geotiff(filename, and_sum, parallelism)
266
275
 
267
276
  def sum(self):
@@ -391,7 +400,7 @@ class LayerOperation(LayerMathMixin):
391
400
  def area(self) -> Area:
392
401
  return self._get_operation_area(self.map_projection)
393
402
 
394
- def _get_operation_area(self, projection: Optional[MapProjection]) -> Area:
403
+ def _get_operation_area(self, projection: MapProjection | None) -> Area:
395
404
  lhs_area = self.lhs._get_operation_area(projection)
396
405
  try:
397
406
  rhs_area = self.rhs._get_operation_area(projection)
@@ -475,8 +484,16 @@ class LayerOperation(LayerMathMixin):
475
484
 
476
485
  @property
477
486
  def datatype(self) -> DataType:
478
- # TODO: Work out how to indicate type promotion via numpy
479
- return self.lhs.datatype
487
+ internal_types: list[DataType] = [
488
+ self.lhs.datatype
489
+ ]
490
+ if self.rhs is not None:
491
+ internal_types.append(self.rhs.datatype)
492
+ if self.other is not None:
493
+ internal_types.append(self.other.datatype)
494
+ internal_types_as_numpy_types = [dtype_to_numpy(x) for x in internal_types]
495
+ coerced_type = np.result_type(*internal_types_as_numpy_types)
496
+ return numpy_to_dtype(coerced_type)
480
497
 
481
498
  @property
482
499
  @deprecation.deprecated(
@@ -496,7 +513,7 @@ class LayerOperation(LayerMathMixin):
496
513
  return projection
497
514
 
498
515
  @property
499
- def map_projection(self) -> Optional[MapProjection]:
516
+ def map_projection(self) -> MapProjection | None:
500
517
  try:
501
518
  projection = self.lhs.map_projection
502
519
  except AttributeError:
@@ -515,7 +532,7 @@ class LayerOperation(LayerMathMixin):
515
532
  projection: MapProjection,
516
533
  index: int,
517
534
  step: int,
518
- target_window:Optional[Window]=None
535
+ target_window: Window | None = None
519
536
  ):
520
537
 
521
538
  if self.buffer_padding:
@@ -592,7 +609,7 @@ class LayerOperation(LayerMathMixin):
592
609
  res = chunk_max
593
610
  return res
594
611
 
595
- def save(self, destination_layer, and_sum=False, callback=None, band=1) -> Optional[float]:
612
+ def save(self, destination_layer, and_sum=False, callback=None, band=1) -> float | None:
596
613
  """
597
614
  Calling save will write the output of the operation to the provied layer.
598
615
  If you provide sum as true it will additionall compute the sum and return that.
@@ -708,7 +725,7 @@ class LayerOperation(LayerMathMixin):
708
725
  callback=None,
709
726
  parallelism=None,
710
727
  band=1
711
- ) -> Optional[float]:
728
+ ) -> float | None:
712
729
  assert (destination_layer is not None) or and_sum
713
730
  try:
714
731
  computation_window = self.window
@@ -748,7 +765,7 @@ class LayerOperation(LayerMathMixin):
748
765
  or (computation_window.ysize != destination_window.ysize):
749
766
  raise ValueError("Destination raster window size does not match input raster window size.")
750
767
 
751
- np_type_map : Dict[int, np.dtype] = {
768
+ np_type_map: dict[int, np.dtype] = {
752
769
  gdal.GDT_Byte: np.dtype('byte'),
753
770
  gdal.GDT_Float32: np.dtype('float32'),
754
771
  gdal.GDT_Float64: np.dtype('float64'),
@@ -867,7 +884,7 @@ class LayerOperation(LayerMathMixin):
867
884
  callback=None,
868
885
  parallelism=None,
869
886
  band=1
870
- ) -> Optional[float]:
887
+ ) -> float | None:
871
888
  if destination_layer is None:
872
889
  raise ValueError("Layer is required")
873
890
  return self._parallel_save(destination_layer, and_sum, callback, parallelism, band)
@@ -877,25 +894,20 @@ class LayerOperation(LayerMathMixin):
877
894
 
878
895
  def to_geotiff(
879
896
  self,
880
- filename: Union[Path,str],
897
+ filename: Path | str,
881
898
  and_sum: bool = False,
882
- parallelism:Optional[Union[int,bool]] = None
883
- ) -> Optional[float]:
899
+ parallelism: int | bool | None = None
900
+ ) -> float | None:
884
901
  """Saves a calculation to a raster file, optionally also returning the sum of pixels.
885
902
 
886
- Parameters
887
- ----------
888
- filename : Path
889
- Path of the raster to save the result to.
890
- and_sum : bool, default=False
891
- If true then the function will also calculate the sum of the raster as it goes and return that value.
892
- parallelism : int or bool, optional, default=None
893
- If passed, attempt to use multiple CPU cores up to the number provided, or if set to True, yirgacheffe
894
- will pick a sensible value.
895
-
896
- Returns
897
- -------
898
- float, optional
903
+ Args:
904
+ filename: Path of the raster to save the result to.
905
+ and_sum: If true then the function will also calculate the sum of the raster as it goes and return
906
+ that value.
907
+ parallelism: If passed, attempt to use multiple CPU cores up to the number provided, or if set to True,
908
+ yirgacheffe will pick a sensible value.
909
+
910
+ Returns:
899
911
  Either returns None, or the sum of the pixels in the resulting raster if `and_sum` was specified.
900
912
  """
901
913
 
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  from math import ceil, floor
2
- from typing import Any, Optional
4
+ from pathlib import Path
5
+ from typing import Any
3
6
 
4
7
  import numpy
5
8
  from osgeo import gdal
@@ -18,7 +21,7 @@ class UniformAreaLayer(RasterLayer):
18
21
  """
19
22
 
20
23
  @staticmethod
21
- def generate_narrow_area_projection(source_filename: str, target_filename: str) -> None:
24
+ def generate_narrow_area_projection(source_filename: Path | str, target_filename: Path | str) -> None:
22
25
  source = gdal.Open(source_filename, gdal.GA_ReadOnly)
23
26
  if source is None:
24
27
  raise FileNotFoundError(source_filename)
@@ -56,7 +59,7 @@ class UniformAreaLayer(RasterLayer):
56
59
  return False
57
60
  return True
58
61
 
59
- def __init__(self, dataset, name: Optional[str] = None, band: int = 1, ignore_nodata: bool = False):
62
+ def __init__(self, dataset, name: str | None = None, band: int = 1, ignore_nodata: bool = False):
60
63
  if dataset.RasterXSize > 1:
61
64
  raise ValueError("Expected a shrunk dataset")
62
65
  self.databand = dataset.GetRasterBand(1).ReadAsArray(0, 0, 1, dataset.RasterYSize)