atmospheric_lidar/generic.py

Sun, 18 Mar 2018 11:22:17 +0200

author
Iannis B <ioannis@inoe.ro>
date
Sun, 18 Mar 2018 11:22:17 +0200
changeset 140
7a92985c5458
parent 139
3cb38ecae5be
child 142
b1cac5351db6
permissions
-rwxr-xr-x

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

mercurial