atmospheric_lidar/generic.py

Sat, 17 Apr 2021 14:24:15 +0000

author
Ioannis Binietoglou <ulalume3@gmail.com>
date
Sat, 17 Apr 2021 14:24:15 +0000
changeset 212
0391ec7ba69c
parent 196
27a0024331fd
permissions
-rwxr-xr-x

Update licel.ipynb

binietoglou@0 1 import datetime
ulalume3@92 2 import logging
binietoglou@0 3 from operator import itemgetter
i@116 4 import itertools
ioannis@194 5 import collections
binietoglou@0 6
binietoglou@0 7 import matplotlib as mpl
ioannis@55 8 import netCDF4 as netcdf
ioannis@55 9 import numpy as np
binietoglou@0 10 from matplotlib import pyplot as plt
ioannis@55 11 from matplotlib.ticker import ScalarFormatter
binietoglou@0 12
ulalume3@92 13 NETCDF_FORMAT = 'NETCDF4' # choose one of 'NETCDF3_CLASSIC', 'NETCDF3_64BIT', 'NETCDF4_CLASSIC' and 'NETCDF4'
binietoglou@0 14
i@101 15 logger = logging.getLogger(__name__)
i@101 16
binietoglou@0 17
binietoglou@39 18 class BaseLidarMeasurement(object):
ulalume3@92 19 """
ulalume3@92 20 This class represents a general measurement object, independent of the input files.
binietoglou@0 21
binietoglou@0 22 Each subclass should implement the following:
ulalume3@92 23 * the _import_file method;
ulalume3@92 24 * set the "extra_netcdf_parameters" variable to a dictionary that includes the appropriate parameters;
ulalume3@92 25
ulalume3@92 26 You can override the set_PT method to define a custom procedure to get ground temperature and pressure.
binietoglou@0 27
ulalume3@93 28 The class assumes that the input files are consecutive, i.e. there are no measurements gaps.
binietoglou@0 29 """
i@103 30 extra_netcdf_parameters = None
i@103 31
ulalume3@92 32 def __init__(self, file_list=None):
ulalume3@92 33 """
ulalume3@92 34 This is run when creating a new object.
ulalume3@92 35
ulalume3@92 36 Parameters
ulalume3@92 37 ----------
i@150 38 file_list : list or str
i@150 39 A list of the full paths to the input file(s).
ulalume3@92 40 """
binietoglou@0 41 self.info = {}
binietoglou@0 42 self.dimensions = {}
ioannis@50 43 self.variables = {}
ioannis@194 44 self.channels = collections.OrderedDict()
binietoglou@0 45 self.attributes = {}
binietoglou@0 46 self.files = []
binietoglou@0 47 self.dark_measurement = None
ioannis@50 48
ulalume3@92 49 if file_list:
ulalume3@92 50 self._import_files(file_list)
ioannis@50 51
ulalume3@92 52 def _import_files(self, file_list):
ulalume3@92 53 """
ulalume3@92 54 Imports a list of files, and updates the object parameters.
ulalume3@92 55
ulalume3@92 56 Parameters
ulalume3@92 57 ----------
ulalume3@92 58 file_list : list
ulalume3@92 59 A list of the full paths to the input file.
ulalume3@92 60 """
ulalume3@92 61 for f in file_list:
ulalume3@92 62 self._import_file(f)
binietoglou@0 63 self.update()
binietoglou@0 64
ulalume3@92 65 def _import_file(self, filename):
ulalume3@92 66 """
ulalume3@92 67 Reads a single lidar file.
ulalume3@92 68
ulalume3@92 69 This method should be overwritten at all subclasses.
ulalume3@92 70
ulalume3@92 71 Parameters
ulalume3@92 72 ----------
ulalume3@92 73 filename : str
ulalume3@92 74 Path to the lidar file.
ulalume3@92 75 """
binietoglou@0 76 raise NotImplementedError('Importing files should be defined in the instrument-specific subclass.')
binietoglou@0 77
binietoglou@0 78 def update(self):
ulalume3@92 79 """
ulalume3@92 80 Update the info dictionary, variables, and dimensions of the measurement object
ulalume3@92 81 based on the information found in the channels objects.
binietoglou@0 82
i@132 83 Notes
i@132 84 -----
binietoglou@0 85 Reading of the scan_angles parameter is not implemented.
ulalume3@92 86 """
victor@87 87 # Initialize
ioannis@50 88 start_time = []
binietoglou@0 89 stop_time = []
binietoglou@0 90 points = []
binietoglou@0 91 all_time_scales = []
i@116 92 channel_names = []
ioannis@50 93
binietoglou@0 94 # Get the information from all the channels
binietoglou@0 95 for channel_name, channel in self.channels.items():
binietoglou@0 96 channel.update()
binietoglou@0 97 start_time.append(channel.start_time)
binietoglou@0 98 stop_time.append(channel.stop_time)
binietoglou@0 99 points.append(channel.points)
binietoglou@0 100 all_time_scales.append(channel.time)
i@116 101 channel_names.append(channel_name)
ioannis@50 102
binietoglou@0 103 # Find the unique time scales used in the channels
binietoglou@0 104 time_scales = set(all_time_scales)
ioannis@50 105
binietoglou@0 106 # Update the info dictionary
binietoglou@0 107 self.info['start_time'] = min(start_time)
binietoglou@0 108 self.info['stop_time'] = max(stop_time)
binietoglou@0 109 self.info['duration'] = self.info['stop_time'] - self.info['start_time']
ioannis@50 110
binietoglou@0 111 # Update the dimensions dictionary
binietoglou@0 112 self.dimensions['points'] = max(points)
binietoglou@0 113 self.dimensions['channels'] = len(self.channels)
binietoglou@0 114 # self.dimensions['scan angles'] = 1
binietoglou@0 115 self.dimensions['nb_of_time_scales'] = len(time_scales)
ioannis@50 116
i@116 117 # Make a dictionaries to match time scales and channels
i@116 118 channel_timescales = {}
i@116 119 timescale_channels = dict((ts, []) for ts in time_scales)
i@116 120 for (channel_name, current_time_scale) in zip(channel_names, all_time_scales):
ioannis@168 121 for (ts, n) in zip(time_scales, list(range(len(time_scales)))):
i@116 122 if current_time_scale == ts:
i@116 123 channel_timescales[channel_name] = n
i@116 124 timescale_channels[ts].append(channel_name)
i@116 125
i@116 126 self.variables['id_timescale'] = channel_timescales
i@116 127
binietoglou@0 128 # Update the variables dictionary
binietoglou@0 129 # Write time scales in seconds
ulalume3@92 130 raw_data_start_time = []
ulalume3@92 131 raw_data_stop_time = []
ioannis@50 132
ioannis@50 133 for current_time_scale in list(time_scales):
ioannis@50 134 raw_start_time = np.array(current_time_scale) - min(start_time) # Time since start_time
ioannis@50 135 raw_start_in_seconds = np.array([t.seconds for t in raw_start_time]) # Convert in seconds
ulalume3@92 136 raw_data_start_time.append(raw_start_in_seconds) # And add to the list
ioannis@50 137
i@116 138 channel_name = timescale_channels[current_time_scale][0]
i@116 139 channel = self.channels[channel_name]
ioannis@50 140
ulalume3@92 141 # TODO: Define stop time for each measurement based on real data
i@116 142 raw_stop_in_seconds = raw_start_in_seconds + channel.get_duration()
ulalume3@92 143 raw_data_stop_time.append(raw_stop_in_seconds)
ioannis@50 144
ulalume3@92 145 self.variables['Raw_Data_Start_Time'] = raw_data_start_time
ulalume3@92 146 self.variables['Raw_Data_Stop_Time'] = raw_data_stop_time
ioannis@50 147
binietoglou@0 148 def subset_by_channels(self, channel_subset):
ulalume3@92 149 """
ulalume3@92 150 Create a measurement object containing only a subset of channels.
ulalume3@92 151
ulalume3@92 152 Parameters
ulalume3@92 153 ----------
ulalume3@92 154 channel_subset : list
ulalume3@92 155 A list of channel names (str) to be included in the new measurement object.
ulalume3@92 156
ulalume3@92 157 Returns
ulalume3@92 158 -------
ulalume3@92 159 m : BaseLidarMeasurements object
ulalume3@92 160 A new measurements object
ulalume3@92 161 """
ioannis@50 162
binietoglou@39 163 m = self.__class__() # Create an object of the same type as this one.
ioannis@50 164 m.channels = dict([(channel, self.channels[channel]) for channel
ioannis@50 165 in channel_subset])
i@103 166 m.files = self.files
i@164 167
i@103 168 m.update()
ulalume3@92 169
i@164 170 # Dark measurements should also be subsetted.
i@103 171 if self.dark_measurement is not None:
i@103 172 dark_subset = self.dark_measurement.subset_by_channels(channel_subset)
i@103 173 m.dark_measurement = dark_subset
ulalume3@92 174
binietoglou@0 175 return m
ioannis@50 176
ulalume3@54 177 def subset_by_scc_channels(self):
ulalume3@54 178 """
ulalume3@92 179 Subset the measurement based on the channels provided in the
ulalume3@92 180 extra_netecdf_parameter file.
ulalume3@92 181
ulalume3@92 182 Returns
ulalume3@92 183 -------
ulalume3@92 184 m : BaseLidarMeasurements object
ulalume3@92 185 A new measurements object
ulalume3@54 186 """
ulalume3@92 187 if self.extra_netcdf_parameters is None:
ulalume3@92 188 raise RuntimeError("Extra netCDF parameters not defined, cannot subset measurement.")
ulalume3@92 189
ioannis@168 190 scc_channels = list(self.extra_netcdf_parameters.channel_parameters.keys())
ioannis@168 191 common_channels = list(set(scc_channels).intersection(list(self.channels.keys())))
ioannis@55 192
ioannis@55 193 if not common_channels:
i@116 194 logger.debug("Config channels: %s." % ','.join(scc_channels))
ioannis@168 195 logger.debug("Measurement channels: %s." % ','.join(list(self.channels.keys())))
i@117 196 raise ValueError('No common channels between measurement and configuration files.')
ioannis@55 197
ioannis@55 198 return self.subset_by_channels(common_channels)
ulalume3@54 199
binietoglou@0 200 def subset_by_time(self, start_time, stop_time):
ulalume3@92 201 """
ulalume3@92 202 Subset the measurement for a specific time interval
ulalume3@92 203
ulalume3@92 204 Parameters
ulalume3@92 205 ----------
ulalume3@92 206 start_time : datetime
ulalume3@92 207 The starting datetime to subset.
ulalume3@92 208 stop_time : datetime
ulalume3@92 209 The stopping datetime to subset.
ulalume3@92 210
ulalume3@92 211 Returns
ulalume3@92 212 -------
ulalume3@92 213 m : BaseLidarMeasurements object
ulalume3@92 214 A new measurements object
ulalume3@92 215 """
ioannis@50 216
binietoglou@0 217 if start_time > stop_time:
binietoglou@0 218 raise ValueError('Stop time should be after start time')
ioannis@50 219
binietoglou@0 220 if (start_time < self.info['start_time']) or (stop_time > self.info['stop_time']):
binietoglou@0 221 raise ValueError('The time interval specified is not part of the measurement')
binietoglou@0 222
ioannis@50 223 m = self.__class__() # Create an object of the same type as this one.
ioannis@168 224 for channel_name, channel in self.channels.items():
binietoglou@0 225 m.channels[channel_name] = channel.subset_by_time(start_time, stop_time)
ulalume3@92 226
binietoglou@0 227 m.update()
i@103 228
i@103 229 # Transfer dark measurement to the new object. They don't need time subsetting.
i@103 230 m.dark_measurement = self.dark_measurement
binietoglou@0 231 return m
ioannis@50 232
ioannis@50 233 def subset_by_bins(self, b_min=0, b_max=None):
ulalume3@27 234 """
ulalume3@93 235 Remove some height bins from the measurement data.
ulalume3@92 236
ulalume3@92 237 This could be needed to remove acquisition artifacts at
ulalume3@92 238 the first or last bins of the profiles.
ulalume3@92 239
ulalume3@92 240 Parameters
ulalume3@92 241 ----------
ulalume3@92 242 b_min : int
ulalume3@92 243 The fist valid data bin
ulalume3@92 244 b_max : int or None
ulalume3@92 245 The last valid data bin. If equal to None, all bins are used.
ulalume3@92 246
ulalume3@92 247 Returns
ulalume3@92 248 -------
ulalume3@92 249 m : BaseLidarMeasurements object
ulalume3@92 250 A new measurements object
ulalume3@92 251 """
ioannis@50 252 m = self.__class__() # Create an object of the same type as this one.
ioannis@50 253
ioannis@168 254 for channel_name, channel in self.channels.items():
ulalume3@27 255 m.channels[channel_name] = channel.subset_by_bins(b_min, b_max)
ioannis@50 256
ulalume3@27 257 m.update()
ioannis@50 258
i@103 259 # Dark measurements should also be subseted.
i@103 260 if self.dark_measurement is not None:
i@103 261 dark_subset = self.dark_measurement.subset_by_bins(b_min, b_max)
i@103 262 m.dark_measurement = dark_subset
i@103 263
ulalume3@27 264 return m
binietoglou@39 265
ulalume3@92 266 def rename_channels(self, prefix="", suffix=""):
ulalume3@92 267 """ Add a prefix and a suffix to all channel name.
ulalume3@92 268
ulalume3@92 269 This is uses when processing Delta90 depolarization calibration measurements.
ulalume3@92 270
ulalume3@92 271 Parameters
ulalume3@92 272 ----------
ulalume3@92 273 prefix : str
ulalume3@92 274 The prefix to add to channel names.
ulalume3@92 275 suffix : str
ulalume3@92 276 The suffix to add to channel names.
binietoglou@39 277 """
ioannis@168 278 channel_names = list(self.channels.keys())
binietoglou@39 279
binietoglou@39 280 for channel_name in channel_names:
binietoglou@39 281 new_name = prefix + channel_name + suffix
binietoglou@39 282 self.channels[new_name] = self.channels.pop(channel_name)
binietoglou@39 283
ulalume3@92 284 def set_PT(self):
ulalume3@92 285 """
ulalume3@92 286 Sets the pressure and temperature at station level at the info dictionary .
ulalume3@92 287
ulalume3@92 288 In this method, default values are used. It can be overwritten by subclasses
ulalume3@92 289 to define more appropriate values for each system.
ulalume3@92 290 """
ulalume3@92 291 self.info['Temperature'] = 15.0 # Temperature in degC
ulalume3@92 292 self.info['Pressure'] = 1013.15 # Pressure in hPa
ioannis@50 293
binietoglou@15 294 def subtract_dark(self):
ulalume3@92 295 """
ulalume3@92 296 Subtract dark measurements from the raw lidar signals.
i@103 297
ulalume3@92 298 This method is here just for testing.
ulalume3@92 299
i@132 300 Notes
i@132 301 -----
ulalume3@92 302 This method should not be called if processing the data with the SCC. The SCC
ulalume3@92 303 performs this operations anyway.
ulalume3@92 304 """
binietoglou@15 305 if not self.dark_measurement:
binietoglou@15 306 raise IOError('No dark measurements have been imported yet.')
ioannis@50 307
ioannis@168 308 for (channel_name, dark_channel) in self.dark_measurement.channels.items():
binietoglou@15 309 dark_profile = dark_channel.average_profile()
binietoglou@15 310 channel = self.channels[channel_name]
ioannis@50 311
ioannis@168 312 for measurement_time, data in channel.data.items():
binietoglou@15 313 channel.data[measurement_time] = data - dark_profile
ioannis@50 314
binietoglou@15 315 channel.update()
binietoglou@39 316
binietoglou@43 317 def set_measurement_id(self, measurement_id=None, measurement_number="00"):
binietoglou@39 318 """
binietoglou@39 319 Sets the measurement id for the SCC file.
binietoglou@39 320
binietoglou@43 321 Parameters
binietoglou@43 322 ----------
binietoglou@43 323 measurement_id: str
binietoglou@43 324 A measurement id with the format YYYYMMDDccNN, where YYYYMMDD the date,
ulalume3@92 325 cc the EARLiNet call sign and NN a number between 00 and 99.
binietoglou@43 326 measurement_number: str
binietoglou@43 327 If measurement id is not provided the method will try to create one
ulalume3@92 328 based on the input date. The measurement number can specify the value
binietoglou@43 329 of NN in the created ID.
binietoglou@39 330 """
binietoglou@43 331 if measurement_id is None:
binietoglou@43 332 date_str = self.info['start_time'].strftime('%Y%m%d')
binietoglou@43 333 try:
binietoglou@43 334 earlinet_station_id = self.extra_netcdf_parameters.general_parameters['Call sign']
binietoglou@43 335 except:
ioannis@50 336 raise ValueError("No valid SCC netcdf parameters found. Did you define the proper subclass?")
binietoglou@43 337 measurement_id = "{0}{1}{2}".format(date_str, earlinet_station_id, measurement_number)
binietoglou@43 338
binietoglou@39 339 self.info['Measurement_ID'] = measurement_id
binietoglou@39 340
ulalume3@92 341 def save_as_SCC_netcdf(self, filename=None):
ulalume3@92 342 """Saves the measurement in the netCDF format as required by the SCC.
ulalume3@92 343
ulalume3@92 344 If no filename is provided <measurement_id>.nc will be used.
ulalume3@92 345
ulalume3@92 346 Parameters
ulalume3@92 347 ----------
ulalume3@92 348 filename : str
ulalume3@92 349 Output file name. If None, <measurement_id>.nc will be used.
binietoglou@0 350 """
i@116 351 parameters = self.extra_netcdf_parameters
binietoglou@43 352
binietoglou@43 353 # Guess measurement ID if none is provided
binietoglou@43 354 if 'Measurement_ID' not in self.info:
binietoglou@43 355 self.set_measurement_id()
binietoglou@43 356
binietoglou@43 357 # Check if temperature and pressure are defined
i@116 358 for attribute in ['Temperature', 'Pressure']:
i@116 359 stored_value = self.info.get(attribute, None)
binietoglou@0 360 if stored_value is None:
binietoglou@43 361 try:
ulalume3@92 362 self.set_PT()
binietoglou@43 363 except:
i@116 364 raise ValueError('No value specified for %s' % attribute)
ioannis@50 365
ulalume3@23 366 if not filename:
ulalume3@23 367 filename = "%s.nc" % self.info['Measurement_ID']
binietoglou@43 368
binietoglou@43 369 self.scc_filename = filename
binietoglou@43 370
binietoglou@0 371 dimensions = {'points': 1,
ioannis@50 372 'channels': 1,
ioannis@50 373 'time': None,
ioannis@50 374 'nb_of_time_scales': 1,
ioannis@50 375 'scan_angles': 1, } # Mandatory dimensions. Time bck not implemented
binietoglou@0 376
i@116 377 global_attributes = {'Measurement_ID': None,
i@116 378 'RawData_Start_Date': None,
i@116 379 'RawData_Start_Time_UT': None,
i@116 380 'RawData_Stop_Time_UT': None,
i@116 381 'RawBck_Start_Date': None,
i@116 382 'RawBck_Start_Time_UT': None,
i@116 383 'RawBck_Stop_Time_UT': None,
i@116 384 'Sounding_File_Name': None,
i@116 385 'LR_File_Name': None,
i@116 386 'Overlap_File_Name': None,
i@116 387 'Location': None,
i@116 388 'System': None,
i@116 389 'Latitude_degrees_north': None,
i@116 390 'Longitude_degrees_east': None,
i@121 391 'Altitude_meter_asl': None,}
binietoglou@0 392
ioannis@168 393 channel_names = list(self.channels.keys())
binietoglou@0 394
binietoglou@0 395 input_values = dict(self.dimensions, **self.variables)
ioannis@50 396
binietoglou@0 397 # Add some mandatory global attributes
binietoglou@0 398 input_values['Measurement_ID'] = self.info['Measurement_ID']
ioannis@21 399 input_values['RawData_Start_Date'] = self.info['start_time'].strftime('%Y%m%d')
ioannis@21 400 input_values['RawData_Start_Time_UT'] = self.info['start_time'].strftime('%H%M%S')
ioannis@21 401 input_values['RawData_Stop_Time_UT'] = self.info['stop_time'].strftime('%H%M%S')
ioannis@50 402
i@116 403 # Add any global attributes provided by the subclass
i@116 404 for attribute in self._get_custom_global_attributes():
i@116 405 input_values[attribute["name"]] = attribute["value"]
ulalume3@92 406
i@116 407 # Override global attributes, if provided in the settings file.
i@121 408 for attribute_name in ['System', 'Latitude_degrees_north', 'Longitude_degrees_east', 'Altitude_meter_asl',
i@121 409 'Overlap_File_Name', 'LR_File_Name', 'Sounding_File_Name']:
i@116 410 if attribute_name in parameters.general_parameters.keys():
i@116 411 if attribute_name in input_values:
i@116 412 logger.info("Overriding {0} attribute, using the value provided in the parameter file.".format(
i@116 413 attribute_name))
i@116 414 input_values[attribute_name] = parameters.general_parameters[attribute_name]
ioannis@50 415
ulalume3@92 416 # Open a netCDF file. The format is specified in the beginning of this module.
ulalume3@92 417 with netcdf.Dataset(filename, 'w', format=NETCDF_FORMAT) as f:
ulalume3@92 418
ulalume3@92 419 # Create the dimensions in the file
ioannis@168 420 for (d, v) in dimensions.items():
ulalume3@92 421 v = input_values.pop(d, v)
ulalume3@92 422 f.createDimension(d, v)
ioannis@50 423
ulalume3@92 424 # Create global attributes
ioannis@168 425 for (attrib, value) in global_attributes.items():
ulalume3@92 426 val = input_values.pop(attrib, value)
ulalume3@92 427 if val:
ulalume3@92 428 setattr(f, attrib, val)
ulalume3@92 429
ulalume3@92 430 # Variables
ulalume3@92 431 # Write either channel_id or string_channel_id in the file
ioannis@168 432 first_channel_keys = list(parameters.channel_parameters.items())[0][1].keys()
ulalume3@92 433 if "channel_ID" in first_channel_keys:
ulalume3@92 434 channel_var = 'channel_ID'
ulalume3@92 435 variable_type = 'i'
i@116 436 elif "channel_string_ID" in first_channel_keys:
i@116 437 channel_var = 'channel_string_ID'
ulalume3@92 438 variable_type = str
ulalume3@92 439 else:
ulalume3@92 440 raise ValueError('Channel parameters should define either "chanel_id" or "channel_string_ID".')
ioannis@50 441
ulalume3@92 442 temp_v = f.createVariable(channel_var, variable_type, ('channels',))
i@116 443 for n, channel in enumerate(channel_names):
i@116 444 temp_v[n] = parameters.channel_parameters[channel][channel_var]
ulalume3@92 445
i@116 446 # Write the custom subclass variables:
i@116 447 for variable in self._get_custom_variables(channel_names):
i@128 448 logger.debug("Saving variable {0}".format(variable['name']))
i@116 449 temp_v = f.createVariable(variable["name"], variable["type"], variable["dimensions"])
i@116 450 temp_v[:] = variable["values"]
ulalume3@54 451
ulalume3@92 452 # Write the values of fixed channel parameters:
i@116 453 channel_variable_specs = self._get_scc_channel_variables()
i@116 454
i@139 455 provided_variable_names = self._get_provided_variable_names()
i@116 456
i@139 457 for variable_name in provided_variable_names:
i@116 458 # Check if the variable already defined (e.g. from values in Licel files).
i@116 459 if variable_name in f.variables.keys():
i@116 460 logger.warning(
i@116 461 "Provided values of \"{0}\" were ignored because they were read from other source.".format(
i@116 462 variable_name))
i@116 463 continue
i@116 464
i@116 465 if variable_name not in channel_variable_specs.keys():
ioannis@168 466 logger.warning("Provided variable {0} is not parts of the specs: {1}".format(variable_name, list(channel_variable_specs.keys())))
i@116 467 continue
i@116 468
i@116 469 temp_v = f.createVariable(variable_name,
i@116 470 channel_variable_specs[variable_name][1],
i@116 471 channel_variable_specs[variable_name][0])
i@116 472
i@116 473 for n, channel_name in enumerate(channel_names):
ulalume3@92 474 try:
i@116 475 temp_v[n] = parameters.channel_parameters[channel_name][variable_name]
ulalume3@92 476 except KeyError: # The parameter was not provided for this channel so we mask the value.
i@116 477 pass # Default value should be a missing value # TODO: Check this.
ulalume3@92 478
ulalume3@92 479 # Write the id_timescale values
ulalume3@92 480 temp_id_timescale = f.createVariable('id_timescale', 'i', ('channels',))
ioannis@168 481 for (channel, n) in zip(channel_names, list(range(len(channel_names)))):
ulalume3@92 482 temp_id_timescale[n] = self.variables['id_timescale'][channel]
ioannis@50 483
ulalume3@92 484 # Laser pointing angle
ulalume3@92 485 temp_v = f.createVariable('Laser_Pointing_Angle', 'd', ('scan_angles',))
i@116 486 temp_v[:] = parameters.general_parameters['Laser_Pointing_Angle']
ulalume3@92 487
ulalume3@92 488 # Molecular calculation
ulalume3@92 489 temp_v = f.createVariable('Molecular_Calc', 'i')
i@116 490 temp_v[:] = parameters.general_parameters['Molecular_Calc']
ioannis@50 491
ulalume3@92 492 # Laser pointing angles of profiles
ulalume3@92 493 temp_v = f.createVariable('Laser_Pointing_Angle_of_Profiles', 'i', ('time', 'nb_of_time_scales'))
ulalume3@92 494 for (time_scale, n) in zip(self.variables['Raw_Data_Start_Time'],
ioannis@168 495 list(range(len(self.variables['Raw_Data_Start_Time'])))):
ulalume3@92 496 temp_v[:len(time_scale), n] = 0 # The lidar has only one laser pointing angle
ioannis@50 497
ulalume3@92 498 # Raw data start/stop time
ulalume3@92 499 temp_raw_start = f.createVariable('Raw_Data_Start_Time', 'i', ('time', 'nb_of_time_scales'))
ulalume3@92 500 temp_raw_stop = f.createVariable('Raw_Data_Stop_Time', 'i', ('time', 'nb_of_time_scales'))
ulalume3@92 501 for (start_time, stop_time, n) in zip(self.variables['Raw_Data_Start_Time'],
ulalume3@92 502 self.variables['Raw_Data_Stop_Time'],
ioannis@168 503 list(range(len(self.variables['Raw_Data_Start_Time'])))):
ulalume3@92 504 temp_raw_start[:len(start_time), n] = start_time
ulalume3@92 505 temp_raw_stop[:len(stop_time), n] = stop_time
ioannis@50 506
ulalume3@92 507 # Laser shots
i@139 508 if "Laser_Shots" in provided_variable_names:
i@139 509 if "Laser_Shots" in f.variables.keys():
i@139 510 logger.warning("Provided values of \"Laser_Shots\" were ignored because they were read from other source.")
i@139 511 else:
i@139 512 temp_v = f.createVariable('Laser_Shots', 'i', ('time', 'channels'))
ioannis@168 513 for (channel, n) in zip(channel_names, list(range(len(channel_names)))):
i@139 514 time_length = len(self.variables['Raw_Data_Start_Time'][self.variables['id_timescale'][channel]])
i@139 515 # Array slicing stopped working as usual ex. temp_v[:10] = 100 does not work. ??? np.ones was added.
i@139 516 temp_v[:time_length, n] = np.ones(time_length) * parameters.channel_parameters[channel][
i@139 517 'Laser_Shots']
i@116 518 else:
i@139 519 if "Laser_Shots" not in f.variables.keys():
i@139 520 logger.error("Mandatory variable \"Laser_Shots\" was not found in the settings or input files.")
i@139 521 else:
i@139 522 pass # Laser shots already in variables, so all good.
victor@84 523
ulalume3@92 524 # Raw lidar data
ioannis@192 525 temp_v = f.createVariable('Raw_Lidar_Data', 'd', ('time', 'channels', 'points'), zlib=True)
ioannis@168 526 for (channel, n) in zip(channel_names, list(range(len(channel_names)))):
ulalume3@92 527 c = self.channels[channel]
ulalume3@92 528 temp_v[:len(c.time), n, :c.points] = c.matrix
ulalume3@24 529
i@116 530 self.add_dark_measurements_to_netcdf(f, channel_names)
ioannis@50 531
ulalume3@92 532 # Pressure at lidar station
ulalume3@92 533 temp_v = f.createVariable('Pressure_at_Lidar_Station', 'd')
ulalume3@92 534 temp_v[:] = self.info['Pressure']
binietoglou@0 535
ulalume3@92 536 # Temperature at lidar station
ulalume3@92 537 temp_v = f.createVariable('Temperature_at_Lidar_Station', 'd')
ulalume3@92 538 temp_v[:] = self.info['Temperature']
ioannis@50 539
ulalume3@92 540 self.save_netcdf_extra(f)
binietoglou@43 541
i@116 542 def _get_scc_channel_variables(self):
binietoglou@43 543 channel_variables = \
ioannis@142 544 {'Laser_Repetition_Rate': (('channels',), 'i'),
i@116 545 'Scattering_Mechanism': (('channels',), 'i'),
i@116 546 'Signal_Type': (('channels',), 'i'),
i@116 547 'Emitted_Wavelength': (('channels',), 'd'),
i@116 548 'Detected_Wavelength': (('channels',), 'd'),
i@116 549 'Raw_Data_Range_Resolution': (('channels',), 'd'),
i@116 550 'Background_Mode': (('channels',), 'i'),
ioannis@142 551 'Background_Low': (('channels',), 'd'),
ioannis@142 552 'Background_High': (('channels',), 'd'),
i@116 553 'Dead_Time_Corr_Type': (('channels',), 'i'),
i@116 554 'Dead_Time': (('channels',), 'd'),
i@116 555 'Acquisition_Mode': (('channels',), 'i'),
i@116 556 'Trigger_Delay': (('channels',), 'd'),
binietoglou@43 557 'LR_Input': (('channels',), 'i'),
binietoglou@43 558 'DAQ_Range': (('channels',), 'd'),
ioannis@142 559 'First_Signal_Rangebin': (('channels',), 'i'),
binietoglou@43 560 }
binietoglou@43 561 return channel_variables
ulalume3@92 562
i@116 563 def _get_provided_variable_names(self):
i@116 564 """ Return a list of """
i@116 565 # When looking for channel parameters, ignore the following parameter names:
i@116 566 ignore = {'channel_ID', 'channel_string_ID', 'Depolarization_Factor', 'Laser_Shots'} # Set
ulalume3@92 567
ioannis@168 568 channels = list(self.channels.keys())
i@116 569 channel_parameters = self.extra_netcdf_parameters.channel_parameters
ulalume3@92 570
i@116 571 # Get all the provided parameters (both mandatory and optional):
ioannis@168 572 all_provided_variables = [list(channel_parameters[channel].keys()) for channel in channels]
i@116 573 provided_variables = set(itertools.chain.from_iterable(all_provided_variables))
ulalume3@92 574
i@116 575 # Discard certain parameter names:
i@116 576 provided_variables -= ignore
ulalume3@92 577
i@116 578 return provided_variables
binietoglou@43 579
binietoglou@0 580 def add_dark_measurements_to_netcdf(self, f, channels):
ulalume3@92 581 """
ulalume3@92 582 Adds dark measurement variables and properties to an open netCDF file.
ulalume3@92 583
ulalume3@92 584 Parameters
ulalume3@92 585 ----------
ulalume3@92 586 f : netcdf Dataset
ulalume3@92 587 A netCDF Dataset, open for writing.
ulalume3@92 588 channels : list
ulalume3@92 589 A list of channels names to consider when adding dark measurements.
ulalume3@92 590 """
binietoglou@0 591 # Get dark measurements. If it is not given in self.dark_measurement
binietoglou@0 592 # try to get it using the get_dark_measurements method. If none is found
binietoglou@0 593 # return without adding something.
binietoglou@0 594 if self.dark_measurement is None:
binietoglou@0 595 self.dark_measurement = self.get_dark_measurements()
ioannis@50 596
binietoglou@0 597 if self.dark_measurement is None:
binietoglou@0 598 return
ioannis@50 599
binietoglou@0 600 dark_measurement = self.dark_measurement
ioannis@50 601
binietoglou@0 602 # Calculate the length of the time_bck dimensions
binietoglou@0 603 number_of_profiles = [len(c.time) for c in dark_measurement.channels.values()]
binietoglou@0 604 max_number_of_profiles = np.max(number_of_profiles)
ioannis@50 605
binietoglou@0 606 # Create the dimension
binietoglou@0 607 f.createDimension('time_bck', max_number_of_profiles)
ioannis@50 608
binietoglou@0 609 # Save the dark measurement data
ioannis@192 610 temp_v = f.createVariable('Background_Profile', 'd', ('time_bck', 'channels', 'points'), zlib=True)
ioannis@168 611 for (channel, n) in zip(channels, list(range(len(channels)))):
binietoglou@0 612 c = dark_measurement.channels[channel]
ioannis@50 613 temp_v[:len(c.time), n, :c.points] = c.matrix
ioannis@50 614
binietoglou@0 615 # Dark profile start/stop time
ioannis@50 616 temp_raw_start = f.createVariable('Raw_Bck_Start_Time', 'i', ('time_bck', 'nb_of_time_scales'))
ioannis@50 617 temp_raw_stop = f.createVariable('Raw_Bck_Stop_Time', 'i', ('time_bck', 'nb_of_time_scales'))
ioannis@50 618 for (start_time, stop_time, n) in zip(dark_measurement.variables['Raw_Data_Start_Time'],
ioannis@50 619 dark_measurement.variables['Raw_Data_Stop_Time'],
ioannis@168 620 list(range(len(dark_measurement.variables['Raw_Data_Start_Time'])))):
ioannis@50 621 temp_raw_start[:len(start_time), n] = start_time
ioannis@50 622 temp_raw_stop[:len(stop_time), n] = stop_time
ioannis@50 623
binietoglou@0 624 # Dark measurement start/stop time
binietoglou@0 625 f.RawBck_Start_Date = dark_measurement.info['start_time'].strftime('%Y%m%d')
binietoglou@0 626 f.RawBck_Start_Time_UT = dark_measurement.info['start_time'].strftime('%H%M%S')
binietoglou@0 627 f.RawBck_Stop_Time_UT = dark_measurement.info['stop_time'].strftime('%H%M%S')
binietoglou@0 628
binietoglou@0 629 def save_netcdf_extra(self, f):
ulalume3@92 630 """ Save extra netCDF parameters to an open netCDF file.
ulalume3@92 631
ulalume3@92 632 If required, this method should be overwritten by subclasses of BaseLidarMeasurement.
ulalume3@92 633 """
binietoglou@0 634 pass
ioannis@50 635
binietoglou@0 636 def plot(self):
binietoglou@0 637 for channel in self.channels:
ioannis@50 638 self.channels[channel].plot(show_plot=False)
binietoglou@0 639 plt.show()
ioannis@50 640
binietoglou@0 641 def get_dark_measurements(self):
binietoglou@0 642 return None
ulalume3@92 643
i@116 644 def _get_custom_global_attributes(self):
victor@84 645 """
i@116 646 Abstract method to provide NetCDF global attributes that should be included
ulalume3@92 647 in the final NetCDF file.
victor@87 648
ulalume3@92 649 If required, this method should be implemented by subclasses of BaseLidarMeasurement.
victor@87 650 """
victor@87 651 return []
ioannis@50 652
i@116 653 def _get_custom_variables(self, channel_names=None):
ulalume3@92 654 """
i@116 655 Abstract method to provide custom NetCDF variables that should be included in the final NetCDF file.
ulalume3@92 656
ulalume3@92 657 If required, this method should be implemented by subclasses of BaseLidarMeasurement.
ulalume3@92 658 """
ulalume3@92 659 return []
binietoglou@0 660
binietoglou@0 661
i@129 662 class LidarChannel(object):
ulalume3@93 663 """
ulalume3@93 664 This class represents a general measurement channel, independent of the input files.
ulalume3@93 665
ulalume3@93 666 The class assumes that the input files are consecutive, i.e. there are no measurements gaps.
ulalume3@93 667 """
ulalume3@93 668
ulalume3@27 669 def __init__(self, channel_parameters):
ulalume3@93 670 """
ulalume3@93 671 This is run when creating a new object.
ulalume3@93 672
ulalume3@93 673 The input dictionary should contain 'name', 'binwidth', and 'data' keys.
ulalume3@93 674
ulalume3@93 675 Parameters
ulalume3@93 676 ----------
ulalume3@93 677 channel_parameters : dict
ulalume3@93 678 A dict containing channel parameters.
ulalume3@93 679 """
ulalume3@93 680 # TODO: Change channel_parameters to explicit keyword arguments?
ulalume3@93 681
ulalume3@93 682 c = 299792458. # Speed of light
binietoglou@0 683 self.wavelength = channel_parameters['name']
binietoglou@0 684 self.name = str(self.wavelength)
ulalume3@27 685 self.binwidth = float(channel_parameters['binwidth']) # in microseconds
binietoglou@0 686 self.data = {}
binietoglou@0 687 self.resolution = self.binwidth * c / 2
ioannis@50 688 self.z = np.arange(
ioannis@50 689 len(channel_parameters['data'])) * self.resolution + self.resolution / 2.0 # Change: add half bin in the z
binietoglou@0 690 self.points = len(channel_parameters['data'])
binietoglou@0 691 self.rc = []
i@116 692
i@116 693 def get_duration(self):
i@116 694 """ Get an array with the duration of each profile in seconds.
i@116 695
i@116 696 If the duration property already exists, returns this.
i@116 697 If not, it tries to estimate it based on the time difference of the profiles.
i@116 698
i@116 699 Returns
i@116 700 -------
i@116 701 duration : ndarray
i@116 702 1D array containing profile durations.
i@116 703 """
i@116 704
i@116 705 current_value = getattr(self, 'duration', None)
i@116 706
i@116 707 if current_value:
i@116 708 return np.array(current_value)
i@116 709
i@116 710 time_diff = np.diff(self.time)
i@116 711 durations = [dt.seconds for dt in time_diff]
i@116 712 # Assume the last two profiles have the same duration
i@116 713 duration = np.array(durations)
i@116 714 return duration
ioannis@50 715
i@179 716 def calculate_rc(self, idx_min=-2000, idx_max=-500, first_signal_bin=0):
ulalume3@93 717 """ Calculate range corrected signal.
ulalume3@93 718
ulalume3@93 719 The background signal is estimated as the mean signal between idx_min and idx_max.
ulalume3@93 720
ulalume3@93 721 The calculations is stored in the self.rc parameters.
ulalume3@93 722
ulalume3@93 723 Parameters
ulalume3@93 724 ----------
ulalume3@93 725 idx_min : int
ulalume3@93 726 Minimum index to calculate background signal.
ulalume3@93 727 idx_max : int
ulalume3@93 728 Maximum index to calculate background signal.
i@179 729 first_signal_bin : int
i@179 730 First bin with useful signal. Positive integer.
ulalume3@93 731 """
i@174 732 background = np.mean(self.matrix[:, idx_min:idx_max], axis=1)
i@179 733 background_corrected = (self.matrix.transpose() - background).transpose()
i@179 734 background_corrected = np.roll(background_corrected, -first_signal_bin, axis=1)
ioannis@196 735 background_corrected[:, -first_signal_bin:] = 0
i@179 736 self.rc = background_corrected * (self.z ** 2)
ioannis@50 737
i@174 738 def noise_mask(self, idx_min=-2000, idx_max=-500, threshold=1.):
i@174 739 """ Calculate points that are probably noise.
i@174 740
i@174 741 To calculate this, we find the max value of the background region. Then we reject all points that
i@174 742 are less than `threshold` times this value.
i@174 743 Parameters
i@174 744 ----------
i@174 745 idx_min : int
i@174 746 Minimum index to calculate background signal.
i@174 747 idx_max : int
i@174 748 Maximum index to calculate background signal.
i@174 749 threshold : float
i@174 750 Threshold value.
i@174 751 """
i@174 752 background_max = np.max(self.matrix[:, idx_min:idx_max], axis=1)
i@174 753 background_mean = np.mean(self.matrix[:, idx_min:idx_max], axis=1)
i@174 754 mean_to_max = background_max - background_mean
i@174 755
i@174 756 noise_limit = background_mean + mean_to_max * threshold
i@174 757
i@174 758 mask = self.matrix <= noise_limit[:, np.newaxis]
i@174 759
i@174 760 return mask
i@174 761
binietoglou@0 762 def update(self):
ulalume3@93 763 """
ulalume3@93 764 Update the time parameters and data according to the raw input data.
ulalume3@93 765 """
binietoglou@0 766 self.start_time = min(self.data.keys())
i@116 767 self.stop_time = max(self.data.keys()) + datetime.timedelta(seconds=self.duration[-1])
binietoglou@0 768 self.time = tuple(sorted(self.data.keys()))
ioannis@168 769 sorted_data = sorted(iter(self.data.items()), key=itemgetter(0))
ioannis@168 770 self.matrix = np.array(list(map(itemgetter(1), sorted_data)))
ioannis@50 771
ulalume3@93 772 def _nearest_datetime(self, input_time):
ulalume3@93 773 """
ulalume3@93 774 Find the profile datetime and index that is nearest to the given datetime.
ulalume3@93 775
ulalume3@93 776 Parameters
ulalume3@93 777 ----------
ulalume3@93 778 input_time : datetime.datetime
ulalume3@93 779 Input datetime object.
ulalume3@93 780
ulalume3@93 781 Returns
ulalume3@93 782 -------
ulalume3@93 783 profile_datetime : datetime
ulalume3@93 784 The datetime of the selected profile.
ulalume3@93 785 profile_idx : int
ulalume3@93 786 The index of the selected profile.
ulalume3@93 787 """
i@116 788 max_duration = np.max(self.duration)
i@116 789
i@116 790 margin = datetime.timedelta(seconds=max_duration * 5)
ulalume3@93 791
ulalume3@93 792 if ((input_time + margin) < self.start_time) | ((input_time - margin) > self.stop_time):
i@116 793 logger.error("Requested date not covered in this file")
ioannis@55 794 raise ValueError("Requested date not covered in this file")
ulalume3@93 795 dt = np.abs(np.array(self.time) - input_time)
ulalume3@93 796
ulalume3@93 797 profile_idx = np.argmin(dt)
ulalume3@93 798 profile_datetime = self.time[profile_idx]
binietoglou@0 799
ulalume3@93 800 dt_min = dt[profile_idx]
i@116 801 if dt_min > datetime.timedelta(seconds=max_duration):
i@116 802 logger.warning("Nearest profile more than %s seconds away. dt = %s." % (max_duration, dt_min))
ulalume3@93 803
ulalume3@93 804 return profile_datetime, profile_idx
ioannis@50 805
binietoglou@0 806 def subset_by_time(self, start_time, stop_time):
ulalume3@93 807 """
ulalume3@93 808 Subset the channel for a specific time interval.
ioannis@50 809
ulalume3@93 810 Parameters
ulalume3@93 811 ----------
ulalume3@93 812 start_time : datetime
ulalume3@93 813 The starting datetime to subset.
ulalume3@93 814 stop_time : datetime
ulalume3@93 815 The stopping datetime to subset.
ulalume3@93 816
ulalume3@93 817 Returns
ulalume3@93 818 -------
ulalume3@93 819 c : LidarChannel object
ulalume3@93 820 A new channel object
ulalume3@93 821 """
binietoglou@0 822 time_array = np.array(self.time)
binietoglou@0 823 condition = (time_array >= start_time) & (time_array <= stop_time)
ioannis@50 824
binietoglou@0 825 subset_time = time_array[condition]
binietoglou@0 826 subset_data = dict([(c_time, self.data[c_time]) for c_time in subset_time])
ioannis@50 827
ioannis@50 828 # Create a list with the values needed by channel's __init__()
ioannis@50 829 parameter_values = {'name': self.wavelength,
ioannis@50 830 'binwidth': self.binwidth,
ioannis@50 831 'data': subset_data[subset_time[0]], }
ioannis@50 832
ulalume3@27 833 # We should use __class__ instead of class name, so that this works properly
ulalume3@27 834 # with subclasses
ulalume3@93 835 # Eg: c = self.__class__(parameters_values)
ulalume3@27 836 # This does not currently work with Licel files though
ulalume3@93 837 # TODO: Fix this!
ulalume3@27 838 c = LidarChannel(parameter_values)
ulalume3@27 839 c.data = subset_data
ulalume3@27 840 c.update()
ulalume3@27 841 return c
ioannis@50 842
ioannis@50 843 def subset_by_bins(self, b_min=0, b_max=None):
ulalume3@93 844 """
ulalume3@93 845 Remove some height bins from the channel data.
ulalume3@93 846
ulalume3@93 847 This could be needed to remove acquisition artifacts at
ulalume3@93 848 the first or last bins of the profiles.
ulalume3@93 849
ulalume3@93 850 Parameters
ulalume3@93 851 ----------
ulalume3@93 852 b_min : int
ulalume3@93 853 The fist valid data bin
ulalume3@93 854 b_max : int or None
ulalume3@93 855 The last valid data bin. If equal to None, all bins are used.
ulalume3@93 856
ulalume3@93 857 Returns
ulalume3@93 858 -------
ulalume3@93 859 m : LidarChannel object
ulalume3@93 860 A new channel object
ulalume3@27 861 """
ioannis@50 862
ulalume3@27 863 subset_data = {}
ioannis@50 864
ulalume3@27 865 for ctime, cdata in self.data.items():
ulalume3@27 866 subset_data[ctime] = cdata[b_min:b_max]
ioannis@50 867
ioannis@50 868 # Create a list with the values needed by channel's __init__()
ioannis@50 869 parameters_values = {'name': self.wavelength,
ioannis@50 870 'binwidth': self.binwidth,
ioannis@50 871 'data': subset_data[
ioannis@168 872 list(subset_data.keys())[0]], } # We just need any array with the correct length
binietoglou@0 873
ulalume3@27 874 c = LidarChannel(parameters_values)
binietoglou@0 875 c.data = subset_data
binietoglou@0 876 c.update()
binietoglou@0 877 return c
binietoglou@39 878
ulalume3@93 879 def get_profile(self, input_datetime, range_corrected=True):
ulalume3@93 880 """ Returns a single profile, that is nearest to the input datetime.
ulalume3@93 881
ulalume3@93 882 Parameters
ulalume3@93 883 ----------
ulalume3@93 884 input_datetime : datetime
ulalume3@93 885 The required datetime of the profile
ulalume3@93 886 range_corrected : bool
ulalume3@93 887 If True, the range corrected profile is returned. Else, normal signal is returned.
ulalume3@93 888
ulalume3@93 889 Returns
ulalume3@93 890 -------
ulalume3@93 891 profile : ndarray
ulalume3@93 892 The profile nearest to input_datetime.
ulalume3@93 893 t : datetime
ulalume3@93 894 The actual datetime corresponding to the profile.
ulalume3@93 895 """
ulalume3@93 896 t, idx = self._nearest_datetime(input_datetime)
ulalume3@93 897 if range_corrected:
binietoglou@0 898 data = self.rc
binietoglou@0 899 else:
binietoglou@0 900 data = self.matrix
ioannis@50 901
ulalume3@93 902 profile = data[idx, :][0]
ulalume3@93 903
ulalume3@93 904 return profile, t
ulalume3@93 905
ulalume3@93 906 def get_slice(self, start_time, stop_time, range_corrected=True):
ulalume3@93 907 """ Returns a slice of data, between the provided start and stop time.
binietoglou@0 908
ulalume3@93 909 Parameters
ulalume3@93 910 ----------
ulalume3@93 911 start_time : datetime
ulalume3@93 912 The starting time of the slice
ulalume3@93 913 stop_time : datetime
ulalume3@93 914 The stoping time of the slice
ulalume3@93 915 range_corrected : bool
ulalume3@93 916 If True, the range corrected profile is returned. Else, normal signal is returned.
ulalume3@93 917
ulalume3@93 918 Returns
ulalume3@93 919 -------
ulalume3@93 920 slice : ndarray
ulalume3@93 921 The slice of profiles.
ulalume3@93 922 slice_datetimes : ndarray of datetime
ulalume3@93 923 The actual datetimes corresponding to the slice.
ulalume3@93 924 """
ulalume3@93 925 if range_corrected:
binietoglou@0 926 data = self.rc
binietoglou@0 927 else:
binietoglou@0 928 data = self.matrix
ulalume3@93 929
ulalume3@93 930 time_array = np.array(self.time)
ulalume3@93 931 start_time = self._nearest_datetime(start_time)[0]
ulalume3@93 932 stop_time = self._nearest_datetime(stop_time)[0]
ioannis@50 933
ulalume3@93 934 condition = (time_array >= start_time) & (time_array <= stop_time)
ulalume3@93 935
ulalume3@93 936 slice = data[condition, :]
ulalume3@93 937 slice_datetimes = time_array[condition]
ulalume3@93 938
ulalume3@93 939 return slice, slice_datetimes
ioannis@50 940
binietoglou@15 941 def average_profile(self):
ulalume3@93 942 """ Return the average profile (NOT range corrected) for all the duration of
ulalume3@93 943 the measurement.
ulalume3@93 944
ulalume3@93 945 Returns
ulalume3@93 946 -------
ulalume3@93 947 profile : ndarray
ulalume3@93 948 The average profile for all data.
ulalume3@93 949 """
ulalume3@93 950 profile = np.mean(self.matrix, axis=0)
ulalume3@93 951 return profile
ioannis@50 952
ioannis@189 953 def plot(self, figsize=(8, 4), signal_type='rc', zoom=[0, 12000, 0, None], show_plot=True,
ulalume3@93 954 cmap=plt.cm.jet, z0=None, title=None, vmin=0, vmax=1.3 * 10 ** 7):
ulalume3@93 955 """
ulalume3@93 956 Plot of the channel data.
ulalume3@93 957
ulalume3@93 958 Parameters
ulalume3@93 959 ----------
ulalume3@93 960 figsize : tuple
ulalume3@93 961 (width, height) of the output figure (inches)
ulalume3@93 962 signal_type : str
ulalume3@93 963 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
ulalume3@93 964 zoom : list
ulalume3@93 965 A four elemet list defined as [xmin, xmax, ymin, ymax]. Use ymin=0, ymax=-1 to plot full range.
ulalume3@93 966 show_plot : bool
ulalume3@93 967 If True, the show_plot command is run.
ulalume3@93 968 cmap : cmap
ulalume3@93 969 An instance of a matplotlib colormap to use.
ulalume3@93 970 z0 : float
ulalume3@93 971 The ground-level altitude. If provided the plot shows altitude above sea level.
ulalume3@93 972 title : str
ulalume3@93 973 Optional title for the plot.
ulalume3@93 974 vmin : float
ulalume3@93 975 Minimum value for the color scale.
ulalume3@93 976 vmax : float
ulalume3@93 977 Maximum value for the color scale.
ulalume3@93 978 """
ulalume3@77 979 fig = plt.figure(figsize=figsize)
binietoglou@0 980 ax1 = fig.add_subplot(111)
ulalume3@77 981
ioannis@189 982 self.draw_plot_new(ax1, cmap=cmap, signal_type=signal_type, zoom=zoom, z0=z0, vmin=vmin, vmax=vmax)
ioannis@50 983
binietoglou@17 984 if title:
binietoglou@17 985 ax1.set_title(title)
binietoglou@17 986 else:
ioannis@50 987 ax1.set_title("%s signal - %s" % (signal_type.upper(), self.name))
ioannis@50 988
ulalume3@77 989 if show_plot:
ulalume3@77 990 plt.show()
ioannis@50 991
ioannis@50 992 def draw_plot(self, ax1, cmap=plt.cm.jet, signal_type='rc',
ioannis@189 993 zoom=[0, 12000, 0, None], z0=None,
ioannis@50 994 add_colorbar=True, cmap_label='a.u.', cb_format=None,
ioannis@50 995 vmin=0, vmax=1.3 * 10 ** 7):
ulalume3@93 996 """
ulalume3@93 997 Draw channel data on the given axis.
ulalume3@93 998
ulalume3@93 999 Parameters
ulalume3@93 1000 ----------
ulalume3@93 1001 ax1 : axis object
ulalume3@93 1002 The axis object to draw.
ulalume3@93 1003 cmap : cmap
ulalume3@93 1004 An instance of a matplotlib colormap to use.
ulalume3@93 1005 signal_type : str
ulalume3@93 1006 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
ulalume3@93 1007 zoom : list
ulalume3@93 1008 A four elemet list defined as [xmin, xmax, ymin, ymax]. Use ymin=0, ymax=-1 to plot full range.
ulalume3@93 1009 z0 : float
ulalume3@93 1010 The ground-level altitude. If provided the plot shows altitude above sea level.
ulalume3@93 1011 add_colorbar : bool
ulalume3@93 1012 If True, a colorbar will be added to the plot.
ulalume3@93 1013 cmap_label : str
ulalume3@93 1014 Label for the colorbar. Ignored if add_colorbar is False.
ulalume3@93 1015 cb_format : str
i@132 1016 Colorbar tick format string.
ulalume3@93 1017 vmin : float
ulalume3@93 1018 Minimum value for the color scale.
ulalume3@93 1019 vmax : float
ulalume3@93 1020 Maximum value for the color scale.
ulalume3@93 1021 """
binietoglou@0 1022 if signal_type == 'rc':
binietoglou@0 1023 if len(self.rc) == 0:
binietoglou@0 1024 self.calculate_rc()
binietoglou@0 1025 data = self.rc
binietoglou@0 1026 else:
binietoglou@0 1027 data = self.matrix
ioannis@50 1028
ulalume3@92 1029 hmax_idx = self._index_at_height(zoom[1])
ioannis@50 1030
binietoglou@17 1031 # If z0 is given, then the plot is a.s.l.
binietoglou@17 1032 if z0:
binietoglou@17 1033 ax1.set_ylabel('Altitude a.s.l. [km]')
binietoglou@17 1034 else:
binietoglou@17 1035 ax1.set_ylabel('Altitude a.g.l. [km]')
binietoglou@17 1036 z0 = 0
ioannis@50 1037
binietoglou@17 1038 ax1.set_xlabel('Time UTC [hh:mm]')
ioannis@50 1039 # y axis in km, xaxis /2 to make 30s measurements in minutes. Only for 1064
ioannis@50 1040 # dateFormatter = mpl.dates.DateFormatter('%H.%M')
ioannis@50 1041 # hourlocator = mpl.dates.HourLocator()
ioannis@50 1042
ioannis@50 1043 # dayFormatter = mpl.dates.DateFormatter('\n\n%d/%m')
ioannis@50 1044 # daylocator = mpl.dates.DayLocator()
binietoglou@17 1045 hourFormatter = mpl.dates.DateFormatter('%H:%M')
ioannis@50 1046 hourlocator = mpl.dates.AutoDateLocator(minticks=3, maxticks=8, interval_multiples=True)
binietoglou@0 1047
ioannis@50 1048 # ax1.axes.xaxis.set_major_formatter(dayFormatter)
ioannis@50 1049 # ax1.axes.xaxis.set_major_locator(daylocator)
binietoglou@0 1050 ax1.axes.xaxis.set_major_formatter(hourFormatter)
binietoglou@0 1051 ax1.axes.xaxis.set_major_locator(hourlocator)
binietoglou@0 1052
binietoglou@0 1053 ts1 = mpl.dates.date2num(self.start_time)
binietoglou@0 1054 ts2 = mpl.dates.date2num(self.stop_time)
ioannis@50 1055
ioannis@50 1056 im1 = ax1.imshow(data.transpose()[zoom[0]:hmax_idx, zoom[2]:zoom[3]],
ioannis@50 1057 aspect='auto',
ioannis@50 1058 origin='lower',
ioannis@50 1059 cmap=cmap,
ioannis@50 1060 vmin=vmin,
ioannis@50 1061 # vmin = data[:,10:400].max() * 0.1,
ioannis@50 1062 vmax=vmax,
ioannis@50 1063 # vmax = data[:,10:400].max() * 0.9,
ioannis@50 1064 extent=[ts1, ts2, self.z[zoom[0]] / 1000.0 + z0 / 1000.,
ioannis@50 1065 self.z[hmax_idx] / 1000.0 + z0 / 1000.],
ioannis@50 1066 )
ioannis@50 1067
ulalume3@27 1068 if add_colorbar:
ulalume3@27 1069 if cb_format:
ioannis@50 1070 cb1 = plt.colorbar(im1, format=cb_format)
ulalume3@27 1071 else:
ulalume3@27 1072 cb1 = plt.colorbar(im1)
ulalume3@27 1073 cb1.ax.set_ylabel(cmap_label)
ulalume3@27 1074
ulalume3@27 1075 # Make the ticks of the colorbar smaller, two points smaller than the default font size
ulalume3@27 1076 cb_font_size = mpl.rcParams['font.size'] - 2
ulalume3@27 1077 for ticklabels in cb1.ax.get_yticklabels():
ulalume3@27 1078 ticklabels.set_fontsize(cb_font_size)
ulalume3@27 1079 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size)
ioannis@50 1080
ioannis@50 1081 def draw_plot_new(self, ax1, cmap=plt.cm.jet, signal_type='rc',
ioannis@50 1082 zoom=[0, 12000, 0, None], z0=None,
ioannis@50 1083 add_colorbar=True, cmap_label='a.u.',
ioannis@50 1084 cb_format=None, power_limits=(-2, 2),
ioannis@50 1085 date_labels=False,
ioannis@50 1086 vmin=0, vmax=1.3 * 10 ** 7):
ioannis@50 1087
ulalume3@93 1088 """
ulalume3@93 1089 Updated drawing routine, using pcolormesh. It is slower but provides more flexibility / precision
ulalume3@93 1090 in time plotting. The 2 drawing routines should be merged.
ulalume3@93 1091
ulalume3@93 1092 Check draw_plot method for the meaning of the input arguments.
ulalume3@93 1093 """
ulalume3@93 1094 # TODO: Merge the two drawing routines.
ulalume3@93 1095
ioannis@50 1096 if signal_type == 'rc':
ioannis@50 1097 if len(self.rc) == 0:
ioannis@50 1098 self.calculate_rc()
ioannis@50 1099 data = self.rc
ioannis@50 1100 else:
ioannis@50 1101 data = self.matrix
ioannis@50 1102
ioannis@148 1103 self.draw_data(ax1, data, add_colorbar, cmap, cb_format, cmap_label, date_labels, power_limits, vmax, vmin,
ioannis@148 1104 z0, zoom)
ioannis@148 1105
ioannis@148 1106 def draw_data(self, ax1, data, add_colorbar, cmap, cb_format, cmap_label, date_labels, power_limits, vmax, vmin,
ioannis@148 1107 z0, zoom):
ioannis@148 1108
ioannis@148 1109 """
ioannis@148 1110 This method plots generic lidar data in 2d Plot.
ioannis@148 1111
ioannis@148 1112 Updated drawing routine, using pcolormesh. It is slower but provides more flexibility / precision
ioannis@148 1113 in time plotting. The 2 drawing routines should be merged.
ioannis@148 1114
ioannis@148 1115
ioannis@148 1116 """
ulalume3@92 1117 hmax_idx = self._index_at_height(zoom[1])
ulalume3@92 1118 hmin_idx = self._index_at_height(zoom[0])
ioannis@50 1119
ioannis@50 1120 # If z0 is given, then the plot is a.s.l.
ioannis@50 1121 if z0:
ioannis@50 1122 ax1.set_ylabel('Altitude a.s.l. [km]')
ioannis@50 1123 else:
ioannis@50 1124 ax1.set_ylabel('Altitude a.g.l. [km]')
ioannis@50 1125 z0 = 0
ioannis@148 1126 ax1.set_xlabel('Time UTC [hh:mm]')
ioannis@50 1127
ioannis@50 1128 # y axis in km, xaxis /2 to make 30s measurements in minutes. Only for 1064
ioannis@50 1129 # dateFormatter = mpl.dates.DateFormatter('%H.%M')
ioannis@50 1130 # hourlocator = mpl.dates.HourLocator()
ioannis@50 1131 if date_labels:
ioannis@50 1132 dayFormatter = mpl.dates.DateFormatter('%H:%M\n%d/%m/%Y')
ioannis@50 1133 daylocator = mpl.dates.AutoDateLocator(minticks=3, maxticks=8, interval_multiples=True)
ioannis@50 1134 ax1.axes.xaxis.set_major_formatter(dayFormatter)
ioannis@50 1135 ax1.axes.xaxis.set_major_locator(daylocator)
ioannis@50 1136 else:
ioannis@50 1137 hourFormatter = mpl.dates.DateFormatter('%H:%M')
ioannis@50 1138 hourlocator = mpl.dates.AutoDateLocator(minticks=3, maxticks=8, interval_multiples=True)
ioannis@50 1139 ax1.axes.xaxis.set_major_formatter(hourFormatter)
ioannis@50 1140 ax1.axes.xaxis.set_major_locator(hourlocator)
ioannis@50 1141
ioannis@50 1142 # Get the values of the time axis
i@116 1143 dt = datetime.timedelta(seconds=self.duration[-1])
ioannis@50 1144
ioannis@50 1145 time_cut = self.time[zoom[2]:zoom[3]]
ioannis@189 1146
ioannis@50 1147 time_last = time_cut[-1] + dt # The last element needed for pcolormesh
ioannis@50 1148 time_all = time_cut + (time_last,)
ioannis@50 1149 t_axis = mpl.dates.date2num(time_all)
ioannis@172 1150
ioannis@50 1151 # Get the values of the z axis
ioannis@50 1152 z_cut = self.z[hmin_idx:hmax_idx] - self.resolution / 2.
ioannis@50 1153 z_last = z_cut[-1] + self.resolution
ioannis@50 1154 z_axis = np.append(z_cut, z_last) / 1000. + z0 / 1000. # Convert to km
ioannis@50 1155 # Plot
ioannis@50 1156 im1 = ax1.pcolormesh(t_axis, z_axis, data.T[hmin_idx:hmax_idx, zoom[2]:zoom[3]],
ioannis@50 1157 cmap=cmap,
ioannis@50 1158 vmin=vmin,
ioannis@50 1159 vmax=vmax,
ioannis@50 1160 )
ioannis@50 1161 if add_colorbar:
ioannis@50 1162 if cb_format:
ioannis@146 1163 cb1 = plt.colorbar(im1, format=cb_format, extend='both')
ioannis@50 1164 else:
ioannis@50 1165 cb_formatter = ScalarFormatter()
ioannis@50 1166 cb_formatter.set_powerlimits(power_limits)
ioannis@146 1167 cb1 = plt.colorbar(im1, format=cb_formatter, extend='both')
ioannis@50 1168 cb1.ax.set_ylabel(cmap_label)
ioannis@50 1169
ioannis@50 1170 # Make the ticks of the colorbar smaller, two points smaller than the default font size
ioannis@50 1171 cb_font_size = mpl.rcParams['font.size'] - 2
ioannis@50 1172 for ticklabels in cb1.ax.get_yticklabels():
ioannis@50 1173 ticklabels.set_fontsize(cb_font_size)
ioannis@50 1174 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size)
ioannis@50 1175
ulalume3@92 1176 def _index_at_height(self, height):
ulalume3@93 1177 """
ulalume3@93 1178 Get the altitude index nearest to the specified height.
ulalume3@93 1179
ulalume3@93 1180 Parameters
ulalume3@93 1181 ----------
ulalume3@93 1182 height : float
ulalume3@93 1183 Height (m)
ulalume3@93 1184
ulalume3@93 1185 Returns
ulalume3@93 1186 -------
ulalume3@93 1187 idx : int
ulalume3@93 1188 Index corresponding to the provided height.
ulalume3@93 1189 """
binietoglou@0 1190 idx = np.array(np.abs(self.z - height).argmin())
ulalume3@24 1191 if idx.size > 1:
ioannis@50 1192 idx = idx[0]
binietoglou@0 1193 return idx

mercurial