yirgacheffe 1.2.0__py3-none-any.whl

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.
@@ -0,0 +1,17 @@
1
+ from osgeo import gdal
2
+ try:
3
+ from importlib import metadata
4
+ __version__ = metadata.version(__name__)
5
+ except ModuleNotFoundError:
6
+ __version__ = "unknown"
7
+
8
+ gdal.UseExceptions()
9
+
10
+ # I don't really want this here, but it's just too useful having it exposed
11
+ WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
12
+ 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
13
+ 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
14
+ 'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
15
+
16
+ # For legacy reasons [facepalm]
17
+ WSG_84_PROJECTION = WGS_84_PROJECTION
@@ -0,0 +1,13 @@
1
+ import os
2
+
3
+ BACKEND = os.environ.get("YIRGACHEFFE_BACKEND", "NUMPY").upper()
4
+
5
+ match BACKEND:
6
+ case "MLX":
7
+ from . import mlx
8
+ backend = mlx
9
+ case "NUMPY":
10
+ from . import numpy
11
+ backend = numpy
12
+ case _:
13
+ raise NotImplementedError("Only NUMPY and MLX backends supported")
@@ -0,0 +1,33 @@
1
+ from enum import Enum
2
+
3
+ class operators(Enum):
4
+ ADD = 1
5
+ SUB = 2
6
+ MUL = 3
7
+ TRUEDIV = 4
8
+ POW = 5
9
+ EQ = 6
10
+ NE = 7
11
+ LT = 8
12
+ LE = 9
13
+ GT = 10
14
+ GE = 11
15
+ AND = 12
16
+ OR = 13
17
+ LOG = 14
18
+ LOG2 = 15
19
+ LOG10 = 16
20
+ EXP = 17
21
+ EXP2 = 18
22
+ CLIP = 19
23
+ WHERE = 20
24
+ MIN = 21
25
+ MAX = 22
26
+ SUM = 23
27
+ MINIMUM = 24
28
+ MAXIMUM = 25
29
+ NAN_TO_NUM = 26
30
+ ISIN = 27
31
+ REMAINDER = 28
32
+ FLOORDIV = 29
33
+ CONV2D = 30
@@ -0,0 +1,156 @@
1
+
2
+ import numpy as np
3
+ import mlx.core as mx # pylint: disable=E0001,E0611,E0401
4
+ import mlx.nn
5
+
6
+ from .enumeration import operators as op
7
+
8
+ array_t = mx.array
9
+ float_t = mx.float32
10
+
11
+ promote = mx.array
12
+ demote_array = np.asarray
13
+ demote_scalar = np.float64
14
+
15
+ eval_op = mx.eval
16
+
17
+ add_op = mx.add
18
+ sub_op = mx.array.__sub__
19
+ truediv_op = mx.array.__truediv__
20
+ pow_op = mx.array.__pow__
21
+ eq_op = mx.array.__eq__
22
+ ne_op =mx.array.__ne__
23
+ lt_op = mx.less
24
+ le_op = mx.less_equal
25
+ gt_op = mx.greater
26
+ ge_op = mx.greater_equal
27
+ and_op = mx.array.__and__
28
+ or_op = mx.array.__or__
29
+ log = mx.log
30
+ log2 = mx.log2
31
+ log10 = mx.log10
32
+ exp = mx.exp
33
+ clip = mx.clip
34
+ where = mx.where
35
+ min_op = mx.min
36
+ max_op = mx.max
37
+ maximum = mx.maximum
38
+ minimum = mx.minimum
39
+ zeros = mx.zeros
40
+ pad = mx.pad
41
+ isscalar = np.isscalar
42
+ full = mx.full
43
+ allclose = mx.allclose
44
+ remainder_op = mx.remainder
45
+ floordiv_op = mx.array.__floordiv__
46
+
47
+ def sum_op(a):
48
+ # There are weird issues around how MLX overflows int8, so just promote the data ahead of summing
49
+ match a.dtype:
50
+ case mx.int8:
51
+ res = mx.sum(a.astype(mx.int32))
52
+ case mx.uint8:
53
+ res = mx.sum(a.astype(mx.uint32))
54
+ case _:
55
+ res = mx.sum(a)
56
+ return demote_scalar(res)
57
+
58
+ def _is_float(x):
59
+ if isinstance(x, float):
60
+ return True
61
+ try:
62
+ np_floats = [np.dtype('float16'), np.dtype('float32'), np.dtype('float64')]
63
+ if x.dtype in np_floats:
64
+ return True
65
+ match x.dtype:
66
+ case mx.float32 | mx.float64:
67
+ return True
68
+ case _:
69
+ return False
70
+ except AttributeError:
71
+ return False
72
+
73
+ def mul_op(a, b):
74
+ # numpy will promote an operation between float and int to float, whereas it looks like mlx does the inverse
75
+ # and so for consistency with the numpy path, we do some fiddling here if necessary
76
+ if _is_float(b):
77
+ match a.dtype:
78
+ case mx.int8 | mx.int32 | mx.uint8 | mx.uint32:
79
+ a = a.astype(mx.float32)
80
+ case mx.int64 | mx.uint64:
81
+ a = a.astype(mx.float64)
82
+ case _:
83
+ pass
84
+ return mx.multiply(a, b)
85
+
86
+ def exp2(a):
87
+ mx.eval(a)
88
+ return promote(np.exp2(a))
89
+
90
+ def nan_to_num(a, nan, posinf, neginf, copy): # pylint: disable=W0613
91
+ return mx.nan_to_num(a, float(nan), posinf, neginf)
92
+
93
+ def isin(a, test_elements):
94
+ # There is no `isin` on MLX currently, so we need to fallback to CPU behaviour here
95
+ # https://ml-explore.github.io/mlx/build/html/dev/custom_metal_kernels.html#using-shape-strides
96
+ mx.eval(a)
97
+ return promote(np.isin(a, test_elements))
98
+
99
+ def conv2d_op(data, weights):
100
+ # From numpy.py: torch wants to process dimensions of channels of width of height
101
+ # but mlx wants to process dimensions of width of height of channels, so we end up
102
+ # having to reshape the data, as we only ever use one channel.
103
+ # Which is why both the data and weights get nested into two arrays here,
104
+ # and then we have to unpack it from that nesting.
105
+
106
+ weights = mx.array(weights)
107
+
108
+ original_data_shape = data.shape
109
+ original_weights_shape = weights.shape
110
+
111
+ unshifted_preped_weights = np.array([[weights]])
112
+ conv_weights_shape = [1] + list(original_weights_shape) + [1]
113
+ preped_weights = mx.array(np.reshape(unshifted_preped_weights, conv_weights_shape))
114
+
115
+ conv = mlx.nn.Conv2d(1, 1, weights.shape, bias=False)
116
+ conv.weight = preped_weights
117
+
118
+ conv_data_shape = [1] + list(original_data_shape) + [1]
119
+ unshifted_data_shape = np.array([[data]])
120
+ preped_data = mx.array(np.reshape(unshifted_data_shape, conv_data_shape))
121
+
122
+ shifted_res = conv(preped_data)[0]
123
+ res = np.reshape(shifted_res, [1] + list(shifted_res.shape)[:-1])
124
+ return res[0]
125
+
126
+ operator_map = {
127
+ op.ADD: mx.array.__add__,
128
+ op.SUB: mx.array.__sub__,
129
+ op.MUL: mul_op,
130
+ op.TRUEDIV: mx.array.__truediv__,
131
+ op.POW: mx.array.__pow__,
132
+ op.EQ: mx.array.__eq__,
133
+ op.NE: mx.array.__ne__,
134
+ op.LT: mx.array.__lt__,
135
+ op.LE: mx.array.__le__,
136
+ op.GT: mx.array.__gt__,
137
+ op.GE: mx.array.__ge__,
138
+ op.AND: mx.array.__and__,
139
+ op.OR: mx.array.__or__,
140
+ op.LOG: mx.log,
141
+ op.LOG2: mx.log2,
142
+ op.LOG10: mx.log10,
143
+ op.EXP: mx.exp,
144
+ op.EXP2: exp2,
145
+ op.CLIP: mx.clip,
146
+ op.WHERE: mx.where,
147
+ op.MIN: mx.min,
148
+ op.MAX:mx.max,
149
+ op.MINIMUM: mx.minimum,
150
+ op.MAXIMUM: mx.maximum,
151
+ op.NAN_TO_NUM: nan_to_num,
152
+ op.ISIN: isin,
153
+ op.REMAINDER: mx.remainder,
154
+ op.FLOORDIV: mx.array.__floordiv__,
155
+ op.CONV2D: conv2d_op,
156
+ }
@@ -0,0 +1,110 @@
1
+
2
+ import numpy as np
3
+ import torch
4
+
5
+ from .enumeration import operators as op
6
+
7
+ array_t = np.ndarray
8
+ float_t = np.float64
9
+
10
+ promote = lambda a: a
11
+ demote_array = lambda a: a
12
+ demote_scalar = lambda a: a
13
+ eval_op = lambda a: a
14
+
15
+ add_op = np.ndarray.__add__
16
+ sub_op = np.ndarray.__sub__
17
+ mul_op = np.ndarray.__mul__
18
+ truediv_op = np.ndarray.__truediv__
19
+ pow_op = np.ndarray.__pow__
20
+ eq_op = np.ndarray.__eq__
21
+ ne_op = np.ndarray.__ne__
22
+ lt_op = np.ndarray.__lt__
23
+ le_op = np.ndarray.__le__
24
+ gt_op = np.ndarray.__gt__
25
+ ge_op = np.ndarray.__ge__
26
+ and_op = np.ndarray.__and__
27
+ or_op = np.ndarray.__or__
28
+ nan_to_num = np.nan_to_num
29
+ isin = np.isin
30
+ log = np.log
31
+ log2 = np.log2
32
+ log10 = np.log10
33
+ exp = np.exp
34
+ exp2 = np.exp2
35
+ clip = np.clip
36
+ where = np.where
37
+ min_op = np.min
38
+ max_op = np.max
39
+ maximum = np.maximum
40
+ minimum = np.minimum
41
+ zeros = np.zeros
42
+ pad = np.pad
43
+ sum_op = lambda a: np.sum(a.astype(np.float64))
44
+ isscalar = np.isscalar
45
+ full = np.full
46
+ allclose = np.allclose
47
+ remainder_op = np.ndarray.__mod__
48
+ floordiv_op = np.ndarray.__floordiv__
49
+
50
+ def conv2d_op(data, weights):
51
+ # torch wants to process dimensions of channels of width of height
52
+ # Which is why both the data and weights get nested into two arrays here,
53
+ # and then we have to unpack it from that nesting.
54
+
55
+ preped_weights = np.array([[weights]])
56
+ conv = torch.nn.Conv2d(1, 1, weights.shape, bias=False)
57
+ conv.weight = torch.nn.Parameter(torch.from_numpy(preped_weights))
58
+ preped_data = torch.from_numpy(np.array([[data]]))
59
+
60
+ res = conv(preped_data)
61
+ return res.detach().numpy()[0][0]
62
+
63
+ operator_map = {
64
+ op.ADD: np.ndarray.__add__,
65
+ op.SUB: np.ndarray.__sub__,
66
+ op.MUL: np.ndarray.__mul__,
67
+ op.TRUEDIV: np.ndarray.__truediv__,
68
+ op.POW: np.ndarray.__pow__,
69
+ op.EQ: np.ndarray.__eq__,
70
+ op.NE: np.ndarray.__ne__,
71
+ op.LT: np.ndarray.__lt__,
72
+ op.LE: np.ndarray.__le__,
73
+ op.GT: np.ndarray.__gt__,
74
+ op.GE: np.ndarray.__ge__,
75
+ op.AND: np.ndarray.__and__,
76
+ op.OR: np.ndarray.__or__,
77
+ op.LOG: np.log,
78
+ op.LOG2: np.log2,
79
+ op.LOG10: np.log10,
80
+ op.EXP: np.exp,
81
+ op.EXP2: np.exp2,
82
+ op.CLIP: np.clip,
83
+ op.WHERE: np.where,
84
+ op.MIN: np.min,
85
+ op.MAX: np.max,
86
+ op.MINIMUM: np.minimum,
87
+ op.MAXIMUM: np.maximum,
88
+ op.NAN_TO_NUM: np.nan_to_num,
89
+ op.ISIN: np.isin,
90
+ op.REMAINDER: np.ndarray.__mod__,
91
+ op.FLOORDIV: np.ndarray.__floordiv__,
92
+ op.CONV2D: conv2d_op,
93
+ }
94
+
95
+ operator_str_map = {
96
+ op.POW: "np.ndarray.__pow__(%s, %s)",
97
+ op.LOG: "np.log(%s)",
98
+ op.LOG2: "np.log2(%s)",
99
+ op.LOG10: "np.log10(%s)",
100
+ op.EXP: "np.exp(%s)",
101
+ op.EXP2: "np.exp2(%s)",
102
+ op.CLIP: "np.clip",
103
+ op.WHERE: "np.where(%s, %s, %s)",
104
+ op.MIN: "np.min(%s)",
105
+ op.MAX: "np.max(%s)",
106
+ op.MINIMUM: "np.minimum(%s)",
107
+ op.MAXIMUM: "np.maximum(%s)",
108
+ op.NAN_TO_NUM: "np.nan_to_num(%s)",
109
+ op.ISIN: "np.isin(%s, %s)",
110
+ }
@@ -0,0 +1 @@
1
+ YSTEP = 512
yirgacheffe/h3layer.py ADDED
@@ -0,0 +1,2 @@
1
+ # for legacy compatibility
2
+ from .layers.h3layer import H3CellLayer # pylint: disable=W0611
@@ -0,0 +1,44 @@
1
+ from osgeo import ogr
2
+
3
+ from ..window import PixelScale
4
+ from .base import YirgacheffeLayer
5
+ from .rasters import RasterLayer, InvalidRasterBand
6
+ from .rescaled import RescaledRasterLayer
7
+ from .vectors import RasteredVectorLayer, VectorLayer
8
+ from .area import UniformAreaLayer
9
+ from .constant import ConstantLayer
10
+ from .group import GroupLayer, TiledGroupLayer
11
+ try:
12
+ from .h3layer import H3CellLayer
13
+ except ModuleNotFoundError:
14
+ pass
15
+
16
+
17
+ class Layer(RasterLayer):
18
+ """A place holder for now, at some point I want to replace Layer with RasterLayer."""
19
+
20
+
21
+ class VectorRangeLayer(RasteredVectorLayer):
22
+ """Deprecated older name for VectorLayer"""
23
+
24
+ def __init__(self, range_vectors: str, where_filter: str, scale: PixelScale, projection: str):
25
+ vectors = ogr.Open(range_vectors)
26
+ if vectors is None:
27
+ raise FileNotFoundError(range_vectors)
28
+ layer = vectors.GetLayer()
29
+ if where_filter is not None:
30
+ layer.SetAttributeFilter(where_filter)
31
+ super().__init__(layer, scale, projection)
32
+
33
+
34
+ class DynamicVectorRangeLayer(VectorLayer):
35
+ """Deprecated older name DynamicVectorLayer"""
36
+
37
+ def __init__(self, range_vectors: str, where_filter: str, scale: PixelScale, projection: str):
38
+ vectors = ogr.Open(range_vectors)
39
+ if vectors is None:
40
+ raise FileNotFoundError(range_vectors)
41
+ layer = vectors.GetLayer()
42
+ if where_filter is not None:
43
+ layer.SetAttributeFilter(where_filter)
44
+ super().__init__(layer, scale, projection)
@@ -0,0 +1,91 @@
1
+ from math import ceil, floor
2
+ from typing import Any, Optional
3
+
4
+ import numpy
5
+ from osgeo import gdal
6
+
7
+ from ..window import Area, Window
8
+ from .rasters import RasterLayer
9
+
10
+ class UniformAreaLayer(RasterLayer):
11
+ """If you have a pixel area map where all the row entries are identical, then you
12
+ can speed up the AoH calculations by simplifying that to a 1 pixel wide map and then
13
+ synthesizing the rest of the data at calc time, as decompressing the large compressed
14
+ TIFF files is quite slow. This class is used to load such a dataset.
15
+
16
+ If you have a file that is large that you'd like to shrink you can call the static method
17
+ generate_narrow_area_projection which will shrink the file and correct the geo info.
18
+ """
19
+
20
+ @staticmethod
21
+ def generate_narrow_area_projection(source_filename: str, target_filename: str) -> None:
22
+ source = gdal.Open(source_filename, gdal.GA_ReadOnly)
23
+ if source is None:
24
+ raise FileNotFoundError(source_filename)
25
+ if not UniformAreaLayer.is_uniform_area_projection(source):
26
+ raise ValueError("Data in area pixel map is not uniform across rows")
27
+ source_band = source.GetRasterBand(1)
28
+ target = gdal.GetDriverByName('GTiff').Create(
29
+ target_filename,
30
+ 1,
31
+ source.RasterYSize,
32
+ 1,
33
+ source_band.DataType,
34
+ ['COMPRESS=LZW']
35
+ )
36
+ target.SetProjection(source.GetProjection())
37
+ target.SetGeoTransform(source.GetGeoTransform())
38
+ # Although the output is 1 pixel wide, the input can be very wide, so we do this in stages
39
+ # otherwise gdal eats all the memory
40
+ step = 1000
41
+ target_band = target.GetRasterBand(1)
42
+ for yoffset in range(0, source.RasterYSize, step):
43
+ this_step = step
44
+ if (yoffset + this_step) > source.RasterYSize:
45
+ this_step = source.RasterYSize - yoffset
46
+ data = source_band.ReadAsArray(0, yoffset, 1, this_step)
47
+ target_band.WriteArray(data, 0, yoffset)
48
+
49
+ @staticmethod
50
+ def is_uniform_area_projection(dataset) -> bool:
51
+ "Check that the dataset conforms to the assumption that all rows contain the same value. Likely to be slow."
52
+ band = dataset.GetRasterBand(1)
53
+ for yoffset in range(dataset.RasterYSize):
54
+ row = band.ReadAsArray(0, yoffset, dataset.RasterXSize, 1)
55
+ if not numpy.all(numpy.isclose(row, row[0])):
56
+ return False
57
+ return True
58
+
59
+ def __init__(self, dataset, name: Optional[str] = None, band: int = 1):
60
+ if dataset.RasterXSize > 1:
61
+ raise ValueError("Expected a shrunk dataset")
62
+ self.databand = dataset.GetRasterBand(1).ReadAsArray(0, 0, 1, dataset.RasterYSize)
63
+
64
+ super().__init__(dataset, name, band)
65
+
66
+ transform = dataset.GetGeoTransform()
67
+
68
+ pixel_scale = self.pixel_scale
69
+ assert pixel_scale # from raster we should always have one
70
+
71
+ self._underlying_area = Area(
72
+ floor(-180 / pixel_scale.xstep) * pixel_scale.xstep,
73
+ self.area.top,
74
+ ceil(180 / pixel_scale.xstep) * pixel_scale.xstep,
75
+ self.area.bottom
76
+ )
77
+ self._active_area = self._underlying_area
78
+
79
+ self._window = Window(
80
+ xoff=0,
81
+ yoff=0,
82
+ xsize=int((self.area.right - self.area.left) / transform[1]),
83
+ ysize=dataset.RasterYSize,
84
+ )
85
+ self._raster_xsize = self.window.xsize
86
+
87
+ def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
88
+ if ysize <= 0:
89
+ raise ValueError("Request dimensions must be positive and non-zero")
90
+ offset = window.yoff + yoffset
91
+ return self.databand[offset:offset + ysize]