/
arrays.py
1907 lines (1638 loc) · 61.6 KB
/
arrays.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import geopandas as gpd
import numpy as np
import pandas as pd
import xarray as xr
import copy
import pytz
import rasterio
import rioxarray
import warnings
from scipy import ndimage
from semantique import exceptions, components
from semantique.processor import operators, reducers, utils
from semantique.dimensions import TIME, SPACE, X, Y
@xr.register_dataarray_accessor("sq")
class Array():
"""Internal representation of a multi-dimensional array.
This data structure is modelled as an accessor of :class:`xarray.DataArray`.
Using accessors instead of the common class inheritance is recommended by the
developers of xarray, see `here`_. In practice, this means that each method
of this class can be called as method of :obj:`xarray.DataArray` objects by
using the ``.sq`` prefix: ::
xarray_obj.sq.method
Parameters
----------
xarray_obj : :obj:`xarray.DataArray`
The content of the array.
.. _here:
https://xarray.pydata.org/en/stable/internals/extending-xarray.html
"""
def __init__(self, xarray_obj):
self._obj = xarray_obj
#
# PROPERTIES
#
@property
def value_type(self):
""":obj:`str`: The value type of the array."""
try:
return self._obj.attrs["value_type"]
except KeyError:
return None
@value_type.setter
def value_type(self, value):
self._obj.attrs["value_type"] = value
@value_type.deleter
def value_type(self):
try:
del self._obj.attrs["value_type"]
except KeyError:
pass
@property
def value_labels(self):
""":obj:`dict`: Character labels of data values."""
try:
return self._obj.attrs["value_labels"]
except KeyError:
return None
@value_labels.setter
def value_labels(self, value):
self._obj.attrs["value_labels"] = value
@value_labels.deleter
def value_labels(self):
try:
del self._obj.attrs["value_labels"]
except KeyError:
pass
@property
def crs(self):
""":obj:`pyproj.crs.CRS`: Coordinate reference system in which the spatial
coordinates of the array are expressed."""
return self._obj.rio.crs
@property
def spatial_resolution(self):
""":obj:`list`: Spatial resolution [x,y] of the array in units of the CRS."""
try:
return [self._obj[Y].attrs["resolution"], self._obj[X].attrs["resolution"]]
except KeyError:
return None
@property
def tz(self):
""":obj:`datetime.tzinfo`: Time zone in which the temporal coordinates of
the array are expressed."""
try:
return pytz.timezone(self._obj["temporal_ref"].attrs["zone"])
except KeyError:
return None
@property
def is_empty(self):
""":obj:`bool`: Is the array empty."""
try:
return self._obj.values.size == 0 or not np.any(np.isfinite(self._obj))
except TypeError:
# For some value types np.isfinite cannot be applied.
# This is the case e.g. for spatial coordinate tuples.
# However, given that these occur it already means the array is not empty.
return False
@property
def grid_points(self):
""":obj:`geopandas.GeoSeries`: Spatial grid points of the array."""
if X not in self._obj.dims or Y not in self._obj.dims:
return self._obj
# Extract coordinates of spatial pixels.
cells = self.stack_spatial_dims()[SPACE]
xcoords = cells[X]
ycoords = cells[Y]
# Convert to point geometries.
points = gpd.points_from_xy(xcoords, ycoords)
return gpd.GeoSeries(points, crs = self.crs)
#
# VERBS
#
def evaluate(self, operator, y = None, track_types = True, **kwargs):
"""Apply the evaluate verb to the array.
The evaluate verb evaluates an expression for each pixel in an array.
Parameters
----------
operator : :obj:`callable`
Operator function to be used in the expression.
y : optional
Right-hand side of the expression. May be a constant, meaning that the
same value is used in each expression. May also be another array
which can be aligned to the same shape as the input array. In the latter
case, when evaluating the expression for a pixel in the input array the
second operand is the value of the pixel in array ``y`` that has the same
dimension coordinates. Ignored when the operator is univariate.
track_types : :obj:`bool`
Should the operator promote the value type of the output object, based
on the value type(s) of the operand(s)?
**kwargs:
Additional keyword arguments passed on to the operator function.
Returns
--------
:obj:`xarray.DataArray`
"""
operands = tuple([self._obj]) if y is None else tuple([self._obj, y])
out = operator(*operands, track_types = track_types, **kwargs)
return out
def extract(self, dimension, component = None, **kwargs):
"""Apply the extract verb to the array.
The extract verb extracts coordinate labels of a dimension as a new
array.
Parameters
-----------
dimension : :obj:`str`
Name of the dimension to be extracted.
component : :obj:`str`, optional
Name of a specific component of the dimension coordinates to be
extracted, e.g. *year*, *month* or *day* for temporal dimension
coordinates.
**kwargs:
Ignored.
Returns
--------
:obj:`xarray.DataArray`
Raises
-------
:obj:`exceptions.UnknownDimensionError`
If a dimension with the given name is not present in the array.
:obj:`exceptions.UnknownComponentError`
If the given dimension does not contain the given component.
"""
# Get array.
obj = self._obj
# Extract spatial or temporal dimension(s).
if dimension == TIME:
return self._extract_time(obj, component)
if dimension == SPACE:
return self._extract_space(obj, component)
# Extract any other dimension.
try:
out = obj[dimension]
except KeyError:
raise exceptions.UnknownDimensionError(
f"Dimension '{dimension}' is not present in the array"
)
if component is not None:
try:
out = out[component]
except KeyError:
raise exceptions.UnknownComponentError(
f"Component '{component}' is not defined for dimension '{dimension}'"
)
return out
@staticmethod
def _extract_space(obj, component = None):
if component is None:
try:
out = obj.sq.stack_spatial_dims()[SPACE]
except KeyError:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
out._variable = out._variable.to_base_variable()
out = out.sq.unstack_spatial_dims()
out.sq.value_type = "coords"
else:
# Component FEATURE should extract spatial feature indices.
if component == components.space.FEATURE:
cname = "spatial_feats"
else:
cname = component
try:
out = obj[cname]
except KeyError:
raise exceptions.UnknownComponentError(
f"Component '{cname}' is not defined for dimension '{SPACE}'"
)
return out
@staticmethod
def _extract_time(obj, component = None):
try:
out = obj[TIME]
except KeyError:
raise exceptions.UnknownDimensionError(
f"Dimension '{TIME}' is not present in the array"
)
if component is not None:
try:
out = out[component]
except KeyError:
aliases = {
"day_of_week": "dayofweek",
"day_of_year": "dayofyear"
}
try:
component = aliases[component]
except KeyError:
pass
try:
out = getattr(out.dt, component)
except AttributeError:
raise exceptions.UnknownComponentError(
f"Component '{component}' is not defined for dimension '{TIME}'"
)
else:
out = utils.parse_datetime_component(component, out)
return out
def filter(self, filterer, track_types = True, **kwargs):
"""Apply the filter verb to the array.
The filter verb filters the values in an array.
Parameters
-----------
filterer : :obj:`xarray.DataArray`
Binary array which can be aligned to the same shape as the input array.
Each pixel in the input array will be kept if the pixel in the filterer
with the same dimension coordinates is true, and dropped otherwise
(i.e. assigned a nodata value).
track_types : :obj:`bool`
Should it be checked that the filterer has value type *binary*?
**kwargs:
Ignored.
Returns
--------
:obj:`xarray.DataArray`
Raises
-------
:obj:`exceptions.InvalidValueTypeError`
If ``track_types = True`` and the value type of ``filterer`` is not
*binary*.
"""
if track_types:
vtype = filterer.sq.value_type
if vtype is not None and vtype != "binary":
raise exceptions.InvalidValueTypeError(
f"Filterer must be of value type 'binary', not '{vtype}'"
)
# Update filterer.
# Xarray treats null values as True but they should not pass the filter.
filterer.values = utils.null_as_zero(filterer)
# Apply filter.
out = self._obj.where(filterer.sq.align_with(self._obj))
return out
def assign(self, y, at = None, track_types = True, **kwargs):
"""Apply the assign verb to the array.
The assign verb assigns new values to the pixels in an array, without any
computation. Unless specifically stated through the ``at`` argument, it
does not assign new values to missing pixels (i.e. those pixels having a
missing value).
Parameters
----------
y :
Value(s) to be assigned. May be a constant, meaning that the same value
is assigned to every pixel. May also be another array which can be
aligned to the same shape as the input array. In the latter case, the
value assigned to a pixel in the input array is the value of the pixel in
array ``y`` that has the same dimension coordinates.
at : :obj:`xarray.DataArray`, optional
Binary array which can be aligned to the same shape as the input array.
To be used for conditional assignment, in which a pixel in the input will
only be assigned a new value if the if the pixel in ``at`` with the same
dimension coordinates is true.
track_types : :obj:`bool`
Should the value type of the output object be promoted, and should it be
checked that ``at`` has value type *binary*?
**kwargs:
Ignored.
Returns
--------
:obj:`xarray.DataArray`
Raises
-------
:obj:`exceptions.InvalidValueTypeError`
If ``track_types = True`` and the value type of ``at`` is not *binary*.
"""
if at is None:
out = operators.assign_(self._obj, y, track_types = track_types)
else:
if track_types:
vtype = at.sq.value_type
if vtype is not None and vtype != "binary":
raise exceptions.InvalidValueTypeError(
f"Array 'at' must be of value type 'binary', not '{vtype}'"
)
out = operators.assign_at_(self._obj, y, at, track_types = track_types)
return out
def groupby(self, grouper, labels_as_names = True, **kwargs):
"""Apply the groupby verb to the array.
The groupby verb groups the values in an array.
Parameters
-----------
grouper : :obj:`xarray.DataArray` or :obj:`Collection`
Array which can be aligned to the same shape as the input array. Pixels
in the input array that have equal values in the grouper will be
grouped together. Alternatively, it may be a collection of such arrays.
Then, pixels in the input array that have equal values in all of the
grouper arrays will be grouped together.
labels_as_names : :obj:`bool`
If value labels are defined, should they be used as group names instead
of the values themselves?
**kwargs:
Ignored.
Returns
--------
:obj:`Collection`
Raises
-------
:obj:`exceptions.MissingDimensionError`
If the grouper is zero-dimensional.
:obj:`exceptions.UnknownDimensionError`
If the grouper contains dimensions that are not present in the input.
:obj:`exceptions.MixedDimensionsError`
If the grouper is a collection and its elements don't all have the same
dimensions.
"""
# Get dimensions of the input.
obj = self._obj
odims = obj.dims
# Get dimensions of the grouper(s).
if isinstance(grouper, list):
is_list = True
gdims = [x.dims for x in grouper]
if not all([x == gdims[0] for x in gdims]):
raise exceptions.MixedDimensionsError(
"Dimensions of grouper arrays do not match"
)
else:
is_list = False
gdims = [grouper.dims]
grouper = [grouper]
# Parse grouper.
# When grouper is multi-dimensional, dimensions should be stacked.
if len(gdims[0]) == 0:
raise exceptions.MissingDimensionError(
"Cannot group with a zero-dimensional grouper"
)
elif len(gdims[0]) == 1:
is_spatial = False
is_multidim = False
if not gdims[0][0] in odims:
raise exceptions.UnknownDimensionError(
f"Grouper dimension '{gdims[0][0]}' is not present in the array"
)
elif len(gdims[0]) == 2 and X in gdims[0] and Y in gdims[0]:
is_spatial = True
is_multidim = False
grouper = [x.sq.stack_spatial_dims() for x in grouper]
try:
obj = obj.sq.stack_spatial_dims()
except KeyError:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
else:
is_spatial = False
is_multidim = True
if not all(x in odims for x in gdims[0]):
raise exceptions.UnknownDimensionError(
"Not all grouper dimensions are present in the array"
)
grouper = [x.sq.align_with(obj).sq.stack_all_dims() for x in grouper]
obj = obj.sq.stack_all_dims()
# Split input into groups based on unique grouper values.
if is_list:
idx = pd.MultiIndex.from_arrays([x.data for x in grouper])
dim = grouper[0].dims
partition = list(obj.groupby(xr.IndexVariable(dim, idx)))
# Use value labels as group names if defined.
if labels_as_names:
labs = [x.sq.value_labels for x in grouper]
names = [i[0] for i in partition]
for i, x in enumerate(labs):
if x is None:
pass
else:
for j, y in enumerate(names):
y = list(y)
y[i] = x[y[i]]
names[j] = tuple(y)
groups = [i[1].rename(j) for i, j in zip(partition, names)]
else:
groups = [i[1].rename(i[0]) for i in partition]
else:
partition = list(obj.groupby(grouper[0]))
# Use value labels as group names if defined.
if labels_as_names:
labs = grouper[0].sq.value_labels
if labs is not None:
groups = [i[1].rename(labs[i[0]]) for i in partition]
else:
groups = [i[1].rename(i[0]) for i in partition]
else:
groups = [i[1].rename(i[0]) for i in partition]
# Post-process.
# Stacked arrays must be unstacked again.
if is_spatial:
groups = [x.sq.unstack_spatial_dims() for x in groups]
elif is_multidim:
# Multi-dimensional grouping may create irregular spatial dimensions.
# Therefore besides unstacking we also need to regularize the arrays.
groups = [x.sq.unstack_all_dims().sq.regularize() for x in groups]
# Stacking messes up the spatial feature indices coordinate.
# We need to re-create this coordinate for each group array.
if "spatial_feats" in self._obj.coords:
def fix(x, y):
x["spatial_feats"] = y["spatial_feats"].reindex_like(x)
return x
groups = [fix(x, self._obj) for x in groups]
# Collect and return.
out = Collection(groups)
return out
def reduce(self, reducer, dimension = None, track_types = True, **kwargs):
"""Apply the reduce verb to the array.
The reduce verb reduces the dimensionality of an array.
Parameters
-----------
reducer : :obj:`callable`
The reducer function to be applied.
dimension : :obj:`str`
Name of the dimension to apply the reduction function to. If
:obj:`None`, all dimensions are reduced.
track_types : :obj:`bool`
Should the reducer promote the value type of the output object, based
on the value type of the input object?
**kwargs:
Additional keyword arguments passed on to the reducer function. These
should not include a keyword argument "dim", which is reserved for
specifying the dimension to reduce over.
Returns
--------
:obj:`xarray.DataArray`
Raises
------
:obj:`exceptions.UnknownDimensionError`
If a dimension with the given name is not present in the array.
"""
# Get array and set reduction dimension.
obj = self._obj
if dimension is not None:
if dimension == SPACE:
if X not in obj.dims or Y not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
obj = self.stack_spatial_dims()
else:
if dimension not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Dimension '{dimension}' is not present in the array"
)
kwargs["dim"] = dimension
# Reduce.
out = reducer(obj, track_types = track_types, **kwargs)
return out
def shift(self, dimension, steps, **kwargs):
"""Apply the shift verb to the array.
The shift verb shifts the values in an array a given amount of steps along
a dimension.
Parameters
-----------
dimension : :obj:`str`
Name of the dimension to shift along.
steps : :obj:`int`
Amount of steps each value should be shifted. A negative integer will
result in a shift to the left, while a positive integer will result in
a shift to the right. A shift along the spatial dimension follows the
pixel order defined by the CRS, e.g. starting in the top-left and
moving down each column.
**kwargs:
Ignored.
Returns
--------
:obj:`xarray.DataArray`
Raises
------
:obj:`exceptions.UnknownDimensionError`
If a dimension with the given name is not present in the array.
"""
# Get array.
obj = self._obj
if dimension == SPACE:
if X not in obj.dims or Y not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
obj = self.stack_spatial_dims()
stacked = True
else:
if dimension not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Dimension '{dimension}' is not present in the array"
)
stacked = False
# Shift values.
out = self._obj.shift({dimension: steps})
# Post-process.
if stacked:
out = out.sq.unstack_spatial_dims()
return out
def smooth(self, reducer, dimension, size, limit = 2, fill = False,
track_types = True, **kwargs):
"""Apply the smooth verb to the array.
The smooth verb smoothes the values in an array by applying a reducer
function to a rolling window along a dimension.
Parameters
-----------
reducer : :obj:`callable`
The reducer function to be applied to the rolling window.
dimension : :obj:`str`
Name of the dimension to smooth along.
size : :obj:`int`
Size k defining the extent of the rolling window. The pixel being
smoothed will always be in the center of the window, with k pixels at
its left and k pixels at its right. If the dimension to smooth over is
the spatial dimension, the size will be used for both the X and Y
dimension, forming a square window with the smoothed pixel in the
middle.
limit : :obj:`int`
Minimum number of valid data values inside a window. If the window
contains less than this number of data values (excluding nodata) the
smoothed value will be nodata.
fill : :obj:`bool`
Should pixels with a nodata value also be smoothed?
track_types : :obj:`bool`
Should the reducer promote the value type of the output object, based
on the value type of the input object?
**kwargs:
Additional keyword arguments passed on to the reducer function. These
should not include a keyword argument "dim", which is reserved for
specifying the dimension to reduce over.
Returns
--------
:obj:`xarray.DataArray`
Raises
------
:obj:`exceptions.UnknownDimensionError`
If a dimension with the given name is not present in the array.
"""
# Get array.
obj = self._obj
# Check dimension presence.
if dimension == SPACE:
if X not in obj.dims or Y not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
else:
if dimension not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Dimension '{dimension}' is not present in the array"
)
# Parse size.
# Size parameter defines neighborhood size at each side of the pixel.
size = size * 2 + 1
# Create the rolling window object.
if dimension == SPACE:
obj = obj.rolling({X: size, Y: size}, center = True, min_periods = limit)
else:
obj = obj.rolling({dimension: size}, center = True, min_periods = limit)
# Apply the reducer to each window.
out = reducer(obj, track_types = track_types, **kwargs)
# Post-process.
if not fill:
out = out.where(pd.notnull(self._obj)) # Preserve nan.
return out
def trim(self, dimension = None, **kwargs):
"""Apply the trim verb to the array.
The trim verb trims the dimensions of an array, meaning that all dimension
coordinates for which all values are missing are removed from the array.
The spatial dimensions are only trimmed at their edges, to preserve their
regularity.
Parameters
----------
dimension : :obj:`str`
Name of the dimension to be trimmed. If :obj:`None`, all dimensions
will be trimmed.
Returns
-------
:obj:`xarray.DataArray`
Raises
------
:obj:`exceptions.UnknownDimensionError`
If a dimension with the given name is not present in the array.
"""
obj = self._obj
dims = obj.dims
if dimension is None:
if X in dims and Y in dims:
regular_dims = [d for d in dims if d not in [X, Y]]
out = self._trim_space(self._trim(obj, regular_dims))
else:
out = self._trim(obj, dims)
else:
if dimension == SPACE:
if X not in dims or Y not in dims:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
out = self._trim_space(obj)
else:
if dimension not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Dimension '{dimension}' is not present in the array"
)
out = self._trim(obj, [dimension])
return out
@staticmethod
def _trim(obj, dimensions):
for dim in dimensions:
other_dims = [d for d in obj.dims if d != dim]
out = obj.isel({dim: obj.count(other_dims) > 0})
return out
@staticmethod
def _trim_space(obj):
# Find the smallest and largest spatial coords containing valid values.
y_idxs = np.nonzero(obj.count(list(set(obj.dims) - set([Y]))).data)[0]
x_idxs = np.nonzero(obj.count(list(set(obj.dims) - set([X]))).data)[0]
y_slice = slice(y_idxs.min(), y_idxs.max() + 1)
x_slice = slice(x_idxs.min(), x_idxs.max() + 1)
# Limit the x and y coordinates to only those ranges.
out = obj.isel({Y: y_slice, X: x_slice})
return out
def delineate(self, track_types = True, **kwargs):
"""Apply the delineate verb to the array.
The delineate verb deliniates spatio-temporal objects in a binary array.
Parameters
-----------
track_types : :obj:`bool`
Should the value type of the output object be promoted, and should it be
checked that the input has value type *binary*?
**kwargs:
Ignored.
Returns
--------
:obj:`xarray.DataArray`
Raises
-------
:obj:`exceptions.InvalidValueTypeError`
If ``track_types = True`` and the array is not binary.
:obj:`exceptions.TooManyDimensionsError`
If the array has more dimensions besides the spatial and/or temporal.
:obj:`exceptions.MissingDimensionError`
If the array has neither spatial nor temporal dimensions.
"""
# Get and check array.
obj = xr.apply_ufunc(utils.null_as_zero, self._obj)
if track_types:
vtype = obj.sq.value_type
if vtype is not None and vtype != "binary":
raise exceptions.InvalidValueTypeError(
f"Array to be delineated must be of value type 'binary', not '{vtype}'"
)
# Inspect dimensions.
dims = obj.dims
is_spatial = X in dims and Y in dims
is_temporal = TIME in dims
# Define neighborhood matrix.
if is_spatial and is_temporal:
if len(dims) > 3:
raise exceptions.TooManyDimensionsError(
f"Delineate is only supported for arrays with dimension '{TIME}' "
f"and/or '[{Y},{X}]', not: {list(dims)}"
)
obj = obj.transpose(TIME, Y, X) # Make sure dimension order is correct.
nb = np.array([
[[0,0,0],[0,1,0],[0,0,0]],
[[1,1,1],[1,1,1],[1,1,1]],
[[0,0,0],[0,1,0],[0,0,0]]
])
elif is_spatial:
if len(dims) > 2:
raise exceptions.TooManyDimensionsError(
f"Delineate is only supported for arrays with dimension '{TIME}' "
f"and/or '[{Y},{X}]', not: {list(dims)}"
)
nb = np.array([
[1,1,1],
[1,1,1],
[1,1,1]
])
elif is_temporal:
if len(dims) > 1:
raise exceptions.TooManyDimensionsError(
f"Delineate is only supported for arrays with dimension '{TIME}' "
f"and/or '[{Y},{X}]', not: {list(dims)}"
)
nb = np.array([1,1,1])
else:
raise exceptions.MissingDimensionError(
f"Delineate is only supported for arrays with dimension '{TIME}' "
f"and/or '{SPACE}', not: {list(dims)}"
)
# Delineate.
out = xr.apply_ufunc(lambda x, y: ndimage.label(x, y)[0], obj, nb)
# Post-process.
out = out.where(pd.notnull(self._obj)) # Preserve nan.
if track_types:
out.sq.value_type = "ordinal"
return out
def fill(self, dimension, method, extrapolate = True, track_types = True,
**kwargs):
"""Apply the fill verb to the array.
The fill verb fills nodata values by interpolating valid data values.
Parameters
-----------
dimension : :obj:`str`
Name of the dimension along which to interpolate.
method : :obj:`str`
Interpolation method to use. One of nearest, linear or cubic. When
interpolation along the stacked space dimensions, the two-dimensional
versions of these interpolation methods are used, i.e. 2D nearest
neighbour, bilinear and bicubic.
extrapolate : :obj:`bool`
Should nodata values at the edge be extrapolated? Only applied to
one-dimensional interpolation.
track_types : :obj:`bool`
Should the value type(s) of the input(s) be checked, and the value
type of the output be promoted, whenever applicable?
**kwargs"
Additional keyword arguments passed on to the interpolation function.
When interpolating along a single dimension, the interpolation function
is :meth:`xarray.DataArray.interpolate_na`.
When interpolation along the stacked space dimension, the interpolation
funtion is :meth:`rioxarray.raster_array.RasterArray.interpolate_na`.
Returns
--------
:obj:`xarray.DataArray`
Raises
------
:obj:`exceptions.UnknownDimensionError`
If a dimension with the given name is not present in the array.
:obj:`exceptions.InvalidValueTypeError`
If ``track_types = True`` and the value type of the array is not
supported by the chosen interpolation method.
"""
obj = self._obj
# Check if arrays value type is supported by the interpolation method.
if track_types:
if method != "nearest":
vtype = obj.sq.value_type
if vtype is not None and vtype not in ["continuous", "discrete"]:
raise exceptions.InvalidValueTypeError(
f"Unsupported value type for interpolation method '{method}': '{vtype}'"
)
# Interpolate missing values.
# --> 2D interpolation for the stacked space dimension.
# --> 1D interpolation for any regular dimension.
if dimension == SPACE:
if X not in obj.dims or Y not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Spatial dimensions '{X}' and '{Y}' are not present in the array"
)
if obj.rio.nodata is None:
nodata = utils.get_null(obj)
out = obj.rio.write_nodata(nodata).rio.interpolate_na(method, **kwargs)
else:
out = obj.rio.interpolate_na(method, **kwargs)
else:
if dimension not in obj.dims:
raise exceptions.UnknownDimensionError(
f"Dimension '{dimension}' is not present in the array"
)
if extrapolate:
kwargs.update({"fill_value": "extrapolate"})
out = obj.interpolate_na(dimension, method = method, **kwargs)
return out
def name(self, value, **kwargs):
"""Apply the name verb to the array.
The name verb assigns a name to an array.
Parameters
-----------
value : :obj:`str`
Character sting to be assigned as name to the input array.
**kwargs:
Ignored.
Returns
--------
:obj:`xarray.DataArray`
"""
out = self._obj.rename(value)
return out
def apply_custom(self, verb, track_types = True, **kwargs):
"""Apply a user-defined verb to the array.
Parameters
-----------
verb : :obj:`callable`
Implementation of the custom verb which will be provided to
:meth:`xarray.DataArray.pipe`.
track_types : :obj:`bool`
Should the value type(s) of the input(s) be checked, and the value
type of the output be promoted, whenever applicable?
**kwargs:
Additional keyword arguments passed on to the verb function.
Returns
--------
:obj:`xarray.DataArray`
"""
out = self._obj.pipe(verb, track_types = track_types, **kwargs)
return out
#
# INTERNAL PROCESSING
#
def align_with(self, other):
"""Align the array to the shape of another array.
An input array is alinged to another array if the pixel at position *i* in
the input array has the same coordinates as the pixel at position *i* in the
other array. Aligning can be done in several ways:
* Consider the case where the input array has exactly the same dimensions
and coordinates as the other array, but the order of them is different.
In that case, the input array is simply re-ordered to match the other
array.
* Consider the case where the input array has the same dimensions as the
other array, but not all coordinates match. In that case, the coordinates
that are in the input array but not in the other array are removed from the
input array, and at the same time the coordinates that are in the other
array but not in the input array are added to the input array, with nodata
values assigned.
* Consider the case where all dimensions of the input array are also present
in the other array, but not all dimensions of the other array are present
in the input array. In that case, the pixels of the input array are
duplicated along those dimensions that are missing.
Alignment may also be a combination of more than one of these ways.
Parameters
-----------
other : :obj:`xarray.DataArray`
Array to which the input array should be aligned.
Returns
--------
:obj:`xarray.DataArray`
The aligned input array.
Raises
-------
:obj:`exceptions.AlignmentError`
If the input array cannot be aligned to the other array, for example when
the two arrays have no dimensions in common at all, or when the input
array has dimensions that are not present in the other array.
"""
out = xr.align(other, self._obj, join = "left")[1].broadcast_like(other)
if not out.shape == other.shape:
raise exceptions.AlignmentError(
f"Array '{other.name if other.name is not None else 'y'}' "
f"cannot be aligned with "
f"input array '{self._obj.name if self._obj.name is not None else 'x'}'"
)
return out
def regularize(self):
"""Regularize the spatial dimension of the array.
Regularizing makes sure that the steps between subsequent coordinates of
the spatial dimensions are always equal to the resolution of that
dimensions.
Returns
-------
:obj:`xarray.DataArray`
The regularized input array.
"""