IODA Bundle
giirs_ssec2ioda.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 
3 #
4 # (C) Copyright 2019 UCAR
5 #
6 # This software is licensed under the terms of the Apache Licence Version 2.0
7 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
8 #
9 
10 import argparse
11 import collections
12 from datetime import datetime, timedelta
13 import itertools
14 import math
15 from pathlib import Path
16 import sys
17 
18 import dateutil.parser
19 import numpy as np
20 import netCDF4 as nc
21 
22 IODA_CONV_PATH = Path(__file__).parent/"@SCRIPT_LIB_PATH@"
23 if not IODA_CONV_PATH.is_dir():
24  IODA_CONV_PATH = Path(__file__).parent/'..'/'lib-python'
25 sys.path.append(str(IODA_CONV_PATH.resolve()))
26 
27 import ioda_conv_ncio as iconv
28 
29 
30 class Giirs2Ioda:
31  VAR_NAME = "brightness_temperature"
32 
33  LOCATION_KEYS = [
34  ("latitude", "float"),
35  ("longitude", "float"),
36  ("datetime", "string")
37  ]
38 
39  # map from iooda metadata var name to ssec var name
40  LOC_MDATA_MAP = {'latitude': 'IRLW_Latitude',
41  'longitude': 'IRLW_Longitude',
42  'solar_zenith_angle': 'IRLW_SolarZenith',
43  'solar_azimuth_angle': 'IRLW_SolarAzimuth',
44  'sensor_zenith_angle': 'IRLW_SatelliteZenith',
45  'sensor_view_angle': 'IRLW_SatelliteZenith',
46  'sensor_azimuth_angle': 'IRLW_SatelliteAzimuth',
47  'cloud_fraction': 'Cloud_Fraction'}
48 
49  # Filtering parameters
50  SAT_ZEN_MAX = 74 # Maximum allowable IRLW_SatelliteZenith value before filtering
51 
52  def __init__(self, center_datetime, out_file):
53  """
54  Arguments:
55  center_datetime: Window center (type: datetime or string representation)
56  out_file: Name of IODA file to write (type: str or Path)
57  """
58  if isinstance(center_datetime, str):
59  center_datetime = datetime.strptime(center_datetime, '%Y%m%d%H')
60  self.center_datetimecenter_datetime = center_datetime
61  self.outfileoutfile = Path(out_file)
62  self.writerwriter = iconv.NcWriter(str(out_file), [], self.LOCATION_KEYSLOCATION_KEYS)
63 
64  self.datadata = collections.defaultdict(dict)
65  self.units_valuesunits_values = {} # cannot use an ordered dict for this
66  self.LocMdataLocMdata = collections.defaultdict(dict)
67  self.VarMdataVarMdata = collections.defaultdict(dict)
68  self.AttrDataAttrData = collections.defaultdict(dict)
69  self.AttrDataAttrData['date_time_string'] = self.center_datetimecenter_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
70 
71  def readInFiles(self, filenames):
72  # This method will transfer variable data from the input file into a
73  # dictionary (self.data) that the netcdf writer understands.
74  #
75  # Input:
76  # Filenames list of filenames to process
77  #
78  # The mapping from input variables to ioda variables for IR LW radiance is:
79  #
80  # dimensions:
81  # LWchannel -> nvars
82  # LWdetector -> nlocs
83  #
84  # global attributes:
85  # 'Observing Beginning Date' -> YYYY-MM-DD
86  # 'Observing Beginning Time' -> HH:MM:SS
87  # The two above attributes get translated to a single datetime value
88  # which becomes the value for each location in datetime@MetaData.
89  # The seconds value is a float number possibly containing fractional seconds.
90  #
91  # variables:
92  # LW_wnum(LWchannel) -> channel_wavenumber@VarMetaData
93  # Number the channels starting with 1 assuming that the order
94  # in variables indexed by LWchannel are sequential channel numbers (1..n).
95  #
96  # IRLW_Latitude(LWdetector) -> latitude@MetaData
97  # IRLW_Longitude(LWdetector) -> longitude@MetaData
98  # IRLW_SolarZenith(LWdetector) -> solar_zenith_angle@MetaData
99  # IRLW_SolarAzimuth(LWdetector) -> solar_azimuth_angle@MetaData
100  # IRLW_SatelliteZenith(LWdetector) -> sensor_zenith_angle@MetaData
101  # IRLW_SatelliteAzimuth(LWdetector) -> sensor_azimuth_angle@MetaData
102  #
103  # ES_RealLW(LWchannel, Lwdetector) -> brightness_temperature_#@ObsValue
104  # For now, fabricate the QC marks and error esitmate by setting all
105  # locations to 0 for brightness_temperature_#@PreQC and setting all locations
106  # to 2.0 for brightness_temperature_#@ObsError.
107  valKey = self.VAR_NAMEVAR_NAME, self.writerwriter.OvalName()
108  errKey = self.VAR_NAMEVAR_NAME, self.writerwriter.OerrName()
109  qcKey = self.VAR_NAMEVAR_NAME, self.writerwriter.OqcName()
110 
111  totalFiles = 0 # Total number of files read in
112  succFiles = 0 # Number of files with at least one valid obs
113  nlocs_tot = 0 # Total localization read in
114  self.writerwriter._nlocs = 0
115  self.writerwriter._nrecs = 1
116  meta_datetime = []
117  for f in (ff.strip() for ff in filenames):
118  with nc.Dataset(f, 'r') as ncd:
119  totalFiles += 1
120  try:
121  sat_zen = ncd.variables['IRLW_SatelliteZenith'][:]
122  except KeyError as e:
123  print(f"[GIIRS2IODA] Processing {totalFiles}/{len(filenames)} Missing metadata variables. Skipping. -- {Path(f).name} ")
124  continue
125  mask = sat_zen < self.SAT_ZEN_MAXSAT_ZEN_MAX
126  nlocs = np.count_nonzero(mask)
127  if nlocs == 0:
128  print(f"[GIIRS2IODA] Processing {totalFiles}/{len(filenames)} NO VALID OBS. Skipping. -- {Path(f).name} ")
129  continue
130  succFiles += 1
131  print(f"[GIIRS2IODA] Processing {totalFiles}/{len(filenames)} #locs:{nlocs} -- {Path(f).name}")
132 
133  # Initialize data structures based on channel and detector layout
134  if (succFiles == 1):
135  # Get number of channels
136  nchans = ncd.dimensions["LWchannel"].size
137  self.writerwriter._nvars = nchans
138 
139  # Vectorized constants for vectorized rad2bt transform
140  # bt = K1/log(1+K3/rad) in native units
141  LW_wnum = np.asarray(ncd.variables["LW_wnum"])
142  K1 = LW_wnum*100*self._K1_K1
143  K1 = K1.astype(np.float32)[:, np.newaxis] # column vector
144  K3 = (1e5*self._K2_K2*(LW_wnum*100)**3.).astype(np.float32)
145  K3 = K3.astype(np.float32)[:, np.newaxis] # column vector
146 
147  # Get number of detectors (obs) per-channel
148  ndetectors = ncd.dimensions["LWdetector"].size
149  detector_array = np.arange(1, ndetectors+1, dtype=np.int32)
150 
151  # Determine maximum size of output and pre-allocate output arrays
152  self.nlocs_maxnlocs_max = ndetectors * len(filenames) # Maximum possible locs
153  # obs_vals (nchans, nlocs) will contain all observed values
154  # values are appended for each dataset
155  obs_vals = np.full((nchans, self.nlocs_maxnlocs_max), nc.default_fillvals['f4'], dtype=np.float32, order='F')
156  # Note, scan_position is integer valued, but currently must be a float32 to be recognized by UFO/CRTM operator
157  self.LocMdataLocMdata['scan_position'] = np.full(self.nlocs_maxnlocs_max, nc.default_fillvals['f4'], dtype=np.float32)
158  self.units_valuesunits_values['scan_position'] = '1'
159 
160  self.VarMdataVarMdata['channel_wavenumber'] = self.writerwriter.FillNcVector(LW_wnum, 'float')
161  self.VarMdataVarMdata['channel_number'] = self.writerwriter.FillNcVector(np.arange(1, nchans+1, dtype=np.int32), 'integer')
162  self.units_valuesunits_values['channel_wavenumber'] = ncd.variables['LW_wnum'].getncattr('units')
163  self.units_valuesunits_values['channel_number'] = '1'
164  for new_key, old_key in self.LOC_MDATA_MAPLOC_MDATA_MAP.items():
165  self._initialize_metadata_key_initialize_metadata_key(new_key, old_key, ncd)
166  else:
167  # Check this file uses the same dimensions as the initial file
168  this_nchans = ncd.dimensions["LWchannel"].size
169  this_ndetectors = ncd.dimensions["LWdetector"].size
170  if this_nchans != nchans:
171  raise RuntimeError(f"Number of channels:{this_nchans} does not match with initial value channels:{nchans}")
172  if this_ndetectors != ndetectors:
173  raise RuntimeError(f"Number of detectors:{this_ndetectors} does not match with initial value channels:{ndetectors}")
174 
175  # The file contains a single image with a sub-second scan time. We
176  # are currently retaining date-time stamps to the nearest second so
177  # for now just grab the beginning scan time and use that for all locations.
178  obsDate = ncd.getncattr("Observing Beginning Date").split("-")
179  obsTime = ncd.getncattr("Observing Beginning Time").split(":")
180  obsSeconds = timedelta(seconds=int(round(float(obsTime[2]))))
181  obsDateTime = datetime(year=int(obsDate[0]), month=int(obsDate[1]),
182  day=int(obsDate[2]), hour=int(obsTime[0]),
183  minute=int(obsTime[1])) + obsSeconds
184  obsDtimeString = obsDateTime.strftime("%Y-%m-%dT%H:%M:%SZ")
185 
186  meta_datetime.extend([obsDtimeString]*nlocs)
187 
188  # Read metadata
189  for new_key, old_key in self.LOC_MDATA_MAPLOC_MDATA_MAP.items():
190  if old_key in ncd.variables:
191  if new_key not in self.LocMdataLocMdata:
192  # If we haven't seen this key before initialize it now and set to fillvalue for all previous locs
193  self._initialize_metadata_key_initialize_metadata_key(new_key, old_key, ncd)
194  self.LocMdataLocMdata[new_key][nlocs_tot:nlocs_tot+nlocs] = np.asarray(ncd.variables[old_key])[mask]
195  self.LocMdataLocMdata['scan_position'][nlocs_tot:nlocs_tot+nlocs] = detector_array[mask] # detector number
196 
197  # Read in the long wave radiance
198  # For now fabricate the QC marks and error estimates
199  ncVar = ncd.variables['ES_RealLW']
200  lwRad = np.array(ncVar[:, mask])
201 
202  lwRadiance_apo = np.copy(lwRad)
203  lwRadiance_apo[1:-1, :] = 0.23*lwRad[:-2, :] + 0.54*lwRad[1:-1, :] + 0.23*lwRad[2:, :]
204  valid = (lwRadiance_apo > 0) & (lwRadiance_apo < 300)
205  # Vectorized conversion: valid radiances to brightness temperature
206  shape = (nchans, nlocs)
207  np.place(obs_vals[:, nlocs_tot:nlocs_tot+nlocs], valid,
208  np.broadcast_to(K1, shape)[valid] / np.log1p(np.broadcast_to(K3, shape)[valid]/lwRadiance_apo[valid]))
209 
210  nlocs_tot += nlocs
211 
212  # Write final data arrays as NcVectors
213  self.writerwriter._nlocs = nlocs_tot
214  self.LocMdataLocMdata['datetime'] = self.writerwriter.FillNcVector(meta_datetime, 'datetime')
215  self.units_valuesunits_values['datetime'] = 'ISO 8601 format'
216  self.LocMdataLocMdata = {k: v[:nlocs_tot] for k, v in self.LocMdataLocMdata.items()} # Remove unused extra space in metadata
217  for ivar in range(nchans):
218  varname = f"brightness_temperature_{ivar+1}"
219  self.units_valuesunits_values[varname] = 'K'
220  self.datadata[(varname, 'ObsValue')] = self.writerwriter.FillNcVector(obs_vals[ivar, :nlocs_tot], 'float')
221  self.datadata[(varname, 'PreQC')] = self.writerwriter.FillNcVector(np.full(nlocs_tot, 0, dtype=np.int32), 'integer')
222  self.datadata[(varname, 'ObsError')] = self.writerwriter.FillNcVector(np.full(nlocs_tot, 2., dtype=np.float32), 'float')
223  print(f"[GIIRS2IODA] Processed {nlocs_tot} total locs from {succFiles}/{totalFiles} valid files.")
224 
225  def writeIODA(self):
226  self.writerwriter.BuildNetcdf(self.datadata, self.LocMdataLocMdata, self.VarMdataVarMdata, self.AttrDataAttrData, self.units_valuesunits_values)
227 
228  # Private Methods
229 
230  # Radiance to brightness temp conversion
231  _H_PLANCK = 6.62606957 * 1e-34 # SI-unit = [J*s]
232  _K_BOLTZMANN = 1.3806488 * 1e-23 # SI-unit = [J/K]
233  _C_SPEED = 2.99792458 * 1e8 # SI-unit = [m/s]
234  _K1 = _H_PLANCK * _C_SPEED / _K_BOLTZMANN
235  _K2 = 2 * _H_PLANCK * _C_SPEED**2
236 
237  @classmethod
238  def _rad2bt(cls, wavnum, radiance):
239  # Radiance to brightness temp conversion
240  K3 = cls._K2_K2 * wavnum**3
241  bt = cls._K1_K1 * wavnum / np.log1p(K3/radiance)
242  return bt
243 
244  def _initialize_metadata_key(self, new_key, old_key, ncd):
245  """
246  Common initialization for LocMdata metadata for new variable names
247  new_key - IODA variable name
248  old_key - GIIRS SSEC NetCDF variable name
249  ncd - GIIRS NetCDF scan file
250  """
251  if old_key in ncd.variables:
252  self.LocMdataLocMdata[new_key] = np.full(self.nlocs_maxnlocs_max, nc.default_fillvals['f4'], dtype=np.float32)
253  if 'units' in ncd.variables[old_key].ncattrs():
254  self.units_valuesunits_values[new_key] = ncd.variables[old_key].getncattr('units')
255 
256 
257 def main():
258  parser = argparse.ArgumentParser(
259  description=(
260  'Read SSEC GIIRS long wave radiance file(s) and convert'
261  ' to a concatenated IODA formatted output file converting radiance'
262  ' to brightness-temperature units.')
263  )
264  required = parser.add_argument_group(title='required arguments')
265  required.add_argument(
266  '-i', '--input',
267  help="file name of giirs input file(s)",
268  type=str, nargs='+', required=True)
269  required.add_argument(
270  '-o', '--output',
271  help="path to ioda output file",
272  type=str, required=True)
273  required.add_argument(
274  '-d', '--date',
275  help="base date for the center of the window",
276  metavar="YYYYMMDDHH", type=str, required=True)
277  args = parser.parse_args()
278 
279  conv = Giirs2Ioda(args.date, args.output)
280  conv.readInFiles(args.input)
281  conv.writeIODA()
282 
283 
284 if __name__ == '__main__':
285  main()
def readInFiles(self, filenames)
def __init__(self, center_datetime, out_file)
def _initialize_metadata_key(self, new_key, old_key, ncd)
def _rad2bt(cls, wavnum, radiance)