4 A simple python module for creating images out of netcdf arrays and outputing
5 kml files for Google Earth. The base class ncEarth cannot be used on its own,
6 it must be subclassed with certain functions overloaded to provide location and
7 plotting that are specific to a model's output files.
9 Requires matplotlib and netCDF4 python modules.
14 kml=ncEarth.ncEpiSim('episim_0010.nc')
15 kml.write_kml(['Susceptible','Infected','Recovered','Dead'])
19 kmz=ncEarth.ncWRFFire_mov('wrfout')
20 kmz.write('FGRNHFX','fire.kmz')
22 Author: Jonathan Beezley (jon.beezley@gmail.com)
25 kmz=ncEarth.ncWRFFire_mov('wrfout')
26 kmz.write_preload('FGRNHFX')
38 from matplotlib
import pylab
39 from matplotlib
.colorbar
import ColorbarBase
40 from matplotlib
.colors
import LogNorm
,Normalize
41 from matplotlib
.ticker
import LogFormatter
44 from netCDF4
import Dataset
46 from Scientific
.IO
.NetCDF
import NetCDFFile
as Dataset
48 from datetime
import datetime
55 ncpu
=max(1,os
.sysconf('SC_NPROCESSORS_ONLN'))
59 warnings
.simplefilter('ignore')
67 lock
=threading
.RLock()
68 dlock
=threading
.RLock()
69 queue
=threading
.Semaphore(ncpu
)
72 class ZeroArray(Exception):
75 class ncEarth(object):
77 '''Base class for reading NetCDF files and writing kml for Google Earth.'''
79 kmlname
='ncEarth.kml' # default name for kml output file
80 progname
='baseClass' # string describing the model (overload in subclass)
82 # base kml file format string
83 # creates a folder containing all images
85 '''<?xml version="1.0" encoding="UTF-8"?>
86 <kml xmlns="http://www.opengis.net/kml/2.2">
88 <name>%(prog)s visualization</name>
89 <description>Variables from %(prog)s output files visualized in Google Earth</description>
94 # string for static Ground Overlays
98 <color>00ffffff</color>
100 <href>%(filename)s</href>
101 <viewBoundScale>0.75</viewBoundScale>
103 <altitude>0.0</altitude>
104 <altitudeMode>clampToGround</altitudeMode>
106 <north>%(lat2)f</north>
107 <south>%(lat1)f</south>
108 <east>%(lon2)f</east>
109 <west>%(lon1)f</west>
110 <rotation>0.0</rotation>
114 # format string for each image
117 <name>%(name)s</name>
118 <color>%(alpha)02xffffff</color>
120 <href>%(filename)s</href>
121 <viewBoundScale>0.75</viewBoundScale>
123 <altitude>0.0</altitude>
124 <altitudeMode>clampToGround</altitudeMode>
126 <north>%(lat2)f</north>
127 <south>%(lat1)f</south>
128 <east>%(lon2)f</east>
129 <west>%(lon1)f</west>
130 <rotation>0.0</rotation>
138 <name>%(name)s colorbar</name>
139 <color>ffffffff</color>
141 <href>%(file)s</href>
143 <overlayXY x=".15" y=".5" xunits="fraction" yunits="fraction"/>
144 <screenXY x="0" y=".5" xunits="fraction" yunits="fraction"/>
145 <rotationXY x="0" y="0" xunits="fraction" yunits="fraction"/>
146 <size x="0" y=".75" xunits="fraction" yunits="fraction"/>
150 # time interval specification for animated output
157 beginstr
='<begin>%s</begin>'
158 endstr
='<end>%s</end>'
160 def __init__(self
,filename
,hsize
=5):
161 '''Class constructor:
162 filename : string NetCDF file to read
163 hsize : optional, width of output images in inches'''
167 if not ncfile
.has_key(filename
):
168 ncfile
[filename
]=Dataset(filename
,'r')
169 self
.f
=ncfile
[filename
]
172 def get_minmax(self
,vname
):
175 if minmax
.has_key(vname
):
178 mm
=self
.compute_minmax(vname
)
182 def compute_minmax(self
,vname
):
183 v
=self
.f
.variables
[vname
][:]
184 return (v
.min(),v
.max())
186 def get_bounds(self
):
187 '''Return the latitude and longitude bounds of the image. Must be provided
189 raise Exception("Non-implemented base class method.")
191 def get_array(self
,vname
):
192 '''Return a given array from the output file. Must be returned as a
193 2D array with top to bottom orientation (like an image).'''
194 v
=self
.f
.variables
[vname
]
198 def view_function(self
,v
):
199 '''Any function applied to the image data before plotting. For example,
200 to show the color on a log scale.'''
203 def get_image(self
,v
,min,max):
204 '''Create an image from a given data. Returns a png image as a string.'''
206 # kludge to get the image to have no border
207 fig
=pylab
.figure(figsize
=(self
.hsize
,self
.hsize
*float(v
.shape
[0])/v
.shape
[1]))
208 ax
=fig
.add_axes([0,0,1,1])
212 norm
=self
.get_norm(min,max)
213 ax
.imshow(self
.view_function(v
),cmap
=cmap
,norm
=norm
)
217 # create a string buffer to save the file
218 im
=cStringIO
.StringIO()
219 fig
.savefig(im
,format
='png',transparent
=True)
227 def get_colorbar(self
,title
,label
,min,max):
228 '''Create a colorbar from given data. Returns a png image as a string.'''
230 fig
=pylab
.figure(figsize
=(2,5))
231 ax
=fig
.add_axes([0.35,0.03,0.1,0.9])
232 norm
=self
.get_norm(min,max)
233 formatter
=self
.get_formatter()
235 cb1
= ColorbarBase(ax
,norm
=norm
,format
=formatter
,spacing
='proportional',orientation
='vertical')
237 cb1
= ColorbarBase(ax
,norm
=norm
,spacing
='proportional',orientation
='vertical')
238 cb1
.set_label(label
,color
='1')
239 ax
.set_title(title
,color
='1')
240 for tl
in ax
.get_yticklabels():
242 im
=cStringIO
.StringIO()
243 fig
.savefig(im
,dpi
=300,format
='png',transparent
=True)
249 def get_norm(self
,min,max):
250 norm
=Normalize(min,max)
253 def get_formatter(self
):
256 def process_image(self
):
257 '''Do anything to the current figure window before saving it as an image.'''
260 def get_kml_dict(self
,name
,filename
,alpha
=143):
261 '''returns a dictionary of relevant info the create the image
262 portion of the kml file'''
264 lon1
,lon2
,lat1
,lat2
=self
.get_bounds()
265 d
={'lat1':lat1
,'lat2':lat2
,'lon1':lon1
,'lon2':lon2
, \
266 'name':name
,'filename':filename
,'time':self
.get_time(),'alpha':alpha
}
270 '''Return the time interval information for this image using the kml
271 format string `timestr'. Or an empty string to disable animations.'''
274 def image2kmlStatic(self
,varname
,filename
=None):
275 '''Read data from the NetCDF file, create a psuedo-color image as a png,
276 then create a kml string for displaying the image in Google Earth. Returns
277 the kml string describing the GroundOverlay. Optionally, the filename
278 used to write the image can be specified, otherwise a default will be used.'''
280 vdata
=self
.get_array(varname
)
281 min,max=self
.get_minmax(varname
)
282 im
=self
.get_image(vdata
,min,max)
284 filename
='%s.png' % varname
288 d
=self
.get_kml_dict(varname
,filename
)
290 return self
.__class
__.kmlimageStatic
% d
292 def image2kml(self
,varname
,filename
=None):
293 '''Read data from the NetCDF file, create a psuedo-color image as a png,
294 then create a kml string for displaying the image in Google Earth. Returns
295 the kml string describing the GroundOverlay. Optionally, the filename
296 used to write the image can be specified, otherwise a default will be used.'''
298 vdata
=self
.get_array(varname
)
299 min,max=self
.get_minmax(varname
)
300 im
=self
.get_image(vdata
,min,max)
302 filename
='%s.png' % varname
306 d
=self
.get_kml_dict(varname
,filename
)
307 return self
.__class
__.kmlimage
% d
309 def colorbar2kml(self
,varname
,filename
=None):
310 min,max=self
.get_minmax(varname
)
311 label
=self
.get_label(varname
)
312 cdata
=self
.get_colorbar(varname
,label
,min,max)
314 filename
='colorbar_%s.png' % varname
319 return self
.__class
__.kmlcolorbar
% {'name':varname
,'file':filename
}
321 def get_label(self
,varname
):
324 def write_kml(self
,varnames
):
325 '''Create the actual kml file for a list of variables by calling image2kml
326 for each variable in a list of variable names.'''
327 if type(varnames
) is str:
330 for varname
in varnames
:
331 label
=self
.get_label(varname
)
332 content
.append(self
.image2kml(varname
))
333 content
.append(self
.colorbar2kml(varname
))
334 kml
=self
.__class
__.kmlstr
% \
335 {'content':'\n'.join(content
),\
336 'prog':self
.__class
__.progname
}
337 f
=open(self
.__class
__.kmlname
,'w')
341 class ncEarth_log(ncEarth
):
342 def view_function(self
,v
):
345 v
=np
.ma
.masked_equal(v
,0.,copy
=False)
350 def get_norm(self
,min,max):
351 return LogNorm(min,max)
353 def get_formatter(self
):
354 return LogFormatter(10,labelOnlyBase
=False)
356 def compute_minmax(self
,vname
):
357 v
=self
.f
.variables
[vname
][:]
366 class ncEpiSimBase(object):
367 '''Epidemic model file class.'''
369 kmlname
='epidemic.kml'
372 def get_bounds(self
):
373 '''Get the lat/lon bounds of the output file... assumes regular lat/lon (no projection)'''
374 lat
=self
.f
.variables
['latitude']
375 lon
=self
.f
.variables
['longitude']
382 return (lon1
,lon2
,lat1
,lat2
)
384 class ncEpiSim(ncEpiSimBase
,ncEarth_log
):
387 class ncWRFFireBase(object):
388 '''WRF-Fire model file class.'''
392 wrftimestr
='%Y-%m-%d_%H:%M:%S'
394 def __init__(self
,filename
,hsize
=5,istep
=0):
395 '''Overloaded constructor for WRF output files:
396 filename : output NetCDF file
397 hsize : output image width in inches
398 istep : time slice to output (between 0 and the number of timeslices in the file - 1)'''
399 ncEarth
.__init
__(self
,filename
,hsize
)
402 def get_bounds(self
):
403 '''Get the latitude and longitude bounds for an output domain. In general,
404 we need to reproject the data to a regular lat/lon grid. This can be done
405 with matplotlib's BaseMap module, but is not done here.'''
407 lat
=self
.f
.variables
['XLAT'][0,:,:].squeeze()
408 lon
=self
.f
.variables
['XLONG'][0,:,:].squeeze()
411 #lat1=np.min(lat)-dy/2.
412 #lat2=np.max(lat)+dy/2
413 #lon1=np.min(lon)-dx/2.
414 #lon2=np.max(lon)+dx/2
419 return (lon1
,lon2
,lat1
,lat2
)
421 def isfiregrid(self
,vname
):
422 xdim
=self
.f
.variables
[vname
].dimensions
[-1]
423 return xdim
[-7:] == 'subgrid'
427 s
=len(self
.f
.dimensions
['west_east_subgrid'])/(len(self
.f
.dimensions
['west_east'])+1)
429 s
=(self
.f
.dimensions
['west_east_subgrid'])/((self
.f
.dimensions
['west_east'])+1)
434 s
=len(self
.f
.dimensions
['south_north_subgrid'])/(len(self
.f
.dimensions
['south_north'])+1)
436 s
=(self
.f
.dimensions
['south_north_subgrid'])/((self
.f
.dimensions
['south_north'])+1)
439 def get_array(self
,vname
):
440 '''Return a single time slice of a variable from a WRF output file.'''
441 v
=self
.f
.variables
[vname
]
442 v
=v
[self
.istep
,:,:].squeeze()
443 if self
.isfiregrid(vname
):
444 v
=v
[:-self
.sry(),:-self
.srx()]
446 if vname
== 'FGRNHFX' or vname
== 'GRNHFX':
451 t1
=self
.f
.variables
["Times"][0,:].tostring()
452 t2
=self
.f
.variables
["Times"][-1,:].tostring()
453 return '%s - %s' % (t1
,t2
)
456 '''Process the time information from the WRF output file to create a
457 proper kml TimeInterval specification.'''
463 times
=g
.variables
["Times"]
466 start
=ncEarth
.beginstr
% \
467 datetime
.strptime(times
[self
.istep
,:].tostring(),\
468 self
.__class
__.wrftimestr
).isoformat()
469 if self
.istep
< times
.shape
[0]-1:
471 end
=ncEarth
.endstr
% \
472 datetime
.strptime(times
[self
.istep
+1,:].tostring(),\
473 self
.__class
__.wrftimestr
).isoformat()
474 if start
is not '' or end
is not '':
475 time
=ncEarth
.timestr
% {'begin':start
,'end':end
}
478 def get_label(self
,varname
):
479 v
=self
.f
.variables
[varname
]
482 class ncWRFFire(ncWRFFireBase
,ncEarth
):
485 class ncWRFFireLog(ncWRFFireBase
,ncEarth_log
):
488 def create_image(fname
,istep
,nstep
,vname
,vstr
,logscale
,colorbar
,imgs
,content
):
494 kml
=ncWRFFireLog(fname
,istep
=istep
)
496 kml
=ncWRFFire(fname
,istep
=istep
)
498 img
='files/colorbar_%s.png' % vname
499 img_string
=kml
.colorbar2kml(vname
,img
)
501 content
.append(img_string
)
504 img
=vstr
% (vname
,istep
)
505 img_string
=kml
.image2kml(vname
,img
)
507 content
.append(img_string
)
509 print 'creating frame %i of %i' % (i
,nstep
)
512 print 'skipping frame %i of %i' % (i
,nstep
)
515 class ncWRFFire_mov(object):
517 '''A class the uses ncWRFFire to create animations from WRF history output file.'''
519 def __init__(self
,filename
,hsize
=5,nstep
=None):
520 '''Class constructor:
521 filename : NetCDF output file name
522 hsize : output image width in inces
523 nstep : the number of frames to process (default all frames in the file)'''
525 self
.filename
=filename
526 f
=Dataset(filename
,'r')
530 # in case nstep was not specified read the total number of time slices from the file
531 self
.nstep
=g
.variables
['Times'].shape
[0]
533 def write_preload(self
,vname
,kmz
='fire_preload.kmz'):
534 '''Create a kmz file from multiple time steps of a wrfout file. The kml file consists of a set of
535 GroundOverlays with time tag and a copy of the set without the time tag to preload the
536 images that are used in the GroundOverlays.'''
539 imgs
=[] # to store a list of all images created
540 content
=[] # the content of the main kml
541 vstr
='files/%s_%05i.png' # format specification for images (all stored in `files/' subdirectory)
543 # create empty files subdirectory for output images
545 shutil
.rmtree('files')
550 # loop through all time slices and create the image data
551 # appending to the kml content string for each image
552 for i
in xrange(0,self
.nstep
,1):
554 kml
=ncWRFFire(self
.filename
,istep
=i
)
557 content
.append(kml
.image2kmlStatic(vname
,img
))
560 # create the main kml file
561 kml
=ncWRFFire
.kmlstr
% \
562 {'content':'\n'.join(content
),\
563 'prog':ncWRFFire
.progname
}
565 # create a zipfile to store all images + kml into a single compressed file
566 z
=zipfile
.ZipFile(kmz
,'w',compression
=zipfile
.ZIP_DEFLATED
)
567 z
.writestr(kmz
[:-3]+'kml',kml
)
572 def write(self
,vname
,kmz
='fire.kmz',hsize
=5,logscale
=True,colorbar
=True):
573 '''Create a kmz file from multiple time steps of a wrfout file.
574 vname : the variable name to visualize
575 kmz : optional, the name of the file to save the kmz to'''
577 imgs
=[] # to store a list of all images created
578 content
=[] # the content of the main kml
580 vstr
='files/%s_%05i.png' # format specification for images (all stored in `files/' subdirectory)
582 # create empty files subdirectory for output images
584 shutil
.rmtree('files')
589 # loop through all time slices and create the image data
590 # appending to the kml content string for each image
592 for i
in xrange(0,self
.nstep
,1):
593 t
=threading
.Thread(target
=create_image
,args
=(self
.filename
,i
,self
.nstep
,vname
,vstr
,logscale
,colorbar
and i
== 0,imgs
,content
))
599 # create the main kml file
600 kml
=ncWRFFire
.kmlstr
% \
601 {'content':'\n'.join(content
),\
602 'prog':ncWRFFire
.progname
}
604 # create a zipfile to store all images + kml into a single compressed file
605 z
=zipfile
.ZipFile(kmz
,'w',compression
=zipfile
.ZIP_DEFLATED
)
606 z
.writestr(kmz
[:-3]+'kml',kml
)
613 if vname
in ('FGRNHFX','GRNHFX'):
618 if __name__
== '__main__':
620 if len(sys
.argv
) < 2:
621 print "Takes a WRF-Fire output file and writes fire.kmz."
622 print "usage: %s filename"%sys
.argv
[0]
625 if len(sys
.argv
) == 2:
629 kmz
=ncWRFFire_mov(filename
)
631 kmz
.write(v
,hsize
=8,kmz
='fire_'+v
+'.kmz',logscale
=uselog(v
))