atmospheric_lidar/generic.py

Wed, 10 Jan 2018 17:04:33 +0200

author
Iannis <i.binietoglou@impworks.gr>
date
Wed, 10 Jan 2018 17:04:33 +0200
changeset 118
f9990fc845c4
parent 117
44a43b0e452f
child 121
4522191fe936
permissions
-rwxr-xr-x

Adding changelog.

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 ----------
ulalume3@92 37 file_list : list
ulalume3@92 38 A list of the full paths to the input file.
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
ulalume3@92 82 Note
ulalume3@92 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
ulalume3@92 298 Note
ulalume3@92 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@116 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@116 406 for attribute_name in ['System', 'Latitude_degrees_north', 'Longitude_degrees_east', 'Altitude_meter_asl']:
i@116 407 if attribute_name in parameters.general_parameters.keys():
i@116 408 if attribute_name in input_values:
i@116 409 logger.info("Overriding {0} attribute, using the value provided in the parameter file.".format(
i@116 410 attribute_name))
i@116 411 input_values[attribute_name] = parameters.general_parameters[attribute_name]
ioannis@50 412
ulalume3@92 413 # Open a netCDF file. The format is specified in the beginning of this module.
ulalume3@92 414 with netcdf.Dataset(filename, 'w', format=NETCDF_FORMAT) as f:
ulalume3@92 415
ulalume3@92 416 # Create the dimensions in the file
ulalume3@92 417 for (d, v) in dimensions.iteritems():
ulalume3@92 418 v = input_values.pop(d, v)
ulalume3@92 419 f.createDimension(d, v)
ioannis@50 420
ulalume3@92 421 # Create global attributes
i@116 422 for (attrib, value) in global_attributes.iteritems():
ulalume3@92 423 val = input_values.pop(attrib, value)
ulalume3@92 424 if val:
ulalume3@92 425 setattr(f, attrib, val)
ulalume3@92 426
ulalume3@92 427 # Variables
ulalume3@92 428 # Write either channel_id or string_channel_id in the file
i@116 429 first_channel_keys = parameters.channel_parameters.items()[0][1].keys()
ulalume3@92 430 if "channel_ID" in first_channel_keys:
ulalume3@92 431 channel_var = 'channel_ID'
ulalume3@92 432 variable_type = 'i'
i@116 433 elif "channel_string_ID" in first_channel_keys:
i@116 434 channel_var = 'channel_string_ID'
ulalume3@92 435 variable_type = str
ulalume3@92 436 else:
ulalume3@92 437 raise ValueError('Channel parameters should define either "chanel_id" or "channel_string_ID".')
ioannis@50 438
ulalume3@92 439 temp_v = f.createVariable(channel_var, variable_type, ('channels',))
i@116 440 for n, channel in enumerate(channel_names):
i@116 441 temp_v[n] = parameters.channel_parameters[channel][channel_var]
ulalume3@92 442
i@116 443 # Write the custom subclass variables:
i@116 444 for variable in self._get_custom_variables(channel_names):
i@116 445 temp_v = f.createVariable(variable["name"], variable["type"], variable["dimensions"])
i@116 446 temp_v[:] = variable["values"]
ulalume3@54 447
ulalume3@92 448 # Write the values of fixed channel parameters:
i@116 449 channel_variable_specs = self._get_scc_channel_variables()
i@116 450
i@116 451 for variable_name in self._get_provided_variable_names():
i@116 452
i@116 453 # Check if the variable already defined (e.g. from values in Licel files).
i@116 454 if variable_name in f.variables.keys():
i@116 455 logger.warning(
i@116 456 "Provided values of \"{0}\" were ignored because they were read from other source.".format(
i@116 457 variable_name))
i@116 458 continue
i@116 459
i@116 460 if variable_name not in channel_variable_specs.keys():
i@116 461 logger.warning("Provided variable {0} is not parts of the specs: {1}".format(variable_name, channel_variable_specs.keys()))
i@116 462 continue
i@116 463
i@116 464 temp_v = f.createVariable(variable_name,
i@116 465 channel_variable_specs[variable_name][1],
i@116 466 channel_variable_specs[variable_name][0])
i@116 467
i@116 468 for n, channel_name in enumerate(channel_names):
ulalume3@92 469 try:
i@116 470 temp_v[n] = parameters.channel_parameters[channel_name][variable_name]
ulalume3@92 471 except KeyError: # The parameter was not provided for this channel so we mask the value.
i@116 472 pass # Default value should be a missing value # TODO: Check this.
ulalume3@92 473
ulalume3@92 474 # Write the id_timescale values
ulalume3@92 475 temp_id_timescale = f.createVariable('id_timescale', 'i', ('channels',))
i@116 476 for (channel, n) in zip(channel_names, range(len(channel_names))):
ulalume3@92 477 temp_id_timescale[n] = self.variables['id_timescale'][channel]
ioannis@50 478
ulalume3@92 479 # Laser pointing angle
ulalume3@92 480 temp_v = f.createVariable('Laser_Pointing_Angle', 'd', ('scan_angles',))
i@116 481 temp_v[:] = parameters.general_parameters['Laser_Pointing_Angle']
ulalume3@92 482
ulalume3@92 483 # Molecular calculation
ulalume3@92 484 temp_v = f.createVariable('Molecular_Calc', 'i')
i@116 485 temp_v[:] = parameters.general_parameters['Molecular_Calc']
ioannis@50 486
ulalume3@92 487 # Laser pointing angles of profiles
ulalume3@92 488 temp_v = f.createVariable('Laser_Pointing_Angle_of_Profiles', 'i', ('time', 'nb_of_time_scales'))
ulalume3@92 489 for (time_scale, n) in zip(self.variables['Raw_Data_Start_Time'],
ulalume3@92 490 range(len(self.variables['Raw_Data_Start_Time']))):
ulalume3@92 491 temp_v[:len(time_scale), n] = 0 # The lidar has only one laser pointing angle
ioannis@50 492
ulalume3@92 493 # Raw data start/stop time
ulalume3@92 494 temp_raw_start = f.createVariable('Raw_Data_Start_Time', 'i', ('time', 'nb_of_time_scales'))
ulalume3@92 495 temp_raw_stop = f.createVariable('Raw_Data_Stop_Time', 'i', ('time', 'nb_of_time_scales'))
ulalume3@92 496 for (start_time, stop_time, n) in zip(self.variables['Raw_Data_Start_Time'],
ulalume3@92 497 self.variables['Raw_Data_Stop_Time'],
ulalume3@92 498 range(len(self.variables['Raw_Data_Start_Time']))):
ulalume3@92 499 temp_raw_start[:len(start_time), n] = start_time
ulalume3@92 500 temp_raw_stop[:len(stop_time), n] = stop_time
ioannis@50 501
ulalume3@92 502 # Laser shots
i@116 503 if "Laser_Shots" in f.variables.keys():
i@116 504 logger.warning("Provided values of \"Laser_Shots\" were ignored because they were read from other source.")
i@116 505 else:
ulalume3@92 506 temp_v = f.createVariable('Laser_Shots', 'i', ('time', 'channels'))
i@116 507 for (channel, n) in zip(channel_names, range(len(channel_names))):
ulalume3@92 508 time_length = len(self.variables['Raw_Data_Start_Time'][self.variables['id_timescale'][channel]])
i@116 509 # Array slicing stopped working as usual ex. temp_v[:10] = 100 does not work. ??? np.ones was added.
i@116 510 temp_v[:time_length, n] = np.ones(time_length) * parameters.channel_parameters[channel][
i@116 511 'Laser_Shots']
victor@84 512
ulalume3@92 513 # Raw lidar data
ulalume3@92 514 temp_v = f.createVariable('Raw_Lidar_Data', 'd', ('time', 'channels', 'points'))
i@116 515 for (channel, n) in zip(channel_names, range(len(channel_names))):
ulalume3@92 516 c = self.channels[channel]
ulalume3@92 517 temp_v[:len(c.time), n, :c.points] = c.matrix
ulalume3@24 518
i@116 519 self.add_dark_measurements_to_netcdf(f, channel_names)
ioannis@50 520
ulalume3@92 521 # Pressure at lidar station
ulalume3@92 522 temp_v = f.createVariable('Pressure_at_Lidar_Station', 'd')
ulalume3@92 523 temp_v[:] = self.info['Pressure']
binietoglou@0 524
ulalume3@92 525 # Temperature at lidar station
ulalume3@92 526 temp_v = f.createVariable('Temperature_at_Lidar_Station', 'd')
ulalume3@92 527 temp_v[:] = self.info['Temperature']
ioannis@50 528
ulalume3@92 529 self.save_netcdf_extra(f)
binietoglou@43 530
i@116 531 def _get_scc_channel_variables(self):
binietoglou@43 532 channel_variables = \
ulalume3@54 533 {'Background_Low': (('channels',), 'd'),
binietoglou@43 534 'Background_High': (('channels',), 'd'),
i@116 535 'Laser_Repetition_Rate': (('channels',), 'i'),
i@116 536 'Scattering_Mechanism': (('channels',), 'i'),
i@116 537 'Signal_Type': (('channels',), 'i'),
i@116 538 'Emitted_Wavelength': (('channels',), 'd'),
i@116 539 'Detected_Wavelength': (('channels',), 'd'),
i@116 540 'Raw_Data_Range_Resolution': (('channels',), 'd'),
i@116 541 'Background_Mode': (('channels',), 'i'),
i@116 542 'Dead_Time_Corr_Type': (('channels',), 'i'),
i@116 543 'Dead_Time': (('channels',), 'd'),
i@116 544 'Acquisition_Mode': (('channels',), 'i'),
i@116 545 'Trigger_Delay': (('channels',), 'd'),
i@116 546 'Pol_Calib_Range_Min': (('channels',), 'i'),
i@116 547 'Pol_Calib_Range_Max': (('channels',), 'i'),
binietoglou@43 548 'LR_Input': (('channels',), 'i'),
binietoglou@43 549 'DAQ_Range': (('channels',), 'd'),
binietoglou@43 550 }
binietoglou@43 551 return channel_variables
ulalume3@92 552
i@116 553 def _get_provided_variable_names(self):
i@116 554 """ Return a list of """
i@116 555 # When looking for channel parameters, ignore the following parameter names:
i@116 556 ignore = {'channel_ID', 'channel_string_ID', 'Depolarization_Factor', 'Laser_Shots'} # Set
ulalume3@92 557
victor@76 558 channels = self.channels.keys()
i@116 559 channel_parameters = self.extra_netcdf_parameters.channel_parameters
ulalume3@92 560
i@116 561 # Get all the provided parameters (both mandatory and optional):
i@116 562 all_provided_variables = [channel_parameters[channel].keys() for channel in channels]
i@116 563 provided_variables = set(itertools.chain.from_iterable(all_provided_variables))
ulalume3@92 564
i@116 565 # Discard certain parameter names:
i@116 566 provided_variables -= ignore
ulalume3@92 567
i@116 568 return provided_variables
binietoglou@43 569
binietoglou@0 570 def add_dark_measurements_to_netcdf(self, f, channels):
ulalume3@92 571 """
ulalume3@92 572 Adds dark measurement variables and properties to an open netCDF file.
ulalume3@92 573
ulalume3@92 574 Parameters
ulalume3@92 575 ----------
ulalume3@92 576 f : netcdf Dataset
ulalume3@92 577 A netCDF Dataset, open for writing.
ulalume3@92 578 channels : list
ulalume3@92 579 A list of channels names to consider when adding dark measurements.
ulalume3@92 580 """
binietoglou@0 581 # Get dark measurements. If it is not given in self.dark_measurement
binietoglou@0 582 # try to get it using the get_dark_measurements method. If none is found
binietoglou@0 583 # return without adding something.
binietoglou@0 584 if self.dark_measurement is None:
binietoglou@0 585 self.dark_measurement = self.get_dark_measurements()
ioannis@50 586
binietoglou@0 587 if self.dark_measurement is None:
binietoglou@0 588 return
ioannis@50 589
binietoglou@0 590 dark_measurement = self.dark_measurement
ioannis@50 591
binietoglou@0 592 # Calculate the length of the time_bck dimensions
binietoglou@0 593 number_of_profiles = [len(c.time) for c in dark_measurement.channels.values()]
binietoglou@0 594 max_number_of_profiles = np.max(number_of_profiles)
ioannis@50 595
binietoglou@0 596 # Create the dimension
binietoglou@0 597 f.createDimension('time_bck', max_number_of_profiles)
ioannis@50 598
binietoglou@0 599 # Save the dark measurement data
ioannis@50 600 temp_v = f.createVariable('Background_Profile', 'd', ('time_bck', 'channels', 'points'))
ioannis@50 601 for (channel, n) in zip(channels, range(len(channels))):
binietoglou@0 602 c = dark_measurement.channels[channel]
ioannis@50 603 temp_v[:len(c.time), n, :c.points] = c.matrix
ioannis@50 604
binietoglou@0 605 # Dark profile start/stop time
ioannis@50 606 temp_raw_start = f.createVariable('Raw_Bck_Start_Time', 'i', ('time_bck', 'nb_of_time_scales'))
ioannis@50 607 temp_raw_stop = f.createVariable('Raw_Bck_Stop_Time', 'i', ('time_bck', 'nb_of_time_scales'))
ioannis@50 608 for (start_time, stop_time, n) in zip(dark_measurement.variables['Raw_Data_Start_Time'],
ioannis@50 609 dark_measurement.variables['Raw_Data_Stop_Time'],
ioannis@50 610 range(len(dark_measurement.variables['Raw_Data_Start_Time']))):
ioannis@50 611 temp_raw_start[:len(start_time), n] = start_time
ioannis@50 612 temp_raw_stop[:len(stop_time), n] = stop_time
ioannis@50 613
binietoglou@0 614 # Dark measurement start/stop time
binietoglou@0 615 f.RawBck_Start_Date = dark_measurement.info['start_time'].strftime('%Y%m%d')
binietoglou@0 616 f.RawBck_Start_Time_UT = dark_measurement.info['start_time'].strftime('%H%M%S')
binietoglou@0 617 f.RawBck_Stop_Time_UT = dark_measurement.info['stop_time'].strftime('%H%M%S')
binietoglou@0 618
binietoglou@0 619 def save_netcdf_extra(self, f):
ulalume3@92 620 """ Save extra netCDF parameters to an open netCDF file.
ulalume3@92 621
ulalume3@92 622 If required, this method should be overwritten by subclasses of BaseLidarMeasurement.
ulalume3@92 623 """
binietoglou@0 624 pass
ioannis@50 625
binietoglou@0 626 def plot(self):
binietoglou@0 627 for channel in self.channels:
ioannis@50 628 self.channels[channel].plot(show_plot=False)
binietoglou@0 629 plt.show()
ioannis@50 630
binietoglou@0 631 def get_dark_measurements(self):
binietoglou@0 632 return None
ulalume3@92 633
i@116 634 def _get_custom_global_attributes(self):
victor@84 635 """
i@116 636 Abstract method to provide NetCDF global attributes that should be included
ulalume3@92 637 in the final NetCDF file.
victor@87 638
ulalume3@92 639 If required, this method should be implemented by subclasses of BaseLidarMeasurement.
victor@87 640 """
victor@87 641 return []
ioannis@50 642
i@116 643 def _get_custom_variables(self, channel_names=None):
ulalume3@92 644 """
i@116 645 Abstract method to provide custom NetCDF variables that should be included in the final NetCDF file.
ulalume3@92 646
ulalume3@92 647 If required, this method should be implemented by subclasses of BaseLidarMeasurement.
ulalume3@92 648 """
ulalume3@92 649 return []
binietoglou@0 650
binietoglou@0 651
ulalume3@27 652 class LidarChannel:
ulalume3@93 653 """
ulalume3@93 654 This class represents a general measurement channel, independent of the input files.
ulalume3@93 655
ulalume3@93 656 The class assumes that the input files are consecutive, i.e. there are no measurements gaps.
ulalume3@93 657 """
ulalume3@93 658
ulalume3@27 659 def __init__(self, channel_parameters):
ulalume3@93 660 """
ulalume3@93 661 This is run when creating a new object.
ulalume3@93 662
ulalume3@93 663 The input dictionary should contain 'name', 'binwidth', and 'data' keys.
ulalume3@93 664
ulalume3@93 665 Parameters
ulalume3@93 666 ----------
ulalume3@93 667 channel_parameters : dict
ulalume3@93 668 A dict containing channel parameters.
ulalume3@93 669 """
ulalume3@93 670 # TODO: Change channel_parameters to explicit keyword arguments?
ulalume3@93 671
ulalume3@93 672 c = 299792458. # Speed of light
binietoglou@0 673 self.wavelength = channel_parameters['name']
binietoglou@0 674 self.name = str(self.wavelength)
ulalume3@27 675 self.binwidth = float(channel_parameters['binwidth']) # in microseconds
binietoglou@0 676 self.data = {}
binietoglou@0 677 self.resolution = self.binwidth * c / 2
ioannis@50 678 self.z = np.arange(
ioannis@50 679 len(channel_parameters['data'])) * self.resolution + self.resolution / 2.0 # Change: add half bin in the z
binietoglou@0 680 self.points = len(channel_parameters['data'])
binietoglou@0 681 self.rc = []
i@116 682
i@116 683 def get_duration(self):
i@116 684 """ Get an array with the duration of each profile in seconds.
i@116 685
i@116 686 If the duration property already exists, returns this.
i@116 687 If not, it tries to estimate it based on the time difference of the profiles.
i@116 688
i@116 689 Returns
i@116 690 -------
i@116 691 duration : ndarray
i@116 692 1D array containing profile durations.
i@116 693 """
i@116 694
i@116 695 current_value = getattr(self, 'duration', None)
i@116 696
i@116 697 if current_value:
i@116 698 return np.array(current_value)
i@116 699
i@116 700 time_diff = np.diff(self.time)
i@116 701 durations = [dt.seconds for dt in time_diff]
i@116 702 # Assume the last two profiles have the same duration
i@116 703 duration = np.array(durations)
i@116 704 return duration
ioannis@50 705
ioannis@50 706 def calculate_rc(self, idx_min=4000, idx_max=5000):
ulalume3@93 707 """ Calculate range corrected signal.
ulalume3@93 708
ulalume3@93 709 The background signal is estimated as the mean signal between idx_min and idx_max.
ulalume3@93 710
ulalume3@93 711 The calculations is stored in the self.rc parameters.
ulalume3@93 712
ulalume3@93 713 Parameters
ulalume3@93 714 ----------
ulalume3@93 715 idx_min : int
ulalume3@93 716 Minimum index to calculate background signal.
ulalume3@93 717 idx_max : int
ulalume3@93 718 Maximum index to calculate background signal.
ulalume3@93 719 """
ioannis@50 720 background = np.mean(self.matrix[:, idx_min:idx_max], axis=1) # Calculate the background from 30000m and above
ioannis@50 721 self.rc = (self.matrix.transpose() - background).transpose() * (self.z ** 2)
ioannis@50 722
binietoglou@0 723 def update(self):
ulalume3@93 724 """
ulalume3@93 725 Update the time parameters and data according to the raw input data.
ulalume3@93 726 """
binietoglou@0 727 self.start_time = min(self.data.keys())
i@116 728 self.stop_time = max(self.data.keys()) + datetime.timedelta(seconds=self.duration[-1])
binietoglou@0 729 self.time = tuple(sorted(self.data.keys()))
binietoglou@0 730 sorted_data = sorted(self.data.iteritems(), key=itemgetter(0))
ioannis@50 731 self.matrix = np.array(map(itemgetter(1), sorted_data))
ioannis@50 732
ulalume3@93 733 def _nearest_datetime(self, input_time):
ulalume3@93 734 """
ulalume3@93 735 Find the profile datetime and index that is nearest to the given datetime.
ulalume3@93 736
ulalume3@93 737 Parameters
ulalume3@93 738 ----------
ulalume3@93 739 input_time : datetime.datetime
ulalume3@93 740 Input datetime object.
ulalume3@93 741
ulalume3@93 742 Returns
ulalume3@93 743 -------
ulalume3@93 744 profile_datetime : datetime
ulalume3@93 745 The datetime of the selected profile.
ulalume3@93 746 profile_idx : int
ulalume3@93 747 The index of the selected profile.
ulalume3@93 748 """
i@116 749 max_duration = np.max(self.duration)
i@116 750
i@116 751 margin = datetime.timedelta(seconds=max_duration * 5)
ulalume3@93 752
ulalume3@93 753 if ((input_time + margin) < self.start_time) | ((input_time - margin) > self.stop_time):
i@116 754 logger.error("Requested date not covered in this file")
ioannis@55 755 raise ValueError("Requested date not covered in this file")
ulalume3@93 756 dt = np.abs(np.array(self.time) - input_time)
ulalume3@93 757
ulalume3@93 758 profile_idx = np.argmin(dt)
ulalume3@93 759 profile_datetime = self.time[profile_idx]
binietoglou@0 760
ulalume3@93 761 dt_min = dt[profile_idx]
i@116 762 if dt_min > datetime.timedelta(seconds=max_duration):
i@116 763 logger.warning("Nearest profile more than %s seconds away. dt = %s." % (max_duration, dt_min))
ulalume3@93 764
ulalume3@93 765 return profile_datetime, profile_idx
ioannis@50 766
binietoglou@0 767 def subset_by_time(self, start_time, stop_time):
ulalume3@93 768 """
ulalume3@93 769 Subset the channel for a specific time interval.
ioannis@50 770
ulalume3@93 771 Parameters
ulalume3@93 772 ----------
ulalume3@93 773 start_time : datetime
ulalume3@93 774 The starting datetime to subset.
ulalume3@93 775 stop_time : datetime
ulalume3@93 776 The stopping datetime to subset.
ulalume3@93 777
ulalume3@93 778 Returns
ulalume3@93 779 -------
ulalume3@93 780 c : LidarChannel object
ulalume3@93 781 A new channel object
ulalume3@93 782 """
binietoglou@0 783 time_array = np.array(self.time)
binietoglou@0 784 condition = (time_array >= start_time) & (time_array <= stop_time)
ioannis@50 785
binietoglou@0 786 subset_time = time_array[condition]
binietoglou@0 787 subset_data = dict([(c_time, self.data[c_time]) for c_time in subset_time])
ioannis@50 788
ioannis@50 789 # Create a list with the values needed by channel's __init__()
ioannis@50 790 parameter_values = {'name': self.wavelength,
ioannis@50 791 'binwidth': self.binwidth,
ioannis@50 792 'data': subset_data[subset_time[0]], }
ioannis@50 793
ulalume3@27 794 # We should use __class__ instead of class name, so that this works properly
ulalume3@27 795 # with subclasses
ulalume3@93 796 # Eg: c = self.__class__(parameters_values)
ulalume3@27 797 # This does not currently work with Licel files though
ulalume3@93 798 # TODO: Fix this!
ulalume3@27 799 c = LidarChannel(parameter_values)
ulalume3@27 800 c.data = subset_data
ulalume3@27 801 c.update()
ulalume3@27 802 return c
ioannis@50 803
ioannis@50 804 def subset_by_bins(self, b_min=0, b_max=None):
ulalume3@93 805 """
ulalume3@93 806 Remove some height bins from the channel data.
ulalume3@93 807
ulalume3@93 808 This could be needed to remove acquisition artifacts at
ulalume3@93 809 the first or last bins of the profiles.
ulalume3@93 810
ulalume3@93 811 Parameters
ulalume3@93 812 ----------
ulalume3@93 813 b_min : int
ulalume3@93 814 The fist valid data bin
ulalume3@93 815 b_max : int or None
ulalume3@93 816 The last valid data bin. If equal to None, all bins are used.
ulalume3@93 817
ulalume3@93 818 Returns
ulalume3@93 819 -------
ulalume3@93 820 m : LidarChannel object
ulalume3@93 821 A new channel object
ulalume3@27 822 """
ioannis@50 823
ulalume3@27 824 subset_data = {}
ioannis@50 825
ulalume3@27 826 for ctime, cdata in self.data.items():
ulalume3@27 827 subset_data[ctime] = cdata[b_min:b_max]
ioannis@50 828
ioannis@50 829 # Create a list with the values needed by channel's __init__()
ioannis@50 830 parameters_values = {'name': self.wavelength,
ioannis@50 831 'binwidth': self.binwidth,
ioannis@50 832 'data': subset_data[
ioannis@50 833 subset_data.keys()[0]], } # We just need any array with the correct length
binietoglou@0 834
ulalume3@27 835 c = LidarChannel(parameters_values)
binietoglou@0 836 c.data = subset_data
binietoglou@0 837 c.update()
binietoglou@0 838 return c
binietoglou@39 839
ulalume3@93 840 def get_profile(self, input_datetime, range_corrected=True):
ulalume3@93 841 """ Returns a single profile, that is nearest to the input datetime.
ulalume3@93 842
ulalume3@93 843 Parameters
ulalume3@93 844 ----------
ulalume3@93 845 input_datetime : datetime
ulalume3@93 846 The required datetime of the profile
ulalume3@93 847 range_corrected : bool
ulalume3@93 848 If True, the range corrected profile is returned. Else, normal signal is returned.
ulalume3@93 849
ulalume3@93 850 Returns
ulalume3@93 851 -------
ulalume3@93 852 profile : ndarray
ulalume3@93 853 The profile nearest to input_datetime.
ulalume3@93 854 t : datetime
ulalume3@93 855 The actual datetime corresponding to the profile.
ulalume3@93 856 """
ulalume3@93 857 t, idx = self._nearest_datetime(input_datetime)
ulalume3@93 858 if range_corrected:
binietoglou@0 859 data = self.rc
binietoglou@0 860 else:
binietoglou@0 861 data = self.matrix
ioannis@50 862
ulalume3@93 863 profile = data[idx, :][0]
ulalume3@93 864
ulalume3@93 865 return profile, t
ulalume3@93 866
ulalume3@93 867 def get_slice(self, start_time, stop_time, range_corrected=True):
ulalume3@93 868 """ Returns a slice of data, between the provided start and stop time.
binietoglou@0 869
ulalume3@93 870 Parameters
ulalume3@93 871 ----------
ulalume3@93 872 start_time : datetime
ulalume3@93 873 The starting time of the slice
ulalume3@93 874 stop_time : datetime
ulalume3@93 875 The stoping time of the slice
ulalume3@93 876 range_corrected : bool
ulalume3@93 877 If True, the range corrected profile is returned. Else, normal signal is returned.
ulalume3@93 878
ulalume3@93 879 Returns
ulalume3@93 880 -------
ulalume3@93 881 slice : ndarray
ulalume3@93 882 The slice of profiles.
ulalume3@93 883 slice_datetimes : ndarray of datetime
ulalume3@93 884 The actual datetimes corresponding to the slice.
ulalume3@93 885 """
ulalume3@93 886 if range_corrected:
binietoglou@0 887 data = self.rc
binietoglou@0 888 else:
binietoglou@0 889 data = self.matrix
ulalume3@93 890
ulalume3@93 891 time_array = np.array(self.time)
ulalume3@93 892 start_time = self._nearest_datetime(start_time)[0]
ulalume3@93 893 stop_time = self._nearest_datetime(stop_time)[0]
ioannis@50 894
ulalume3@93 895 condition = (time_array >= start_time) & (time_array <= stop_time)
ulalume3@93 896
ulalume3@93 897 slice = data[condition, :]
ulalume3@93 898 slice_datetimes = time_array[condition]
ulalume3@93 899
ulalume3@93 900 return slice, slice_datetimes
ioannis@50 901
binietoglou@15 902 def average_profile(self):
ulalume3@93 903 """ Return the average profile (NOT range corrected) for all the duration of
ulalume3@93 904 the measurement.
ulalume3@93 905
ulalume3@93 906 Returns
ulalume3@93 907 -------
ulalume3@93 908 profile : ndarray
ulalume3@93 909 The average profile for all data.
ulalume3@93 910 """
ulalume3@93 911 profile = np.mean(self.matrix, axis=0)
ulalume3@93 912 return profile
ioannis@50 913
ulalume3@93 914 def plot(self, figsize=(8, 4), signal_type='rc', zoom=[0, 12000, 0, -1], show_plot=True,
ulalume3@93 915 cmap=plt.cm.jet, z0=None, title=None, vmin=0, vmax=1.3 * 10 ** 7):
ulalume3@93 916 """
ulalume3@93 917 Plot of the channel data.
ulalume3@93 918
ulalume3@93 919 Parameters
ulalume3@93 920 ----------
ulalume3@93 921 figsize : tuple
ulalume3@93 922 (width, height) of the output figure (inches)
ulalume3@93 923 signal_type : str
ulalume3@93 924 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
ulalume3@93 925 zoom : list
ulalume3@93 926 A four elemet list defined as [xmin, xmax, ymin, ymax]. Use ymin=0, ymax=-1 to plot full range.
ulalume3@93 927 show_plot : bool
ulalume3@93 928 If True, the show_plot command is run.
ulalume3@93 929 cmap : cmap
ulalume3@93 930 An instance of a matplotlib colormap to use.
ulalume3@93 931 z0 : float
ulalume3@93 932 The ground-level altitude. If provided the plot shows altitude above sea level.
ulalume3@93 933 title : str
ulalume3@93 934 Optional title for the plot.
ulalume3@93 935 vmin : float
ulalume3@93 936 Minimum value for the color scale.
ulalume3@93 937 vmax : float
ulalume3@93 938 Maximum value for the color scale.
ulalume3@93 939 """
ulalume3@77 940 fig = plt.figure(figsize=figsize)
binietoglou@0 941 ax1 = fig.add_subplot(111)
ulalume3@77 942
ioannis@50 943 self.draw_plot(ax1, cmap=cmap, signal_type=signal_type, zoom=zoom, z0=z0, vmin=vmin, vmax=vmax)
ioannis@50 944
binietoglou@17 945 if title:
binietoglou@17 946 ax1.set_title(title)
binietoglou@17 947 else:
ioannis@50 948 ax1.set_title("%s signal - %s" % (signal_type.upper(), self.name))
ioannis@50 949
ulalume3@77 950 if show_plot:
ulalume3@77 951 plt.show()
ioannis@50 952
ioannis@50 953 def draw_plot(self, ax1, cmap=plt.cm.jet, signal_type='rc',
ioannis@50 954 zoom=[0, 12000, 0, -1], z0=None,
ioannis@50 955 add_colorbar=True, cmap_label='a.u.', cb_format=None,
ioannis@50 956 vmin=0, vmax=1.3 * 10 ** 7):
ulalume3@93 957 """
ulalume3@93 958 Draw channel data on the given axis.
ulalume3@93 959
ulalume3@93 960 Parameters
ulalume3@93 961 ----------
ulalume3@93 962 ax1 : axis object
ulalume3@93 963 The axis object to draw.
ulalume3@93 964 cmap : cmap
ulalume3@93 965 An instance of a matplotlib colormap to use.
ulalume3@93 966 signal_type : str
ulalume3@93 967 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
ulalume3@93 968 zoom : list
ulalume3@93 969 A four elemet list defined as [xmin, xmax, ymin, ymax]. Use ymin=0, ymax=-1 to plot full range.
ulalume3@93 970 z0 : float
ulalume3@93 971 The ground-level altitude. If provided the plot shows altitude above sea level.
ulalume3@93 972 add_colorbar : bool
ulalume3@93 973 If True, a colorbar will be added to the plot.
ulalume3@93 974 cmap_label : str
ulalume3@93 975 Label for the colorbar. Ignored if add_colorbar is False.
ulalume3@93 976 cb_format : str
ulalume3@93 977
ulalume3@93 978 vmin : float
ulalume3@93 979 Minimum value for the color scale.
ulalume3@93 980 vmax : float
ulalume3@93 981 Maximum value for the color scale.
ulalume3@93 982 """
binietoglou@0 983 if signal_type == 'rc':
binietoglou@0 984 if len(self.rc) == 0:
binietoglou@0 985 self.calculate_rc()
binietoglou@0 986 data = self.rc
binietoglou@0 987 else:
binietoglou@0 988 data = self.matrix
ioannis@50 989
ulalume3@92 990 hmax_idx = self._index_at_height(zoom[1])
ioannis@50 991
binietoglou@17 992 # If z0 is given, then the plot is a.s.l.
binietoglou@17 993 if z0:
binietoglou@17 994 ax1.set_ylabel('Altitude a.s.l. [km]')
binietoglou@17 995 else:
binietoglou@17 996 ax1.set_ylabel('Altitude a.g.l. [km]')
binietoglou@17 997 z0 = 0
ioannis@50 998
binietoglou@17 999 ax1.set_xlabel('Time UTC [hh:mm]')
ioannis@50 1000 # y axis in km, xaxis /2 to make 30s measurements in minutes. Only for 1064
ioannis@50 1001 # dateFormatter = mpl.dates.DateFormatter('%H.%M')
ioannis@50 1002 # hourlocator = mpl.dates.HourLocator()
ioannis@50 1003
ioannis@50 1004 # dayFormatter = mpl.dates.DateFormatter('\n\n%d/%m')
ioannis@50 1005 # daylocator = mpl.dates.DayLocator()
binietoglou@17 1006 hourFormatter = mpl.dates.DateFormatter('%H:%M')
ioannis@50 1007 hourlocator = mpl.dates.AutoDateLocator(minticks=3, maxticks=8, interval_multiples=True)
binietoglou@0 1008
ioannis@50 1009 # ax1.axes.xaxis.set_major_formatter(dayFormatter)
ioannis@50 1010 # ax1.axes.xaxis.set_major_locator(daylocator)
binietoglou@0 1011 ax1.axes.xaxis.set_major_formatter(hourFormatter)
binietoglou@0 1012 ax1.axes.xaxis.set_major_locator(hourlocator)
binietoglou@0 1013
binietoglou@0 1014 ts1 = mpl.dates.date2num(self.start_time)
binietoglou@0 1015 ts2 = mpl.dates.date2num(self.stop_time)
ioannis@50 1016
ioannis@50 1017 im1 = ax1.imshow(data.transpose()[zoom[0]:hmax_idx, zoom[2]:zoom[3]],
ioannis@50 1018 aspect='auto',
ioannis@50 1019 origin='lower',
ioannis@50 1020 cmap=cmap,
ioannis@50 1021 vmin=vmin,
ioannis@50 1022 # vmin = data[:,10:400].max() * 0.1,
ioannis@50 1023 vmax=vmax,
ioannis@50 1024 # vmax = data[:,10:400].max() * 0.9,
ioannis@50 1025 extent=[ts1, ts2, self.z[zoom[0]] / 1000.0 + z0 / 1000.,
ioannis@50 1026 self.z[hmax_idx] / 1000.0 + z0 / 1000.],
ioannis@50 1027 )
ioannis@50 1028
ulalume3@27 1029 if add_colorbar:
ulalume3@27 1030 if cb_format:
ioannis@50 1031 cb1 = plt.colorbar(im1, format=cb_format)
ulalume3@27 1032 else:
ulalume3@27 1033 cb1 = plt.colorbar(im1)
ulalume3@27 1034 cb1.ax.set_ylabel(cmap_label)
ulalume3@27 1035
ulalume3@27 1036 # Make the ticks of the colorbar smaller, two points smaller than the default font size
ulalume3@27 1037 cb_font_size = mpl.rcParams['font.size'] - 2
ulalume3@27 1038 for ticklabels in cb1.ax.get_yticklabels():
ulalume3@27 1039 ticklabels.set_fontsize(cb_font_size)
ulalume3@27 1040 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size)
ioannis@50 1041
ioannis@50 1042 def draw_plot_new(self, ax1, cmap=plt.cm.jet, signal_type='rc',
ioannis@50 1043 zoom=[0, 12000, 0, None], z0=None,
ioannis@50 1044 add_colorbar=True, cmap_label='a.u.',
ioannis@50 1045 cb_format=None, power_limits=(-2, 2),
ioannis@50 1046 date_labels=False,
ioannis@50 1047 vmin=0, vmax=1.3 * 10 ** 7):
ioannis@50 1048
ulalume3@93 1049 """
ulalume3@93 1050 Updated drawing routine, using pcolormesh. It is slower but provides more flexibility / precision
ulalume3@93 1051 in time plotting. The 2 drawing routines should be merged.
ulalume3@93 1052
ulalume3@93 1053 Check draw_plot method for the meaning of the input arguments.
ulalume3@93 1054 """
ulalume3@93 1055 # TODO: Merge the two drawing routines.
ulalume3@93 1056
ioannis@50 1057 if signal_type == 'rc':
ioannis@50 1058 if len(self.rc) == 0:
ioannis@50 1059 self.calculate_rc()
ioannis@50 1060 data = self.rc
ioannis@50 1061 else:
ioannis@50 1062 data = self.matrix
ioannis@50 1063
ulalume3@92 1064 hmax_idx = self._index_at_height(zoom[1])
ulalume3@92 1065 hmin_idx = self._index_at_height(zoom[0])
ioannis@50 1066
ioannis@50 1067 # If z0 is given, then the plot is a.s.l.
ioannis@50 1068 if z0:
ioannis@50 1069 ax1.set_ylabel('Altitude a.s.l. [km]')
ioannis@50 1070 else:
ioannis@50 1071 ax1.set_ylabel('Altitude a.g.l. [km]')
ioannis@50 1072 z0 = 0
ioannis@50 1073
ioannis@50 1074 ax1.set_xlabel('Time UTC [hh:mm]')
ioannis@50 1075 # y axis in km, xaxis /2 to make 30s measurements in minutes. Only for 1064
ioannis@50 1076 # dateFormatter = mpl.dates.DateFormatter('%H.%M')
ioannis@50 1077 # hourlocator = mpl.dates.HourLocator()
ioannis@50 1078
ioannis@50 1079 if date_labels:
ioannis@50 1080 dayFormatter = mpl.dates.DateFormatter('%H:%M\n%d/%m/%Y')
ioannis@50 1081 daylocator = mpl.dates.AutoDateLocator(minticks=3, maxticks=8, interval_multiples=True)
ioannis@50 1082 ax1.axes.xaxis.set_major_formatter(dayFormatter)
ioannis@50 1083 ax1.axes.xaxis.set_major_locator(daylocator)
ioannis@50 1084 else:
ioannis@50 1085 hourFormatter = mpl.dates.DateFormatter('%H:%M')
ioannis@50 1086 hourlocator = mpl.dates.AutoDateLocator(minticks=3, maxticks=8, interval_multiples=True)
ioannis@50 1087 ax1.axes.xaxis.set_major_formatter(hourFormatter)
ioannis@50 1088 ax1.axes.xaxis.set_major_locator(hourlocator)
ioannis@50 1089
ioannis@50 1090 # Get the values of the time axis
i@116 1091 dt = datetime.timedelta(seconds=self.duration[-1])
ioannis@50 1092
ioannis@50 1093 time_cut = self.time[zoom[2]:zoom[3]]
ioannis@50 1094 time_last = time_cut[-1] + dt # The last element needed for pcolormesh
ioannis@50 1095
ioannis@50 1096 time_all = time_cut + (time_last,)
ioannis@50 1097
ioannis@50 1098 t_axis = mpl.dates.date2num(time_all)
ioannis@50 1099
ioannis@50 1100 # Get the values of the z axis
ioannis@50 1101 z_cut = self.z[hmin_idx:hmax_idx] - self.resolution / 2.
ioannis@50 1102 z_last = z_cut[-1] + self.resolution
ioannis@50 1103
ioannis@50 1104 z_axis = np.append(z_cut, z_last) / 1000. + z0 / 1000. # Convert to km
ioannis@50 1105
ioannis@50 1106 # Plot
ioannis@50 1107 im1 = ax1.pcolormesh(t_axis, z_axis, data.T[hmin_idx:hmax_idx, zoom[2]:zoom[3]],
ioannis@50 1108 cmap=cmap,
ioannis@50 1109 vmin=vmin,
ioannis@50 1110 vmax=vmax,
ioannis@50 1111 )
ioannis@50 1112
ioannis@50 1113 if add_colorbar:
ioannis@50 1114 if cb_format:
ioannis@50 1115 cb1 = plt.colorbar(im1, format=cb_format)
ioannis@50 1116 else:
ioannis@50 1117 cb_formatter = ScalarFormatter()
ioannis@50 1118 cb_formatter.set_powerlimits(power_limits)
ioannis@50 1119 cb1 = plt.colorbar(im1, format=cb_formatter)
ioannis@50 1120 cb1.ax.set_ylabel(cmap_label)
ioannis@50 1121
ioannis@50 1122 # Make the ticks of the colorbar smaller, two points smaller than the default font size
ioannis@50 1123 cb_font_size = mpl.rcParams['font.size'] - 2
ioannis@50 1124 for ticklabels in cb1.ax.get_yticklabels():
ioannis@50 1125 ticklabels.set_fontsize(cb_font_size)
ioannis@50 1126 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size)
ioannis@50 1127
ulalume3@92 1128 def _index_at_height(self, height):
ulalume3@93 1129 """
ulalume3@93 1130 Get the altitude index nearest to the specified height.
ulalume3@93 1131
ulalume3@93 1132 Parameters
ulalume3@93 1133 ----------
ulalume3@93 1134 height : float
ulalume3@93 1135 Height (m)
ulalume3@93 1136
ulalume3@93 1137 Returns
ulalume3@93 1138 -------
ulalume3@93 1139 idx : int
ulalume3@93 1140 Index corresponding to the provided height.
ulalume3@93 1141 """
binietoglou@0 1142 idx = np.array(np.abs(self.z - height).argmin())
ulalume3@24 1143 if idx.size > 1:
ioannis@50 1144 idx = idx[0]
binietoglou@0 1145 return idx

mercurial