Improved handling of photodiodes in licel files.

Mon, 26 Feb 2018 18:47:42 +0200

author
Iannis <i.binietoglou@impworks.gr>
date
Mon, 26 Feb 2018 18:47:42 +0200
changeset 130
24e450fdd25c
parent 129
6f902a45b83d
child 131
7225c844bdc5

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

mercurial