atmospheric_lidar/generic.py

Thu, 13 Sep 2018 13:59:32 +0300

author
Iannis B <ioannis@inoe.ro>
date
Thu, 13 Sep 2018 13:59:32 +0300
changeset 153
24ce9e10906c
parent 150
a2be81b7ace3
child 164
e4915d84dd7d
permissions
-rwxr-xr-x

Start of telecover conversion function (just for merging).

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

mercurial