/
grid2d.py
executable file
·1631 lines (1482 loc) · 60.5 KB
/
grid2d.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
#!/usr/bin/env python
# python 3 compatibility
from __future__ import print_function
# stdlib imports
import abc
import textwrap
import sys
# third party imports
from mapio.gridbase import Grid
from mapio.dataset import DataSetException
from mapio.geodict import GeoDict, affine_from_geodict, is_valid_projection
import numpy as np
from scipy import interpolate
import shapely
from affine import Affine
from rasterio import features
from rasterio.warp import reproject, calculate_default_transform
from rasterio.crs import CRS
from rasterio.enums import Resampling
def _center_projection(projection):
parts = projection.split("+")[1:]
newparts = []
for part in parts:
if "lon_0" in part:
part = "lon_0=0 "
newparts.append(part)
newprojection = "+" + "+".join(newparts)
return newprojection
class Grid2D(Grid):
reqfields = set(["xmin", "xmax", "ymin", "ymax", "dx", "dy", "nx", "ny"])
def __init__(self, data=None, geodict=None):
"""
Construct a Grid object.
Args:
data (:obj:`array`, optional):
A 2D numpy array (can be None).
geodict (:obj:`GeoDict`, optional):
A GeoDict Object (or None) containing the following fields:
Returns:
A Grid2D object.
Raises:
DataSetException:
When data is not 2D or the number of rows and columns do not
match the geodict.
"""
if data is not None and geodict is not None:
# complain if data is not 2D (squeeze 1d dimensions out)
dims = data.shape
if len(dims) != 2:
raise DataSetException(
"Grid data must be 2D. Input data has "
"shape of %s" % str(data.shape)
)
ny, nx = dims
if ny != geodict.ny or nx != geodict.nx:
raise DataSetException(
"Input geodict does not match shape " "of input data."
)
self._geodict = geodict.copy()
self._data = data.copy()
else:
self._data = None
self._geodict = None
@staticmethod
def checkFirstColumnDuplicated(geodict):
"""Check to see if the first column in a file described by geodict
is duplicated by the last column.
Args:
geodict (GeoDict):
GeoDict object which may have duplicate column.
Returns:
Tuple containing:
- GeoDict object representing a grid with the last column removed.
- Boolean indicating whether the last column was a duplicate.
"""
first_column_duplicated = False
cols_per_degree = 1 / geodict.dx
is_even = np.isclose(cols_per_degree, np.floor(cols_per_degree))
is_global = (geodict.xmax - geodict.xmin) >= 360 or (
geodict.xmax - geodict.xmin
) < 0
newgeodict = geodict.copy()
if is_even and is_global:
if np.floor(cols_per_degree) * 360 == geodict.nx - 1:
first_column_duplicated = True
nx = geodict.nx - 1
xmax = geodict.xmin + (nx - 1) * geodict.dx
newgeodict = GeoDict(
{
"xmin": geodict.xmin,
"xmax": xmax,
"ymin": geodict.ymin,
"ymax": geodict.ymax,
"nx": nx,
"ny": geodict.ny,
"dx": geodict.dx,
"dy": geodict.dy,
}
)
return (newgeodict, first_column_duplicated)
@staticmethod
def getDataRange(fgeodict, sampledict, first_column_duplicated=False, padding=None):
"""For a given set of input bounds and information about a file,
determine the rows and columns for bounds.
Args:
fgeodict (GeoDict):
GeoDict object for a given file.
sampledict (GeoDict):
Sampling GeoDict object.
first_column_duplicated (bool):
indicating whether the last column in a file is a
duplicate of the first column.
Returns:
Dictionary containing fields:
- iulx1 Upper left X of first (perhaps only) segment.
- iuly1 Upper left Y of first (perhaps only) segment.
- ilrx1 Lower right X of first (perhaps only) segment.
- ilry1 Lower right Y of first (perhaps only) segment.
(if bounds cross 180 meridian...)
- iulx2 Upper left X of second segment.
- iuly2 Upper left Y of second segment.
- ilrx2 Lower right X of second segment.
- ilry2 Lower right Y of second segment.
"""
data_range = {}
# get the file values
gxmin = fgeodict.xmin
gxmax = fgeodict.xmax
nx = fgeodict.nx
ny = fgeodict.ny
# make sure fgeodict and bounds are in the same longitude
# system (-180 to 180, -360 to 0, or 0 to 360)
xmin, xmax, ymax = (sampledict.xmin, sampledict.xmax, sampledict.ymax)
file_normal = (
gxmin >= -180
and gxmin <= 180
and gxmax >= -180
and gxmax <= 180
and gxmax > gxmin
)
bounds_negative = xmin < -180
# bounds_360 = xmin >= 0 and xmin <= 360 and xmax >= 0 and
# xmax <= 360 and xmax > gxmin
bounds_360 = xmin >= 180 or xmax >= 180
# if the file coordinates are between -180 and 180, inclusive...
if file_normal:
# if the input sampling bound xmin is less than -180...
if bounds_negative:
xmin += 360
if xmax < -180:
xmax += 360
if bounds_360:
if xmin > 180:
xmin -= 360
if xmax > 180:
xmax -= 360
# these three values are the same regardless of whether we
# cross meridian
iuly1, iulx1 = fgeodict.getRowCol(ymax, xmin)
ilrx1 = iulx1 + sampledict.nx
ilry1 = iuly1 + sampledict.ny
iulx2 = None
ilrx2 = None
iuly2 = None
ilry2 = None
if ilrx1 > nx:
ilrx2 = ilrx1 - nx
ilrx1 = nx
iulx2 = 0
iuly2 = iuly1
ilry2 = ilry1
if ilry1 > ny:
ilry1 = ny
data_range["iulx1"] = int(iulx1)
data_range["ilrx1"] = int(ilrx1)
data_range["iuly1"] = int(iuly1)
data_range["ilry1"] = int(ilry1)
if iulx2 is not None:
data_range["iulx2"] = int(iulx2)
data_range["ilrx2"] = int(ilrx2)
data_range["iuly2"] = int(iuly2)
data_range["ilry2"] = int(ilry2)
return data_range
@staticmethod
def verifyBounds(filegeodict, samplegeodict, resample=False):
"""Ensure that the two grids represented at least 1) intersect
and 2) are aligned if resampling is True.
Args:
filegeodict (GeoDict):
object describing file.
samplegeodict (GeoDict):
object describing grid to use for sampling.
resample (bool):
indicating whether we want to resample.
Raises:
DataSetException when geodicts do not intersect or if the grids
are not aligned.
"""
if samplegeodict is not None and not filegeodict.intersects(samplegeodict):
msg = "Input samplegeodict must at least intersect with the " "file bounds"
raise DataSetException(msg)
# verify that if not resampling, the dimensions of the sampling
# geodict must match the file.
if resample is False and samplegeodict is not None:
ddx = np.abs(filegeodict.dx - samplegeodict.dx)
ddy = np.abs(filegeodict.dy - samplegeodict.dy)
if ddx > GeoDict.EPS or ddy > GeoDict.EPS:
raise DataSetException(
"File dimensions are different " "from sampledict dimensions."
)
@staticmethod
def bufferBounds(
samplegeodict, filegeodict, resample=False, buffer_pixels=1, doPadding=False
):
"""Buffer requested bounds out by buffer_pixels pixels, or edge
of grid.
Buffer pixels shoud be at filegeodict resolution.
Args:
samplegeodict (GeoDict):
object describing grid to use for sampling.
filegeodict (GeoDict):
object describing file.
resample (bool):
Boolean indicating whether we want to resample.
buffer_pixels (int):
Number of pixels to buffer bounds in any possible direction.
Returns:
GeoDict which has been buffered by the appropriate number of pixels.
"""
if not resample:
return samplegeodict
dx = filegeodict.dx
dy = filegeodict.dy
fxmin, fxmax = filegeodict.xmin, filegeodict.xmax
fymin, fymax = filegeodict.ymin, filegeodict.ymax
sxmin, sxmax = samplegeodict.xmin, samplegeodict.xmax
symin, symax = samplegeodict.ymin, samplegeodict.ymax
if not filegeodict.intersects(samplegeodict):
if not doPadding:
msg = (
"Cannot buffer bounds when sampling grid is "
"completely outside file grid, unless doPadding=True."
)
raise DataSetException(msg)
else:
return samplegeodict
buffer_geo_x = buffer_pixels * dx
buffer_geo_y = buffer_pixels * dy
xmin = sxmin - buffer_geo_x
xmax = sxmax + buffer_geo_x
ymin = symin - buffer_geo_y
ymax = symax + buffer_geo_y
# assign the "most minimum" x value, taking 180 meridian into account
is_180 = fxmin >= -180 and fxmax <= 180
if xmin < fxmin:
if is_180:
if xmin > -180:
xmin = fxmin
# assign the "most maximum" x value, taking 180 meridian into account
if xmax > fxmax:
if is_180:
if xmax < 180:
xmax = fxmax
if ymin < fymin:
ymin = fymin
if ymax > fymax:
ymax = fymax
# make sure that boundaries are on filegeodict boundaries -
# if not, go outwards until we hit one
ddxmin = xmin / dx
if int(ddxmin) != ddxmin:
xmin = np.floor(ddxmin) * dx
ddxmax = xmax / dx
if int(ddxmax) != ddxmax:
xmax = np.ceil(ddxmax) * dx
ddymin = ymin / dy
if int(ddymin) != ddymin:
ymin = np.floor(ddymin) * dy
ddymax = ymax / dy
if int(ddymax) != ddymax:
ymax = np.ceil(ddymax) * dy
geodict = GeoDict.createDictFromBox(xmin, xmax, ymin, ymax, dx, dy, inside=True)
return geodict
@staticmethod
def padGrid(data, geodict, pad_dict):
"""Pad input data array with pixels specified by pad_dict on each side.
Args:
data (array):
2D numpy array of data.
geodict (GeoDict):
GeoDict object describing data.
pad_dict (dict):
containing fields:
- padleft The number of padding pixels on the left edge.
- padright The number of padding pixels on the right edge.
- padbottom The number of padding pixels on the bottom edge.
- padtop The number of padding pixels on the top edge.
Returns:
Tuple of (data,geodict) where data has been padded and geodict
represents new padded data.
"""
if (
pad_dict["padleft"] == 0
and pad_dict["padright"] == 0
and pad_dict["padbottom"] == 0
and pad_dict["padtop"] == 0
):
return (data, geodict)
newdata = data.copy()
ny, nx = newdata.shape
dx, dy = geodict.dx, geodict.dy
# we're padding with inf so that we don't interfere with other nan
# pixels in the data
leftcols = np.ones((ny, pad_dict["padleft"])) * np.inf
newdata = np.hstack((leftcols, newdata))
ny, nx = newdata.shape
rightcols = np.ones((ny, pad_dict["padright"])) * np.inf
newdata = np.hstack((newdata, rightcols))
ny, nx = newdata.shape
bottomrows = np.ones((pad_dict["padbottom"], nx)) * np.inf
newdata = np.vstack((newdata, bottomrows))
ny, nx = newdata.shape
toprows = np.ones((pad_dict["padtop"], nx)) * np.inf
newdata = np.vstack((toprows, newdata))
ny, nx = newdata.shape
# get the shapes of the pads - sometimes these can have a shape
# of (3,0) and a length of 3
leftheight, leftwidth = leftcols.shape
rightheight, rightwidth = rightcols.shape
bottomheight, bottomwidth = bottomrows.shape
topheight, topwidth = toprows.shape
newxmin = geodict.xmin - leftwidth * dx
newxmax = geodict.xmax + rightwidth * dx
newymin = geodict.ymin - bottomheight * dy
newymax = geodict.ymax + topheight * dy
newdict = GeoDict(
{
"xmin": newxmin,
"xmax": newxmax,
"ymin": newymin,
"ymax": newymax,
"nx": nx,
"ny": ny,
"dx": dx,
"dy": dy,
},
adjust="res",
)
return (newdata, newdict)
@staticmethod
def getPadding(filegeodict, samplegeodict, doPadding=False):
"""Determine how many pixels of padding there need to be on each
side of requested grid.
Args:
filegeodict (GeoDict):
specifying the spatial information from a source file.
samplegeodict (GeoDict):
specifying the spatial information for a desired
sampling regime.
doPadding (bool):
Boolean indicating that user does or does not want to add any padding.
Raises:
DataSetException:
When resampling is False and filegeodict and samplegeodict are
not pixel aligned.
Returns:
A dictionary containing fields:
- padleft The number of padding pixels on the left edge.
- padright The number of padding pixels on the right edge.
- padbottom The number of padding pixels on the bottom edge.
- padtop The number of padding pixels on the top edge.
"""
if not doPadding:
pad_dict = {"padleft": 0, "padright": 0, "padbottom": 0, "padtop": 0}
else:
# get pad left columns - go outside specified bounds if not
# exact edge
txmax = samplegeodict.xmax
if samplegeodict.xmin > samplegeodict.xmax:
txmax = samplegeodict.xmax + 360
pxmin, pxmax, pymin, pymax = (
samplegeodict.xmin,
txmax,
samplegeodict.ymin,
samplegeodict.ymax,
)
txmax = filegeodict.xmax
if filegeodict.xmin > filegeodict.xmax:
txmax = filegeodict.xmax + 360
gxmin, gxmax, gymin, gymax = (
filegeodict.xmin,
txmax,
filegeodict.ymin,
filegeodict.ymax,
)
dx, dy = (filegeodict.dx, filegeodict.dy)
# very small differences between bounds can wind up
# still being ceil'd up to 1. Check for very small numbers here
dleft = gxmin - pxmin
dright = pxmax - gxmax
dbottom = gymin - pymin
dtop = pymax - gymax
if dleft < GeoDict.EPS:
dleft = 0.0
if dright < GeoDict.EPS:
dright = 0.0
if dtop < GeoDict.EPS:
dtop = 0.0
if dbottom < GeoDict.EPS:
dbottom = 0.0
padleftcols = int(np.ceil(dleft / dx))
padrightcols = int(np.ceil(dright / dx))
padbottomrows = int(np.ceil(dbottom / dy))
padtoprows = int(np.ceil(dtop / dy))
# if any of these are negative, set them to zero
if padleftcols < 0:
padleftcols = 0
if padrightcols < 0:
padrightcols = 0
if padbottomrows < 0:
padbottomrows = 0
if padtoprows < 0:
padtoprows = 0
pad_dict = {
"padleft": padleftcols,
"padright": padrightcols,
"padbottom": padbottomrows,
"padtop": padtoprows,
}
return pad_dict
def __repr__(self):
"""
String representation of a Grid2D object.
Returns:
String containing description of Grid2D object.
"""
xmin, xmax, ymin, ymax = (
self._geodict.xmin,
self._geodict.xmax,
self._geodict.ymin,
self._geodict.ymax,
)
ny, nx = self._data.shape
dx, dy = (self._geodict.dx, self._geodict.dy)
zmin = np.nanmin(self._data)
zmax = np.nanmax(self._data)
rstr = """<%s Object:
ny: %i
nx: %i
xmin: %.4f
xmax: %.4f
ymin: %.4f
ymax: %.4f
dx: %.4f
dy: %.4f
zmin: %.6f
zmax: %.6f>""" % (
self.__class__.__name__,
ny,
nx,
xmin,
xmax,
ymin,
ymax,
dx,
dy,
zmin,
zmax,
)
parts = rstr.split("\n")
newrstr = "\n".join([p.strip() for p in parts])
return textwrap.dedent(newrstr)
@classmethod
def _createSampleData(self, M, N):
"""Used for internal testing - create an NxN grid with lower left
corner at 0.5,0.5, dx/dy = 1.0.
Args:
M (int):
Number of rows in output grid
N (int):
Number of columns in output grid
Returns:
GMTGrid object where data values are an NxN array of values
from 0 to N-squared minus 1, and geodict
lower left corner is at 0.5/0.5 and cell dimensions are 1.0.
"""
data = np.arange(0, M * N).reshape(M, N)
# arange gives int64 by default, not supported by netcdf3
data = data.astype(np.int32)
xvar = np.arange(0.5, 0.5 + N, 1.0)
yvar = np.arange(0.5, 0.5 + M, 1.0)
geodict = {
"ny": M,
"nx": N,
"xmin": 0.5,
"xmax": xvar[-1],
"ymin": 0.5,
"ymax": yvar[-1],
"dx": 1.0,
"dy": 1.0,
}
gd = GeoDict(geodict)
return (data, gd)
@abc.abstractmethod
def getFileGeoDict(filename):
# this should return a geodict and a boolean indicating whether
# the first
# column is duplicated at the end of each row (some data providers
# do this).
raise NotImplementedError("Load method not implemented in base class")
@abc.abstractmethod
def readFile(filename, data_range):
"""Read in data from the given file, at the pixels specified in
data_range.
Args:
filename (str):
Name of file to read.
data_range (dict):
Dictionary containing fields:
- iulx1 Upper left X of first (perhaps only) segment.
- iuly1 Upper left Y of first (perhaps only) segment.
- ilrx1 Lower right X of first (perhaps only) segment.
- ilry1 Lower right Y of first (perhaps only) segment.
(if bounds cross 180 meridian...)
- iulx2 Upper left X of second segment.
- iuly2 Upper left Y of second segment.
- ilrx2 Lower right X of second segment.
- ilry2 Lower right Y of second segment.
"""
raise NotImplementedError(
"readFile is only implemented in Grid2D " "subclasses."
)
@classmethod
def copyFromGrid(cls, grid):
"""
Copy constructor - can be used to create an instance of any
Grid2D subclass from another.
Args:
grid (Grid2D):
Any Grid2D instance.
Returns:
A copy of the data in that input Grid2D instance.
"""
if not isinstance(grid, Grid2D):
raise DataSetException(
"Input to copyFromGrid must be an "
"instance of a Grid2D object (inc. "
"subclasses)"
)
return cls(grid.getData(), grid.getGeoDict())
# This should be a @classmethod in subclasses
@abc.abstractmethod
def save(self, filename):
# would we ever want to save a subset of the data?
raise NotImplementedError("Save method not implemented in base class")
@classmethod
def _createSections(self, bounds, geodict, firstColumnDuplicated, isScanLine=False):
"""Given a grid that goes from -180 to 180 degrees, figure out
the two pixel regions that up both sides of the subset.
Args:
bounds (tuple):
Tuple of (xmin,xmax,ymin,ymax)
geodict (Geodict):
Geodict dictionary
firstColumnDuplicated (bool):
Boolean indicating whether this is a global data set where the
first and last columns are identical.
isScanLine (bool):
Boolean indicating whether this array is in scan line order
(pixel[0,0] is the geographic upper left).
Returns:
Two tuples of 4 elements each - (iulx,iuly,ilrx,ilry). The
first tuple defines the pixel offsets for the left
side of the subsetted region, and the second tuple
defines the pixel offsets for the right side.
"""
(bxmin, bxmax, bymin, bymax) = bounds
ulx = geodict.xmin
uly = geodict.ymax
dx = geodict.dx
dy = geodict.dy
nx = geodict.nx
# section 1
iulx1 = int(np.floor((bxmin - ulx) / dx))
ilrx1 = int(nx)
if not isScanLine:
iuly1 = int(np.ceil((uly - bymax) / dy))
ilry1 = int(np.floor((uly - bymin) / dy)) + 1
else:
ilry1 = int(np.ceil((uly - bymin) / dy))
iuly1 = int(np.floor((uly - bymax) / dy)) + 1
# section 2
iulx2 = 0
ilrx2 = int(np.ceil((bxmax - ulx) / dx)) + 1
iuly2 = iuly1
ilry2 = ilry1
if firstColumnDuplicated:
ilrx1 -= 1
region1 = (iulx1, iuly1, ilrx1, ilry1)
region2 = (iulx2, iuly2, ilrx2, ilry2)
return (region1, region2)
def getData(self):
"""Return a reference to the data inside the Grid.
Returns:
A reference to a 2D numpy array.
"""
return self._data # should we return a copy of the data?
def setData(self, data):
"""Set the data inside the Grid.
Args:
data (array):
A 2D numpy array.
Raises:
DataSetException if the number of rows and columns do not match
the the internal GeoDict, or if the input
is not a numpy array.
"""
if not isinstance(data, np.ndarray):
raise DataSetException("setData() input is not a numpy array.")
if len(data.shape) != 2:
raise DataSetException("setData() input array must have " "two dimensions.")
m, n = data.shape
if m != self._geodict.ny or n != self._geodict.nx:
raise DataSetException(
"setData() input array must match rows " "and columns of existing data."
)
self._data = data
def getGeoDict(self):
"""
Return a reference to the geodict inside the Grid.
Returns::
A reference to a dictionary (see constructor).
"""
return self._geodict.copy()
def getBounds(self):
"""
Return the lon/lat range of the data.
Returns:
Tuple of (lonmin,lonmax,latmin,latmax)
"""
return (
self._geodict.xmin,
self._geodict.xmax,
self._geodict.ymin,
self._geodict.ymax,
)
def applyNaN(self, force=False):
"""Apply no data value to internal data, cast to float if necessary.
Intelligently cast data in grid to be able to handle NaN values.
Usage:
Integer data with a precision of 16 bits or less
will be cast to 32 bit floating point.
Integer data with precision of 32 bits will be cast to 32 bit
floating point if maximum/minimum values can be cast without
losing precision.
Integer data with precision of 64 bits or greater will be cast to
64 bit floating point if maximum/minimum values can be cast without
losing precision. Otherwise, this method will raise an OverflowError
unless the force option is set to True.
Args:
force (bool):
Boolean indicating whether to override OverflowError (see Usage).
"""
nodata = self._geodict.nodata
if nodata is None or np.isnan(nodata) or np.isnan(self._data).any():
return
isint = "int" in str(self._data.dtype)
precision = self._data.dtype.itemsize
if not isint:
self._data[self._data == nodata] = np.nan
if isint:
if precision <= 2:
self._data = self._data.astype(np.float32)
elif precision <= 4:
dmax = self._data.max()
dmin = self._data.min()
fdmax = np.float32(dmax)
fdmin = np.float32(dmin)
if dmax == fdmax and dmin == fdmin:
self._data = self._data.astype(np.float32)
else:
self._data = self._data.astype(np.float64)
else:
dmax = self._data.max()
dmin = self._data.min()
fdmax = np.float64(dmax)
fdmin = np.float64(dmin)
if dmax == fdmax and dmin == fdmin:
if not force:
raise OverflowError(
"Data cannot be represented as 64-bit float."
)
self._data = self._data.astype(np.float64)
# if we've gotten this far, we have upcasted successfully
self._data[self._data == nodata] = np.nan
def subdivide(self, finerdict, cellFill="max"):
"""Subdivide the cells of the host grid into finer-resolution cells.
Args:
finerdict (GeoDict):
GeoDict object defining a grid with a finer resolution than the
host grid.
cellFill (:obj:`str`, optional):
String defining how to fill cells that span more than one host
grid cell.
Choices are:
'max': Choose maximum value of host grid cells.
'min': Choose minimum value of host grid cells.
'mean': Choose mean value of host grid cells.
Returns:
Grid2D instance with host grid values subdivided onto finer grid.
Raises:
DataSetException:
When finerdict is not a) finer resolution or b) does not
intersect.x or cellFill is not valid.
"""
fillvals = ["min", "max", "mean"]
if cellFill not in fillvals:
raise DataSetException("cellFill input must be one of %s." % fillvals)
if finerdict.dx >= self._geodict.dx or finerdict.dy >= self._geodict.dy:
raise DataSetException(
"subdivide() input GeoDict must be finer " "resolution than host grid."
)
if not finerdict.intersects(self._geodict):
raise DataSetException(
"subdivide() input GeoDict must intersect " "host grid."
)
# things are simple if the host grid cell dx/dy are a multiple
# of finer grid dx/dy and are
# aligned in the sense that every host grid cell edge matches
# an edge of finer grid cell.
resXMultiple = self._geodict.dx / finerdict.dx == int(
self._geodict.dx / finerdict.dx
)
resYMultiple = self._geodict.dy / finerdict.dy == int(
self._geodict.dy / finerdict.dy
)
# this stuff below may not be right...?
dxmin = (self._geodict.xmin - finerdict.xmin) / finerdict.dx
isXAligned = np.isclose(dxmin, int(dxmin))
dymin = (self._geodict.ymin - finerdict.ymin) / finerdict.dy
isYAligned = np.isclose(dymin, int(dymin))
isAligned = resXMultiple and resYMultiple and isXAligned and isYAligned
finedata = (
np.ones((finerdict.ny, finerdict.nx), dtype=self._data.dtype) * np.nan
)
if isAligned:
for i in range(0, self._geodict.ny):
for j in range(0, self._geodict.nx):
cellvalue = self._data[i, j]
# what is the longitude of the first finer cell inside
# the host cell?
# coordinates of center of host cell
clat, clon = self.getLatLon(i, j)
# get the left edge of the cell
fleftlon = clon - (self._geodict.dx / 2) + finerdict.dx / 2
ftoplat = clat + (self._geodict.dy / 2) - finerdict.dy / 2
frightlon = clon + (self._geodict.dx / 2) - finerdict.dx / 2
fbottomlat = clat - (self._geodict.dy / 2) + finerdict.dy / 2
itop, jleft = finerdict.getRowCol(ftoplat, fleftlon)
itop = itop
jleft = jleft
ibottom, jright = finerdict.getRowCol(fbottomlat, frightlon)
ibottom = ibottom
jright = jright
finedata[itop : ibottom + 1, jleft : jright + 1] = cellvalue
else:
for i in range(0, self._geodict.ny):
for j in range(0, self._geodict.nx):
cellvalue = self._data[i, j]
# get the indices of all cells that are
# completely contained inside this one.
# coordinates of center of host cell
clat, clon = self.getLatLon(i, j)
# what is the longitude of of our first approximated
# left edge fine
# cell that is contained by host cell?
fleftlon = clon - self._geodict.dx / 2.0 + finerdict.dx / 2
frightlon = clon + self._geodict.dx / 2.0 - finerdict.dx / 2
jleft = int(np.ceil((fleftlon - finerdict.xmin) / finerdict.dx))
jright = int(np.floor((frightlon - finerdict.xmin) / finerdict.dx))
# what is the latitude of of our first approximated
# bottom edge fine
# cell that is contained by host cell?
fbottomlat = clat - self._geodict.dy / 2.0 + finerdict.dy / 2
ftoplat = clat + self._geodict.dy / 2.0 - finerdict.dy / 2
ibottom = int(
np.floor((finerdict.ymax - fbottomlat) / finerdict.dy)
)
itop = int(np.ceil((finerdict.ymax - ftoplat) / finerdict.dy))
# ibottom = int(np.ceil((fbottomlat - finerdict.ymin) /
# finerdict.dy))
# itop = int(np.floor((ftoplat - finerdict.ymin) /
# finerdict.dy))
finedata[itop : ibottom + 1, jleft : jright + 1] = cellvalue
# now what do I do about cells that aren't completely
# contained?
# we have to now find all rows/columns where there are NaN
# values and deal with them
# accordingly - let's look at output rows first, looking for a
# row that is all NaN
# and doesn't have an all NaN row above or below it.
# np.minimum and maximum throw warnings with NaN values, even
# the behavior with respect
# to those is clearly documented. So, let's ignore those
# warnings in this
# section where we use those methods.
with np.errstate(invalid="ignore"):
colidx = finerdict.nx // 2
while colidx > -1:
col = finedata[:, colidx]
if not np.isnan(col).all():
nanrows = np.where(np.isnan(col))
break
colidx -= 1
for i in nanrows[0]:
if i == 0 or i == finerdict.ny - 1:
continue
if cellFill == "min":
finedata[i, :] = np.minimum(
finedata[i - 1, :], finedata[i + 1, :]
)
elif cellFill == "max":
finedata[i, :] = np.maximum(
finedata[i - 1, :], finedata[i + 1, :]
)
else: # cellFill == 'mean':
finedata[i, :] = (finedata[i - 1, :] + finedata[i + 1, :]) / 2.0
# now look at output columns
rowidx = finerdict.ny // 2
while rowidx > -1:
row = finedata[rowidx, :]
if not np.isnan(row).all():
nancols = np.where(np.isnan(row))
break
rowidx -= 1
for j in nancols[0]:
if j == 0 or j == finerdict.nx - 1:
continue
if cellFill == "min":
finedata[:, j] = np.minimum(
finedata[:, j - 1], finedata[:, j + 1]
)
elif cellFill == "max":
finedata[:, j] = np.maximum(
finedata[:, j - 1], finedata[:, j + 1]
)
else: # cellFill == 'mean':
finedata[:, j] = (finedata[:, j - 1] + finedata[:, j + 1]) / 2.0
finegrid = Grid2D(finedata, finerdict)
return finegrid
def cut(self, xmin, xmax, ymin, ymax, align=False):
"""Cut out a section of Grid and return it.
Args:
xmin (float):
Longitude coordinate of upper left pixel, must be
aligned with Grid.
xmax (float):
Longitude coordinate of lower right pixel, must be
aligned with Grid.
ymin (float):
Latitude coordinate of upper left pixel, must be
aligned with Grid.
ymax (float:):
Latitude coordinate of lower right pixel, must be
aligned with Grid.
align (bool):
Boolean indicating whether input boundaries
should be modified to align with host grid.
"""
td1 = GeoDict.createDictFromBox(
xmin, xmax, ymin, ymax, self._geodict.dx, self._geodict.dy, inside=True
)
td = None
if not td1.isAligned(self._geodict):
if not align:
raise DataSetException("Input bounds must align with this " "grid.")
else:
td = self._geodict.getAligned(td1, inside=True)
else:
td = td1.copy()
if not self._geodict.contains(td):
raise DataSetException(
"Input bounds must be completely " "contained by this grid."
)
uly, ulx = self._geodict.getRowCol(td.ymax, td.xmin)
lry, lrx = self._geodict.getRowCol(td.ymin, td.xmax)
data = self._data[uly : lry + 1, ulx : lrx + 1]
grid = Grid2D(data, td)
return grid
def getValue(self, lat, lon, method="nearest", default=None):
# return nearest neighbor value
"""Return numpy array at given latitude and longitude (using
nearest neighbor).
Args:
lat (float):
Latitude (in decimal degrees) of desired data value.
lon (float):
Longitude (in decimal degrees) of desired data value.
method (:obj:`str`, optional):
Interpolation method, one of ('nearest','linear','cubic','quintic')
default:
Default value to return when lat/lon is outside of grid bounds.
Returns:
Value at input latitude,longitude position.
Raises:
DataSetException:
When lat/lon is outside of bounds and default is None.