-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathwrf2kmz.py
executable file
·1747 lines (1479 loc) · 60 KB
/
wrf2kmz.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
'''
wrf2kmz
Jonathan Beezley <jon.beezley@gmail.com>
This is a module/commandline script that will create kmz visualizations
from wrf netCDF files. The module contains three main classes:
BaseNetCDF2Raster:
Generates georeferenced rasters from netcdf files. It is designed
to be overloaded for different model output conventions. Most
of the code should be generic enough to work with any netCDF
output file, with the exception of the georeferencing and
time coding. These functions need to be overloaded as in the
subclass FireNetcdf2Raster that is designed to work with WRF-Fire.
This class also contains variables that control how the rasters
will be displayed in the kmz.
FireRasterFile:
Purely a convenience class used for storing defaults for various
variables that are present in WRF-Fire output files.
ncKML:
A subclass of simplekml.Kml that has additional functions taking
subclasses of BaseNetCDF2Raster to construct kml elements such
as ground overlays.
When used as a commandline script as `python wrf2kmz.py wrfout [var1 [var2 [...]]]`
a kmz is produced containing a visualization of the WRF-Fire output file wrfout.
By default, this kmz will contain an animation of the fire perimeter. With additional
variable names specified on the commandline, it will add ground overlays to this file.
At this time, only 2D surface variables are supported.
For reprojection support, there is a fortran module that must be compiled. This should
be done automatically using numpy's f2py. In case compilation of this module fails,
This script will fall back on a slower method based on matplotlib's griddata method.
The fortran module is faster because it makes assumptions about the structure of the
reprojection. In particular, it assumes that convex sets are projected into convex
sets. This may not be true for example in extreme regions of the projection where
the image is warped significantly. However, in most cases, the fortran module
should work well.
Dependencies:
simplekml : http://code.google.com/p/simplekml/
matplotlib : http://matplotlib.sourceforge.net/
netcdf4-python : http://code.google.com/p/netcdf4-python/
'''
# to turn off reprojection support set this to True
no_reprojection=False
# force matplotlib's griddata interpolation
# rather than custom fortran module
use_matplotlib_reproject=False
# use io caching (may help performance, but can use a lot of memory)
use_io_caching=True
# standard library imports
import sys
import os
from datetime import timedelta
from dateutil import parser
from cStringIO import StringIO
# dependency imports
import matplotlib
# set backend to Agg when using as a commandline script
# so it will work without a X11
if __name__ == '__main__':
matplotlib.use('Agg')
import warnings
warnings.simplefilter('ignore')
from matplotlib import pylab
from matplotlib.mlab import griddata
import numpy as np
from netCDF4 import Dataset
try:
from simplekml import *
except Exception:
import sys
print 'Could not find simplekml module.'
print 'If you have setuptools installed, try `easy_install simplekml`.'
print 'Otherwise install from http://code.google.com/p/simplekml/'
sys.exit(1)
# verbose=False to be quiet
verbose=True
# used for storing reprojection information between variables
global _globalReprojectionIdx
_globalReprojectionIdx={}
def reprojectArray(tag,lon,lat,a,interp='nn'):
'''
Main reprojection function. Global variables control what
occurs here.
tag: A name for the coordinate grid, must be unique for
each different grid.
lon,lat: Coordinate arrays for the variable
a: Variable to be reprojected.
interp: Interpolation method, currently only 'nn' is supported
'''
if no_reprojection:
# do no reprojection
return a
if have_reproject:
# fortran reprojection module
idx=globalReprojectIdx(tag,lon,lat)
if have_reproject and idx is not None:
# if index computation was successful, go ahead
# and interpolate
b=reprojectArrayFromTag(tag,a)
else:
# slower griddata reprojection
b=simpleReproject(lon,lat,a,interp)
return b
def simpleReproject(lon,lat,a,interp='nn'):
'''
Reprojection based on matplotlib's griddata.
'''
west=lon.min()
east=lon.max()
south=lat.min()
north=lat.max()
xi=np.linspace(west,east,a.shape[1])
yi=np.linspace(south,north,a.shape[0])
return griddata(lon.ravel(),lat.ravel(),a.ravel(),xi,yi,interp=interp)
def message(s):
'''
Print a message to stdout if verbose is True.
'''
if verbose:
print s
have_reproject=False
#try:
# import reprojectGDAL
# from reprojectGDAL import getEPSGProjectionDef,createGCP,vrtFromArray, \
# georeferenceImage,warpImage,readNC
# have_reproject=True
# message('Using reprojection support.')
#except Exception:
# have_reproject=False
# print >> sys.stderr, "WARNING: Could not import reprojection module."
# print >> sys.stderr, "Ensure that gdalwarp and gdal_translate are in PATH."
# print >> sys.stderr, "Continuing without reprojection support."
# Try to compile the fortran reprojection module if it does not exist
_p=os.path.dirname(__file__)
if not os.path.exists(os.path.join(_p,'reproject.so')):
try:
from numpy.f2py import compile
compile(open(os.path.join(_p,'reproject.F90'),'r').read(),
modulename='reproject',verbose=0,source_fn='tmp.F90')
os.remove('tmp.F90')
if _p != '':
import shutil
shutil.move('reproject.so',_p)
except Exception as e:
print 'Could not compile reprojection module.'
print 'Using matplotlib griddata for reprojection.'
# try and import the reprojection module
# fall back to griddata reprojection
try:
import reproject
have_reproject=True
except ImportError:
have_reproject=False
if use_matplotlib_reproject:
have_reproject=False
def globalReprojectIdx(tag,lon,lat):
'''
Compute reprojection index from a source grid. This is the computationally
expensive part of the reprojection, so we store the index globally for all
variables with the same grid.
The index is defined be creating a rectangular lat/lon grid with the same
extremes as the source projection. The index then contains the indices of
the closest grid points of source grid to that of the target. Nearest
neighbor interpolation can then be done in O(n) time from this
'''
if not have_reproject:
return None
global _globalReprojectionIdx
if not _globalReprojectionIdx.has_key(tag):
# generate target grid
xi=np.linspace(lon.min(),lon.max(),lon.shape[1])
yi=np.linspace(lat.min(),lat.max(),lat.shape[0])
# generate index array
idx,ierr=reproject.reprojectionidx(lon.T,lat.T,xi,yi)
# check for error
if ierr != 0:
print 'Reprojection error for %s' % tag
idx=None
else:
idx=idx
# store result in global dictionary
_globalReprojectionIdx[tag]=(idx,xi,yi)
return _globalReprojectionIdx[tag][0]
def getReprojectionGeoref(tag,lon,lat):
globalReprojectIdx(tag,lon,lat)
if _globalReprojectionIdx.has_key(tag):
ix,iy=_globalReprojectionIdx[tag][1:]
return ix,iy
else:
return (None,None)
def reprojectArrayFromTag(tag,a):
'''
Interpolate the array 'a' from the globally stored index.
'''
global _globalReprojectionIdx
idx=_globalReprojectionIdx[tag][0]
assert idx is not None
return reproject.interparray(idx,a.T,np.nan).T
class MaskedArrayException(Exception):
'''
An exception that is raised when a variable read contains only
masked (or invalid) data. This allows methods that loop over all
time steps in a file to skip those that would otherwise contain
empty or completely transparent images.
'''
pass
class BaseNetCDF2Raster(object):
'''
Base class for creating georeferenced raster images from netCDF data.
Subclasses must at lest override getStepTime.
'''
# default matplotlib norm object (maps variable range to [0,1] for coloring)
defaultNorm=matplotlib.colors.Normalize
# default matplotlib colormap
defaultcmap=pylab.cm.jet
# default colorbar label formatter
defaultFormatter=None
# when true use global variable range for color axis range
# rather than different color scales for each time step
# unimplemented!
defaultminmaxglobal=False
# A list containing values that are ignored in the input data.
# These values will display as transparent in the output raster
# and will not be used for computation of the color axis.
defaultmaskedValues=None
# mask all values > this value
defaultmaskedAbove=None
# mask all value < this value
defaultmaskedBelow=None
def __init__(self,file,var,norm=None,cmap=None,formatter=None,\
minmaxglobal=None,maskedValues=None,maskedAbove=None, \
maskedBelow=None,static=False,displayName=None, \
displayDescription=None,displayColorbar=True, \
displayAlpha=180,name=None,minmax=None,accum=None, \
accumsumhours=None,norestriction=False,colorbarargs={}, \
subdomain=None,cmapboundaries=None,interp='nearest',
derivedVar=False,slice3D=0,dpi=300):
'''
Initialize a raster class object.
Required arguments:
file: opened netCDF4 Dataset or MFDataset object
var: netCDF4 Variable object from file
(the variable to visualize)
Optional (keyword) arguments:
norm: matplotlib color norm (class default: defaultNorm)
cmap: matplotlib color map (class default: defaultcmap)
formatter: matplotlib label formatter
(class default: defaultFormatter)
maskedValues: list of values to be masked in output
(class default: defaultMaskedValues)
maskedBelow: Values less than this will be masked.
(class default: defaultmaskedBelow)
maskedAbove: Values greater than this will be masked.
(class default: defaultmaskedAbove)
static: If true, assume the variable doesn't change in each
timestep. Only one image will be created with a
time range from the begining of the file to the end.
(default False)
displayName: This is how the variable will be called in the kmz file.
(default var._name)
displayDescription:
This is a description that will be added to the variable in
kmz. (default var.description if present)
displayColorbar:If true, a colorbar will be created with the ground overlay.
(default True)
displayAlpha: An integer in the range 0-255 giving the transparency of the
ground overlay. 0 means transparent, 255 means opaque.
(default 180)
minmaxglobal: unimplemented
minmax: set a static min/max for display of the variable as a tuple (min,max)
accum: True if the variable contains accumulated values such as RAINC.
The script will display deltas between time steps.
accumsumhours: Number of hours to accumulate data from each time step.
For example, var=RAINC,accum=True,accumsumhours=3 will display
a three hour rolling sum of rain accumulation.
norestriction: If true, don't restrict images around valid data.
colorbarargs: Extra keyword arguments for colorbar constructor.
subdomain: Only display a subdomain. This should be a dictionary containing
keys, 'centerlat' and 'centerlon' for the center coordinates of the
subdomain, and 'dx' and 'dy' for the width and height of the subdomain
in meters.
interp How to interpolate onto image ('nearest','bilinear','bicubic', etc).
See options in matplotlib imshow method.
derivedVar If true, pass reading of the variable to subclass _readVarRaw method.
slice3D If this is a 3D var then slice at the given level (default 0).
dpi Pixels per inch in the output image (default 300).
'''
# store arguments
self._file=file
self._var=var
# make sure the variable is 2D
ndim=self._getVarNDim(var)
if ndim != 2 and ndim != 3:
raise Exception("Only 2D and 3D variables are supported.")
self._minmax=minmax
self._norm=norm
self._cmap=self.setToDefaultifNone(cmap,self.defaultcmap)
self._formatter=self.setToDefaultifNone(formatter,self.defaultFormatter)
self._minmaxglobal=self.setToDefaultifNone(minmaxglobal,self.defaultminmaxglobal)
self._maskedValues=self.setToDefaultifNone(maskedValues,self.defaultmaskedValues)
self._maskedAbove=self.setToDefaultifNone(maskedAbove,self.defaultmaskedAbove)
self._maskedBelow=self.setToDefaultifNone(maskedBelow,self.defaultmaskedBelow)
self._static=static
self._tdim=self.getTimeDim()
self.displayName=displayName
self.displayDescription=displayDescription
self.displayColorbar=displayColorbar
self.displayAlpha=displayAlpha
self._name=name
self._gref={}
self._accum=accum
self._accumsumhours=accumsumhours
self._norestriction=norestriction
self._colorbarargs=colorbarargs
self._interp=interp
self._derivedVar=derivedVar
self._dpi=dpi
self._ioCache={}
if ndim == 3:
self._slice3D=slice3D
else:
self._slice3D=None
if self._accum:
self._minmaxglobal=False
#if self._minmaxglobal:
# raise Exception("Global min-max computation not yet supported.")
# get the number of time steps in this file from the unlimited dimension
# of the netcdf file
if self._tdim is None:
self._nstep=1
else:
self._nstep=len(self._file.dimensions[self._tdim])
self._subdomain=None
if subdomain is not None:
lon,lat=self.readCoordinates(istep=0,idx=None,orig=True)
lonc=subdomain['centerlon']
latc=subdomain['centerlat']
iy,ix=self._findPointIndex(lonc,latc,lon,lat)
dx,dy=self._getDXDY()
nx=np.ceil(subdomain['dx']/float(dx))
ny=np.ceil(subdomain['dy']/float(dy))
istart=np.floor(ix-nx/2.)
iend=np.ceil(ix+nx/2.)
jstart=np.floor(iy-ny/2.)
jend=np.ceil(iy+ny/2.)
istart=max(istart,0)
iend=min(iend,lon.shape[-1]-1)
jstart=max(jstart,0)
jend=min(jend,lon.shape[-2]-1)
self._subdomain=( int(jstart),int(jend),int(istart),int(iend) )
# The following are some property getters, in case of later abstractions.
@property
def norm(self):
return str(self._norm).split('.')[-1:]
@property
def cmap(self):
return self._cmap.name
@property
def minmaxglobal(self):
return self._minmaxglobal
@property
def maskedValues(self):
return self._maskedValues
@property
def maskedAbove(self):
return self._maskedAbove
@property
def maskedBelow(self):
return self._maskedBelow
@property
def static(self):
return self._static
def _getDXDY(self):
'''
Return the grid spacing in meters as (dx,dy).
'''
raise Exception("Unimplemented super class method.")
@staticmethod
def _findPointIndex(px,py,lon,lat):
'''
Return the index of lon/lat of the closest point to (px,py).
'''
d=( (px-lon)*(px-lon) + (py-lat)*(py-lat) )
imin=d.flatten().argmin()
return np.unravel_index(imin,d.shape)
def _getVarNDim(self,var):
ndim=var.ndim
if self._file.dimensions[var.dimensions[0]].isunlimited():
ndim=ndim-1
return ndim
@staticmethod
def setToDefaultifNone(value,default):
'''
Return value or default if value is None.
'''
if value is None:
target=default
else:
target=value
return target
def _getProj(self):
raise Exception("Unimplmented base class method")
def _getGCPs(self,istep=None,idx=None):
if not have_reproject:
return None
lon,lat=self.readCoordinates(istep,idx,orig=True)
ny,nx=lon.shape
ny=ny-1
nx=nx-1
gcp=[createGCP(0,0,lon[0,0],lat[0,0]),
createGCP(0,ny,lon[ny,0],lat[ny,0]),
createGCP(nx,0,lon[0,nx],lat[0,nx]),
createGCP(nx,ny,lon[ny,nx],lat[ny,nx])]
return gcp
def reprojectArray(self,a,istep=None,idx=None):
if not have_reproject:
return None
gcp=self._getGCPs(istep,idx)
proj=self._getProj()
b=a[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
vrtFromArray('tmp.vrt',np.flipud(b))
georeferenceImage('tmp.vrt',proj,gcp,'tmp1.vrt')
warpImage('tmp1.vrt','tmp.nc')
b,bds=readNC('tmp.nc')
#a[idx[0]:idx[1]+1,idx[2]:idx[3]+1]=b
self._gref[istep]=bds
return b
def _readArrayAll(self):
b=self._readArray(istep=0)
a=np.zeros((self._nstep,)+b.shape)
a[0,...]=b
for i in xrange(1,self._nstep):
a[i,...]=self._readArray(istep=i)
return a
def _readArray(self,istep=None,skipaccumsum=False):
#from traceback import print_stack
#print '*'*40
#print istep
#print_stack()
if istep is None and self._accum:
raise Exception("Don't know how to read non-time step accumulation variables.")
a=self._readVar(self._var,istep,derived=self._derivedVar,ilev=self._slice3D)
if self._accum and istep > 0:
#print 'reading %i' % istep
a=a-self._readVar(self._var,istep-1,derived=self._derivedVar,ilev=self._slice3D)
if self._accumsumhours and not skipaccumsum:
iend=istep
tend=self.getStepTime(istep)
istart=istep
tstart=tend
i=istep-1
while i >= 0 and (tend-tstart).total_seconds()/3600. < self._accumsumhours:
tstart=self.getStepTime(i)
istart=i
i=i-1
#print 'reading steps %i to %i' % (istart,iend-1)
for i in xrange(istart,iend):
b=self._readArray(istep=i,skipaccumsum=True)
b[b != b] = 0.
a=a+b
a=self.applyMask(a)
return a
def _privRead(self,var,istep,ilev):
if ilev is None:
if istep is None:
return var[:]
else:
return var[istep,...].squeeze()
else:
if istep is None:
return var[ilev,...].squeeze()
else:
return var[istep,ilev,...].squeeze()
def _readVarRaw(self,varname,istep=None,ilev=None):
'''
Get data from file without any post-processing. This function is present
to allow subclasses to perform it's own post-processing or use derive
variables.
'''
return self._privRead(self._file.variables[varname],istep,ilev)
def _readVar(self,var,istep=None,derived=False,ilev=None):
'''
Read data from the netCDF variable. If istep is not None, then
read a single time step.
'''
# check cache
if self._ioCache.has_key(var):
cvar=self._ioCache[var]
if cvar.has_key(istep):
return cvar[istep]
# print 'File IO %s %s' % (str(var),str(istep))
# read data
if isinstance(var,basestring) or derived:
a=self._readVarRaw(var,istep,ilev)
else:
ndim=self._getVarNDim(var)
if ndim == 3 and ilev is None:
ilev=self._slice3D
a=self._privRead(var,istep,ilev)
a=self.applyMask(a)
# raise an exception if all elements of the array are masked
if a.mask.all() or (a.min() == a.max()):
raise MaskedArrayException
a=a.filled()
if use_io_caching:
if not self._ioCache.has_key(var):
self._ioCache[var]={}
self._ioCache[var][istep]=a
return a
def clearCache(self):
self._ioCache={}
def applyMask(self,a):
# convert to a masked array
a=np.ma.MaskedArray(a,copy=False)
# mask according to instance properties
if self._maskedValues is not None:
for m in self.maskedValues:
a=np.ma.masked_equal(a,m,copy=False)
if self._maskedAbove is not None:
a=np.ma.masked_greater(a,self.maskedAbove,copy=False)
if self._maskedBelow is not None:
a=np.ma.masked_less(a,self.maskedBelow,copy=False)
# fill masked values with NaN's to display as transparent in matplotlib
a.fill_value=np.nan
return a
def readCoordinates(self,istep=0,idx=None,orig=True):
'''
Read coordinate arrays (as lon/lat) for this variable. The class assumes
that the coordinate arrays have the same shape as the variable array.
istep: The time step to read from the coordinate arrays.
idx: If we are displaying as subarray from the variable, this
contains the index bounds that we are displaying as returned
from _getRestriction.
'''
# get coordinate array names
c=self.getCoordinates()
if c is None:
raise Exception("Could not find coordinate array for %s" % self.getName())
# read coordinate arrays from the file
lon=self._file.variables[c[0]][0,:,:]
lat=self._file.variables[c[1]][0,:,:]
ix,iy=getReprojectionGeoref(self._getTag(),lon,lat)
if ix is not None and iy is not None and not orig:
if idx is None:
lon=ix
lat=iy
else:
lon=ix[idx[2]:idx[3]+1]
lat=iy[idx[0]:idx[1]+1]
else:
# read coordinate arrays from the file
lon=self._file.variables[c[0]]
lat=self._file.variables[c[1]]
# if no index bounds given, then return the full arrays
if idx is None:
idx=(0,lon.shape[-2]-1,0,lon.shape[-1]-1)
# restrict coordinate arrays from index bounds
if istep is None:
lon=lon[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
lat=lat[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
else:
lon=lon[istep,idx[0]:idx[1]+1,idx[2]:idx[3]+1].squeeze()
lat=lat[istep,idx[0]:idx[1]+1,idx[2]:idx[3]+1].squeeze()
return (lon,lat)
def _getRestriction(self,a):
'''
Return indices of the smallest subarray covering the non-masked values of a.
Assumes a is 2D.
'''
assert a.ndim == 2
if self._subdomain is not None:
return self._subdomain
#if (a != a).all():
# raise MaskedArrayException
# get non-masked indices of a
idx=(a == a).nonzero()
if len(idx) == 1 or len(idx[0]) == 0:
# if no indices are masked return full array indices
return (0,a.shape[0]-1,0,a.shape[1]-1)
else:
z=idx
# get the smallest and largest nonmasked index in each coordinate
imin=z[0].min()
imax=z[0].max()
jmin=z[1].min()
jmax=z[1].max()
# make sure the subarray is at least 2x2, otherwise
# things start to break
if imax == imin:
if imax < a.shape[0]-1:
imax=imax+1
else:
imin=imin-1
if jmax == jmin:
if jmax < a.shape[1]-1:
jmax=jmax+1
else:
jmin=jmin-1
z=(imin,imax,jmin,jmax)
return z
def getMinMax(self,istep=None):
'''
Returns the minimum and maximum of the variable at istep.
'''
if self._minmax is not None:
minmax=self._minmax
elif self._minmaxglobal:
if self._minmax is None:
a=self._readArrayAll()
self._minmax=self.arrayMinMax(a)
minmax=self._minmax
else:
a=self._readArray(istep)
self._minmax=self.arrayMinMax(a)
minmax=self._minmax
#print minmax
return minmax
def arrayMinMax(self,a):
if self._subdomain is not None:
idx=self._subdomain
if self._minmaxglobal:
a=a[:,idx[0]:idx[1]+1,idx[2]:idx[3]+1]
else:
a=a[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
try:
min,max=(a[a==a].min(),a[a==a].max())
except ValueError:
raise MaskedArrayException
return min,max
def getUnits(self):
'''
Returns the units of the variable or an empty string if the variable
has no "units" attribute.
'''
return self._var.__dict__.get('units','')
def getName(self):
'''
Returns the name of the variable in the netCDF file.
'''
if self._name is None:
return self._var._name
else:
return self._name
def getDescription(self):
'''
Returns a description of the variable from the attribute "description"
or None if no description is present.
'''
return self._var.__dict__.get('description',None)
def getCoordinates(self):
'''
Returns a tuple containing the coordinate array names for the variable.
Following WRF output standards, this is in the attribute coordinates
containing a 2 element tuple (longitude,latitude).
'''
return self._var.__dict__.get('coordinates',None).split(' ')
def georeference(self,istep=None):
'''
Get a georeference for the variable at istep. The georeference returned
is a dictionary containing the west, east, south, and north bounds of the
variable and assumes unprojected data. This method should return the
coordinates of the subarray computed from the variable mask rather than
the full variable bounds.
'''
if self._gref.has_key(istep):
return self._gref[istep]
a=self._readArray(istep)
if not self._norestriction or self._subdomain is not None:
idx=self._getRestriction(a)
else:
idx=None
lon,lat=self.readCoordinates(istep=istep,idx=idx,orig=False)
return {'west':lon.min(),'east':lon.max(),'south':lat.min(),'north':lat.max()}
def getTimeDim(self):
'''
Returns the name of the unlimited (or time) dimension of the netcdf file or
None if no unlimited dimension is used.
'''
for d in self._file.dimensions:
if self._file.dimensions[d].isunlimited():
return d
return None
def timereference(self,istep=None):
'''
Returns the time span for the variable at istep. By default, this is
time span begins at the time of the current time step to the time of the
next time step. The time span of the last time step is computed from
delta time from the last two time steps in the file.
Returns a dictionary with keywords 'start' and 'end' and values
datetime objects.
'''
# if no istep given return the full file time span
if istep is None:
sstep=0
estep=self._nstep
else:
sstep=istep
estep=istep+1
# get the time at the start
start=self.getStepTime(sstep)
# if the ending time step is valid get it,
# otherwise estimate the ending from the delta
# time of the last two steps
if estep >= self._nstep and self._nstep > 1:
end=self.getStepTime(self._nstep-1)
dt=end-self.getStepTime(self._nstep-2)
end=end+dt
elif self._nstep == 1:
end=start+timedelta(hours=1)
else:
end=self.getStepTime(estep)
return {'start':start,'end':end}
def getStepTime(self,istep):
'''
Must be implemented by a subclasss. Returns a datetime object
representing the time at istep in the output file.
'''
raise Exception('Unimplemented base class method')
def _getTag(self):
'''
Return a string describing the grid overwhich the variable is defined.
'''
return 'default'
def getRasterFromArray(self,a,istep=None,hsize=3):
'''
Returns a string containing a png psuedocolor image of the array a.
Keyword arguments:
hsize: integer, width of the image in inches
'''
lon,lat=self.readCoordinates(istep,orig=True)
a=reprojectArray(self._getTag(),lon,lat,a)
a=self.applyMask(a)
if (a!=a).all():
raise MaskedArrayException
# get subarray restriction from mask
if self._norestriction and self._subdomain is None:
idx=[0,a.shape[0]-1,0,a.shape[1]-1]
else:
idx=self._getRestriction(a)
a=a[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
lon=lon[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
lat=lat[idx[0]:idx[1]+1,idx[2]:idx[3]+1]
# generate a matplotlib figure object
fig=pylab.figure(figsize=(hsize,hsize*float(a.shape[0])/a.shape[1]))
ax=fig.add_axes([0,0,1,1])
minmax=self.getMinMax(istep)
if minmax is None:
minmax=(a[a==a].min(),a[a==a].max())
# get a color norm instance from the min/max of a
if self._norm is not None:
norm=self._norm
else:
norm=self.defaultNorm(minmax[0],minmax[1])
# add image to the axis
ax.imshow(np.flipud(a),cmap=self._cmap,norm=norm,interpolation=self._interp)
# turn the axis off to get a bare raster
ax.axis('off')
# a file like string object to save the png to
im=StringIO()
# save and close the figure
fig.savefig(im,dpi=self._dpi,format='png',transparent=True)
pylab.close(fig)
return im.getvalue()
def getRaster(self,istep=None,**kwargs):
'''
Get a png image for the variable at istep. See getRasterFromArray.
'''
a=self._readArray(istep)
return self.getRasterFromArray(a,istep,**kwargs)
def getColorbarFromMinMax(self,min,max,hsize=2,dpi=200):
'''
Returns a colorbar as a string with given min/max values.
Keyword arguments:
hsize: integer, horizontal size of the image in inches
dpi: integer, dots per inch of the image
'''
# get the color norm from the min/max
if self._norm is not None:
norm=self._norm
else:
norm=self.defaultNorm(min,max)
# construct keyword arguments for ColorbarBase constructor
kwargs={'norm':norm,'spacing':'proportional','orientation':'vertical',
'cmap':self._cmap}
kwargs.update(self._colorbarargs)
if self._formatter is not None:
kwargs['format']=self._formatter
# create a matplotlib figure
fig=pylab.figure(figsize=(hsize,hsize*5./2.))
ax=fig.add_axes([.25,.03,.1,.9])
# create Colorbar instance
cb=matplotlib.colorbar.ColorbarBase(ax,**kwargs)
# set the label as the units of the variable
cb.set_label(self.getUnits(),color='1')
# set the title as the name of the variable
ax.set_title(self.getName(),color='1')
for tl in ax.get_yticklabels():
tl.set_color('1')
# save png to a file-like string
im=StringIO()
fig.savefig(im,dpi=dpi,format='png',transparent=True)
return im.getvalue()
def getColorbar(self,istep=None,**kwargs):
'''
Returns a colorbar for the variable at istep.
See getColorbarFromMinMax
'''
min,max=self.getMinMax(istep)
return self.getColorbarFromMinMax(min,max,**kwargs)
def perimeterFromContour(self,contour=0,istep=None):
'''
Returns a contour of the variable as a list of vertices
of a polygon. Generates the contour from a matplotlib
contour plot.
'''
# read the array
a=self._readArray(istep)
assert len(a.shape) == 2
# get coordinates of the array
lon,lat=self.readCoordinates(istep,orig=True)
# get vertices of the polygon
c=pylab.contour(lon,lat,a,[contour]).collections[0]
p=c.get_paths()
poly=[]
for i in p:
tmp=[ (x,y) for x,y in i.to_polygons()[0] ]
poly.append(tmp)
return poly
class WRFNetcdf2Raster(BaseNetCDF2Raster):
'''
This is an implementation of BaseNetCDF2Raster for WRF output files.
See BaseNetCDF2Raster docstrings for details.
'''
def __init__(self,*args,**kwargs):
super(WRFNetcdf2Raster,self).__init__(*args,**kwargs)
def getStepTime(self,istep):
'''
Returns a datetime object representing the time for time step "istep" from
the output file.
'''
time=parser.parse(self._file.variables['Times'][istep,:].tostring().replace('_',' '))
return time
def _getProj(self):
if not have_reproject:
return None
f=self._file
proj=getEPSGProjectionDef(f.MAP_PROJ,f.CEN_LAT,f.CEN_LON,