atmospheric_lidar/licel.py

Wed, 28 Nov 2018 22:27:28 +0200

author
Iannis <ioannis@inoe.ro>
date
Wed, 28 Nov 2018 22:27:28 +0200
changeset 168
9fed2446a59f
parent 165
392a714d12a2
child 170
9face926ccab
permissions
-rwxr-xr-x

First attempt for python 3 compatibility.

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

mercurial