Wed, 10 Jan 2018 17:04:33 +0200
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 |