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