atmospheric_lidar/licel.py

Fri, 14 Feb 2020 12:38:45 +0200

author
Ioannis <ioannis@inoe.ro>
date
Fri, 14 Feb 2020 12:38:45 +0200
changeset 186
97cabcc90f93
parent 182
a0bf7d88b1dc
child 189
e824878310ed
permissions
-rwxr-xr-x

Support of naming channels by order. In principle should not be required.

ioannis@55 1 import datetime
ioannis@55 2 import logging
i@117 3 import copy
i@156 4 import os
ioannis@55 5
binietoglou@0 6 import numpy as np
i@101 7 import pytz
ioannis@55 8
i@150 9 from .generic import BaseLidarMeasurement, LidarChannel
ioannis@165 10 from .diva import DivaConverterMixin
binietoglou@0 11
i@101 12 logger = logging.getLogger(__name__)
i@101 13
i@116 14 c = 299792458.0 # Speed of light
i@116 15
ioannis@22 16
i@130 17 class LicelChannelData:
i@128 18 """ A class representing a single channel found in a single Licel file."""
i@128 19
ioannis@186 20 def __init__(self, raw_info, raw_data, duration, use_id_as_name=False, channel_name=None):
i@128 21 """
i@128 22 This is run when creating a new object.
i@128 23
i@128 24 Parameters
i@128 25 ----------
i@128 26 raw_info : dict
i@128 27 A dictionary containing raw channel information.
i@128 28 raw_data : dict
i@128 29 An array with raw channel data.
i@128 30 duration : float
i@128 31 Duration of the file, in seconds
i@128 32 use_id_as_name : bool
i@128 33 If True, the transient digitizer name (e.g. BT0) is used as a channel
i@128 34 name. If False, a more descriptive name is used (e.g. '01064.o_an').
ioannis@186 35 channel_name : str or None
ioannis@186 36 If provided, it will override the automatic generated channel name. It can be used if names are not unique.
i@128 37 """
i@128 38 self.raw_info = raw_info
i@128 39 self.raw_data = raw_data
i@128 40 self.duration = duration
i@128 41 self.use_id_as_name = use_id_as_name
ioannis@186 42 self.channel_name_input = channel_name
i@180 43 self._assign_properties()
i@128 44
i@180 45 def _assign_properties(self):
i@180 46 """ Assign properties """
i@180 47 self.adcbits = int(self.raw_info['ADCbits'])
i@180 48 self.active = int(self.raw_info['active'])
i@180 49 self.analog_photon = self.raw_info['analog_photon']
i@180 50 self.bin_width = float(self.raw_info['bin_width'])
i@180 51 self.data_points = int(self.raw_info['number_of_datapoints'])
i@180 52 self.hv = float(self.raw_info['HV'])
i@180 53 self.id = self.raw_info['ID']
i@180 54 self.laser_used = int(self.raw_info['laser_used'])
i@180 55 self.number_of_shots = int(self.raw_info['number_of_shots'])
i@180 56 self.wavelength_str = self.raw_info['wavelength']
i@180 57
i@180 58 self.address = int(self.id[-1:], base=16)
i@128 59
i@128 60 if self.is_analog:
i@180 61 self.discriminator = float(self.raw_info['discriminator']) * 1000 # Analog range in mV
i@128 62 else:
i@180 63 self.discriminator = float(self.raw_info['discriminator'])
i@128 64
i@128 65 @property
i@130 66 def is_photodiode(self):
i@130 67 return self.id[0:2] == 'PD'
i@130 68
i@130 69 @property
i@128 70 def wavelength(self):
i@128 71 """ Property describing the nominal wavelength of the channel.
i@128 72
i@128 73 Returns
i@128 74 -------
i@128 75 : int or None
i@128 76 The integer value describing the wavelength. If no raw_info have been provided,
i@128 77 returns None.
i@128 78 """
i@128 79 wavelength = self.wavelength_str.split('.')[0]
i@128 80 return int(wavelength)
i@128 81
i@128 82 @property
i@128 83 def channel_name(self):
i@128 84 """
i@128 85 Construct the channel name adding analog photon info to avoid duplicates
i@128 86
i@128 87 If use_id_as_name is True, the channel name will be the transient digitizer ID (e.g. BT01).
i@128 88 This could be useful if the lidar system has multiple telescopes, so the descriptive name is
i@128 89 not unique.
i@128 90
i@128 91 Returns
i@128 92 -------
i@128 93 channel_name : str
i@128 94 The channel name
i@128 95 """
ioannis@186 96 if self.channel_name_input is not None:
ioannis@186 97 return self.channel_name_input
ioannis@186 98
i@128 99 if self.use_id_as_name:
i@128 100 channel_name = self.id
i@128 101 else:
i@128 102 acquisition_type = self.analog_photon_string
i@128 103 channel_name = "%s_%s" % (self.wavelength_str, acquisition_type)
i@128 104 return channel_name
i@128 105
i@128 106 @property
i@128 107 def analog_photon_string(self):
i@128 108 """ Convert the analog/photon flag found in the Licel file to a proper sting.
i@128 109
i@128 110 Returns
i@128 111 -------
i@128 112 string : str
i@128 113 'an' or 'ph' string, for analog or photon-counting channel respectively.
i@128 114 """
i@128 115 if self.analog_photon == '0':
i@128 116 string = 'an'
i@180 117 elif self.analog_photon == '1':
i@180 118 string = 'ph'
i@180 119 elif self.analog_photon == '2':
i@180 120 string = 'std_an'
i@180 121 elif self.analog_photon == '3':
i@180 122 string = 'std_ph'
i@128 123 else:
i@180 124 string = str(self.analaog_photon)
i@128 125 return string
i@128 126
i@128 127 def calculate_physical(self):
i@128 128 """ Calculate physically-meaningful data from raw channel data:
i@128 129
i@128 130 * In case of analog signals, the data are converted to mV.
i@128 131 * In case of photon counting signals, data are stored as number of photons.
i@128 132
i@128 133 In addition, some ancillary variables are also calculated (z, dz, number_of_bins).
i@128 134 """
i@128 135 data = self.raw_data
i@128 136
i@128 137 norm = data / float(self.number_of_shots)
i@129 138 dz = self.bin_width
i@128 139
i@129 140 if self.is_analog:
i@128 141 # If the channel is in analog mode
i@128 142 ADCrange = self.discriminator # Discriminator value already in mV
i@128 143
i@130 144 if self.is_photodiode and (self.adcbits == 0):
i@147 145 logger.info("Assuming adcbits equal 1. This is a bug in current licel format when storing photodiode data.")
i@130 146 channel_data = norm * ADCrange / (2 ** self.adcbits)
i@130 147 else:
ioannis@146 148 channel_data = norm * ADCrange / ((2 ** self.adcbits) - 1) # Licel LabView code has a bug (does not account -1).
i@130 149
i@128 150 else:
i@130 151 channel_data = norm * self.number_of_shots
i@128 152
i@128 153 # Calculate Z
i@128 154 self.z = np.array([dz * bin_number + dz / 2.0 for bin_number in range(self.data_points)])
i@128 155 self.dz = dz
i@128 156 self.data = channel_data
i@128 157
i@128 158 @property
i@128 159 def is_analog(self):
i@128 160 return self.analog_photon == '0'
i@128 161
i@128 162
i@129 163 class LicelFile(object):
ulalume3@105 164 """ A class representing a single binary Licel file. """
i@116 165
i@129 166 licel_file_header_format = ['filename',
i@129 167 'start_date start_time end_date end_time altitude longitude latitude zenith_angle',
i@125 168 # Appart from Site that is read manually
i@129 169 'LS1 rate_1 LS2 rate_2 number_of_datasets', ]
i@129 170 licel_file_channel_format = 'active analog_photon laser_used number_of_datapoints 1 HV bin_width wavelength d1 d2 d3 d4 ADCbits number_of_shots discriminator ID'
i@125 171
i@130 172 channel_data_class = LicelChannelData
i@125 173
i@176 174 # If True, it corrects the old Raymetrics convention of zenith angle definition (zenith = -90 degrees)
i@176 175 fix_zenith_angle = False
i@176 176
ioannis@186 177 def __init__(self, file_path, use_id_as_name=False, get_name_by_order=False, licel_timezone="UTC", import_now=True):
ulalume3@105 178 """
ulalume3@105 179 This is run when creating a new object.
i@128 180
ulalume3@105 181 Parameters
ulalume3@105 182 ----------
ulalume3@105 183 file_path : str
ulalume3@105 184 The path to the Licel file.
ulalume3@105 185 use_id_as_name : bool
ulalume3@105 186 If True, the transient digitizer name (e.g. BT0) is used as a channel
ulalume3@105 187 name. If False, a more descriptive name is used (e.g. '01064.o_an').
ioannis@186 188 get_name_by_order : bool
ioannis@186 189 If True, the channel name is given by the order of the channel in the file. In this case the
ioannis@186 190 `use_id_as_name` variable is ignored.
ulalume3@105 191 licel_timezone : str
ulalume3@105 192 The timezone of dates found in the Licel files. Should match the available
i@128 193 timezones in the TZ database.
i@173 194 import_now : bool
ioannis@144 195 If True, the header and data are read immediately. If not, the user has to call the
ioannis@144 196 corresponding methods directly. This is used to speed up reading files when only
ioannis@144 197 header information are required.
ulalume3@105 198 """
i@156 199 self.file_path = file_path
i@156 200 self.file_name = os.path.basename(file_path)
i@156 201
ioannis@55 202 self.use_id_as_name = use_id_as_name
ioannis@186 203 self.get_name_by_order = get_name_by_order
binietoglou@0 204 self.start_time = None
binietoglou@0 205 self.stop_time = None
i@102 206 self.licel_timezone = licel_timezone
ulalume3@47 207
ioannis@144 208 if import_now:
ioannis@144 209 self.import_file()
ioannis@177 210 else:
ioannis@177 211 self.import_header_only()
i@130 212
ioannis@144 213 def import_file(self):
i@129 214 """ Read the header info and data of the Licel file.
ulalume3@105 215 """
binietoglou@0 216 channels = {}
i@130 217 photodiodes = {}
binietoglou@33 218
i@156 219 with open(self.file_path, 'rb') as f:
binietoglou@33 220
binietoglou@33 221 self.read_header(f)
ulalume3@47 222
binietoglou@33 223 # Check the complete header is read
i@101 224 f.readline()
binietoglou@0 225
binietoglou@33 226 # Import the data
ioannis@186 227 for channel_no, current_channel_info in enumerate(self.channel_info):
i@129 228 raw_data = np.fromfile(f, 'i4', int(current_channel_info['number_of_datapoints']))
binietoglou@33 229 a = np.fromfile(f, 'b', 1)
binietoglou@33 230 b = np.fromfile(f, 'b', 1)
ulalume3@47 231
binietoglou@33 232 if (a[0] != 13) | (b[0] != 10):
i@156 233 logger.warning("No end of line found after record. File could be corrupt: %s" % self.file_path)
ioannis@146 234 logger.warning('a: {0}, b: {1}.'.format(a, b))
ulalume3@47 235
ioannis@186 236 if self.get_name_by_order:
ioannis@186 237 channel_name = channel_no
ioannis@186 238 else:
ioannis@186 239 channel_name = None
ioannis@186 240
i@130 241 channel = self.channel_data_class(current_channel_info, raw_data, self.duration(),
ioannis@186 242 use_id_as_name=self.use_id_as_name, channel_name=channel_name)
ulalume3@47 243
i@130 244 # Assign the channel either as normal channel or photodiode
i@130 245 if channel.is_photodiode:
i@130 246 if channel.channel_name in photodiodes.keys():
i@130 247 # Check if current naming convention produces unique files
i@130 248 raise IOError('Trying to import two photodiodes with the same name')
i@130 249 photodiodes[channel.channel_name] = channel
i@130 250 else:
i@130 251 if channel.channel_name in channels.keys():
i@130 252 # Check if current naming convention does not produce unique files
i@130 253 raise IOError('Trying to import two channels with the same name')
i@130 254 channels[channel.channel_name] = channel
ulalume3@47 255
binietoglou@33 256 self.channels = channels
i@130 257 self.photodiodes = photodiodes
ulalume3@47 258
ioannis@144 259 self._calculate_physical()
ioannis@144 260
binietoglou@33 261 def read_header(self, f):
i@128 262 """ Read the header of an open Licel file.
i@128 263
ulalume3@105 264 Parameters
ulalume3@105 265 ----------
ulalume3@105 266 f : file-like object
ulalume3@105 267 An open file object.
ulalume3@105 268 """
ulalume3@47 269 # Read the first 3 lines of the header
binietoglou@0 270 raw_info = {}
binietoglou@33 271 channel_info = []
ulalume3@47 272
binietoglou@33 273 # Read first line
ioannis@170 274 raw_info['Filename'] = f.readline().decode().strip()
ulalume3@47 275
i@125 276 raw_info.update(self._read_second_header_line(f))
ulalume3@47 277
i@125 278 raw_info.update(self._read_rest_of_header(f))
ulalume3@47 279
binietoglou@33 280 # Update the object properties based on the raw info
i@129 281 start_string = '%s %s' % (raw_info['start_date'], raw_info['start_time'])
i@129 282 stop_string = '%s %s' % (raw_info['end_date'], raw_info['end_time'])
binietoglou@0 283 date_format = '%d/%m/%Y %H:%M:%S'
ulalume3@47 284
i@101 285 try:
i@101 286 logger.debug('Creating timezone object %s' % self.licel_timezone)
i@101 287 timezone = pytz.timezone(self.licel_timezone)
i@101 288 except:
i@101 289 raise ValueError("Cloud not create time zone object %s" % self.licel_timezone)
i@101 290
i@101 291 # According to pytz docs, timezones do not work with default datetime constructor.
i@101 292 local_start_time = timezone.localize(datetime.datetime.strptime(start_string, date_format))
i@101 293 local_stop_time = timezone.localize(datetime.datetime.strptime(stop_string, date_format))
i@101 294
i@101 295 # Only save UTC time.
i@101 296 self.start_time = local_start_time.astimezone(pytz.utc)
i@101 297 self.stop_time = local_stop_time.astimezone(pytz.utc)
i@101 298
binietoglou@0 299 # Read the rest of the header.
i@129 300 for c1 in range(int(raw_info['number_of_datasets'])):
ioannis@170 301 channel_info.append(self.match_lines(f.readline().decode(), self.licel_file_channel_format))
ulalume3@47 302
binietoglou@33 303 self.raw_info = raw_info
binietoglou@33 304 self.channel_info = channel_info
ulalume3@47 305
i@129 306 self._assign_properties()
i@129 307
i@129 308 def _assign_properties(self):
i@129 309 """ Assign properties from the raw_info dictionary. """
i@129 310 self.number_of_datasets = int(self.raw_info['number_of_datasets'])
i@129 311 self.altitude = float(self.raw_info['altitude'])
i@129 312 self.longitude = float(self.raw_info['longitude'])
i@129 313 self.latitude = float(self.raw_info['latitude'])
i@173 314
i@173 315 self.zenith_angle_raw = float(self.raw_info['zenith_angle'])
i@173 316
i@176 317 logger.debug('Fix zenith angle? %s' % self.fix_zenith_angle)
i@176 318
i@173 319 if self.fix_zenith_angle:
i@176 320 logger.debug('Fixing zenith angle.')
i@173 321 self.zenith_angle = self._correct_zenith_angle(self.zenith_angle_raw)
i@173 322 else:
i@173 323 self.zenith_angle = self.zenith_angle_raw
i@173 324
i@173 325 @staticmethod
i@173 326 def _correct_zenith_angle(zenith_angle):
i@173 327 """ Correct zenith angle from Raymetrics convention (zenith = -90 degrees).
i@173 328
i@173 329 Parameters
i@173 330 ----------
i@173 331 zenith_angle : float
i@173 332 Zenith angle in Raymetrics convention.
i@173 333
i@173 334 Returns
i@173 335 -------
i@173 336 corrected_angle : float
i@173 337 Corrected zenith angle.
i@173 338 """
i@176 339 corrected_angle = 90 + zenith_angle
i@173 340 return corrected_angle
i@129 341
i@125 342 def _read_second_header_line(self, f):
i@125 343 """ Read the second line of a licel file. """
i@125 344 raw_info = {}
i@125 345
ioannis@170 346 second_line = f.readline().decode()
i@125 347 # Many Licel files don't follow the licel standard. Specifically, the
i@125 348 # measurement site is not always 8 characters, and can include white
i@125 349 # spaces. For this, the site name is detect everything before the first
i@125 350 # date. For efficiency, the first date is found by the first '/'.
i@125 351 # e.g. assuming a string like 'Site name 01/01/2010 ...'
i@125 352
i@125 353 site_name = second_line.split('/')[0][:-2]
i@125 354 clean_site_name = site_name.strip()
i@129 355 raw_info['site'] = clean_site_name
i@156 356 self.site = clean_site_name
i@125 357
i@125 358 raw_info.update(self.match_lines(second_line[len(clean_site_name) + 1:], self.licel_file_header_format[1]))
i@125 359 return raw_info
i@125 360
i@125 361 def _read_rest_of_header(self, f):
i@125 362 """ Read the rest of the header lines, after line 2. """
i@125 363 # Read third line
ioannis@170 364 third_line = f.readline().decode()
i@125 365 raw_dict = self.match_lines(third_line, self.licel_file_header_format[2])
i@125 366 return raw_dict
i@125 367
ioannis@144 368 def _calculate_physical(self):
ioannis@144 369 """ Calculate physical quantities from raw data for all channels in the file. """
ioannis@168 370 for channel in self.channels.values():
ioannis@144 371 channel.calculate_physical()
ioannis@144 372
ioannis@168 373 for photodiode in self.photodiodes.values():
ioannis@144 374 photodiode.calculate_physical()
ioannis@144 375
ulalume3@27 376 def duration(self):
i@128 377 """ Return the duration of the file.
i@128 378
ulalume3@105 379 Returns
ulalume3@105 380 -------
ulalume3@105 381 : float
ulalume3@105 382 The duration of the file in seconds.
ulalume3@105 383 """
ulalume3@27 384 dt = self.stop_time - self.start_time
ulalume3@27 385 return dt.seconds
ulalume3@47 386
ioannis@144 387 def import_header_only(self):
ioannis@146 388 """ Import only the header lines, without reading the actual data."""
i@156 389 with open(self.file_path, 'rb') as f:
ioannis@144 390 self.read_header(f)
ioannis@144 391
i@180 392 @property
i@180 393 def has_photodiode(self):
i@180 394 return len(self.photodiodes) != 0
i@180 395
i@116 396 @staticmethod
i@116 397 def match_lines(f1, f2):
i@116 398 list1 = f1.split()
i@116 399 list2 = f2.split()
i@116 400
i@116 401 if len(list1) != len(list2):
i@129 402 logging.debug("Channel parameter list has different length from LICEL specifications.")
i@116 403 logging.debug("List 1: %s" % list1)
i@116 404 logging.debug("List 2: %s" % list2)
i@129 405
ioannis@168 406 combined = list(zip(list2, list1))
i@116 407 combined = dict(combined)
i@116 408 return combined
i@116 409
ulalume3@47 410
i@128 411 class LicelChannel(LidarChannel):
ulalume3@105 412
i@128 413 def __init__(self):
i@128 414 self.name = None
i@128 415 self.resolution = None
i@128 416 self.points = None
i@128 417 self.wavelength = None
i@128 418 self.laser_used = None
i@116 419
i@128 420 self.rc = []
i@128 421 self.raw_info = []
i@128 422 self.laser_shots = []
i@128 423 self.duration = []
i@128 424 self.discriminator = []
i@128 425 self.hv = []
i@128 426 self.data = {}
ulalume3@47 427
i@131 428 def append_file(self, current_file, file_channel):
i@128 429 """ Append file to the current object """
ulalume3@47 430
i@131 431 self._assign_properties(current_file, file_channel)
i@128 432
i@128 433 self.binwidth = self.resolution * 2. / c # in seconds
i@128 434 self.z = file_channel.z
ulalume3@47 435
i@131 436 self.data[current_file.start_time] = file_channel.data
i@128 437 self.raw_info.append(file_channel.raw_info)
i@180 438
i@128 439 self.duration.append(file_channel.duration)
i@182 440 self.laser_shots.append(file_channel.number_of_shots)
i@128 441 self.discriminator.append(file_channel.discriminator)
i@128 442 self.hv.append(file_channel.hv)
ulalume3@47 443
i@180 444 @property
i@180 445 def number_of_shots(self):
i@180 446 """ Redundant, kept here for backward compatibility """
i@180 447 return self.laser_shots
i@180 448
i@131 449 def _assign_properties(self, current_file, file_channel):
i@128 450 self._assign_unique_property('name', file_channel.channel_name)
i@128 451 self._assign_unique_property('resolution', file_channel.dz)
i@128 452 self._assign_unique_property('wavelength', file_channel.wavelength)
i@128 453 self._assign_unique_property('points', file_channel.data_points)
i@128 454 self._assign_unique_property('adcbits', file_channel.adcbits)
i@128 455 self._assign_unique_property('active', file_channel.active)
i@129 456 self._assign_unique_property('laser_used', file_channel.laser_used)
i@128 457 self._assign_unique_property('analog_photon_string', file_channel.analog_photon_string)
i@181 458 self._assign_unique_property('latitude', current_file.latitude)
i@181 459 self._assign_unique_property('longitude', current_file.longitude)
ulalume3@47 460
i@128 461 def _assign_unique_property(self, property_name, value):
i@128 462
i@128 463 current_value = getattr(self, property_name, None)
i@128 464
i@128 465 if current_value is None:
i@128 466 setattr(self, property_name, value)
binietoglou@0 467 else:
i@128 468 if current_value != value:
i@128 469 raise ValueError('Cannot combine channels with different values of {0}.'.format(property_name))
ioannis@22 470
i@116 471 @property
i@116 472 def is_analog(self):
i@128 473 return self.analog_photon_string == 'an'
i@128 474
i@128 475 @property
i@128 476 def is_photon_counting(self):
i@128 477 return self.analog_photon_string == 'ph'
i@116 478
i@128 479 def __unicode__(self):
i@128 480 return "<Licel channel: %s>" % self.name
i@128 481
i@128 482 def __str__(self):
ioannis@168 483 return str(self).encode('utf-8')
i@130 484
i@130 485
i@130 486 class PhotodiodeChannel(LicelChannel):
i@130 487
i@131 488 def _assign_properties(self, current_channel, file_channel):
i@130 489 """ In contrast with normal channels, don't check for constant points."""
i@130 490 self._assign_unique_property('name', file_channel.channel_name)
i@130 491 self._assign_unique_property('resolution', file_channel.dz)
i@130 492 self._assign_unique_property('wavelength', file_channel.wavelength)
i@130 493 self._assign_unique_property('adcbits', file_channel.adcbits)
i@130 494 self._assign_unique_property('active', file_channel.active)
i@130 495 self._assign_unique_property('laser_used', file_channel.laser_used)
i@130 496 self._assign_unique_property('adcbits', file_channel.adcbits)
i@130 497 self._assign_unique_property('analog_photon_string', file_channel.analog_photon_string)
i@130 498
ulalume3@47 499
binietoglou@0 500 class LicelLidarMeasurement(BaseLidarMeasurement):
ulalume3@47 501
i@125 502 file_class = LicelFile
i@125 503 channel_class = LicelChannel
i@130 504 photodiode_class = PhotodiodeChannel
i@125 505
ioannis@186 506 def __init__(self, file_list=None, use_id_as_name=False, get_name_by_order=False, licel_timezone='UTC'):
i@116 507 self.raw_info = {} # Keep the raw info from the files
i@116 508 self.durations = {} # Keep the duration of the files
i@116 509 self.laser_shots = []
i@116 510
ulalume3@47 511 self.use_id_as_name = use_id_as_name
ioannis@186 512 self.get_name_by_order = get_name_by_order
i@101 513 self.licel_timezone = licel_timezone
i@130 514 self.photodiodes = {}
i@130 515
ulalume3@92 516 super(LicelLidarMeasurement, self).__init__(file_list)
ulalume3@47 517
ulalume3@92 518 def _import_file(self, filename):
i@130 519
binietoglou@0 520 if filename in self.files:
i@116 521 logger.warning("File has been imported already: %s" % filename)
binietoglou@0 522 else:
i@117 523 logger.debug('Importing file {0}'.format(filename))
i@173 524 current_file = self.file_class(filename, use_id_as_name=self.use_id_as_name,
ioannis@186 525 get_name_by_order=self.get_name_by_order,
i@176 526 licel_timezone=self.licel_timezone)
i@156 527 self.raw_info[current_file.file_path] = current_file.raw_info
i@156 528 self.durations[current_file.file_path] = current_file.duration()
i@104 529
victor@84 530 file_laser_shots = []
ulalume3@47 531
i@125 532 self._create_or_append_channel(current_file)
i@104 533
victor@84 534 self.laser_shots.append(file_laser_shots)
i@156 535 self.files.append(current_file.file_path)
ulalume3@47 536
i@125 537 def _create_or_append_channel(self, current_file):
i@125 538
i@125 539 for channel_name, channel in current_file.channels.items():
i@125 540 if channel_name not in self.channels:
i@125 541 self.channels[channel_name] = self.channel_class()
i@131 542 self.channels[channel_name].append_file(current_file, channel)
i@125 543
i@130 544 for photodiode_name, photodiode in current_file.photodiodes.items():
i@130 545 if photodiode_name not in self.photodiodes:
i@130 546 self.photodiodes[photodiode_name] = self.photodiode_class()
i@131 547 self.photodiodes[photodiode_name].append_file(current_file, photodiode)
i@130 548
binietoglou@0 549 def append(self, other):
binietoglou@0 550
binietoglou@0 551 self.start_times.extend(other.start_times)
binietoglou@0 552 self.stop_times.extend(other.stop_times)
binietoglou@0 553
binietoglou@0 554 for channel_name, channel in self.channels.items():
binietoglou@0 555 channel.append(other.channels[channel_name])
binietoglou@0 556
ioannis@22 557 def _get_duration(self, raw_start_in_seconds):
ulalume3@92 558 """ Return the duration for a given time scale. If only a single
ioannis@22 559 file is imported, then this cannot be guessed from the time difference
ioannis@22 560 and the raw_info of the file are checked.
ulalume3@92 561 """
ulalume3@47 562
ulalume3@47 563 if len(raw_start_in_seconds) == 1: # If only one file imported
ioannis@168 564 duration = next(iter(self.durations.values())) # Get the first (and only) raw_info
ioannis@31 565 duration_sec = duration
ioannis@22 566 else:
ioannis@22 567 duration_sec = np.diff(raw_start_in_seconds)[0]
binietoglou@0 568
ioannis@22 569 return duration_sec
i@104 570
i@116 571 def _get_custom_variables(self, channel_names):
i@116 572
i@116 573 daq_ranges = np.ma.masked_all(len(channel_names))
i@116 574 for n, channel_name in enumerate(channel_names):
i@116 575 channel = self.channels[channel_name]
i@116 576 if channel.is_analog:
i@116 577 unique_values = list(set(channel.discriminator))
i@116 578 if len(unique_values) > 1:
i@116 579 logger.warning('More than one discriminator levels for channel {0}: {1}'.format(channel_name, unique_values))
i@116 580 daq_ranges[n] = unique_values[0]
i@116 581
i@116 582 laser_shots = []
i@116 583 for channel_name in channel_names:
i@116 584 channel = self.channels[channel_name]
i@116 585 laser_shots.append(channel.laser_shots)
i@116 586
ioannis@144 587 try:
ioannis@144 588 laser_shots = np.vstack(laser_shots).T
ioannis@144 589 except Exception as e:
ioannis@144 590 logger.error('Could not read laser shots as an array. Maybe files contain different number of channels?')
ioannis@144 591 raise e
i@116 592
victor@84 593 params = [{
i@104 594 "name": "DAQ_Range",
i@104 595 "dimensions": ('channels',),
i@104 596 "type": 'd',
i@116 597 "values": daq_ranges,
i@104 598 }, {
i@104 599 "name": "Laser_Shots",
i@104 600 "dimensions": ('time', 'channels',),
i@104 601 "type": 'i',
i@116 602 "values": laser_shots,
i@104 603 },
victor@84 604 ]
i@104 605
victor@84 606 return params
i@104 607
i@116 608 def _get_custom_global_attributes(self):
i@116 609 """
i@116 610 NetCDF global attributes that should be included
i@116 611 in the final NetCDF file.
i@116 612
i@116 613 Currently the method assumes that all files in the measurement object have the same altitude, lat and lon
i@116 614 properties.
i@116 615 """
i@117 616 logger.debug('Setting custom global attributes')
i@117 617 logger.debug('raw_info keys: {0}'.format(self.raw_info.keys()))
i@117 618
victor@87 619 params = [{
i@104 620 "name": "Altitude_meter_asl",
i@137 621 "value": float(self.raw_info[self.files[0]]["altitude"])
i@104 622 }, {
i@104 623 "name": "Latitude_degrees_north",
i@137 624 "value": float(self.raw_info[self.files[0]]["latitude"])
i@104 625 }, {
i@104 626 "name": "Longitude_degrees_east",
i@137 627 "value": float(self.raw_info[self.files[0]]["longitude"])
i@104 628 },
victor@87 629 ]
victor@87 630
victor@87 631 return params
ioannis@22 632
i@117 633 def subset_by_channels(self, channel_subset):
i@117 634 """
i@117 635 Create a measurement object containing only a subset of channels.
i@117 636
i@117 637 This method overrides the parent method to add some licel-spefic parameters to the new object.
i@117 638
i@117 639 Parameters
i@117 640 ----------
i@117 641 channel_subset : list
i@117 642 A list of channel names (str) to be included in the new measurement object.
i@117 643
i@117 644 Returns
i@117 645 -------
i@117 646 m : BaseLidarMeasurements object
i@117 647 A new measurements object
i@117 648 """
i@117 649 new_measurement = super(LicelLidarMeasurement, self).subset_by_channels(channel_subset)
i@117 650
i@117 651 new_measurement.raw_info = copy.deepcopy(self.raw_info)
i@117 652 new_measurement.durations = copy.deepcopy(self.durations)
i@117 653 new_measurement.laser_shots = copy.deepcopy(self.laser_shots)
i@117 654
i@117 655 return new_measurement
i@117 656
i@119 657 def subset_by_time(self, channel_subset):
i@119 658 """
i@119 659 Subsetting by time does not work yet with Licel files.
i@117 660
i@119 661 This requires changes in generic.py
i@119 662 """
i@119 663 raise NotImplementedError("Subsetting by time, not yet implemented for Licel files.")
i@151 664
i@151 665 def print_channels(self):
i@151 666 """ Print the available channel information on the screen.
i@151 667 """
i@151 668 keys = sorted(self.channels.keys())
i@151 669
i@151 670 print("Name Wavelength Mode Resolution Bins ")
i@151 671
i@151 672 for key in keys:
i@151 673 channel = self.channels[key]
i@151 674 print("{0:<3} {1:<10} {2:<4} {3:<10} {4:<5}".format(channel.name, channel.wavelength,
ioannis@154 675 channel.analog_photon_string, channel.resolution,
ioannis@154 676 channel.points))
i@150 677
ioannis@165 678
ioannis@165 679 class LicelDivaLidarMeasurement(DivaConverterMixin, LicelLidarMeasurement):
i@150 680 pass

mercurial