yirgacheffe 1.7.6__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.

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