Tue, 25 Sep 2018 12:43:14 +0300
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() |