Thu, 19 Nov 2020 10:23:32 +0000
Merge branch 'gdoxastakis-master-patch-40497' into 'master'
Added custom_info field support
See merge request ioannis_binietoglou/atmospheric-lidar!1
i@108 | 1 | """ This is a class for experimenting with the new DIVA / EARLINET NetCDF file format. |
i@108 | 2 | |
i@108 | 3 | In the long run, this should be places as a method in BaseLidarMeasurement class. For now it is kept |
i@108 | 4 | separately not to interfere with normal development. |
i@108 | 5 | """ |
i@108 | 6 | import netCDF4 as netcdf |
i@116 | 7 | import yaml |
i@108 | 8 | import datetime |
i@108 | 9 | import os |
i@116 | 10 | import numpy as np |
i@150 | 11 | import logging |
i@116 | 12 | |
i@116 | 13 | import pytz |
i@108 | 14 | |
i@108 | 15 | from .generic import BaseLidarMeasurement |
i@108 | 16 | |
i@150 | 17 | logger = logging.getLogger(__name__) |
i@108 | 18 | |
i@150 | 19 | |
ioannis@165 | 20 | class DivaConverterMixin: |
i@108 | 21 | |
i@116 | 22 | def save_as_diva_netcdf(self, output_path, parameter_file): |
i@108 | 23 | """ Save the current data in the 'draft' DIVA format. """ |
i@108 | 24 | |
i@108 | 25 | with open(parameter_file, 'r') as f: |
i@116 | 26 | parameters = yaml.load(f) |
i@108 | 27 | |
i@108 | 28 | global_parameters = parameters['global_parameters'] # Shortcut |
i@122 | 29 | global_variables = parameters['global_variables'] # Shortcut |
ulalume3@112 | 30 | channels = parameters['channels'] |
i@108 | 31 | |
ioannis@165 | 32 | iso_date = datetime.datetime.utcnow().strftime('%Y-%d-%mT%H:%M:%SZ') |
i@108 | 33 | python_file_name = os.path.basename(__file__) |
i@108 | 34 | |
i@108 | 35 | with netcdf.Dataset(output_path, 'w', format="NETCDF4") as f: |
i@108 | 36 | |
i@108 | 37 | # Global attributes |
i@108 | 38 | f.title = global_parameters['title'] |
i@108 | 39 | f.source = global_parameters['source'] |
i@108 | 40 | f.institution = global_parameters['institution'] |
i@108 | 41 | f.references = global_parameters['references'] |
i@108 | 42 | f.location = global_parameters['location'] |
i@108 | 43 | f.data_version = global_parameters['data_version'] |
i@122 | 44 | f.PI = global_parameters['PI_name'] |
i@122 | 45 | f.PI_email = global_parameters['PI_email'] |
i@108 | 46 | f.conversion_date = iso_date |
i@108 | 47 | f.comment = global_parameters['comment'] |
i@108 | 48 | f.Conventions = global_parameters['Conventions'] |
i@108 | 49 | f.history = global_parameters['history'].format(date=iso_date, file=python_file_name) |
i@108 | 50 | f.featureType = "timeSeriesProfile" |
i@108 | 51 | |
ulalume3@112 | 52 | # Top level dimensions |
ulalume3@112 | 53 | f.createDimension('name_strlen', size=40) |
ulalume3@112 | 54 | f.createDimension('nv', size=2) |
ulalume3@112 | 55 | |
i@108 | 56 | # Top level variables |
i@108 | 57 | latitude = f.createVariable('latitude', datatype='f4') |
i@108 | 58 | latitude.standard_name = 'latitude' |
i@108 | 59 | latitude.long_name = 'system latitude' |
i@108 | 60 | latitude.units = 'degrees_north' |
i@108 | 61 | |
i@108 | 62 | longitude = f.createVariable('longitude', datatype='f4') |
i@108 | 63 | longitude.standard_name = 'longitude' |
i@108 | 64 | longitude.long_name = 'system longitude' |
i@108 | 65 | longitude.units = 'degrees_east' |
i@108 | 66 | |
ioannis@165 | 67 | lidar_zenith_angle = f.createVariable('lidar_zenith_angle', datatype='f4') |
ioannis@165 | 68 | lidar_zenith_angle.standard_name = 'sensor_zenith_angle' |
ioannis@165 | 69 | lidar_zenith_angle.long_name = 'zenith angle of emitted laser' |
ioannis@165 | 70 | lidar_zenith_angle.units = 'degree' |
i@108 | 71 | |
ioannis@165 | 72 | lidar_azimuth = f.createVariable('lidar_azimuth_angle', datatype='f4') |
ioannis@165 | 73 | lidar_azimuth.standard_name = 'sensor_azimuth_angle' |
ioannis@165 | 74 | lidar_azimuth.long_name = 'azimuth angle of emitted laser' |
ioannis@165 | 75 | lidar_azimuth.units = 'degree' |
ioannis@165 | 76 | lidar_azimuth.comment = 'Based on North. Optional' |
i@108 | 77 | |
i@108 | 78 | altitude = f.createVariable('altitude', datatype='f4') |
i@108 | 79 | altitude.standard_name = 'altitude' |
i@108 | 80 | altitude.long_name = 'system altitude' |
i@108 | 81 | altitude.units = 'm' |
i@108 | 82 | |
i@122 | 83 | # Assign top-level variables |
i@123 | 84 | latitude[:] = global_variables['latitude'] |
i@123 | 85 | longitude[:] = global_variables['longitude'] |
ioannis@165 | 86 | lidar_zenith_angle[:] = global_variables['laser_pointing_angle'] |
i@123 | 87 | altitude[:] = global_variables['system_altitude'] |
i@122 | 88 | |
ulalume3@113 | 89 | # Optional ancillary group |
ulalume3@113 | 90 | ancillary = f.createGroup('ancillary') |
ulalume3@113 | 91 | ancillary.featureType = "timeSeries" |
ulalume3@113 | 92 | |
ulalume3@113 | 93 | ancillary.createDimension('time', size=None) |
ulalume3@113 | 94 | |
ulalume3@113 | 95 | time = ancillary.createVariable('time', datatype='f8', dimensions=('time',)) |
ulalume3@113 | 96 | time.long_name = 'time' |
ulalume3@113 | 97 | time.units = 'seconds since 1970-01-01 00:00' |
ulalume3@113 | 98 | time.standard_name = 'time' |
ulalume3@113 | 99 | |
ulalume3@113 | 100 | temperature = ancillary.createVariable('air_temperature', datatype='f8', dimensions=('time',)) |
ulalume3@113 | 101 | temperature.long_name = 'air temperature at instrument level' |
ulalume3@113 | 102 | temperature.units = 'K' |
ulalume3@113 | 103 | temperature.standard_name = 'air_temperature' |
ulalume3@113 | 104 | |
ulalume3@113 | 105 | pressure = ancillary.createVariable('air_pressure', datatype='f8', dimensions=('time',)) |
ulalume3@113 | 106 | pressure.long_name = 'air pressure at instrument level' |
ulalume3@113 | 107 | pressure.units = 'hPa' |
ulalume3@113 | 108 | pressure.standard_name = 'air_pressure' |
ulalume3@113 | 109 | |
ulalume3@113 | 110 | # Create a separate group for each channel |
ioannis@168 | 111 | for channel_name, channel_parameters in channels.items(): |
i@108 | 112 | |
ioannis@168 | 113 | if channel_name not in list(self.channels.keys()): |
ioannis@168 | 114 | raise ValueError('Channel name not one of {0}: {1}'.format(list(self.channels.keys()), channel_name)) |
i@115 | 115 | |
i@115 | 116 | channel = self.channels[channel_name] |
i@115 | 117 | |
i@116 | 118 | group_name = "channel_{0}".format(channel_name.replace('.', '_')) # Give channels groups a standard name |
i@116 | 119 | |
ulalume3@112 | 120 | g = f.createGroup(group_name) |
ulalume3@112 | 121 | g.long_name = channel_parameters['long_name'] |
i@116 | 122 | g.detector_manufacturer = channel_parameters['detector_manufacturer'] # Optional |
ulalume3@112 | 123 | g.detector_model = channel_parameters['detector_model'] |
ulalume3@112 | 124 | g.daq_manufacturer = channel_parameters['daq_manufacturer'] |
ulalume3@112 | 125 | g.daq_model = channel_parameters['daq_model'] |
i@108 | 126 | |
ulalume3@112 | 127 | # Dimensions |
ulalume3@112 | 128 | g.createDimension('profile', size=None) # Infinite dimension |
i@116 | 129 | g.createDimension('range', len(channel.z)) |
i@108 | 130 | |
ulalume3@112 | 131 | # Variables |
i@116 | 132 | name = g.createVariable('channel_id', 'c', dimensions=('name_strlen',)) |
ulalume3@112 | 133 | name.cf_role = 'timeseries_id' |
ulalume3@112 | 134 | name.long_name = 'channel identification' |
i@108 | 135 | |
i@116 | 136 | laser_rep_rate = g.createVariable('laser_repetition_rate', 'f4') |
ulalume3@112 | 137 | laser_rep_rate.long_name = 'nominal laser repetition rate' |
ulalume3@112 | 138 | laser_rep_rate.units = 'Hz' |
i@108 | 139 | |
ioannis@167 | 140 | emission_wavelength = g.createVariable('emission_wavelength', datatype='f8', ) # or dimensions=('profile',) |
ioannis@167 | 141 | emission_wavelength.long_name = 'emission wavelength' |
ioannis@167 | 142 | emission_wavelength.units = 'nm' |
ioannis@167 | 143 | emission_wavelength.comment = "could have dimension profile if measured." |
i@122 | 144 | |
i@114 | 145 | emission_energy = g.createVariable('emission_energy', datatype='f8', ) # or dimensions=('profile',) |
ulalume3@112 | 146 | emission_energy.long_name = 'emission energy per pulse' |
ulalume3@112 | 147 | emission_energy.units = 'mJ' |
ulalume3@112 | 148 | emission_energy.comment = "could be scalar, if value is nominal." |
i@108 | 149 | |
ulalume3@112 | 150 | emission_pol = g.createVariable('emission_polarization', datatype='b') |
ulalume3@112 | 151 | emission_pol.long_name = 'nominal emission poalrization' |
ulalume3@112 | 152 | emission_pol.flag_values = '0b 1b 2b' |
ulalume3@112 | 153 | emission_pol.flag_meanings = 'linear circular none' |
i@108 | 154 | |
ulalume3@112 | 155 | fov = g.createVariable('fov', datatype='f4') |
ulalume3@112 | 156 | fov.long_name = 'channel field of view full angle' |
ulalume3@112 | 157 | fov.units = 'mrad' |
ulalume3@112 | 158 | fov.comment = 'simulated' |
ulalume3@112 | 159 | |
ulalume3@112 | 160 | detector_type = g.createVariable('detector_type', datatype='b') |
ulalume3@112 | 161 | detector_type.long_name = 'detector type' |
ulalume3@112 | 162 | detector_type.flag_values = '0b 1b' |
ulalume3@112 | 163 | detector_type.flag_meanings = 'PMT APD' |
i@108 | 164 | |
ulalume3@112 | 165 | detection_mode = g.createVariable('detection_mode', datatype='b') |
ulalume3@112 | 166 | detection_mode.long_name = 'detection mode' |
ulalume3@112 | 167 | detection_mode.flag_values = '0b 1b' |
ulalume3@112 | 168 | detection_mode.flag_meanings = 'analog photon_counting' |
ulalume3@112 | 169 | |
ulalume3@112 | 170 | detection_cw = g.createVariable('detection_wavelength', datatype='f8') |
ulalume3@112 | 171 | detection_cw.long_name = 'center wavelength of detection filters' |
ulalume3@112 | 172 | detection_cw.units = 'nm' |
ulalume3@112 | 173 | detection_cw.standard_name = 'sensor_band_central_radiation_wavelength' |
i@108 | 174 | |
ulalume3@112 | 175 | detection_fwhm = g.createVariable('detection_fwhm', datatype='f8') |
ulalume3@112 | 176 | detection_fwhm.long_name = 'FWHM of detection filters' |
ulalume3@112 | 177 | detection_fwhm.units = 'nm' |
i@108 | 178 | |
ulalume3@112 | 179 | detection_pol = g.createVariable('detection_polarization', datatype='b') |
ulalume3@112 | 180 | detection_pol.long_name = 'nominal detection poalrization' |
ulalume3@112 | 181 | detection_pol.flag_values = '0b 1b 2b' |
i@114 | 182 | detection_pol.flag_meanings = 'linear circular total' |
ulalume3@112 | 183 | |
ulalume3@112 | 184 | polarizer_angle = g.createVariable('polarizer_angle', datatype='f4', dimensions=('profile', ), zlib=True) |
ulalume3@112 | 185 | polarizer_angle.long_name = 'polarizer angle in respect to laser plane of polarization' |
ulalume3@112 | 186 | polarizer_angle.units = 'degree' |
ulalume3@112 | 187 | polarizer_angle.comments = 'Optional' |
i@108 | 188 | |
ioannis@166 | 189 | if channel.is_photon_counting: |
i@116 | 190 | dead_time_model = g.createVariable('dead_time_model', datatype='b') |
i@116 | 191 | dead_time_model.long_name = 'optimal dead time model of detection system' |
i@116 | 192 | dead_time_model.flag_values = '0b 1b 2b' |
i@116 | 193 | dead_time_model.flag_meanings = 'paralyzable non_paralyzable other' |
i@108 | 194 | |
i@116 | 195 | dead_time = g.createVariable('dead_time', datatype='f8') |
i@116 | 196 | dead_time.long_name = 'dead time value' |
i@116 | 197 | dead_time.units = 'ns' |
i@116 | 198 | dead_time.comment = "Manufacturer. Source of the value." |
ulalume3@112 | 199 | |
ulalume3@112 | 200 | bin_length = g.createVariable('bin_length', datatype='f4') |
ulalume3@112 | 201 | bin_length.long_name = "time duration of each bin" |
ulalume3@112 | 202 | bin_length.units = 'ns' |
ulalume3@112 | 203 | |
i@116 | 204 | if channel.is_analog: |
i@116 | 205 | adc_bits = g.createVariable('adc_bits', datatype='i4') |
i@116 | 206 | adc_bits.long_name = 'analog-to-digital converter bits' |
i@116 | 207 | adc_bits.coordinates = "time" |
i@115 | 208 | |
ulalume3@112 | 209 | detector_voltage = g.createVariable('detector_voltage', datatype='f4', dimensions=('profile',), zlib=True) |
ulalume3@112 | 210 | detector_voltage.long_name = 'detector voltage' |
ulalume3@112 | 211 | detector_voltage.units = 'V' |
ulalume3@112 | 212 | detector_voltage.coordinates = "time" |
i@108 | 213 | |
i@116 | 214 | if channel.is_photon_counting: |
i@116 | 215 | discriminator = g.createVariable('discriminator', datatype='f8', dimensions=('profile',)) |
i@116 | 216 | discriminator.long_name = 'discriminator level' |
i@116 | 217 | discriminator.units = '' |
ulalume3@112 | 218 | |
i@116 | 219 | if channel.is_analog: |
i@116 | 220 | adc_range = g.createVariable('adc_range', datatype='f4', dimensions=('profile',), |
i@116 | 221 | zlib=True) |
i@116 | 222 | adc_range.long_name = 'analog-to-digital converter range' |
i@116 | 223 | adc_range.units = 'mV' |
i@116 | 224 | adc_range.coordinates = "time" |
ulalume3@112 | 225 | |
ulalume3@112 | 226 | pulses = g.createVariable('pulses', datatype='i4', dimensions=('profile',), |
ulalume3@112 | 227 | zlib=True) |
ulalume3@112 | 228 | pulses.long_name = 'accumulated laser pulses per record' |
ulalume3@112 | 229 | pulses.coordinates = "time" |
i@108 | 230 | |
ulalume3@112 | 231 | nd_filter = g.createVariable('nd_filter_od', datatype='f8', dimensions=('profile',)) |
ulalume3@112 | 232 | nd_filter.long_name = "neutral density filter optical depth " |
ulalume3@112 | 233 | nd_filter.coordinates = "time" |
i@108 | 234 | |
i@116 | 235 | trigger_delay = g.createVariable('trigger_delay', datatype='f4') |
i@122 | 236 | trigger_delay.long_name = "channel trigger difference from pulse emission" |
i@116 | 237 | trigger_delay.units = 'ns' |
i@116 | 238 | trigger_delay.comments = 'Negative values for pre-trigger systems.' |
i@108 | 239 | |
ulalume3@112 | 240 | time = g.createVariable('time', datatype='f8', dimensions=('profile',), |
ulalume3@112 | 241 | zlib=True) |
ulalume3@112 | 242 | time.long_name = 'profile start time ' |
ulalume3@112 | 243 | time.units = "seconds since 1970-01-01 00:00:00" |
ulalume3@112 | 244 | time.standard_name = "time" |
ulalume3@112 | 245 | time.bounds = "time_bnds" |
i@116 | 246 | time_bounds = g.createVariable('time_bnds', datatype='f8', dimensions=('profile', 'nv'), zlib=True) |
i@108 | 247 | |
i@116 | 248 | bin_time = g.createVariable('bin_time', datatype='f4', dimensions=('range',), zlib=True) |
i@116 | 249 | bin_time.long_name = 'bin start time since channel trigger' |
ulalume3@112 | 250 | bin_time.units = "ns" |
i@108 | 251 | |
i@116 | 252 | if channel.is_analog: |
i@116 | 253 | signal_units = 'mV' |
i@116 | 254 | signal_datatype = 'f8' |
i@116 | 255 | else: |
i@116 | 256 | signal_units = 'counts' |
i@116 | 257 | signal_datatype = 'i8' |
i@116 | 258 | |
i@116 | 259 | signal = g.createVariable('signal', datatype=signal_datatype, dimensions=('profile', 'range'), |
ulalume3@112 | 260 | zlib=True) |
ulalume3@112 | 261 | signal.long_name = 'signal' |
i@116 | 262 | signal.units = signal_units |
ulalume3@112 | 263 | signal.coordinates = "time" |
ulalume3@112 | 264 | signal.ancillary_variables = "signal_stddev" |
ulalume3@112 | 265 | |
ulalume3@112 | 266 | # If measured |
i@116 | 267 | signal_stddev = g.createVariable('signal_stddev', datatype=signal_datatype, dimensions=('profile', 'range'), |
ulalume3@112 | 268 | zlib=True) |
ulalume3@112 | 269 | signal_stddev.long_name = 'signal standard deviation' |
i@116 | 270 | signal_stddev.units = signal_units |
ulalume3@112 | 271 | signal_stddev.coordinates = "time" |
i@116 | 272 | signal_stddev.comments = "Only if measured. Should be removed if not." |
ulalume3@112 | 273 | |
ulalume3@112 | 274 | # Assign variables |
i@116 | 275 | name[:len(channel_name)] = channel_name |
i@114 | 276 | laser_rep_rate[:] = channel_parameters['laser_repetition_rate'] |
ioannis@167 | 277 | emission_wavelength[:] = channel_parameters['emission_wavelength'] |
i@114 | 278 | emission_energy[:] = channel_parameters['emission_energy'] |
i@114 | 279 | emission_pol[:] = self._emission_pol_flag(channel_parameters['emission_polarization']) |
i@114 | 280 | fov[:] = channel_parameters['fov'] |
i@114 | 281 | detector_type[:] = self._detector_type_flag(channel_parameters['detector_type']) |
i@114 | 282 | detection_mode[:] = self._detection_mode_flag(channel_parameters['detection_mode']) |
i@114 | 283 | detection_fwhm[:] = channel_parameters['filter_fwhm'] |
i@114 | 284 | detection_pol[:] = self._detection_pol_flag(channel_parameters['detection_polarization']) |
i@116 | 285 | polarizer_angle[:] = channel_parameters['polarizer_angle'] * np.ones(len(channel.time)) # For now, assumed constant. |
i@116 | 286 | |
i@116 | 287 | if channel.is_photon_counting: |
i@116 | 288 | dead_time_model[:] = self._deadtime_model_flag(channel_parameters['dead_time_model']) |
i@116 | 289 | dead_time[:] = channel_parameters['dead_time'] |
i@116 | 290 | |
i@114 | 291 | bin_length[:] = channel_parameters['bin_length'] |
i@116 | 292 | trigger_delay[:] = channel_parameters['trigger_delay'] |
i@116 | 293 | |
i@116 | 294 | detector_voltage[:] = channel.hv |
i@116 | 295 | |
i@116 | 296 | if channel.is_analog: |
i@116 | 297 | adc_range[:] = channel.discriminator |
i@116 | 298 | adc_bits[:] = channel.adcbits |
i@116 | 299 | else: |
i@116 | 300 | discriminator[:] = channel.discriminator |
i@115 | 301 | |
i@116 | 302 | pulses[:] = channel.laser_shots |
i@115 | 303 | |
i@122 | 304 | epoch = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) |
i@116 | 305 | seconds_since_epoch = [(t - epoch).total_seconds() for t in channel.time] |
i@116 | 306 | time[:] = seconds_since_epoch |
i@116 | 307 | time_bounds[:, 0] = seconds_since_epoch |
i@116 | 308 | time_bounds[:, 1] = seconds_since_epoch + channel.get_duration() |
i@116 | 309 | |
i@116 | 310 | bin_time[:] = channel.binwidth * np.arange(len(channel.z)) |
i@116 | 311 | |
i@116 | 312 | signal[:] = channel.matrix |
i@114 | 313 | |
i@114 | 314 | def _deadtime_model_flag(self, model_str): |
i@114 | 315 | """ Convert dead-time model string to byte flag. |
i@114 | 316 | |
i@114 | 317 | Parameters |
i@114 | 318 | ---------- |
i@114 | 319 | model_str : str |
i@114 | 320 | String describing the dead-time model (one of paralyzable, non-paralyzable, or other) |
i@114 | 321 | |
i@114 | 322 | Returns |
i@114 | 323 | ------- |
i@114 | 324 | : int |
i@114 | 325 | Byte encoding of dead-time model |
i@114 | 326 | """ |
i@114 | 327 | choices = {'paralyzable': 0, |
i@114 | 328 | 'non-paralyzable': 1, |
i@114 | 329 | 'other': 2} |
i@114 | 330 | |
ioannis@168 | 331 | if model_str not in list(choices.keys()): |
ioannis@168 | 332 | raise ValueError('Dead-time model is not one of {0}: {1}'.format(list(choices.keys()), model_str)) |
i@114 | 333 | |
i@114 | 334 | return choices[model_str] |
i@114 | 335 | |
i@114 | 336 | def _detection_pol_flag(self, pol_str): |
i@114 | 337 | """ Convert detection polarization string to byte flag. |
i@114 | 338 | |
i@114 | 339 | Parameters |
i@114 | 340 | ---------- |
i@114 | 341 | pol_str : str |
i@114 | 342 | String describing the detection polarization (one of linear, circular, or total) |
i@114 | 343 | |
i@114 | 344 | Returns |
i@114 | 345 | ------- |
i@114 | 346 | : int |
i@114 | 347 | Byte encoding of detection polarization |
i@114 | 348 | """ |
i@114 | 349 | choices = {'linear': 0, |
i@114 | 350 | 'circular': 1, |
i@114 | 351 | 'total': 2} |
i@114 | 352 | |
ioannis@168 | 353 | if pol_str not in list(choices.keys()): |
ioannis@168 | 354 | raise ValueError('Detection polarization is not one of {0}: {1}'.format(list(choices.keys()), pol_str)) |
i@114 | 355 | |
i@114 | 356 | return choices[pol_str] |
i@114 | 357 | |
i@114 | 358 | def _detection_mode_flag(self, mode_str): |
i@114 | 359 | """ Convert detection mode string to byte flag. |
i@114 | 360 | |
i@114 | 361 | Parameters |
i@114 | 362 | ---------- |
i@114 | 363 | mode_str : str |
i@114 | 364 | String describing the detector mode (one of photon-counting or analog) |
i@114 | 365 | |
i@114 | 366 | Returns |
i@114 | 367 | ------- |
i@114 | 368 | : int |
i@114 | 369 | Byte encoding of detection mode |
i@114 | 370 | """ |
i@114 | 371 | choices = {'analog': 0, |
i@114 | 372 | 'photon-counting': 1,} |
i@114 | 373 | |
ioannis@168 | 374 | if mode_str not in list(choices.keys()): |
ioannis@168 | 375 | raise ValueError('Detection mode is not one of {0}: {1}'.format(list(choices.keys()), mode_str)) |
i@114 | 376 | |
i@114 | 377 | return choices[mode_str] |
i@114 | 378 | |
i@114 | 379 | def _detector_type_flag(self, type_string): |
i@114 | 380 | """ Convert emission string to byte flag. |
i@114 | 381 | |
i@114 | 382 | Parameters |
i@114 | 383 | ---------- |
i@114 | 384 | type_string : str |
i@114 | 385 | String describing the detector type (one of APD or PMT) |
i@114 | 386 | |
i@114 | 387 | Returns |
i@114 | 388 | ------- |
i@114 | 389 | : int |
i@114 | 390 | Byte encoding of detector type |
i@114 | 391 | """ |
i@114 | 392 | choices = {'PMT': 0, |
i@114 | 393 | 'APD': 1,} |
i@114 | 394 | |
ioannis@168 | 395 | if type_string not in list(choices.keys()): |
ioannis@168 | 396 | raise ValueError('Detector type is not one of {0}: {1}'.format(list(choices.keys()), type_string)) |
i@114 | 397 | |
i@114 | 398 | return choices[type_string] |
i@114 | 399 | |
i@114 | 400 | def _emission_pol_flag(self, pol_string): |
i@114 | 401 | """ Convert emission string to byte flag. |
i@114 | 402 | |
i@114 | 403 | Parameters |
i@114 | 404 | ---------- |
i@114 | 405 | pol_string : str |
i@114 | 406 | String describing the polarization (one of linear, circular, or none) |
i@114 | 407 | |
i@114 | 408 | Returns |
i@114 | 409 | ------- |
i@114 | 410 | : int |
i@114 | 411 | Byte encoding of polarization state |
i@114 | 412 | """ |
i@116 | 413 | choices = {'linear': 0, |
i@114 | 414 | 'circular': 1, |
i@114 | 415 | 'none': 2} |
i@114 | 416 | |
ioannis@168 | 417 | if pol_string not in list(choices.keys()): |
ioannis@168 | 418 | raise ValueError('Emission polarization not one of {0}: {1}'.format(list(choices.keys()), pol_string)) |
i@114 | 419 | |
i@114 | 420 | return choices[pol_string] |
i@150 | 421 | |
i@150 | 422 | |
ioannis@165 | 423 | class DivaLidarMeasurement(object): |
ioannis@165 | 424 | """ A class to read raw lidar files in DIVA format. |
i@150 | 425 | |
ioannis@165 | 426 | Unlike other classes in this module, it does not inherit from BasicLidarMeasurement. This is done |
ioannis@165 | 427 | to avoid all the burden of backward compatibility. In the future this could be hosted also as a separte moduel. |
ioannis@165 | 428 | """ |
ioannis@165 | 429 | |
ioannis@166 | 430 | def __init__(self, file_path, header_only=False): |
i@150 | 431 | """ |
i@150 | 432 | This is run when creating a new object. |
i@150 | 433 | |
i@150 | 434 | Parameters |
i@150 | 435 | ---------- |
ioannis@165 | 436 | file_path : str |
ioannis@165 | 437 | Paths to the input netCDF file. |
ioannis@166 | 438 | header_only : bool |
ioannis@166 | 439 | If True, channel info are not loaded. |
i@150 | 440 | """ |
ioannis@165 | 441 | self.file_path = file_path |
ioannis@165 | 442 | self.file_name = os.path.basename(file_path) |
ioannis@165 | 443 | |
ioannis@167 | 444 | self.import_file(header_only) |
ioannis@165 | 445 | |
ioannis@166 | 446 | def import_file(self, header_only): |
ioannis@165 | 447 | """ Import data from a single DIVA file. |
ioannis@165 | 448 | """ |
ioannis@165 | 449 | |
ioannis@165 | 450 | logger.debug('Importing file {0}'.format(self.file_name)) |
ioannis@165 | 451 | |
ioannis@165 | 452 | self.channels = {} |
i@150 | 453 | |
ioannis@165 | 454 | with netcdf.Dataset(self.file_path) as input_file: |
ioannis@165 | 455 | self.title = input_file.title |
ioannis@165 | 456 | self.source = input_file.source |
ioannis@165 | 457 | self.institution = input_file.institution |
ioannis@165 | 458 | self.references = input_file.references |
ioannis@165 | 459 | self.location = input_file.location |
ioannis@165 | 460 | self.data_version = input_file.data_version |
ioannis@165 | 461 | self.PI = input_file.PI |
ioannis@165 | 462 | self.PI_email = input_file.PI_email |
ioannis@165 | 463 | self.conversion_date_str = input_file.conversion_date |
ioannis@165 | 464 | self.conversion_date = datetime.datetime.strptime(input_file.conversion_date, '%Y-%d-%mT%H:%M:%SZ') |
ioannis@165 | 465 | self.comment = input_file.comment |
ioannis@165 | 466 | self.conventions = input_file.Conventions |
ioannis@165 | 467 | self.history = input_file.history |
i@150 | 468 | |
ioannis@165 | 469 | self.latitude = input_file.variables['latitude'][:] |
ioannis@165 | 470 | self.longitude = input_file.variables['longitude'][:] |
ioannis@165 | 471 | |
ioannis@165 | 472 | self.lidar_zenith_angle = input_file.variables['lidar_zenith_angle'][:] |
ioannis@165 | 473 | self.lidar_azimuth_angle = input_file.variables['lidar_azimuth_angle'][:] |
ioannis@165 | 474 | self.lidar_altitude = input_file.variables['altitude'][:] |
ioannis@165 | 475 | |
ioannis@165 | 476 | ancillary = input_file.groups.pop('ancillary') |
i@150 | 477 | |
ioannis@165 | 478 | self.meteo_time = ancillary.variables['time'][:] |
ioannis@165 | 479 | self.air_temperature_kelvin = ancillary.variable['air_temperature'][:] |
ioannis@165 | 480 | self.air_pressure_hpa = ancillary.variable['air_pressure'][:] |
i@150 | 481 | |
ioannis@166 | 482 | self.available_channels = [] |
ioannis@168 | 483 | for group_name, group in list(input_file.groups.items()): |
ioannis@165 | 484 | channel_name = group_name[8:] # Remove 'channel_' prefix |
ioannis@166 | 485 | self.available_channels.append(channel_name) |
ioannis@166 | 486 | |
ioannis@166 | 487 | if not header_only: |
ioannis@166 | 488 | self.channels[channel_name] = DivaChannel(channel_name, group) |
ioannis@165 | 489 | |
ioannis@166 | 490 | def import_channel(self, channel_name): |
ioannis@166 | 491 | """ Import a specific channel. """ |
ioannis@166 | 492 | if channel_name not in self.available_channels: |
ioannis@166 | 493 | raise ValueError('Channel {0} not available. Should be one of {1}'.format(channel_name, self.available_channels)) |
ioannis@166 | 494 | |
ioannis@166 | 495 | group_name = 'channel_{0}'.format(channel_name) |
ioannis@166 | 496 | |
ioannis@166 | 497 | with netcdf.Dataset(self.file_path) as input_file: |
ioannis@166 | 498 | group = input_file.groups[group_name] |
ioannis@166 | 499 | self.channels[channel_name] = DivaChannel(channel_name, group) |
i@150 | 500 | |
i@150 | 501 | |
i@150 | 502 | class DivaChannel(object): |
i@150 | 503 | |
ioannis@165 | 504 | def __init__(self, channel_name, group): |
i@150 | 505 | """ This is run when first creating the object. |
i@150 | 506 | |
i@150 | 507 | Parameters |
i@150 | 508 | ---------- |
ioannis@165 | 509 | channel_name : str |
ioannis@165 | 510 | Name of the group |
ioannis@165 | 511 | group : netCDF4.Group object |
ioannis@165 | 512 | An open netcdf group to initialize. |
i@150 | 513 | """ |
ioannis@166 | 514 | self.channel_name = channel_name |
ioannis@166 | 515 | |
ioannis@166 | 516 | self.long_name = group.long_name |
ioannis@167 | 517 | self.detector_manufacturer = getattr(group, 'detector_manufacturer', None) |
ioannis@167 | 518 | self.detector_model = getattr(group, 'detector_model', None) |
ioannis@167 | 519 | self.daq_manufacturer = getattr(group, 'daq_manufacturer', None) |
ioannis@167 | 520 | self.daq_model = getattr(group, 'daq_model', None) |
ioannis@166 | 521 | |
ioannis@166 | 522 | self.number_of_profiles = len(group.dimensions['profile']) |
ioannis@166 | 523 | self.number_of_bins = len(group.dimensions['range']) |
ioannis@166 | 524 | self.channel_id = group.variables['channel_id'][:] |
ioannis@166 | 525 | self.laser_repetition_rate = group.variables['laser_repetition_rate'][:] |
ioannis@166 | 526 | |
ioannis@166 | 527 | self.emission_energy_mJ = group.variables['emission_energy'][:] |
ioannis@166 | 528 | self.emission_polarization_flag = group.variables['emission_polarization'][:] |
ioannis@166 | 529 | self.emission_polarization = self._flag_to_polarization(self.emission_polarization_flag) |
ioannis@166 | 530 | self.field_of_view = group.variables['fov'][:] |
ioannis@166 | 531 | self.field_of_view_comment = group.variables['fov'].comment |
ioannis@166 | 532 | |
ioannis@166 | 533 | self.detector_type_flag = group.variables['detector_type'][:] |
ioannis@166 | 534 | self.detector_type = self._flag_to_detector_type(self.detector_type_flag) |
ioannis@166 | 535 | |
ioannis@166 | 536 | self.detection_mode_flag = group.variables['detection_mode'][:] |
ioannis@166 | 537 | self.detection_mode = self._flag_to_detector_type(self.detection_mode_flag) |
ioannis@166 | 538 | |
ioannis@166 | 539 | self.detection_wavelength_nm = group.variables['detection_wavelength'][:] |
ioannis@166 | 540 | self.detection_fwhm = group.variables['detection_fwhm'][:] |
ioannis@166 | 541 | |
ioannis@166 | 542 | self.detection_polarization_flag = group.variables['detection_polarization'] |
ioannis@166 | 543 | self.detection_polariation = self._flag_to_detection_polarization(self.detection_polarization_flag) |
ioannis@166 | 544 | |
ioannis@166 | 545 | self.polarizer_angle_degrees = group.variables['polarizer_angle'][:] |
ioannis@166 | 546 | |
ioannis@166 | 547 | if self.is_photon_counting: |
ioannis@166 | 548 | self.dead_time_model_flag = group.variables['dead_time_model'][:] |
ioannis@166 | 549 | self.dead_time_model = self._flag_to_dead_time_model(self.dead_time_model_flag) |
ioannis@166 | 550 | |
ioannis@166 | 551 | self.dead_time = group.variables['dead_time'][:] |
ioannis@166 | 552 | self.dead_time_source = group.variables['dead_time'].comment |
ioannis@166 | 553 | self.discriminator = group.variables['discriminator'][:] |
ioannis@166 | 554 | |
ioannis@166 | 555 | if self.is_analog: |
ioannis@166 | 556 | self.adc_bits = group.variables['adc_bits'][:] |
ioannis@166 | 557 | self.adc_range = group.variables['adc_range'][:] |
ioannis@166 | 558 | |
ioannis@166 | 559 | self.bin_length_ns = group.variables['bin_length'][:] |
ioannis@166 | 560 | self.detector_voltage = group.variables['detector_voltage'][:] |
ioannis@166 | 561 | self.pulses = group.variables['pulses'][:] |
ioannis@166 | 562 | self.nd_filter_od = group.variables['nd_filter_od'][:] |
ioannis@166 | 563 | self.trigger_delay_ns = group.variables['trigger_delay'][:] |
ioannis@166 | 564 | self.time_since_epoch = group.variables['time'][:] |
ioannis@166 | 565 | |
ioannis@166 | 566 | self.time = [datetime.datetime.utcfromtimestamp(t) for t in self.time_since_epoch] |
ioannis@166 | 567 | self.bin_time_ns = group.variables['bin_time'][:] |
ioannis@166 | 568 | |
ioannis@166 | 569 | self.signal = group.variables['signal'][:] |
ioannis@166 | 570 | self.signal_units = group.variables['signal'].units |
ioannis@166 | 571 | |
ioannis@166 | 572 | signal_stddev_var = group.variables.pop('signal_stddev', None) |
ioannis@166 | 573 | |
ioannis@166 | 574 | if signal_stddev_var: |
ioannis@166 | 575 | self.signal_stddev = signal_stddev_var[:] |
ioannis@166 | 576 | else: |
ioannis@166 | 577 | self.signal_stddev = None |
i@150 | 578 | |
ioannis@166 | 579 | def _flag_to_polarization(self, flag): |
ioannis@166 | 580 | """ Convert polarization flag to str""" |
ioannis@166 | 581 | if flag not in [0, 1, 2]: |
ioannis@166 | 582 | logger.warning('Polarization flag has unrecognized value: {0}'.format(flag)) |
ioannis@166 | 583 | return "" |
ioannis@166 | 584 | |
ioannis@166 | 585 | values = {0: 'linear', |
ioannis@166 | 586 | 1: 'circular', |
ioannis@166 | 587 | 2: 'None'} |
ioannis@166 | 588 | |
ioannis@166 | 589 | return values[flag] |
ioannis@166 | 590 | |
ioannis@166 | 591 | def _flag_to_detector_type(self, flag): |
ioannis@166 | 592 | """ Convert detector type flag to str""" |
ioannis@166 | 593 | if flag not in [0, 1]: |
ioannis@166 | 594 | logger.warning('Detector type flag has unrecognized value: {0}'.format(flag)) |
ioannis@166 | 595 | return "" |
ioannis@166 | 596 | |
ioannis@166 | 597 | values = {0: 'PMT', |
ioannis@167 | 598 | 1: 'APD'} |
ioannis@166 | 599 | |
ioannis@166 | 600 | return values[flag] |
ioannis@166 | 601 | |
ioannis@166 | 602 | def _flag_to_detection_mode(self, flag): |
ioannis@166 | 603 | """ Convert detector type flag to str""" |
ioannis@166 | 604 | if flag not in [0, 1]: |
ioannis@166 | 605 | logger.warning('Detection mode flag has unrecognized value: {0}'.format(flag)) |
ioannis@166 | 606 | return "" |
ioannis@166 | 607 | |
ioannis@166 | 608 | values = {0: 'analog', |
ioannis@166 | 609 | 1: 'photon counting'} |
ioannis@165 | 610 | |
ioannis@166 | 611 | return values[flag] |
ioannis@166 | 612 | |
ioannis@166 | 613 | def _flag_to_detection_polarization(self, flag): |
ioannis@166 | 614 | """ Convert detector type flag to str""" |
ioannis@166 | 615 | if flag not in [0, 1, 2]: |
ioannis@166 | 616 | logger.warning('Detection polarization flag has unrecognized value: {0}'.format(flag)) |
ioannis@166 | 617 | return "" |
ioannis@166 | 618 | |
ioannis@166 | 619 | values = {0: 'linear', |
ioannis@166 | 620 | 1: 'circular', |
ioannis@167 | 621 | 2: 'total'} |
ioannis@166 | 622 | |
ioannis@166 | 623 | return values[flag] |
ioannis@166 | 624 | |
ioannis@166 | 625 | def _flag_to_dead_time_model(self, flag): |
ioannis@166 | 626 | """ Convert detector type flag to str""" |
ioannis@166 | 627 | if flag not in [0, 1, 2]: |
ioannis@166 | 628 | logger.warning('Dead time model flag has unrecognized value: {0}'.format(flag)) |
ioannis@166 | 629 | return "" |
ioannis@166 | 630 | |
ioannis@166 | 631 | values = {0: 'paralyzable', |
ioannis@166 | 632 | 1: 'non_paralyzable', |
ioannis@167 | 633 | 2: 'other'} |
ioannis@166 | 634 | |
ioannis@166 | 635 | return values[flag] |
ioannis@166 | 636 | |
ioannis@166 | 637 | @property |
ioannis@166 | 638 | def is_analog(self): |
ioannis@166 | 639 | return self.detection_mode_flag==0 |
ioannis@166 | 640 | |
ioannis@166 | 641 | @property |
ioannis@166 | 642 | def is_photon_counting(self): |
ioannis@166 | 643 | return self.detection_mode_flag==1 |