atmospheric_lidar/licel.py

Tue, 12 Dec 2017 11:54:57 +0200

author
Iannis <i.binietoglou@impworks.gr>
date
Tue, 12 Dec 2017 11:54:57 +0200
changeset 101
3e97bd360eda
parent 92
6d26002aaeed
child 102
b71252bb7f87
permissions
-rwxr-xr-x

First attempt to support timezones in licel files.

ioannis@55 1 import datetime
ioannis@55 2 import logging
ioannis@55 3
binietoglou@0 4 import numpy as np
i@101 5 import pytz
ioannis@55 6
victor@89 7 from .systems.musa import musa_2009_netcdf_parameters
victor@89 8 from .systems.musa import musa_netcdf_parameters
victor@89 9
ulalume3@27 10 from generic import BaseLidarMeasurement, LidarChannel
binietoglou@0 11
i@101 12 logger = logging.getLogger(__name__)
i@101 13
i@101 14
binietoglou@0 15 licel_file_header_format = ['Filename',
ulalume3@47 16 'StartDate StartTime EndDate EndTime Altitude Longtitude Latitude ZenithAngle',
ulalume3@47 17 # Appart from Site that is read manually
binietoglou@0 18 'LS1 Rate1 LS2 Rate2 DataSets', ]
binietoglou@0 19 licel_file_channel_format = 'Active AnalogPhoton LaserUsed DataPoints 1 HV BinW Wavelength d1 d2 d3 d4 ADCbits NShots Discriminator ID'
binietoglou@0 20
ioannis@22 21
binietoglou@0 22 class LicelFile:
i@101 23 def __init__(self, filename, use_id_as_name=False, licel_timezone="UTC"):
ulalume3@47 24 self.filename = filename
ioannis@55 25 self.use_id_as_name = use_id_as_name
binietoglou@0 26 self.start_time = None
binietoglou@0 27 self.stop_time = None
binietoglou@0 28 self.import_file(filename)
binietoglou@0 29 self.calculate_physical()
i@101 30 self.licel_timezone=licel_timezone
ulalume3@47 31
binietoglou@0 32 def calculate_physical(self):
binietoglou@0 33 for channel in self.channels.itervalues():
binietoglou@0 34 channel.calculate_physical()
ulalume3@47 35
binietoglou@0 36 def import_file(self, filename):
binietoglou@0 37 """Imports a licel file.
binietoglou@0 38 Input: filename
binietoglou@0 39 Output: object """
binietoglou@0 40
binietoglou@0 41 channels = {}
binietoglou@33 42
binietoglou@33 43 with open(filename, 'rb') as f:
binietoglou@33 44
binietoglou@33 45 self.read_header(f)
ulalume3@47 46
binietoglou@33 47 # Check the complete header is read
i@101 48 f.readline()
binietoglou@0 49
binietoglou@33 50 # Import the data
binietoglou@33 51 for current_channel_info in self.channel_info:
binietoglou@33 52 raw_data = np.fromfile(f, 'i4', int(current_channel_info['DataPoints']))
binietoglou@33 53 a = np.fromfile(f, 'b', 1)
binietoglou@33 54 b = np.fromfile(f, 'b', 1)
ulalume3@47 55
binietoglou@33 56 if (a[0] != 13) | (b[0] != 10):
ioannis@55 57 logging.warning("No end of line found after record. File could be corrupt: %s" % filename)
ioannis@55 58 channel = LicelFileChannel(current_channel_info, raw_data, self.duration(),
ioannis@55 59 use_id_as_name=self.use_id_as_name)
ulalume3@47 60
binietoglou@33 61 channel_name = channel.channel_name
ulalume3@47 62
binietoglou@33 63 if channel_name in channels.keys():
binietoglou@33 64 # If the analog/photon naming scheme is not enough, find a new one!
binietoglou@33 65 raise IOError('Trying to import two channels with the same name')
ulalume3@47 66
binietoglou@33 67 channels[channel_name] = channel
ulalume3@47 68
binietoglou@33 69 self.channels = channels
ulalume3@47 70
binietoglou@33 71 def read_header(self, f):
binietoglou@33 72 """ Read the header of a open file f.
binietoglou@33 73
binietoglou@33 74 Returns raw_info and channel_info. Updates some object properties. """
ulalume3@47 75
ulalume3@47 76 # Read the first 3 lines of the header
binietoglou@0 77 raw_info = {}
binietoglou@33 78 channel_info = []
ulalume3@47 79
binietoglou@33 80 # Read first line
binietoglou@33 81 raw_info['Filename'] = f.readline().strip()
ulalume3@47 82
binietoglou@33 83 # Read second line
binietoglou@33 84 second_line = f.readline()
ulalume3@47 85
binietoglou@33 86 # Many licel files don't follow the licel standard. Specifically, the
binietoglou@33 87 # measurement site is not always 8 characters, and can include white
binietoglou@33 88 # spaces. For this, the site name is detect everything before the first
binietoglou@33 89 # date. For efficiency, the first date is found by the first '/'.
binietoglou@33 90 # e.g. assuming a string like 'Site name 01/01/2010 ...'
ulalume3@47 91
binietoglou@33 92 site_name = second_line.split('/')[0][:-2]
binietoglou@33 93 clean_site_name = site_name.strip()
binietoglou@33 94 raw_info['Site'] = clean_site_name
binietoglou@33 95 raw_info.update(match_lines(second_line[len(clean_site_name) + 1:], licel_file_header_format[1]))
ulalume3@47 96
binietoglou@33 97 # Read third line
binietoglou@33 98 third_line = f.readline()
binietoglou@33 99 raw_info.update(match_lines(third_line, licel_file_header_format[2]))
ulalume3@47 100
binietoglou@33 101 # Update the object properties based on the raw info
binietoglou@0 102 start_string = '%s %s' % (raw_info['StartDate'], raw_info['StartTime'])
binietoglou@0 103 stop_string = '%s %s' % (raw_info['EndDate'], raw_info['EndTime'])
binietoglou@0 104 date_format = '%d/%m/%Y %H:%M:%S'
ulalume3@47 105
i@101 106 try:
i@101 107 logger.debug('Creating timezone object %s' % self.licel_timezone)
i@101 108 timezone = pytz.timezone(self.licel_timezone)
i@101 109 except:
i@101 110 raise ValueError("Cloud not create time zone object %s" % self.licel_timezone)
i@101 111
i@101 112 # According to pytz docs, timezones do not work with default datetime constructor.
i@101 113 local_start_time = timezone.localize(datetime.datetime.strptime(start_string, date_format))
i@101 114 local_stop_time = timezone.localize(datetime.datetime.strptime(stop_string, date_format))
i@101 115
i@101 116 # Only save UTC time.
i@101 117 self.start_time = local_start_time.astimezone(pytz.utc)
i@101 118 self.stop_time = local_stop_time.astimezone(pytz.utc)
i@101 119
binietoglou@0 120 self.latitude = float(raw_info['Latitude'])
binietoglou@0 121 self.longitude = float(raw_info['Longtitude'])
ulalume3@47 122
binietoglou@0 123 # Read the rest of the header.
binietoglou@0 124 for c1 in range(int(raw_info['DataSets'])):
binietoglou@0 125 channel_info.append(match_lines(f.readline(), licel_file_channel_format))
ulalume3@47 126
binietoglou@33 127 self.raw_info = raw_info
binietoglou@33 128 self.channel_info = channel_info
ulalume3@47 129
ulalume3@27 130 def duration(self):
ulalume3@27 131 """ Return the duration of the file. """
ulalume3@27 132 dt = self.stop_time - self.start_time
ulalume3@27 133 return dt.seconds
ulalume3@47 134
ulalume3@47 135
binietoglou@0 136 class LicelFileChannel:
ulalume3@47 137 def __init__(self, raw_info=None, raw_data=None, duration=None, use_id_as_name=False):
binietoglou@0 138 self.raw_info = raw_info
binietoglou@0 139 self.raw_data = raw_data
ulalume3@27 140 self.duration = duration
ulalume3@47 141 self.use_id_as_name = use_id_as_name
ulalume3@47 142
binietoglou@0 143 @property
binietoglou@0 144 def wavelength(self):
binietoglou@0 145 if self.raw_info is not None:
binietoglou@0 146 wave_str = self.raw_info['Wavelength']
binietoglou@0 147 wavelength = wave_str.split('.')[0]
binietoglou@0 148 return int(wavelength)
binietoglou@0 149 else:
binietoglou@0 150 return None
ulalume3@47 151
binietoglou@0 152 @property
binietoglou@0 153 def channel_name(self):
binietoglou@0 154 '''
binietoglou@0 155 Construct the channel name adding analog photon info to avoid duplicates
ulalume3@47 156
ulalume3@47 157 If use_id_as_name is True, the channel name will be the transient digitizer ID (e.g. BT01).
ulalume3@47 158 This could be useful if the lidar system has multiple telescopes, so the descriptive name is
ulalume3@47 159 not unique.
binietoglou@0 160 '''
ulalume3@47 161 if self.use_id_as_name:
ulalume3@47 162 channel_name = self.raw_info['ID']
ulalume3@47 163 else:
ulalume3@47 164 acquisition_type = self.analog_photon_string(self.raw_info['AnalogPhoton'])
ulalume3@47 165 channel_name = "%s_%s" % (self.raw_info['Wavelength'], acquisition_type)
binietoglou@0 166 return channel_name
ulalume3@47 167
binietoglou@0 168 def analog_photon_string(self, analog_photon_number):
binietoglou@0 169 if analog_photon_number == '0':
binietoglou@0 170 string = 'an'
binietoglou@0 171 else:
binietoglou@0 172 string = 'ph'
binietoglou@0 173 return string
ulalume3@47 174
binietoglou@0 175 def calculate_physical(self):
binietoglou@0 176 data = self.raw_data
binietoglou@0 177
binietoglou@0 178 number_of_shots = float(self.raw_info['NShots'])
ulalume3@27 179 norm = data / number_of_shots
binietoglou@0 180 dz = float(self.raw_info['BinW'])
ulalume3@47 181
ulalume3@47 182 if self.raw_info['AnalogPhoton'] == '0':
binietoglou@0 183 # If the channel is in analog mode
binietoglou@0 184 ADCb = int(self.raw_info['ADCbits'])
ulalume3@47 185 ADCrange = float(self.raw_info['Discriminator']) * 1000 # Value in mV
ulalume3@47 186 channel_data = norm * ADCrange / ((2 ** ADCb) - 1)
ulalume3@47 187
binietoglou@0 188 # print ADCb, ADCRange,cdata,norm
binietoglou@0 189 else:
binietoglou@0 190 # If the channel is in photoncounting mode
binietoglou@0 191 # Frequency deduced from range resolution! (is this ok?)
binietoglou@0 192 # c = 300 # The result will be in MHZ
binietoglou@0 193 # SR = c/(2*dz) # To account for pulse folding
binietoglou@0 194 # channel_data = norm*SR
binietoglou@0 195 # CHANGE:
binietoglou@0 196 # For the SCC the data are needed in photons
ulalume3@47 197 channel_data = norm * number_of_shots
ulalume3@47 198 # print res,c,cdata,norm
binietoglou@0 199
ulalume3@47 200 # Calculate Z
ulalume3@47 201 number_of_bins = int(self.raw_info['DataPoints'])
ulalume3@47 202 self.z = np.array([dz * bin_number + dz / 2.0 for bin_number in range(number_of_bins)])
binietoglou@0 203 self.dz = dz
binietoglou@0 204 self.number_of_bins = number_of_bins
binietoglou@0 205 self.data = channel_data
ioannis@22 206
ulalume3@47 207
binietoglou@0 208 class LicelLidarMeasurement(BaseLidarMeasurement):
binietoglou@0 209 '''
binietoglou@0 210
binietoglou@0 211 '''
binietoglou@0 212 extra_netcdf_parameters = musa_netcdf_parameters
ulalume3@47 213 raw_info = {} # Keep the raw info from the files
ulalume3@27 214 durations = {} # Keep the duration of the files
victor@84 215 laser_shots = []
ulalume3@47 216
i@101 217 def __init__(self, file_list=None, use_id_as_name=False, licel_timezone='UTC'):
ulalume3@47 218 self.use_id_as_name = use_id_as_name
i@101 219 self.licel_timezone = licel_timezone
ulalume3@92 220 super(LicelLidarMeasurement, self).__init__(file_list)
ulalume3@47 221
ulalume3@92 222 def _import_file(self, filename):
binietoglou@0 223 if filename in self.files:
ioannis@55 224 logging.warning("File has been imported already: %s" % filename)
binietoglou@0 225 else:
i@101 226 current_file = LicelFile(filename, use_id_as_name=self.use_id_as_name, licel_timezone=self.licel_timezone)
ioannis@22 227 self.raw_info[current_file.filename] = current_file.raw_info
ulalume3@27 228 self.durations[current_file.filename] = current_file.duration()
victor@84 229
victor@84 230 file_laser_shots = []
ulalume3@47 231
binietoglou@0 232 for channel_name, channel in current_file.channels.items():
binietoglou@0 233 if channel_name not in self.channels:
ulalume3@27 234 self.channels[channel_name] = LicelChannel(channel)
ulalume3@47 235 self.channels[channel_name].data[current_file.start_time] = channel.data
victor@84 236 file_laser_shots.append(channel.raw_info['NShots'])
victor@84 237
victor@84 238 self.laser_shots.append(file_laser_shots)
binietoglou@0 239 self.files.append(current_file.filename)
ulalume3@47 240
binietoglou@0 241 def append(self, other):
binietoglou@0 242
binietoglou@0 243 self.start_times.extend(other.start_times)
binietoglou@0 244 self.stop_times.extend(other.stop_times)
binietoglou@0 245
binietoglou@0 246 for channel_name, channel in self.channels.items():
binietoglou@0 247 channel.append(other.channels[channel_name])
binietoglou@0 248
ioannis@22 249 def _get_duration(self, raw_start_in_seconds):
ulalume3@92 250 """ Return the duration for a given time scale. If only a single
ioannis@22 251 file is imported, then this cannot be guessed from the time difference
ioannis@22 252 and the raw_info of the file are checked.
ulalume3@92 253 """
ulalume3@47 254
ulalume3@47 255 if len(raw_start_in_seconds) == 1: # If only one file imported
ulalume3@47 256 duration = self.durations.itervalues().next() # Get the first (and only) raw_info
ioannis@31 257 duration_sec = duration
ioannis@22 258 else:
ioannis@22 259 duration_sec = np.diff(raw_start_in_seconds)[0]
binietoglou@0 260
ioannis@22 261 return duration_sec
victor@84 262
ulalume3@92 263 def get_custom_channel_parameters(self):
victor@84 264 params = [{
victor@84 265 "name": "DAQ_Range",
victor@84 266 "dimensions": ('channels',),
victor@84 267 "type": 'd',
victor@84 268 "values": [self.channels[x].raw_info['Discriminator'] for x in self.channels.keys()]
victor@84 269 }, {
victor@84 270 "name": "LR_Input",
victor@84 271 "dimensions": ('channels',),
victor@84 272 "type": 'i',
victor@84 273 "values": [self.channels[x].raw_info['LaserUsed'] for x in self.channels.keys()]
victor@84 274 }, {
victor@84 275 "name": "Laser_Shots",
victor@84 276 "dimensions": ('time', 'channels',),
victor@84 277 "type": 'i',
victor@84 278 "values": self.laser_shots
victor@84 279 },
victor@84 280 ]
victor@84 281
victor@84 282 return params
victor@87 283
ulalume3@92 284 def get_custom_general_parameters(self):
victor@87 285 params = [{
victor@87 286 "name": "Altitude_meter_asl",
victor@87 287 "value": self.raw_info[ self.files[0] ]["Altitude"]
victor@87 288 }, {
victor@87 289 "name": "Latitude_degrees_north",
victor@87 290 "value": self.raw_info[ self.files[0] ]["Latitude"]
victor@87 291 }, {
victor@87 292 "name": "Longitude_degrees_east",
victor@87 293 "value": self.raw_info[ self.files[0] ]["Longtitude"]
victor@87 294 },
victor@87 295 ]
victor@87 296
victor@87 297 return params
ioannis@22 298
ulalume3@47 299
ulalume3@27 300 class LicelChannel(LidarChannel):
binietoglou@0 301 def __init__(self, channel_file):
binietoglou@33 302 c = 299792458.0 # Speed of light
binietoglou@0 303 self.wavelength = channel_file.wavelength
binietoglou@0 304 self.name = channel_file.channel_name
ulalume3@27 305 self.binwidth = channel_file.dz * 2 / c # in seconds
binietoglou@0 306 self.data = {}
binietoglou@0 307 self.resolution = channel_file.dz
ulalume3@47 308 self.z = np.arange(
ulalume3@47 309 channel_file.number_of_bins) * self.resolution + self.resolution / 2.0 # Change: add half bin in the z
binietoglou@0 310 self.points = channel_file.number_of_bins
binietoglou@0 311 self.rc = []
ulalume3@47 312 self.duration = channel_file.duration
victor@84 313 self.raw_info = channel_file.raw_info
victor@84 314
binietoglou@0 315 def append(self, other):
binietoglou@0 316 if self.info != other.info:
binietoglou@0 317 raise ValueError('Channel info are different. Data can not be combined.')
binietoglou@0 318
binietoglou@0 319 self.data = np.vstack([self.data, other.data])
binietoglou@0 320
binietoglou@0 321 def __unicode__(self):
ioannis@26 322 return "<Licel channel: %s>" % self.name
ulalume3@47 323
binietoglou@0 324 def __str__(self):
binietoglou@0 325 return unicode(self).encode('utf-8')
binietoglou@0 326
ioannis@22 327
binietoglou@0 328 class Licel2009LidarMeasurement(LicelLidarMeasurement):
binietoglou@0 329 extra_netcdf_parameters = musa_2009_netcdf_parameters
binietoglou@0 330
ioannis@22 331
ulalume3@47 332 def match_lines(f1, f2):
binietoglou@0 333 list1 = f1.split()
binietoglou@0 334 list2 = f2.split()
ulalume3@47 335
binietoglou@0 336 if len(list1) != len(list2):
ioannis@55 337 logging.debug("Channel parameter list has different length from licel specifications.")
ioannis@55 338 logging.debug("List 1: %s" % list1)
ioannis@55 339 logging.debug("List 2: %s" % list2)
ulalume3@47 340 combined = zip(list2, list1)
binietoglou@0 341 combined = dict(combined)
binietoglou@0 342 return combined

mercurial