IODA Bundle
wrfda_ncdiag.py
Go to the documentation of this file.
1 # wrfda_ncdiag.py
2 # a collection of classes, and supporting information
3 # to read in WRFDA netCDF diagnostic files and rewrite them
4 # into JEDI UFO GeoVaLs and IODA observation files
5 ###############################################################################
6 ###############################################################################
7 from collections import defaultdict, OrderedDict
8 import datetime as dt
9 import errno
10 import ioda_conv_ncio as iconv
11 import netCDF4 as nc
12 import numpy as np
13 from orddicts import DefaultOrderedDict
14 import os
15 import sys
16 
17 # dictionaries and lists
18 # LocKeyList = { 'wrfdaname':[('IODAname','dtype')]}
19 
20 wrfda_miss_float = -888888.
21 wrfda_miss_int = -888888
22 
23 all_LocKeyList = {
24  'date': [('datetime', 'string')],
25  'lat': [('latitude', 'float')],
26  'lon': [('longitude', 'float')],
27  'elv': [('height_above_mean_sea_level', 'float')],
28  'scanpos': [('scan_position', 'float')],
29  'satzen': [('sensor_zenith_angle', 'float'),
30  ('sensor_view_angle', 'float')],
31  'satazi': [('sensor_azimuth_angle', 'float')],
32  'solzen': [('solar_zenith_angle', 'float')],
33  'solazi': [('solar_azimuth_angle', 'float')],
34  'cloud_frac': [('cloud_area_fraction', 'float')],
35 }
36 
37 wrfda_add_vars = {
38  # 'tb_bak': 'WrfdaHofX',
39  # 'tb_bak_clr': 'WrfdaHofXClrSky',
40  'tb_omb': 'WrfdaOmB',
41  'tb_err': 'WrfdaFinalObsError',
42  # 'cloud_obs': 'WrfdaCloudObs',
43  # 'cloud_mod': 'WrfdaCloudModel',
44 }
45 
46 rad_platform_sensor_combos = [
47  # 'eos-2-airs',
48  # 'fy3-1-mwhs',
49  # 'fy3-1-mwts',
50  # 'fy3-2-mwhs',
51  # 'fy3-2-mwts',
52  # 'fy3-3-mwhs2',
53  # 'gcom-w-1-amsr2',
54  'goes-16-abi',
55  'goes-17-abi',
56  'himawari-8-ahi',
57  # 'jpss-0-atms',
58  # 'metop-1-amsua',
59  # 'metop-1-iasi',
60  # 'metop-1-mhs',
61  # 'metop-2-amsua',
62  # 'metop-2-iasi',
63  # 'metop-2-mhs',
64  # 'msg-2-seviri',
65  # 'msg-3-seviri',
66  # 'noaa-15-amsua',
67  # 'noaa-16-amsua',
68  # 'noaa-16-amsub',
69  # 'noaa-17-amsub',
70  # 'noaa-17-hirs',
71  # 'noaa-18-amsua',
72  # 'noaa-18-hirs',
73  # 'noaa-18-mhs',
74  # 'noaa-19-amsua',
75  # 'noaa-19-mhs'
76 ]
77 
78 wrfda2crtm_satellite_map = {
79  # 'eos-2': 'aqua',
80  # 'fy3-1': 'fy3a',
81  # 'fy3-2': 'fy3b',
82  # 'fy3-3': 'fy3c',
83  # 'gcom-w-1': 'gcom-w1',
84  'goes-16': 'g16',
85  'goes-17': 'g17',
86  'himawari-8': 'himawari8',
87  # 'jpss-0': 'npp',
88  # 'metop-1': 'metop-a',
89  # 'metop-2': 'metop-b',
90  # 'msg-2': 'm09',
91  # 'msg-3': 'm10',
92  # 'noaa-15': 'n15',
93  # 'noaa-16': 'n16',
94  # 'noaa-17': 'n17',
95  # 'noaa-18': 'n18',
96  # 'noaa-19': 'n19',
97  # 'noaa-20': 'n20',
98 }
99 
100 sensor_chanlist_dict = {
101  'abi': list(range(7, 17)),
102  'ahi': list(range(7, 17)),
103 }
104 
105 rad_platform_sensor_ObsError = {
106  'goes-16-abi': [2.720, 1.790, 1.920, 1.740, 5.000, wrfda_miss_float, 3.080, 3.060, 2.820, 1.740],
107  'goes-17-abi': [wrfda_miss_float]*10,
108  'himawari-8-ahi': [1.052, 1.700, 1.700, 1.350, 0.814, wrfda_miss_float, 0.871, 0.926, 0.933, 0.787],
109 }
110 # Note: ABI/AHI channel 12 is sensitive to O3, which is not included in WRFDA RTM interfaces
111 # Therefore ObsError is specified as missing
112 
113 # units (exact copy from gsi_ncdiag.py)
114 # 'IODA/UFO_variable_name': 'Unit'
115 units_values = {
116  'virtual_temperature': 'K',
117  'atmosphere_ln_pressure_coordinate': '1',
118  'specific_humidity': '1',
119  'northward_wind': 'm s-1',
120  'eastward_wind': 'm s-1',
121  'geopotential_height': 'm',
122  'height_above_mean_sea_level': 'm',
123  'surface_pressure': 'Pa',
124  'surface_temperature': 'K',
125  'surface_roughness_length': 'm',
126  'surface_geopotential_height': 'm',
127  'land_area_fraction': '1',
128  'air_temperature': 'K',
129  'air_pressure': 'Pa',
130  'air_pressure_levels': 'Pa',
131  'humidity_mixing_ratio': '1',
132  'mole_fraction_of_carbon_dioxide_in_air': '1',
133  'mole_fraction_of_ozone_in_air': '1',
134  'atmosphere_mass_content_of_cloud_liquid_water': 'kg m-2',
135  'effective_radius_of_cloud_liquid_water_particle': 'm',
136  'atmosphere_mass_content_of_cloud_ice': 'kg m-2',
137  'effective_radius_of_cloud_ice_particle': 'm',
138  'water_area_fraction': '1',
139  'land_area_fraction': '1',
140  'ice_area_fraction': '1',
141  'surface_snow_area_fraction': '1',
142  'vegetation_area_fraction': '1',
143  'surface_temperature_where_sea': 'K',
144  'surface_temperature_where_land': 'K',
145  'surface_temperature_where_ice': 'K',
146  'surface_temperature_where_snow': 'K',
147  'surface_wind_speed': 'm s-1',
148  'surface_wind_from_direction': 'degree',
149  'leaf_area_index': '1',
150  'volume_fraction_of_condensed_water_in_soil': '1',
151  'soil_temperature': 'K',
152  'land_type_index': '1',
153  'vegetation_type_index': '1',
154  'soil_type': '1',
155  'surface_snow_thickness': 'm',
156  'humidity_mixing_ratio': '1',
157  'wind_reduction_factor_at_10m': '1',
158  'sulf': '1',
159  'bc1': '1',
160  'bc2': '1',
161  'oc1': '1',
162  'oc2': '1',
163  'dust1': '1',
164  'dust2': '1',
165  'dust3': '1',
166  'dust4': '1',
167  'dust5': '1',
168  'seas1': '1',
169  'seas2': '1',
170  'seas3': '1',
171  'seas4': '1',
172  'latitude': 'degrees_north',
173  'longitude': 'degrees_east',
174  'station_elevation': 'm',
175  'height': 'm',
176  'height_above_mean_sea_level': 'm',
177  'cloud_area_fraction': '1',
178  'scan_position': '1',
179  'sensor_azimuth_angle': 'degree',
180  'sensor_zenith_angle': 'degree',
181  'sensor_view_angle': 'degree',
182  'solar_zenith_angle': 'degree',
183  'solar_azimuth_angle': 'degree',
184  'modis_deep_blue_flag': '1',
185  'row_anomaly_index': '1',
186  'top_level_pressure': 'Pa',
187  'bottom_level_pressure': 'Pa',
188  'tropopause_pressure': 'Pa',
189  'brightness_temperature_jacobian_surface_temperature': '1',
190  'brightness_temperature_jacobian_surface_emissivity': 'K',
191  'brightness_temperature_jacobian_air_temperature': '1',
192  'brightness_temperature_jacobian_humidity_mixing_ratio': 'K/g/Kg ',
193  'optical_thickness_of_atmosphere_layer': '1',
194 }
195 
196 # @TestReference
197 # fields from WRFDA to compare to computations done in UFO
198 test_fields = {
199 }
200 
201 ###############################################################################
202 ###############################################################################
203 
204 
205 # satellite radiance observations
206 class Radiances:
207  """ class Radiances - satellite radiance observations
208 
209  Use this class to read in satellite radiance observations
210  from WRFDA netCDF diag files
211 
212  Functions:
213 
214  Attributes:
215  filename - string path to file
216  validtime - datetime object of valid observation time
217  nobs - number of observations
218 
219 
220  """
221 
222  def __init__(self, filename):
223  self.filenamefilename = filename
224  splitfname = self.filenamefilename.split('/')[-1].split('_')
225  i = False
226  for s in rad_platform_sensor_combos:
227  if s in splitfname:
228  self.platform_sensorplatform_sensor = s
229  i = splitfname.index(s)
230  if not i:
231  raise ValueError("Observation is not a radiance type...")
232 
233  def read(self):
234  # get valid time
235  df = nc.Dataset(self.filenamefilename)
236  tstr = self.filenamefilename.split('/')[-1].split('_')[-1].split('.')[0]
237  self.validtimevalidtime = dt.datetime.strptime(tstr, "%Y%m%d%H")
238  # sensor and satellite
239  self.sensorsensor = self.platform_sensorplatform_sensor.split('-')[-1]
240  satellite = "-".join(self.platform_sensorplatform_sensor.split('-')[0:-1])
241  if satellite in wrfda2crtm_satellite_map:
242  self.satellitesatellite = wrfda2crtm_satellite_map[satellite]
243  else:
244  print("ERROR: Satellite not found in wrfda2crtm_satellite_map:")
245  print(satellite)
246  sys.exit(1)
247 
248  # number of observations
249  self.nlocsnlocs = len(df.dimensions['npixel'])
250  self.nchansnchans = len(df.dimensions['nchan'])
251  self.nobsnobs = self.nlocsnlocs * self.nchansnchans
252  self.dfdf = df
253 
254  def close(self):
255  self.dfdf.close()
256 
257  def toIODAobs(self, OutDir, clobber=True, dateSubDirs=False):
258  """ toIODAobs(OutDir,clobber=True)
259  output observations from the specified WRFDA diag file
260  to the JEDI/IODA observation format
261  """
262  fullOutDir = OutDir
263  if dateSubDirs:
264  # place files for individual dates in separate directories
265  fullOutDir = fullOutDir + '/' + self.validtimevalidtime.strftime("%Y%m%d%H")
266 
267  try:
268  os.makedirs(fullOutDir)
269  except OSError as exc:
270  if exc.errno == errno.EEXIST and os.path.isdir(fullOutDir):
271  pass
272 
273  # set up a NcWriter class
274  outname = fullOutDir + '/' + self.sensorsensor + '_' + self.satellitesatellite + \
275  '_obs_' + self.validtimevalidtime.strftime("%Y%m%d%H") + '.nc4'
276  if not clobber:
277  if (os.path.exists(outname)):
278  print("File exists. Skipping and not overwriting:")
279  print(outname)
280  return
281  LocKeyList = []
282  TestKeyList = []
283  LocVars = []
284  TestVars = []
285  AttrData = {}
286  varDict = defaultdict(lambda: defaultdict(dict))
287  outdata = defaultdict(lambda: DefaultOrderedDict(OrderedDict))
288  loc_mdata = defaultdict(lambda: DefaultOrderedDict(OrderedDict))
289  var_mdata = defaultdict(lambda: DefaultOrderedDict(OrderedDict))
290  test_mdata = defaultdict(lambda: DefaultOrderedDict(OrderedDict))
291  # get list of location variable for this var/platform
292  for ncv in self.dfdf.variables:
293  if ncv in all_LocKeyList:
294  for val in all_LocKeyList[ncv]:
295  LocKeyList.append(val)
296  LocVars.append(ncv)
297 
298  # get list of TestReference variables for this var/platform
299  for ncv in self.dfdf.variables:
300  if ncv in test_fields:
301  TestKeyList.append(test_fields[ncv])
302  TestVars.append(ncv)
303 
304  # for now, record len is 1 and the list is empty?
305  recKey = 0
306  writer = iconv.NcWriter(outname, LocKeyList, TestKeyList=TestKeyList)
307 
308  if self.sensorsensor in sensor_chanlist_dict:
309  chanlist = sensor_chanlist_dict[self.sensorsensor]
310  else:
311  chanlist = list(range(1, self.nchan+1))
312  nchans = len(chanlist)
313 
314  for chan in chanlist:
315  value = "brightness_temperature_{:d}".format(chan)
316  varDict[value]['valKey'] = value, writer.OvalName()
317  varDict[value]['errKey'] = value, writer.OerrName()
318  varDict[value]['qcKey'] = value, writer.OqcName()
319  units_values[value] = 'K'
320 
321  for ivar, lvar in enumerate(LocVars):
322  loc_mdata_name = LocKeyList[ivar][0]
323  loc_mdata_type = LocKeyList[ivar][1]
324  if lvar == 'date':
325  tmp = self.dfdf[lvar][:]
326  obstimes = [dt.datetime.strptime("".join(a.astype(str)), "%Y-%m-%d_%H:%M:%S") for a in tmp]
327  obstimes = [a.strftime("%Y-%m-%dT%H:%M:%SZ") for a in obstimes]
328  loc_mdata[loc_mdata_name] = writer.FillNcVector(obstimes, "datetime")
329  else:
330  if loc_mdata_type == 'float':
331  tmp = self.dfdf[lvar][:].astype(float)
332  tmp[tmp <= wrfda_miss_float] = nc.default_fillvals['f4']
333  else:
334  tmp = self.dfdf[lvar][:]
335  loc_mdata[loc_mdata_name] = tmp
336 
337  # put the TestReference fields in the structure for writing out
338  for tvar in TestVars:
339  test_mdata_name = test_fields[tvar][0]
340  tmp = self.dfdf[tvar][:]
341  tmp[tmp <= wrfda_miss_float] = nc.default_fillvals['f4']
342  test_mdata[test_mdata_name] = tmp
343 
344  # check for additional WRFDA output for each variable
345  for wrfdavar, iodavar in wrfda_add_vars.items():
346  if wrfdavar in self.dfdf.variables:
347  tmp = np.transpose(np.asarray(self.dfdf[wrfdavar]))
348 
349  tmp[tmp <= wrfda_miss_float] = nc.default_fillvals['f4']
350  for c, chan in enumerate(chanlist):
351  varname = "brightness_temperature_{:d}".format(chan)
352  gvname = varname, iodavar
353  outvals = tmp[c]
354  outdata[gvname] = outvals
355 
356  # tb_obs, tb_err, and tb_qc are nlocs x nchan
357  # --> using transpose speeds up access below
358  obsdata = np.transpose(np.asarray(self.dfdf['tb_obs']))
359  obserr = rad_platform_sensor_ObsError[self.platform_sensorplatform_sensor]
360  # obserr = np.transpose(np.asarray(self.df['tb_err']))
361  obsqc = np.transpose(np.asarray(self.dfdf['tb_qc']))
362 
363  # loop through channels for subset
364  var_names = []
365  for c, chan in enumerate(chanlist):
366  value = "brightness_temperature_{:d}".format(chan)
367  var_names.append(value)
368 
369  obsdatasub = obsdata[c]
370  obsdatasub[obsdatasub <= wrfda_miss_float] = nc.default_fillvals['f4']
371 
372  obserrsub = np.full(self.nlocsnlocs, obserr[c])
373  obserrsub[obserrsub <= wrfda_miss_float] = nc.default_fillvals['f4']
374 
375  obsqcsub = obsqc[c]
376  obsqcsub[obsqcsub <= wrfda_miss_int] = nc.default_fillvals['i4']
377 
378  # store values in output data dictionary
379  outdata[varDict[value]['valKey']] = obsdatasub
380  outdata[varDict[value]['errKey']] = obserrsub
381  outdata[varDict[value]['qcKey']] = obsqcsub.astype(int)
382 
383  # var metadata
384  var_mdata['variable_names'] = writer.FillNcVector(var_names, "string")
385  var_mdata['sensor_channel'] = np.asarray(chanlist)
386 
387  # global attributes
388 
389  AttrData["date_time_string"] = self.validtimevalidtime.strftime("%Y-%m-%dT%H:%M:%SZ")
390  AttrData["satellite"] = self.satellitesatellite
391  AttrData["sensor"] = self.sensorsensor
392 
393  # set dimension lengths in the writer since we are bypassing
394  # ExtractObsData
395  writer._nvars = nchans
396  writer._nlocs = self.nlocsnlocs
397 
398  writer.BuildNetcdf(outdata, loc_mdata, var_mdata,
399  AttrData, units_values, test_mdata)
400  print("Satellite radiance obs processed, wrote to:")
401  print(outname)
def __init__(self, filename)
def toIODAobs(self, OutDir, clobber=True, dateSubDirs=False)