Mon, 26 Feb 2018 18:47:42 +0200
Improved handling of photodiodes in licel files.
atmospheric_lidar/licel.py | file | annotate | diff | comparison | revisions | |
atmospheric_lidar/raymetrics.py | file | annotate | diff | comparison | revisions |
--- a/atmospheric_lidar/licel.py Mon Feb 26 18:08:00 2018 +0200 +++ b/atmospheric_lidar/licel.py Mon Feb 26 18:47:42 2018 +0200 @@ -12,7 +12,7 @@ c = 299792458.0 # Speed of light -class LicelFileChannel: +class LicelChannelData: """ A class representing a single channel found in a single Licel file.""" def __init__(self, raw_info, raw_data, duration, use_id_as_name=False): @@ -53,6 +53,10 @@ self.discriminator = float(raw_info['discriminator']) @property + def is_photodiode(self): + return self.id[0:2] == 'PD' + + @property def wavelength(self): """ Property describing the nominal wavelength of the channel. @@ -117,22 +121,17 @@ if self.is_analog: # If the channel is in analog mode ADCrange = self.discriminator # Discriminator value already in mV - channel_data = norm * ADCrange / ((2 ** self.adcbits) - 1) # TODO: Check this. Agrees with Licel docs, but differs from their LabView code. - # print ADCb, ADCRange,cdata,norm + if self.is_photodiode and (self.adcbits == 0): + logger.warning("Changing adcbits to 1. This is a bug in current licel format.") + channel_data = norm * ADCrange / (2 ** self.adcbits) + else: + channel_data = norm * ADCrange / ((2 ** self.adcbits) - 1) # TODO: Check this. Agrees with Licel docs, but differs from their LabView code. + else: - # If the channel is in photoncounting mode - # Frequency deduced from range resolution! (is this ok?) - # c = 300 # The result will be in MHZ - # SR = c/(2*dz) # To account for pulse folding - # channel_data = norm*SR - # CHANGE: - # For the SCC the data are needed in photons - channel_data = norm * self.number_of_shots - # print res,c,cdata,norm + channel_data = norm * self.number_of_shots # Calculate Z - self.z = np.array([dz * bin_number + dz / 2.0 for bin_number in range(self.data_points)]) self.dz = dz self.data = channel_data @@ -151,7 +150,7 @@ 'LS1 rate_1 LS2 rate_2 number_of_datasets', ] 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' - file_channel_class = LicelFileChannel + channel_data_class = LicelChannelData def __init__(self, file_path, use_id_as_name=False, licel_timezone="UTC"): """ @@ -174,13 +173,16 @@ self.stop_time = None self.licel_timezone = licel_timezone self._import_file(file_path) - self.calculate_physical() + self._calculate_physical() - def calculate_physical(self): + def _calculate_physical(self): """ Calculate physical quantities from raw data for all channels in the file. """ for channel in self.channels.itervalues(): channel.calculate_physical() + for photodiode in self.photodiodes.itervalues(): + photodiode.calculate_physical() + def _import_file(self, file_path): """ Read the header info and data of the Licel file. @@ -190,6 +192,7 @@ The path to the Licel file. """ channels = {} + photodiodes = {} with open(file_path, 'rb') as f: @@ -206,18 +209,24 @@ if (a[0] != 13) | (b[0] != 10): logging.warning("No end of line found after record. File could be corrupt: %s" % file_path) - channel = self.file_channel_class(current_channel_info, raw_data, self.duration(), - use_id_as_name=self.use_id_as_name) - channel_name = channel.channel_name + channel = self.channel_data_class(current_channel_info, raw_data, self.duration(), + use_id_as_name=self.use_id_as_name) - if channel_name in channels.keys(): - # If the analog/photon naming scheme is not enough, find a new one! - raise IOError('Trying to import two channels with the same name') - - channels[channel_name] = channel + # Assign the channel either as normal channel or photodiode + if channel.is_photodiode: + if channel.channel_name in photodiodes.keys(): + # Check if current naming convention produces unique files + raise IOError('Trying to import two photodiodes with the same name') + photodiodes[channel.channel_name] = channel + else: + if channel.channel_name in channels.keys(): + # Check if current naming convention does not produce unique files + raise IOError('Trying to import two channels with the same name') + channels[channel.channel_name] = channel self.channels = channels + self.photodiodes = photodiodes def read_header(self, f): """ Read the header of an open Licel file. @@ -395,12 +404,27 @@ def __str__(self): return unicode(self).encode('utf-8') - + + +class PhotodiodeChannel(LicelChannel): + + def _assign_properties(self, file_channel): + """ In contrast with normal channels, don't check for constant points.""" + self._assign_unique_property('name', file_channel.channel_name) + self._assign_unique_property('resolution', file_channel.dz) + self._assign_unique_property('wavelength', file_channel.wavelength) + self._assign_unique_property('adcbits', file_channel.adcbits) + self._assign_unique_property('active', file_channel.active) + self._assign_unique_property('laser_used', file_channel.laser_used) + self._assign_unique_property('adcbits', file_channel.adcbits) + self._assign_unique_property('analog_photon_string', file_channel.analog_photon_string) + class LicelLidarMeasurement(BaseLidarMeasurement): file_class = LicelFile channel_class = LicelChannel + photodiode_class = PhotodiodeChannel def __init__(self, file_list=None, use_id_as_name=False, licel_timezone='UTC'): self.raw_info = {} # Keep the raw info from the files @@ -409,9 +433,12 @@ self.use_id_as_name = use_id_as_name self.licel_timezone = licel_timezone + self.photodiodes = {} + super(LicelLidarMeasurement, self).__init__(file_list) def _import_file(self, filename): + if filename in self.files: logger.warning("File has been imported already: %s" % filename) else: @@ -434,6 +461,11 @@ self.channels[channel_name] = self.channel_class() self.channels[channel_name].append_file(current_file.start_time, channel) + for photodiode_name, photodiode in current_file.photodiodes.items(): + if photodiode_name not in self.photodiodes: + self.photodiodes[photodiode_name] = self.photodiode_class() + self.photodiodes[photodiode_name].append_file(current_file.start_time, photodiode) + def append(self, other): self.start_times.extend(other.start_times)
--- a/atmospheric_lidar/raymetrics.py Mon Feb 26 18:08:00 2018 +0200 +++ b/atmospheric_lidar/raymetrics.py Mon Feb 26 18:47:42 2018 +0200 @@ -5,32 +5,11 @@ import numpy as np import pytz -from .licel import LicelFile, LicelLidarMeasurement, LicelChannel +from .licel import LicelFile, LicelLidarMeasurement, LicelChannel, PhotodiodeChannel logger = logging.getLogger(__name__) -class RaymetricsLidarMeasurement(LicelLidarMeasurement): - - def __init__(self, file_list=None, use_id_as_name=False, licel_timezone='UTC'): - self.photodiodes = {} # Add photodiode dictionary - super(RaymetricsLidarMeasurement, self).__init__(file_list=file_list, - use_id_as_name=use_id_as_name, - licel_timezone=licel_timezone) - - def _create_or_append_channel(self, current_file): - - for channel_name, channel in current_file.channels.items(): - if channel_name[0:2] == 'PD': - if channel_name not in self.photodiodes: - self.photodiodes[channel_name] = PhotodiodeChannel() - self.photodiodes[channel_name].append_file(current_file.start_time, channel) - else: - if channel_name not in self.channels: - self.channels[channel_name] = self.channel_class() - self.channels[channel_name].append_file(current_file.start_time, channel) - - class ScanningFile(LicelFile): licel_file_header_format = ['filename', 'start_date start_time end_date end_time altitude longitude latitude zenith_angle azimuth_angle temperature pressure', @@ -65,7 +44,6 @@ class ScanningChannel(LicelChannel): - def __init__(self): self.azimuth_start = None self.azimuth_stop = None @@ -77,51 +55,7 @@ super(ScanningChannel, self).__init__() - -class ScanningLidarMeasurement(RaymetricsLidarMeasurement): +class ScanningLidarMeasurement(LicelLidarMeasurement): file_class = ScanningFile channel_class = LicelChannel - - -class PhotodiodeChannel(LicelChannel): - - def _assign_properties(self, file_channel): - """ In contrast with normal channels, don't check for constant points.""" - self._assign_unique_property('name', file_channel.channel_name) - self._assign_unique_property('resolution', file_channel.dz) - self._assign_unique_property('wavelength', file_channel.wavelength) - self._assign_unique_property('adcbits', file_channel.adcbits) - self._assign_unique_property('active', file_channel.active) - self._assign_unique_property('laser_used', file_channel.laser_used) - self._assign_unique_property('adcbits', file_channel.adcbits) - self._assign_unique_property('analog_photon_string', file_channel.analog_photon_string) - - # def calculate_physical(self): - # """ Calculate physically-meaningful data from photodiode channels: - # - # * In case of analog signals, the data are converted to mV. - # * In case of photon counting signals, data are stored as number of photons. - # - # In addition, some ancillary variables are also calculated (z, dz, number_of_bins). - # """ - # data = self.raw_data - # - # norm = data / float(self.number_of_shots) - # dz = self.bin_width - # - # if self.is_analog: - # # If the channel is in analog mode - # ADCrange = self.discriminator # Discriminator value already in mV - # if self.adcbits == 0: - # logger.warning("Changing adcbits to 1. This is a bug in current licel format.") - # channel_data = norm * ADCrange / ((2 ** self.adcbits) ) - # else: - # channel_data = norm * ADCrange / ((2 ** self.adcbits) - 1) - # - # else: - # channel_data = norm * self.number_of_shots - # - # # Calculate Z - # self.z = np.array([dz * bin_number + dz / 2.0 for bin_number in range(self.data_points)]) - # self.dz = dz - # self.data = channel_data \ No newline at end of file + photodiode_class = PhotodiodeChannel