atmospheric_lidar/scripts/licel2tc.py

Tue, 25 Sep 2018 12:43:14 +0300

author
Ioannis <ioannis@inoe.ro>
date
Tue, 25 Sep 2018 12:43:14 +0300
changeset 157
03b470b0a05f
parent 156
1e18b2a416ad
child 168
9fed2446a59f
permissions
-rw-r--r--

Improvment on telecover.

i@156 1 """ Command line tool to convert Licel binary files to EARLINET telecover files.
i@156 2 """
i@156 3 import argparse
i@156 4 import glob
i@156 5 import logging
i@156 6 import os
i@156 7 import sys
i@156 8
i@156 9 import numpy as np
i@156 10 from matplotlib import pyplot as plt
i@156 11 import yaml
i@156 12
i@156 13 from lidar_processing import pre_processing
i@156 14
i@156 15 from ..licel import LicelFile
i@156 16 from ..__init__ import __version__
i@156 17
i@156 18 logger = logging.getLogger(__name__)
i@156 19
i@156 20
i@156 21 class TelecoverDataset(object):
i@156 22 def __init__(self, filenames, settings_path, licel_timezone='UTC'):
i@156 23 """ Create a telecover dataset object.
i@156 24
i@156 25 Parameters
i@156 26 ----------
i@156 27 filenames : list of str
i@156 28 List of candidate files to include in the telecover.
i@156 29 settings_path : str
i@156 30 Path to the YAML settings file.
i@156 31 licel_timezone : str
i@156 32 The timezone of dates found in the Licel files. Should match the available
i@156 33 timezones in the TZ database.
i@156 34 """
i@156 35 self.settings_path = settings_path
i@156 36 self.filenames = filenames
i@156 37 self.licel_timezone = licel_timezone
i@156 38
i@156 39 self.settings = self.read_settings(settings_path)
i@156 40 self.file_sites = {}
i@156 41 self.read_files()
i@156 42
i@156 43 self.check_files_ok()
i@156 44
i@156 45 @staticmethod
i@156 46 def read_settings(settings_path):
i@156 47 """ Read the configuration file.
i@156 48
i@156 49 The file should be in YAML syntax."""
i@156 50
i@156 51 if not os.path.isfile(settings_path):
i@156 52 raise IOError("Wrong path for settings file (%s)" % settings_path)
i@156 53
i@156 54 with open(settings_path) as yaml_file:
i@156 55 try:
i@156 56 settings = yaml.safe_load(yaml_file)
i@156 57 logger.debug("Read settings file(%s)" % settings_path)
i@156 58 except:
i@156 59 raise IOError("Could not parse YAML file (%s)" % settings_path)
i@156 60
i@156 61 return settings
i@156 62
i@156 63 def read_files(self):
i@156 64 for filename in self.filenames:
ioannis@157 65 # Load only header
ioannis@157 66 current_file = LicelFile(filename, use_id_as_name=True, licel_timezone=self.licel_timezone, import_now=False)
ioannis@157 67 current_file.import_header_only()
i@156 68
i@156 69 # Populate the dictionary linking sites with files.
i@156 70 site = current_file.site
ioannis@157 71
ioannis@157 72 if site in self.settings['sectors'].values():
ioannis@157 73 current_file.import_file() # Import full data
ioannis@157 74 if current_file.site in self.file_sites.keys():
ioannis@157 75
ioannis@157 76 self.file_sites[site].append(current_file)
ioannis@157 77 else:
ioannis@157 78 self.file_sites[site] = [current_file, ]
i@156 79
i@156 80 def check_files_ok(self):
i@156 81 """ Check if the available files are enough to create the ASCII files. """
i@156 82 # Check that all sectors are available.
i@156 83 sectors = self.settings['sectors']
i@156 84 for sector_name, site_name in sectors.items():
i@156 85 if site_name not in self.file_sites.keys():
i@156 86 raise ValueError('Site name {0} corresponding to sector {1} not found in any file.'.format(site_name,
i@156 87 sector_name))
i@156 88
i@156 89 len_sector = len(self.file_sites[site_name])
ioannis@157 90 if len_sector > 1:
i@156 91 logger.info('More than one files ({0}) found in sector {1} (site name {2})'.format(len_sector,
ioannis@157 92 sector_name,
ioannis@157 93 site_name))
i@156 94
i@156 95 # Check that all files have the correct channels
i@156 96 channels = self.settings['channels'].keys()
i@156 97 for site_name in sectors.values():
i@156 98 for current_file in self.file_sites[site_name]:
i@156 99 for channel in channels:
i@156 100 if channel not in current_file.channels.keys():
i@156 101 raise ValueError(
i@156 102 'File {0} does not contain the required channel {1}.'.format(current_file.file_name,
i@156 103 channel))
i@156 104
i@156 105 def save_ascii(self, output_dir='.'):
i@156 106 """ Save the files in the appropriate format.
i@156 107
i@156 108 Parameters
i@156 109 ---------
i@156 110 output_dir : str
i@156 111 The output directory.
i@156 112
i@156 113 Returns
i@156 114 -------
i@156 115 file_paths : list of str
i@156 116 A list of output paths for the saved files.
i@156 117 """
i@156 118
i@156 119 date, z = self._get_file_metadata()
i@156 120
i@156 121 file_paths = []
ioannis@157 122
ioannis@157 123 logger.info('Creating {0} files.'.format(len(self.settings['channels'])))
ioannis@157 124
i@156 125 for channel_id, channel_settings in self.settings['channels'].items():
i@156 126
i@156 127 output_filename = '{call_sign}_tc_{date}_{name}.dat'.format(call_sign=self.settings['call_sign'],
i@156 128 date=date.strftime('%Y%m%dT%H'),
i@156 129 name=channel_settings['short_name'])
ioannis@157 130
i@156 131 output_path = os.path.join(output_dir, output_filename)
i@156 132
ioannis@157 133 logger.info("Output file path: {0}".format(output_path))
ioannis@157 134
i@156 135 header_txt = ("{s[call_sign]} ({s[site]})\r\n"
i@156 136 "{s[system_name]}\r\n"
i@156 137 "{name}\r\n"
i@156 138 "{date}\r\n"
i@156 139 "range, {sectors}").format(s=self.settings,
ioannis@157 140 name=channel_settings['name'],
ioannis@157 141 date=date.strftime('%d.%m.%Y, %H'),
ioannis@157 142 sectors=', '.join(self.settings['sector_order']))
i@156 143
i@156 144 # Get altitude data
i@156 145 sectors = self.settings['sector_order']
i@156 146
i@156 147 bin_max = self.settings['bins_to_keep']
i@156 148
i@156 149 data = [z, ]
i@156 150 for sector in sectors:
i@156 151 site_name = self.settings['sectors'][sector]
i@156 152 files = self.file_sites[site_name]
i@156 153 channel_data = [self.pre_process_channel(f.channels[channel_id]) for f in files]
i@156 154 data.append(np.mean(channel_data, axis=0))
i@156 155
i@156 156 data = np.array(data).T[:bin_max, :]
i@156 157
i@156 158 np.savetxt(output_path, data, fmt='%.5e', delimiter=',', newline='\r\n', header=header_txt, comments='')
ioannis@157 159
ioannis@157 160 logger.info("File saved.")
ioannis@157 161
i@156 162 file_paths.append(output_path)
i@156 163
i@156 164 return file_paths
i@156 165
i@156 166 def _get_file_metadata(self):
i@156 167 """ Get date string and altitude array that describes the measurement.
i@156 168
i@156 169 Currently, use the date of the first file in the North direction.
i@156 170 """
i@156 171 first_sector = self.settings['sector_order'][0] # Could be N, or NO, or ...
i@156 172 first_site = self.settings['sectors'][first_sector]
i@156 173 first_file = self.file_sites[first_site][0]
i@156 174
i@156 175 channel_id = self.settings['channels'].keys()[0]
i@156 176 channel = first_file.channels[channel_id]
i@156 177
i@156 178 return first_file.start_time, channel.z
i@156 179
i@156 180 def pre_process_channel(self, channel):
i@156 181 """ Pre-process channel according to the settings.
i@156 182
i@156 183 The role of this method is to choose if the channel is photon or analog and call the approparite
i@156 184 method.
i@156 185 """
i@156 186 settings = self.settings['channels'][channel.channel_name]
i@156 187
ioannis@157 188 trigger_delay = settings.get('trigger_delay', 0)
i@156 189 background_min = settings['background_min']
i@156 190 background_max = settings['background_max']
i@156 191
i@156 192 if channel.is_analog:
i@156 193 data_rc = self.pre_process_analog(channel, trigger_delay, background_min, background_max)
i@156 194 else:
i@156 195 dead_time = settings['dead_time']
i@156 196 data_rc = self.pre_process_photon(channel, dead_time, trigger_delay, background_min, background_max)
i@156 197
i@156 198 return data_rc
i@156 199
i@156 200 @staticmethod
i@156 201 def pre_process_photon(channel, dead_time, trigger_delay, background_min, background_max):
i@156 202 """ Pre-process photon counting signals"""
i@156 203 interval_ns = channel.bin_width * 1e9 * channel.number_of_shots # Interval in ns, assuming constant laser shots
i@156 204
i@156 205 data_dt = pre_processing.correct_dead_time_nonparalyzable(channel.data, interval_ns, dead_time)
i@156 206 data_bs, background_mean, background_std = pre_processing.subtract_background(data_dt, background_min,
i@156 207 background_max)
i@156 208 data_tc = pre_processing.correct_trigger_delay_bins(data_bs, trigger_delay)
i@156 209 data_rc = pre_processing.apply_range_correction(data_tc, channel.z)
i@156 210
i@156 211 return data_rc
i@156 212
i@156 213 @staticmethod
i@156 214 def pre_process_analog(channel, trigger_delay, background_min, background_max):
i@156 215
i@156 216 data_bs, background_mean, background_std = pre_processing.subtract_background(channel.data,
i@156 217 background_min,
i@156 218 background_max)
i@156 219 data_tc = pre_processing.correct_trigger_delay_bins(data_bs, trigger_delay)
i@156 220 data_rc = pre_processing.apply_range_correction(data_tc, channel.z)
i@156 221
i@156 222 return data_rc
i@156 223
ioannis@157 224
ioannis@157 225 def convert_to_telecover(file_paths, settings_path, licel_timezone, output_dir):
i@156 226 """ Convert files to SCC. """
i@156 227
i@156 228 try:
ioannis@157 229 dataset = TelecoverDataset(file_paths, settings_path, licel_timezone)
ioannis@157 230 except Exception as err:
i@156 231 logger.error(err)
i@156 232 sys.exit(1)
i@156 233
ioannis@157 234 try:
ioannis@157 235 dataset.save_ascii(output_dir)
ioannis@157 236 except Exception as err:
ioannis@157 237 logger.error(err)
ioannis@157 238 sys.exit(2)
i@156 239
i@156 240
i@156 241 def main():
i@156 242 # Define the command line argument
i@156 243 parser = argparse.ArgumentParser(
i@156 244 description="A program to convert Licel binary files to EARLIENT telecover ASCII format")
i@156 245 parser.add_argument("settings_file", help="The path to a parameter YAML.")
i@156 246 parser.add_argument("files",
i@156 247 help="Location of licel files. Use relative path and filename wildcards. (default './*.*')",
i@156 248 default="./*.*")
ioannis@157 249 parser.add_argument('-o', '--output', help='Output directory.', default='.')
ioannis@157 250
i@156 251 parser.add_argument('--licel_timezone', help="String describing the timezone according to the tz database.",
i@156 252 default="UTC", dest="licel_timezone",
i@156 253 )
i@156 254 # Verbosity settings from http://stackoverflow.com/a/20663028
i@156 255 parser.add_argument('-d', '--debug', help="Print dubuging information.", action="store_const",
i@156 256 dest="loglevel", const=logging.DEBUG, default=logging.INFO,
i@156 257 )
i@156 258 parser.add_argument('-s', '--silent', help="Show only warning and error messages.", action="store_const",
i@156 259 dest="loglevel", const=logging.WARNING
i@156 260 )
i@156 261 parser.add_argument('--version', help="Show current version.", action='store_true')
i@156 262
i@156 263 args = parser.parse_args()
i@156 264
i@156 265 # Get the logger with the appropriate level
i@156 266 logging.basicConfig(format='%(levelname)s: %(message)s', level=args.loglevel)
i@156 267 logger = logging.getLogger(__name__)
i@156 268
i@156 269 # Check for version
i@156 270 if args.version:
i@156 271 print("Version: %s" % __version__)
i@156 272 sys.exit(0)
i@156 273
i@156 274 # Get a list of files to process
ioannis@157 275 logger.debug("Search path: {0}".format(os.path.expanduser(args.files)))
ioannis@157 276 files = glob.glob(os.path.expanduser(args.files))
i@156 277
i@156 278 # If not files found, exit
i@156 279 if len(files) == 0:
i@156 280 logger.error("No files found when searching for %s." % args.files)
i@156 281 sys.exit(1)
i@156 282
i@156 283 # If everything OK, proceed
ioannis@157 284 logger.info("Found {0} files matching {1}".format(len(files), args.files))
ioannis@157 285
ioannis@157 286 convert_to_telecover(files, args.settings_file, args.licel_timezone, args.output)
i@156 287
ioannis@157 288
ioannis@157 289 if __name__ == "__main__":
ioannis@157 290 main()

mercurial