yirgacheffe 1.7.5__py3-none-any.whl → 1.7.7__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.

Potentially problematic release.


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

yirgacheffe/operators.py CHANGED
@@ -1,1011 +1,7 @@
1
- import logging
2
- import math
3
- import multiprocessing
4
- import os
5
- import sys
6
- import tempfile
7
- import time
8
- import types
9
- from enum import Enum
10
- from multiprocessing import Semaphore, Process
11
- from multiprocessing.managers import SharedMemoryManager
12
- from pathlib import Path
13
- from typing import Callable, Dict, Optional, Union
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
14
3
 
15
- import deprecation
16
- import numpy as np
17
- import numpy.typing as npt
18
- from osgeo import gdal
19
- from dill import dumps, loads # type: ignore
20
-
21
- from . import constants, __version__
22
- from .rounding import round_up_pixels, round_down_pixels
23
- from .window import Area, PixelScale, MapProjection, Window
24
- from ._backends import backend
25
- from ._backends.enumeration import operators as op
26
- from ._backends.enumeration import dtype as DataType
27
-
28
- logger = logging.getLogger(__name__)
29
- logger.setLevel(logging.WARNING)
30
-
31
- class WindowOperation(Enum):
32
- NONE = 1
33
- UNION = 2
34
- INTERSECTION = 3
35
- LEFT = 4
36
- RIGHT = 5
37
-
38
- class LayerConstant:
39
- def __init__(self, val):
40
- self.val = val
41
-
42
- def __str__(self) -> str:
43
- return str(self.val)
44
-
45
- def _eval(self, _area, _projection, _index, _step, _target_window):
46
- return self.val
47
-
48
- @property
49
- def area(self) -> Area:
50
- return Area.world()
51
-
52
- def _get_operation_area(self, _projection) -> Area:
53
- return Area.world()
54
-
55
- class LayerMathMixin:
56
-
57
- def __add__(self, other):
58
- return LayerOperation(self, op.ADD, other, window_op=WindowOperation.UNION)
59
-
60
- def __sub__(self, other):
61
- return LayerOperation(self, op.SUB, other, window_op=WindowOperation.UNION)
62
-
63
- def __mul__(self, other):
64
- return LayerOperation(self, op.MUL, other, window_op=WindowOperation.INTERSECTION)
65
-
66
- def __truediv__(self, other):
67
- return LayerOperation(self, op.TRUEDIV, other, window_op=WindowOperation.INTERSECTION)
68
-
69
- def __floordiv__(self, other):
70
- return LayerOperation(self, op.FLOORDIV, other, window_op=WindowOperation.INTERSECTION)
71
-
72
- def __mod__(self, other):
73
- return LayerOperation(self, op.REMAINDER, other, window_op=WindowOperation.INTERSECTION)
74
-
75
- def __pow__(self, other):
76
- return LayerOperation(self, op.POW, other, window_op=WindowOperation.UNION)
77
-
78
- def __eq__(self, other):
79
- return LayerOperation(self, op.EQ, other, window_op=WindowOperation.INTERSECTION)
80
-
81
- def __ne__(self, other):
82
- return LayerOperation(self, op.NE, other, window_op=WindowOperation.UNION)
83
-
84
- def __lt__(self, other):
85
- return LayerOperation(self, op.LT, other, window_op=WindowOperation.UNION)
86
-
87
- def __le__(self, other):
88
- return LayerOperation(self, op.LE, other, window_op=WindowOperation.UNION)
89
-
90
- def __gt__(self, other):
91
- return LayerOperation(self, op.GT, other, window_op=WindowOperation.UNION)
92
-
93
- def __ge__(self, other):
94
- return LayerOperation(self, op.GE, other, window_op=WindowOperation.UNION)
95
-
96
- def __and__(self, other):
97
- return LayerOperation(self, op.AND, other, window_op=WindowOperation.INTERSECTION)
98
-
99
- def __or__(self, other):
100
- return LayerOperation(self, op.OR, other, window_op=WindowOperation.UNION)
101
-
102
- def _eval(
103
- self,
104
- area,
105
- projection,
106
- index,
107
- step,
108
- target_window=None
109
- ):
110
- try:
111
- window = self.window if target_window is None else target_window
112
- return self._read_array_for_area(area, projection, 0, index, window.xsize, step)
113
- except AttributeError:
114
- return self._read_array_for_area(
115
- area,
116
- projection,
117
- 0,
118
- index,
119
- target_window.xsize if target_window else 1,
120
- step
121
- )
122
-
123
- def nan_to_num(self, nan=0, posinf=None, neginf=None):
124
- return LayerOperation(
125
- self,
126
- op.NAN_TO_NUM,
127
- window_op=WindowOperation.NONE,
128
- copy=False,
129
- nan=nan,
130
- posinf=posinf,
131
- neginf=neginf,
132
- )
133
-
134
- def isin(self, test_elements):
135
- return LayerOperation(
136
- self,
137
- op.ISIN,
138
- window_op=WindowOperation.NONE,
139
- test_elements=test_elements,
140
- )
141
-
142
- def isnan(self):
143
- return LayerOperation(
144
- self,
145
- op.ISNAN,
146
- window_op=WindowOperation.NONE,
147
- )
148
-
149
- def abs(self):
150
- return LayerOperation(
151
- self,
152
- op.ABS,
153
- window_op=WindowOperation.NONE,
154
- )
155
-
156
- def floor(self):
157
- return LayerOperation(
158
- self,
159
- op.FLOOR,
160
- window_op=WindowOperation.NONE,
161
- )
162
-
163
- def round(self):
164
- return LayerOperation(
165
- self,
166
- op.ROUND,
167
- window_op=WindowOperation.NONE,
168
- )
169
-
170
- def ceil(self):
171
- return LayerOperation(
172
- self,
173
- op.CEIL,
174
- window_op=WindowOperation.NONE,
175
- )
176
-
177
- def log(self):
178
- return LayerOperation(
179
- self,
180
- op.LOG,
181
- window_op=WindowOperation.NONE,
182
- )
183
-
184
- def log2(self):
185
- return LayerOperation(
186
- self,
187
- op.LOG2,
188
- window_op=WindowOperation.NONE,
189
- )
190
-
191
- def log10(self):
192
- return LayerOperation(
193
- self,
194
- op.LOG10,
195
- window_op=WindowOperation.NONE,
196
- )
197
-
198
- def exp(self):
199
- return LayerOperation(
200
- self,
201
- op.EXP,
202
- window_op=WindowOperation.NONE,
203
- )
204
-
205
- def exp2(self):
206
- return LayerOperation(
207
- self,
208
- op.EXP2,
209
- window_op=WindowOperation.NONE,
210
- )
211
-
212
- def clip(self, min=None, max=None): # pylint: disable=W0622
213
- # In the numpy 1 API np.clip(array) used a_max, a_min arguments and array.clip() used max and min as arguments
214
- # In numpy 2 they moved so that max and min worked on both, but still support a_max, and a_min on np.clip.
215
- # For now I'm only going to support the newer max/min everywhere notion, but I have to internally call
216
- # a_max, a_min so that yirgacheffe can work on older numpy installs.
217
- return LayerOperation(
218
- self,
219
- op.CLIP,
220
- window_op=WindowOperation.NONE,
221
- a_min=min,
222
- a_max=max,
223
- )
224
-
225
- def conv2d(self, weights):
226
- # A set of limitations that are just down to implementation time restrictions
227
- weights_width, weights_height = weights.shape
228
- if weights_width != weights_height:
229
- raise ValueError("Currently only square matrixes are supported for weights")
230
- padding = (weights_width - 1) / 2
231
- if padding != int(padding):
232
- raise ValueError("Currently weights dimensions must be odd")
233
-
234
- return LayerOperation(
235
- self,
236
- op.CONV2D,
237
- window_op=WindowOperation.NONE,
238
- buffer_padding=padding,
239
- weights=weights.astype(np.float32),
240
- )
241
-
242
- def numpy_apply(self, func, other=None):
243
- return LayerOperation(self, func, other)
244
-
245
- def shader_apply(self, func, other=None):
246
- return ShaderStyleOperation(self, func, other)
247
-
248
- def save(self, destination_layer, and_sum=False, callback=None, band=1):
249
- return LayerOperation(self).save(destination_layer, and_sum, callback, band)
250
-
251
- def parallel_save(self, destination_layer, and_sum=False, callback=None, parallelism=None, band=1):
252
- return LayerOperation(self).parallel_save(destination_layer, and_sum, callback, parallelism, band)
253
-
254
- def parallel_sum(self, callback=None, parallelism=None, band=1):
255
- return LayerOperation(self).parallel_sum(callback, parallelism, band)
256
-
257
- def to_geotiff(
258
- self,
259
- filename: Union[Path,str],
260
- and_sum: bool = False,
261
- parallelism:Optional[Union[int,bool]]=None
262
- ) -> Optional[float]:
263
- return LayerOperation(self).to_geotiff(filename, and_sum, parallelism)
264
-
265
- def sum(self):
266
- return LayerOperation(self).sum()
267
-
268
- def min(self):
269
- return LayerOperation(self).min()
270
-
271
- def max(self):
272
- return LayerOperation(self).max()
273
-
274
- def astype(self, datatype):
275
- return LayerOperation(
276
- self,
277
- op.ASTYPE,
278
- window_op=WindowOperation.NONE,
279
- datatype=datatype
280
- )
281
-
282
-
283
- class LayerOperation(LayerMathMixin):
284
-
285
- @staticmethod
286
- def where(cond, a, b):
287
- return LayerOperation(
288
- cond,
289
- op.WHERE,
290
- rhs=a,
291
- other=b
292
- )
293
-
294
- @staticmethod
295
- def maximum(a, b):
296
- return LayerOperation(
297
- a,
298
- op.MAXIMUM,
299
- b,
300
- window_op=WindowOperation.UNION,
301
- )
302
-
303
- @staticmethod
304
- def minimum(a, b):
305
- return LayerOperation(
306
- a,
307
- op.MINIMUM,
308
- rhs=b,
309
- window_op=WindowOperation.UNION,
310
- )
311
-
312
- def __init__(
313
- self,
314
- lhs,
315
- operator=None,
316
- rhs=None,
317
- other=None,
318
- window_op=WindowOperation.NONE,
319
- buffer_padding=0,
320
- **kwargs
321
- ):
322
- self.ystep = constants.YSTEP
323
- self.kwargs = kwargs
324
- self.window_op = window_op
325
- self.buffer_padding = buffer_padding
326
-
327
- if lhs is None:
328
- raise ValueError("LHS on operation should not be none")
329
- self.lhs = lhs
330
-
331
- self.operator = operator
332
-
333
- if rhs is not None:
334
- if backend.isscalar(rhs):
335
- self.rhs = LayerConstant(rhs)
336
- elif isinstance(rhs, (backend.array_t)):
337
- if rhs.shape == ():
338
- self.rhs = LayerConstant(rhs.item())
339
- else:
340
- raise ValueError("Numpy arrays are no allowed")
341
- else:
342
- if not lhs.map_projection == rhs.map_projection:
343
- raise ValueError("Not all layers are at the same pixel scale")
344
- self.rhs = rhs
345
- else:
346
- self.rhs = None
347
-
348
- if other is not None:
349
- if backend.isscalar(other):
350
- self.other = LayerConstant(other)
351
- elif isinstance(other, (backend.array_t)):
352
- if other.shape == ():
353
- self.rhs = LayerConstant(other.item())
354
- else:
355
- raise ValueError("Numpy arrays are no allowed")
356
- else:
357
- if not lhs.map_projection == other.map_projection:
358
- raise ValueError("Not all layers are at the same pixel scale")
359
- self.other = other
360
- else:
361
- self.other = None
362
-
363
- def __str__(self) -> str:
364
- try:
365
- return f"({self.lhs} {self.operator} {self.rhs})"
366
- except AttributeError:
367
- try:
368
- return f"({self.operator} {self.lhs})"
369
- except AttributeError:
370
- return str(self.lhs)
371
-
372
- def __len__(self) -> int:
373
- return len(self.lhs)
374
-
375
- def __getstate__(self) -> object:
376
- odict = self.__dict__.copy()
377
- if isinstance(self.operator, types.LambdaType):
378
- odict['operator_dill'] = dumps(self.operator)
379
- del odict['operator']
380
- return odict
381
-
382
- def __setstate__(self, state) -> None:
383
- if 'operator_dill' in state:
384
- state['operator'] = loads(state['operator_dill'])
385
- del state['operator_dill']
386
- self.__dict__.update(state)
387
-
388
- @property
389
- 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"
430
-
431
- def _get_operation_area(self, projection: Optional[MapProjection]) -> Area:
432
-
433
- # The type().__name__ here is to avoid a circular import dependancy
434
- lhs_area = self.lhs._get_operation_area(projection)
435
- try:
436
- rhs_area = self.rhs._get_operation_area(projection)
437
- except AttributeError:
438
- rhs_area = None
439
- try:
440
- other_area = self.other._get_operation_area(projection)
441
- except AttributeError:
442
- other_area = None
443
-
444
- all_areas = [x for x in [lhs_area, rhs_area, other_area] if (x is not None) and (not x.is_world)]
445
-
446
- match self.window_op:
447
- case WindowOperation.NONE:
448
- return all_areas[0]
449
- case WindowOperation.LEFT:
450
- return lhs_area
451
- case WindowOperation.RIGHT:
452
- assert rhs_area is not None
453
- return rhs_area
454
- case WindowOperation.INTERSECTION:
455
- intersection = Area(
456
- left=max(x.left for x in all_areas),
457
- top=min(x.top for x in all_areas),
458
- right=min(x.right for x in all_areas),
459
- bottom=max(x.bottom for x in all_areas)
460
- )
461
- if (intersection.left >= intersection.right) or (intersection.bottom >= intersection.top):
462
- raise ValueError('No intersection possible')
463
- return intersection
464
- case WindowOperation.UNION:
465
- return Area(
466
- left=min(x.left for x in all_areas),
467
- top=max(x.top for x in all_areas),
468
- right=max(x.right for x in all_areas),
469
- bottom=min(x.bottom for x in all_areas)
470
- )
471
- case _:
472
- assert False, "Should not be reached"
473
-
474
- @property
475
- @deprecation.deprecated(
476
- deprecated_in="1.7",
477
- removed_in="2.0",
478
- current_version=__version__,
479
- details="Use `map_projection` instead."
480
- )
481
- def pixel_scale(self) -> PixelScale:
482
- # Because we test at construction that pixel scales for RHS/other are roughly equal,
483
- # I believe this should be sufficient...
484
- try:
485
- pixel_scale = self.lhs.pixel_scale
486
- except AttributeError:
487
- pixel_scale = None
488
-
489
- if pixel_scale is None:
490
- return self.rhs.pixel_scale
491
- return pixel_scale
492
-
493
- @property
494
- def window(self) -> Window:
495
- projection = self.map_projection
496
- if projection is None:
497
- # This can happen if your source layers are say just constants
498
- raise AttributeError("No window without projection")
499
- area = self._get_operation_area(projection)
500
- assert area is not None
501
-
502
- return Window(
503
- xoff=round_down_pixels(area.left / projection.xstep, projection.xstep),
504
- yoff=round_down_pixels(area.top / (projection.ystep * -1.0), projection.ystep * -1.0),
505
- xsize=round_up_pixels(
506
- (area.right - area.left) / projection.xstep, projection.xstep
507
- ),
508
- ysize=round_up_pixels(
509
- (area.top - area.bottom) / (projection.ystep * -1.0),
510
- (projection.ystep * -1.0)
511
- ),
512
- )
513
-
514
- @property
515
- def datatype(self) -> DataType:
516
- # TODO: Work out how to indicate type promotion via numpy
517
- return self.lhs.datatype
518
-
519
- @property
520
- @deprecation.deprecated(
521
- deprecated_in="1.7",
522
- removed_in="2.0",
523
- current_version=__version__,
524
- details="Use `map_projection` instead."
525
- )
526
- def projection(self):
527
- try:
528
- projection = self.lhs.projection
529
- except AttributeError:
530
- projection = None
531
-
532
- if projection is None:
533
- projection = self.rhs.projection
534
- return projection
535
-
536
- @property
537
- def map_projection(self) -> Optional[MapProjection]:
538
- try:
539
- projection = self.lhs.map_projection
540
- except AttributeError:
541
- projection = None
542
-
543
- if projection is None:
544
- try:
545
- projection = self.rhs.map_projection
546
- except AttributeError:
547
- pass
548
- return projection
549
-
550
- def _eval(
551
- self,
552
- area: Area,
553
- projection: MapProjection,
554
- index: int,
555
- step: int,
556
- target_window:Optional[Window]=None
557
- ):
558
-
559
- if self.buffer_padding:
560
- if target_window:
561
- target_window = target_window.grow(self.buffer_padding)
562
- area = area.grow(self.buffer_padding * projection.xstep)
563
- # The index doesn't need updating because we updated area/window
564
- step += (2 * self.buffer_padding)
565
-
566
- lhs_data = self.lhs._eval(area, projection, index, step, target_window)
567
-
568
- if self.operator is None:
569
- return lhs_data
570
-
571
- try:
572
- operator: Callable = backend.operator_map[self.operator]
573
- except KeyError:
574
- # Handles things like `numpy_apply` where a custom operator is provided
575
- operator = self.operator
576
-
577
- if self.other is not None:
578
- assert self.rhs is not None
579
- rhs_data = self.rhs._eval(area, projection, index, step, target_window)
580
- other_data = self.other._eval(area, projection, index, step, target_window)
581
- return operator(lhs_data, rhs_data, other_data, **self.kwargs)
582
-
583
- if self.rhs is not None:
584
- rhs_data = self.rhs._eval(area, projection, index, step, target_window)
585
- return operator(lhs_data, rhs_data, **self.kwargs)
586
-
587
- return operator(lhs_data, **self.kwargs)
588
-
589
- def sum(self):
590
- # The result accumulator is float64, and for precision reasons
591
- # we force the sum to be done in float64 also. Otherwise we
592
- # see variable results depending on chunk size, as different parts
593
- # of the sum are done in different types.
594
- res = 0.0
595
- computation_window = self.window
596
- projection = self.map_projection
597
- for yoffset in range(0, computation_window.ysize, self.ystep):
598
- step=self.ystep
599
- if yoffset+step > computation_window.ysize:
600
- step = computation_window.ysize - yoffset
601
- chunk = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
602
- res += backend.sum_op(chunk)
603
- return res
604
-
605
- def min(self):
606
- res = None
607
- computation_window = self.window
608
- projection = self.map_projection
609
- for yoffset in range(0, computation_window.ysize, self.ystep):
610
- step=self.ystep
611
- if yoffset+step > computation_window.ysize:
612
- step = computation_window.ysize - yoffset
613
- chunk = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
614
- chunk_min = backend.min_op(chunk)
615
- if (res is None) or (res > chunk_min):
616
- res = chunk_min
617
- return res
618
-
619
- def max(self):
620
- res = None
621
- computation_window = self.window
622
- projection = self.map_projection
623
- for yoffset in range(0, computation_window.ysize, self.ystep):
624
- step=self.ystep
625
- if yoffset+step > computation_window.ysize:
626
- step = computation_window.ysize - yoffset
627
- chunk = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
628
- chunk_max = backend.max_op(chunk)
629
- if (res is None) or (chunk_max > res):
630
- res = chunk_max
631
- return res
632
-
633
- def save(self, destination_layer, and_sum=False, callback=None, band=1) -> Optional[float]:
634
- """
635
- Calling save will write the output of the operation to the provied layer.
636
- If you provide sum as true it will additionall compute the sum and return that.
637
- """
638
-
639
- if destination_layer is None:
640
- raise ValueError("Layer is required")
641
- try:
642
- band = destination_layer._dataset.GetRasterBand(band)
643
- except AttributeError as exc:
644
- raise ValueError("Layer must be a raster backed layer") from exc
645
-
646
- projection = self.map_projection
647
-
648
- destination_window = destination_layer.window
649
- destination_projection = destination_layer.map_projection
650
- assert destination_projection is not None
651
-
652
- if projection is None:
653
- projection = destination_projection
654
- else:
655
- if projection != destination_projection:
656
- raise ValueError("Destination layer and input layers have different projection/scale")
657
-
658
- # If we're calculating purely from a constant layer, then we don't have a window or area
659
- # so we should use the destination raster details.
660
- try:
661
- computation_window = self.window
662
- computation_area = self._get_operation_area(projection)
663
- except (AttributeError, IndexError):
664
- computation_window = destination_window
665
- computation_area = destination_layer.area
666
-
667
- if (computation_window.xsize != destination_window.xsize) \
668
- or (computation_window.ysize != destination_window.ysize):
669
- raise ValueError((f"Destination raster window size does not match input raster window size: "
670
- f"{(destination_window.xsize, destination_window.ysize)} vs "
671
- f"{(computation_window.xsize, computation_window.ysize)}"))
672
-
673
- total = 0.0
674
-
675
- for yoffset in range(0, computation_window.ysize, self.ystep):
676
- if callback:
677
- callback(yoffset / computation_window.ysize)
678
- step=self.ystep
679
- if yoffset+step > computation_window.ysize:
680
- step = computation_window.ysize - yoffset
681
- chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
682
- if isinstance(chunk, (float, int)):
683
- chunk = backend.full((step, destination_window.xsize), chunk)
684
- band.WriteArray(
685
- backend.demote_array(chunk),
686
- destination_window.xoff,
687
- yoffset + destination_window.yoff,
688
- )
689
- if and_sum:
690
- total += backend.sum_op(chunk)
691
- if callback:
692
- callback(1.0)
693
-
694
- return total if and_sum else None
695
-
696
- def _parallel_worker(self, index, shared_mem, sem, np_dtype, width, input_queue, output_queue, computation_window):
697
- arr = np.ndarray((self.ystep, width), dtype=np_dtype, buffer=shared_mem.buf)
698
- projection = self.map_projection
699
- try:
700
- while True:
701
- # We acquire the lock so we know we have somewhere to put the
702
- # result before we take work. This is because in practice
703
- # it seems the writing to GeoTIFF is the bottleneck, and
704
- # we had workers taking a task, then waiting for somewhere to
705
- # write to for ages when other workers were exiting because there
706
- # was nothing to do.
707
- sem.acquire()
708
-
709
- task = input_queue.get()
710
- if task is None:
711
- sem.release()
712
- output_queue.put(None)
713
- break
714
- yoffset, step = task
715
-
716
- result = self._eval(self._get_operation_area(projection), projection, yoffset, step, computation_window)
717
- backend.eval_op(result)
718
-
719
- arr[:step] = backend.demote_array(result)
720
-
721
- output_queue.put((index, yoffset, step))
722
-
723
- except Exception as e: # pylint: disable=W0718
724
- logger.exception(e)
725
- sem.release()
726
- output_queue.put(None)
727
-
728
- def _park(self):
729
- try:
730
- self.lhs._park()
731
- except AttributeError:
732
- pass
733
- try:
734
- self.rhs._park()
735
- except AttributeError:
736
- pass
737
- try:
738
- self.other._park()
739
- except AttributeError:
740
- pass
741
-
742
- def _parallel_save(
743
- self,
744
- destination_layer,
745
- and_sum=False,
746
- callback=None,
747
- parallelism=None,
748
- band=1
749
- ) -> Optional[float]:
750
- assert (destination_layer is not None) or and_sum
751
- try:
752
- computation_window = self.window
753
- except (AttributeError, IndexError):
754
- # This is most likely because the calculation is on a constant layer (or combination of only constant
755
- # layers) and there's no real benefit to parallel saving then, so to keep this code from getting yet
756
- # more complicated just fall back to the single threaded path
757
- if destination_layer:
758
- return self.save(destination_layer, and_sum, callback, band)
759
- elif and_sum:
760
- return self.sum()
761
- else:
762
- assert False
763
-
764
- worker_count = parallelism or multiprocessing.cpu_count()
765
- work_blocks = len(range(0, computation_window.ysize, self.ystep))
766
- adjusted_blocks = math.ceil(work_blocks / constants.MINIMUM_CHUNKS_PER_THREAD)
767
- worker_count = min(adjusted_blocks, worker_count)
768
-
769
- if worker_count == 1:
770
- if destination_layer:
771
- return self.save(destination_layer, and_sum, callback, band)
772
- elif and_sum:
773
- return self.sum()
774
- else:
775
- assert False
776
-
777
- if destination_layer is not None:
778
- try:
779
- band = destination_layer._dataset.GetRasterBand(band)
780
- except AttributeError as exc:
781
- raise ValueError("Layer must be a raster backed layer") from exc
782
-
783
- destination_window = destination_layer.window
784
-
785
- if (computation_window.xsize != destination_window.xsize) \
786
- or (computation_window.ysize != destination_window.ysize):
787
- raise ValueError("Destination raster window size does not match input raster window size.")
788
-
789
- np_type_map : Dict[int, np.dtype] = {
790
- gdal.GDT_Byte: np.dtype('byte'),
791
- gdal.GDT_Float32: np.dtype('float32'),
792
- gdal.GDT_Float64: np.dtype('float64'),
793
- gdal.GDT_Int8: np.dtype('int8'),
794
- gdal.GDT_Int16: np.dtype('int16'),
795
- gdal.GDT_Int32: np.dtype('int32'),
796
- gdal.GDT_Int64: np.dtype('int64'),
797
- gdal.GDT_UInt16: np.dtype('uint16'),
798
- gdal.GDT_UInt32: np.dtype('uint32'),
799
- gdal.GDT_UInt64: np.dtype('uint64'),
800
- }
801
- np_dtype = np_type_map[band.DataType]
802
- else:
803
- band = None
804
- np_dtype = np.dtype('float64')
805
-
806
- # The parallel save will cause a fork on linux, so we need to
807
- # remove all SWIG references
808
- self._park()
809
-
810
- total = 0.0
811
-
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,
870
- )
871
- if and_sum:
872
- total += np.sum(np.array(arr[0:step]).astype(np.float64))
873
- sem.release()
874
- retired_blocks += 1
875
- 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)
889
-
890
- return total if and_sum else None
891
-
892
- def parallel_save(
893
- self,
894
- destination_layer,
895
- and_sum=False,
896
- callback=None,
897
- parallelism=None,
898
- band=1
899
- ) -> Optional[float]:
900
- if destination_layer is None:
901
- raise ValueError("Layer is required")
902
- return self._parallel_save(destination_layer, and_sum, callback, parallelism, band)
903
-
904
- def parallel_sum(self, callback=None, parallelism=None, band=1):
905
- return self._parallel_save(None, True, callback, parallelism, band)
906
-
907
- def to_geotiff(
908
- self,
909
- filename: Union[Path,str],
910
- and_sum: bool = False,
911
- parallelism:Optional[Union[int,bool]] = None
912
- ) -> Optional[float]:
913
- """Saves a calculation to a raster file, optionally also returning the sum of pixels.
914
-
915
- Parameters
916
- ----------
917
- filename : Path
918
- Path of the raster to save the result to.
919
- and_sum : bool, default=False
920
- If true then the function will also calculate the sum of the raster as it goes and return that value.
921
- parallelism : int or bool, optional, default=None
922
- If passed, attempt to use multiple CPU cores up to the number provided, or if set to True, yirgacheffe
923
- will pick a sensible value.
924
-
925
- Returns
926
- -------
927
- float, optional
928
- Either returns None, or the sum of the pixels in the resulting raster if `and_sum` was specified.
929
- """
930
-
931
- # We want to write to a tempfile before we move the result into place, but we can't use
932
- # the actual $TMPDIR as that might be on a different device, and so we use a file next to where
933
- # the final file will be, so we just need to rename the file at the end, not move it.
934
- if isinstance(filename, str):
935
- filename = Path(filename)
936
- target_dir = filename.parent
937
-
938
- with tempfile.NamedTemporaryFile(dir=target_dir, delete=False) as tempory_file:
939
- # Local import due to circular dependancy
940
- from yirgacheffe.layers.rasters import RasterLayer # type: ignore # pylint: disable=C0415
941
- with RasterLayer.empty_raster_layer_like(self, filename=tempory_file.name) as layer:
942
- if parallelism is None:
943
- result = self.save(layer, and_sum=and_sum)
944
- else:
945
- if isinstance(parallelism, bool):
946
- # Parallel save treats None as "work it out"
947
- parallelism = None
948
- result = self.parallel_save(layer, and_sum=and_sum, parallelism=parallelism)
949
-
950
- os.makedirs(target_dir, exist_ok=True)
951
- os.rename(src=tempory_file.name, dst=filename)
952
-
953
- return result
954
-
955
- class ShaderStyleOperation(LayerOperation):
956
-
957
- def _eval(self, area, projection, index, step, target_window=None):
958
- if target_window is None:
959
- target_window = self.window
960
- lhs_data = self.lhs._eval(area, projection, index, step, target_window)
961
- if self.rhs is not None:
962
- rhs_data = self.rhs._eval(area, projection, index, step, target_window)
963
- else:
964
- rhs_data = None
965
-
966
- # Constant results make this a bit messier. Might in future
967
- # be nicer to promote them to arrays sooner?
968
- if isinstance(lhs_data, (int, float)):
969
- if rhs_data is None:
970
- return self.operator(lhs_data, **self.kwargs)
971
- if isinstance(rhs_data, (int, float)):
972
- return self.operator(lhs_data, rhs_data, **self.kwargs)
973
- else:
974
- result = np.empty_like(rhs_data)
975
- else:
976
- result = np.empty_like(lhs_data)
977
-
978
- window = self.window
979
- for yoffset in range(step):
980
- for xoffset in range(window.xsize):
981
- try:
982
- lhs_val = lhs_data[yoffset][xoffset]
983
- except TypeError:
984
- lhs_val = lhs_data
985
- if rhs_data is not None:
986
- try:
987
- rhs_val = rhs_data[yoffset][xoffset]
988
- except TypeError:
989
- rhs_val = rhs_data
990
- result[yoffset][xoffset] = self.operator(lhs_val, rhs_val, **self.kwargs)
991
- else:
992
- result[yoffset][xoffset] = self.operator(lhs_val, **self.kwargs)
993
-
994
- return result
995
-
996
- # We provide these module level accessors as it's often nicer to write `log(x/y)` rather than `(x/y).log()`
997
- where = LayerOperation.where
998
- minumum = LayerOperation.minimum
999
- maximum = LayerOperation.maximum
1000
- clip = LayerOperation.clip
1001
- log = LayerOperation.log
1002
- log2 = LayerOperation.log2
1003
- log10 = LayerOperation.log10
1004
- exp = LayerOperation.exp
1005
- exp2 = LayerOperation.exp2
1006
- nan_to_num = LayerOperation.nan_to_num
1007
- isin = LayerOperation.isin
1008
- abs = LayerOperation.abs # pylint: disable=W0622
1009
- floor = LayerOperation.floor
1010
- round = LayerOperation.round # pylint: disable=W0622
1011
- ceil = LayerOperation.ceil
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