1
|
#!/usr/bin/python
|
2
|
#
|
3
|
# Early version of assorted Python code to download MODIS 11A1 (daily)
|
4
|
# data associated with specified dates and tiles, then interface with
|
5
|
# GRASS to calculate temporally averaged LST in a way that accounts for
|
6
|
# QC flags.
|
7
|
#
|
8
|
# TODO:
|
9
|
# - functionalize to encapsulate high level procedural steps
|
10
|
# - create a 'main' function to allow script to be run as a command
|
11
|
# that can accept arguments?
|
12
|
# - put downloaded data somewhere other than working directory?
|
13
|
# - write code to set up and take down a temporary GRASS location/mapset
|
14
|
# - write code to export climatology rasters to file (geotiff? compressed?)
|
15
|
# - calculate other aggregate stats? (stddev, ...)
|
16
|
# - deal with nightly LST too?
|
17
|
# - deal with more than just first two bits of QC flags?
|
18
|
# e.g.: r.mapcalc 'qc_level2 = (qc>>2) & 0x03'
|
19
|
# - record all downloads to a log file?
|
20
|
#
|
21
|
# Jim Regetz
|
22
|
# NCEAS
|
23
|
# Created on 16-May-2012
|
24
|
|
25
|
import os, glob
|
26
|
import datetime
|
27
|
import ftplib
|
28
|
import grass.script as gs
|
29
|
|
30
|
#------------------
|
31
|
# helper functions
|
32
|
#------------------
|
33
|
|
34
|
# quick function to reformat e.g. 200065 to 2000.03.05
|
35
|
def yj_to_ymd(year, doy):
|
36
|
date = datetime.datetime.strptime('%d%d' % (year, doy), '%Y%j')
|
37
|
return date.strftime('%Y.%m.%d')
|
38
|
|
39
|
# quick function to return list of dirs in wd
|
40
|
def list_contents(ftp):
|
41
|
listing = []
|
42
|
ftp.dir(listing.append)
|
43
|
contents = [item.split()[-1] for item in listing[1:]]
|
44
|
return contents
|
45
|
|
46
|
def download_mod11a1(destdir, tile, start_doy, end_doy, year, ver=5):
|
47
|
"""Download into destination directory the MODIS 11A1 HDF files
|
48
|
matching a given tile, year, and day range. If for some unexpected
|
49
|
reason there are multiple matches for a given day, only the first is
|
50
|
used. If no matches, the day is skipped with a warning message.
|
51
|
|
52
|
Arguments:
|
53
|
destdir -- path to local destination directory
|
54
|
tile -- tile identifier (e.g., 'h08v04')
|
55
|
start_doy -- integer start of day range (0-366)
|
56
|
end_doy -- integer end of day range (0-366)
|
57
|
year -- integer year (>=2000)
|
58
|
ver -- MODIS version (4 or 5)
|
59
|
|
60
|
Returns list of absolute paths to the downloaded HDF files.
|
61
|
"""
|
62
|
# connect to data pool and change to the right directory
|
63
|
ftp = ftplib.FTP('e4ftl01.cr.usgs.gov')
|
64
|
ftp.login('anonymous', '')
|
65
|
ftp.cwd('MOLT/MOD11A1.%03d' % ver)
|
66
|
# make list of daily directories to traverse
|
67
|
available_days = list_contents(ftp)
|
68
|
desired_days = [yj_to_ymd(year, x) for x in range(start_doy, end_doy+1)]
|
69
|
days_to_get = filter(lambda day: day in desired_days, available_days)
|
70
|
if len(days_to_get) < len(desired_days):
|
71
|
missing_days = [day for day in desired_days if day not in days_to_get]
|
72
|
print 'skipping %d day(s) not found on server:' % len(missing_days)
|
73
|
print '\n'.join(missing_days)
|
74
|
# get each tile in turn
|
75
|
hdfs = list()
|
76
|
for day in days_to_get:
|
77
|
ftp.cwd(day)
|
78
|
files_to_get = [file for file in list_contents(ftp)
|
79
|
if tile in file and file[-3:]=='hdf']
|
80
|
if len(files_to_get)==0:
|
81
|
# no file found -- print message and move on
|
82
|
print 'no hdf found on server for tile', tile, 'on', day
|
83
|
continue
|
84
|
elif 1<len(files_to_get):
|
85
|
# multiple files found! -- just use the first...
|
86
|
print 'multiple hdfs found on server for tile', tile, 'on', day
|
87
|
file = files_to_get[0]
|
88
|
local_file = os.path.join(destdir, file)
|
89
|
ftp.retrbinary('RETR %s' % file, open(local_file, 'wb').write)
|
90
|
hdfs.append(os.path.abspath(local_file))
|
91
|
ftp.cwd('..')
|
92
|
# politely disconnect
|
93
|
ftp.quit()
|
94
|
# return list of downloaded paths
|
95
|
return hdfs
|
96
|
|
97
|
def get_hdf_paths(hdfdir, tile, start_doy, end_doy, year):
|
98
|
"""Look in supplied directory to find the MODIS 11A1 HDF files
|
99
|
matching a given tile, year, and day range. If for some unexpected
|
100
|
reason there are multiple matches for a given day, only the first is
|
101
|
used. If no matches, the day is skipped with a warning message.
|
102
|
|
103
|
Arguments:
|
104
|
hdfdir -- path to directory containing the desired HDFs
|
105
|
tile -- tile identifier (e.g., 'h08v04')
|
106
|
start_doy -- integer start of day range (0-366)
|
107
|
end_doy -- integer end of day range (0-366)
|
108
|
year -- integer year (>=2000)
|
109
|
|
110
|
Returns list of absolute paths to the located HDF files.
|
111
|
"""
|
112
|
hdfs = list()
|
113
|
for doy in range(start_doy, end_doy+1):
|
114
|
fileglob = 'MOD11A1.A%d%03d.%s*hdf' % (year, doy, tile)
|
115
|
pathglob = os.path.join(hdfdir, fileglob)
|
116
|
files = glob.glob(pathglob)
|
117
|
if len(files)==0:
|
118
|
# no file found -- print message and move on
|
119
|
print 'cannot access %s: no such file' % pathglob
|
120
|
continue
|
121
|
elif 1<len(files):
|
122
|
# multiple files found! -- just use the first...
|
123
|
print 'multiple hdfs found for tile', tile, 'on', day
|
124
|
hdfs.append(os.path.abspath(files[0]))
|
125
|
return hdfs
|
126
|
|
127
|
# create raster of pixelwise means for a list of input rasters
|
128
|
def calc_mean(maplist, name, overwrite=False):
|
129
|
numerator = '(%s)' % '+'.join(['if(isnull(%s), 0, %s)' % (m, m)
|
130
|
for m in maplist])
|
131
|
denominator = '(%s)' % '+'.join(['if(!isnull(%s))' % m
|
132
|
for m in maplist])
|
133
|
gs.mapcalc('mean_%s = %s/%s' % (name, numerator, denominator),
|
134
|
overwrite=overwrite)
|
135
|
|
136
|
# create raster of pixelwise N for a list of input rasters
|
137
|
def calc_nobs(maplist, name, overwrite=False):
|
138
|
denominator = '(%s)' % '+'.join(['if(!isnull(%s))' % m
|
139
|
for m in maplist])
|
140
|
gs.mapcalc('nobs_%s = %s' % (name, denominator), overwrite=overwrite)
|
141
|
|
142
|
# ...combined version of the above two functions...
|
143
|
def calc_clim(maplist, name, overwrite=False):
|
144
|
denominator = '(%s)' % '+'.join(['if(!isnull(%s))' % m
|
145
|
for m in maplist])
|
146
|
gs.mapcalc('nobs_%s = %s' % (name, denominator), overwrite=overwrite)
|
147
|
numerator = '(%s)' % '+'.join(['if(isnull(%s), 0, %s)' % (m, m)
|
148
|
for m in maplist])
|
149
|
gs.mapcalc('mean_%s = round(float(%s)/nobs_%s)' % (name, numerator, name),
|
150
|
overwrite=overwrite)
|
151
|
|
152
|
def load_qc_adjusted_lst(hdf):
|
153
|
"""Load LST_Day_1km into GRASS from the specified hdf file, and
|
154
|
nullify any cells for which QA flags indicate anything other than
|
155
|
high quality.
|
156
|
|
157
|
Argument:
|
158
|
hdf -- local path to the 11A1 HDF file
|
159
|
|
160
|
Returns the name of the QC-adjusted LST map created in GRASS.
|
161
|
"""
|
162
|
lstname = 'LST_' + '_'.join(os.path.basename(hdf).split('.')[1:3])
|
163
|
# read in lst grid
|
164
|
gs.run_command('r.in.gdal',
|
165
|
input='HDF4_EOS:EOS_GRID:%s:MODIS_Grid_Daily_1km_LST:LST_Day_1km' % hdf,
|
166
|
output=lstname)
|
167
|
# read in qc grid
|
168
|
gs.run_command('r.in.gdal',
|
169
|
input = 'HDF4_EOS:EOS_GRID:%s:MODIS_Grid_Daily_1km_LST:QC_Day' % hdf,
|
170
|
output = 'qc_this_day')
|
171
|
gs.run_command('g.region', rast=lstname)
|
172
|
# null out any LST cells for which QC is not high [00]
|
173
|
gs.mapcalc('${lst} = if((qc_this_day & 0x03)==0, ${lst}, null())',
|
174
|
lst=lstname, overwrite=True)
|
175
|
# clean up
|
176
|
gs.run_command('g.remove', rast='qc_this_day')
|
177
|
# return name of qc-adjusted LST raster in GRASS
|
178
|
return lstname
|
179
|
|
180
|
#---------------------------------------------
|
181
|
# test: download then aggregate for one month
|
182
|
#---------------------------------------------
|
183
|
|
184
|
# TODO: set up a (temporary?) GRASS database to use for processing? code
|
185
|
# currently assumes it's being run within an existing GRASS session
|
186
|
# using an appropriately configured LOCATION...
|
187
|
#
|
188
|
# note the following trick to fix datum for modis sinu;
|
189
|
# TODO: check that this works properly...compare to MRT output?
|
190
|
# gs.run_command('g.proj', flags='c',
|
191
|
# proj4='+proj=sinu +a=6371007.181 +b=6371007.181 +ellps=sphere')
|
192
|
## proj4='+proj=sinu +R=6371007.181 +nadgrids=@null +wktext')
|
193
|
|
194
|
tile = 'h09v04'
|
195
|
year = 2005
|
196
|
start_doy = 1
|
197
|
end_doy = 31
|
198
|
hdfdir = '.'
|
199
|
|
200
|
# download data
|
201
|
### [atlas 17-May-2012] Wall time: 111.62 s
|
202
|
hdfs = download_mod11a1(hdfdir, tile, start_doy, end_doy, year)
|
203
|
|
204
|
# note: now that we've downloaded the files into local directory
|
205
|
# 'hdfdir', this should yield the same list...
|
206
|
#hdfs = get_hdf_paths(hdfdir, tile, start_doy, end_doy, year)
|
207
|
|
208
|
# generate monthly pixelwise mean & count of high-quality daytime LST
|
209
|
# values
|
210
|
### [atlas 17-May-2012] Wall time: 53.79 s
|
211
|
gs.os.environ['GRASS_OVERWRITE'] = '1'
|
212
|
LST = [load_qc_adjusted_lst(hdf) for hdf in hdfs]
|
213
|
calc_clim(LST, 'LST_%s_%d_%03d_%03d' % (tile, year, start_doy, end_doy))
|
214
|
# clean up
|
215
|
gs.run_command('g.remove', rast=','.join(LST))
|
216
|
gs.os.environ['GRASS_OVERWRITE'] = '0'
|