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@103 | 1 | import logging |
i@103 | 2 | |
binietoglou@38 | 3 | import numpy as np |
binietoglou@38 | 4 | |
ioannis@168 | 5 | from .licel import LicelLidarMeasurement |
binietoglou@38 | 6 | |
i@103 | 7 | logger = logging.getLogger(__name__) |
i@103 | 8 | |
binietoglou@38 | 9 | |
binietoglou@38 | 10 | class LicelCalibrationMeasurement(LicelLidarMeasurement): |
binietoglou@38 | 11 | |
ioannis@198 | 12 | def __init__(self, plus45_files=None, minus45_files=None, use_id_as_name=False, get_name_by_order=False, licel_timezone='UTC'): |
i@164 | 13 | """Class to handle depolarization calibration measurements according to the SCC. |
i@164 | 14 | |
i@164 | 15 | |
i@164 | 16 | Parameters |
i@164 | 17 | ---------- |
i@164 | 18 | plus45_files : list of str |
i@164 | 19 | List of paths for the plus 45 files. |
i@164 | 20 | minus45_files : list of str |
i@164 | 21 | List of paths for the minus 45 files. |
i@164 | 22 | use_id_as_name : bool |
i@164 | 23 | Defines if channels names are descriptive or transient digitizer IDs. |
ioannis@198 | 24 | get_name_by_order : bool |
ioannis@198 | 25 | If True, channels are named by the order they appear. |
i@164 | 26 | licel_timezone : str |
i@164 | 27 | String describing the timezone according to the tz database. |
i@164 | 28 | """ |
binietoglou@38 | 29 | # Setup the empty class |
ioannis@198 | 30 | super(LicelCalibrationMeasurement, self).__init__(use_id_as_name=use_id_as_name, licel_timezone=licel_timezone, |
ioannis@198 | 31 | get_name_by_order=get_name_by_order) |
binietoglou@38 | 32 | |
binietoglou@38 | 33 | self.plus45_files = plus45_files |
binietoglou@38 | 34 | self.minus45_files = minus45_files |
binietoglou@38 | 35 | |
ulalume3@69 | 36 | if plus45_files and minus45_files: |
i@160 | 37 | self.files = plus45_files + minus45_files |
i@160 | 38 | |
binietoglou@38 | 39 | self.check_equal_length() |
binietoglou@38 | 40 | self.read_channel_data() |
binietoglou@38 | 41 | self.update() |
binietoglou@38 | 42 | |
i@160 | 43 | def subset_by_scc_channels(self): |
i@160 | 44 | m = super(LicelCalibrationMeasurement, self).subset_by_scc_channels() |
i@160 | 45 | m.plus45_measurement = self.plus45_measurement.subset_by_scc_channels() |
i@160 | 46 | m.minus45_measurement = self.minus45_measurement.subset_by_scc_channels() |
i@160 | 47 | return m |
i@160 | 48 | |
binietoglou@38 | 49 | def update(self): |
i@164 | 50 | """ |
i@164 | 51 | Correct timescales after each update. |
binietoglou@38 | 52 | """ |
binietoglou@38 | 53 | super(LicelCalibrationMeasurement, self).update() |
binietoglou@38 | 54 | self.correct_timescales() |
binietoglou@38 | 55 | |
binietoglou@38 | 56 | def check_equal_length(self): |
binietoglou@38 | 57 | """ |
binietoglou@38 | 58 | Check if input time series have equal lengths. |
binietoglou@38 | 59 | """ |
binietoglou@38 | 60 | len_plus = len(self.plus45_files) |
binietoglou@38 | 61 | len_minus = len(self.minus45_files) |
binietoglou@38 | 62 | if len_plus != len_minus: |
binietoglou@38 | 63 | raise self.UnequalMeasurementLengthError( |
binietoglou@38 | 64 | "Input timeseries have different length: %s vs %s." % (len_plus, len_minus)) |
binietoglou@38 | 65 | |
binietoglou@38 | 66 | def read_channel_data(self): |
binietoglou@38 | 67 | # Read plus and minus 45 measurements |
ioannis@198 | 68 | self.plus45_measurement = LicelLidarMeasurement(self.plus45_files, use_id_as_name=self.use_id_as_name, |
ioannis@198 | 69 | get_name_by_order=self.get_name_by_order, |
ioannis@198 | 70 | licel_timezone=self.licel_timezone) |
i@160 | 71 | self.plus45_measurement.extra_netcdf_parameters = self.extra_netcdf_parameters |
ulalume3@92 | 72 | self.plus45_measurement.rename_channels(suffix='_p45') |
binietoglou@38 | 73 | |
ioannis@198 | 74 | self.minus45_measurement = LicelLidarMeasurement(self.minus45_files, use_id_as_name=self.use_id_as_name, |
ioannis@198 | 75 | get_name_by_order=self.get_name_by_order, |
ioannis@198 | 76 | licel_timezone=self.licel_timezone) |
i@160 | 77 | self.minus45_measurement.extra_netcdf_parameters = self.extra_netcdf_parameters |
ulalume3@92 | 78 | self.minus45_measurement.rename_channels(suffix='_m45') |
binietoglou@38 | 79 | |
binietoglou@38 | 80 | # Combine them in this object |
binietoglou@38 | 81 | self.channels = {} |
binietoglou@38 | 82 | self.channels.update(self.plus45_measurement.channels) |
binietoglou@38 | 83 | self.channels.update(self.minus45_measurement.channels) |
binietoglou@38 | 84 | |
i@160 | 85 | self.raw_info = self.plus45_measurement.raw_info.copy() |
i@160 | 86 | self.raw_info.update(self.minus45_measurement.raw_info) |
i@160 | 87 | |
binietoglou@38 | 88 | def correct_timescales(self): |
binietoglou@38 | 89 | self.check_timescales_are_two() |
binietoglou@38 | 90 | self.combine_scales() |
binietoglou@38 | 91 | |
binietoglou@38 | 92 | def check_timescales_are_two(self): |
binietoglou@38 | 93 | no_timescales = len(self.variables['Raw_Data_Start_Time']) |
binietoglou@38 | 94 | if no_timescales != 2: |
binietoglou@38 | 95 | raise self.WrongNumberOfTimescalesError("Wrong number of timescales: %s instead of 2." % no_timescales) |
binietoglou@38 | 96 | |
binietoglou@38 | 97 | def combine_scales(self): |
binietoglou@38 | 98 | start_times, end_times = self.get_ordered_timescales() |
binietoglou@38 | 99 | new_start_time = start_times[0] |
binietoglou@38 | 100 | new_stop_time = end_times[1] |
binietoglou@38 | 101 | self.variables['Raw_Data_Start_Time'] = [new_start_time, ] |
binietoglou@38 | 102 | self.variables['Raw_Data_Stop_Time'] = [new_stop_time, ] |
binietoglou@38 | 103 | self.reset_timescale_id() |
binietoglou@38 | 104 | |
ioannis@170 | 105 | self.dimensions['nb_of_time_scales'] = 1 |
ioannis@170 | 106 | |
i@160 | 107 | # def _get_custom_global_attributes(self): |
i@160 | 108 | # """ |
i@160 | 109 | # NetCDF global attributes that should be included in the final NetCDF file. |
i@160 | 110 | # |
i@160 | 111 | # Using the values of just p45 measurements. |
i@160 | 112 | # """ |
i@160 | 113 | # return self.plus45_measurement._get_custom_global_attributes() |
i@160 | 114 | |
binietoglou@38 | 115 | def reset_timescale_id(self): |
binietoglou@38 | 116 | """ |
binietoglou@38 | 117 | Set all timescales to 0 |
binietoglou@38 | 118 | :return: |
binietoglou@38 | 119 | """ |
binietoglou@38 | 120 | timescale_dict = self.variables['id_timescale'] |
binietoglou@38 | 121 | self.variables['id_timescale'] = dict.fromkeys(timescale_dict, 0) |
binietoglou@38 | 122 | |
binietoglou@38 | 123 | def get_ordered_timescales(self): |
binietoglou@38 | 124 | scale_start_1, scale_start_2 = self.variables['Raw_Data_Start_Time'] |
binietoglou@38 | 125 | scale_end_1, scale_end_2 = self.variables['Raw_Data_Stop_Time'] |
binietoglou@38 | 126 | |
binietoglou@38 | 127 | if scale_start_1[0] > scale_start_2[0]: |
binietoglou@38 | 128 | scale_start_1, scale_start_2 = scale_start_2, scale_start_1 |
binietoglou@38 | 129 | |
binietoglou@38 | 130 | if scale_end_1[0] > scale_end_2[0]: |
binietoglou@38 | 131 | scale_end_1, scale_end_2 = scale_end_2, scale_end_1 |
binietoglou@38 | 132 | |
binietoglou@38 | 133 | return (scale_start_1, scale_start_2), (scale_end_1, scale_end_2) |
binietoglou@38 | 134 | |
binietoglou@38 | 135 | def add_fake_measurements(self, no_profiles, variation=0.1): |
binietoglou@38 | 136 | """ |
binietoglou@38 | 137 | Add a number of fake measurements. This is done to allow testing with single analog profiles. |
binietoglou@38 | 138 | |
binietoglou@38 | 139 | Adds a predefined variation in each new profile. |
binietoglou@38 | 140 | """ |
binietoglou@38 | 141 | duration = self.info['duration'] |
binietoglou@38 | 142 | for channel_name, channel in self.channels.items(): |
ioannis@168 | 143 | base_time = list(channel.data.keys())[0] |
binietoglou@38 | 144 | base_data = channel.data[base_time] |
binietoglou@38 | 145 | |
binietoglou@38 | 146 | for n in range(no_profiles): |
binietoglou@38 | 147 | random_variation = base_data * (np.random.rand(len(base_data)) * 2 - 1) * variation |
binietoglou@38 | 148 | |
binietoglou@38 | 149 | new_time = base_time + n * duration |
binietoglou@38 | 150 | new_data = channel.data[base_time].copy() + random_variation |
binietoglou@38 | 151 | if 'ph' in channel_name: |
binietoglou@38 | 152 | new_data = new_data.astype('int') |
binietoglou@38 | 153 | channel.data[new_time] = new_data |
binietoglou@38 | 154 | |
binietoglou@38 | 155 | self.update() |
binietoglou@38 | 156 | |
binietoglou@38 | 157 | def subset_photoncounting(self): |
binietoglou@38 | 158 | """ |
binietoglou@38 | 159 | Subset photoncounting channels. |
binietoglou@38 | 160 | """ |
binietoglou@38 | 161 | ph_channels = [channel for channel in self.channels.keys() if 'ph' in channel] |
binietoglou@38 | 162 | new_measurement = self.subset_by_channels(ph_channels) |
binietoglou@38 | 163 | return new_measurement |
binietoglou@38 | 164 | |
i@116 | 165 | def _get_scc_channel_variables(self): |
binietoglou@43 | 166 | """ |
binietoglou@43 | 167 | Get a list of variables to put in the SCC. |
binietoglou@43 | 168 | |
binietoglou@43 | 169 | It can be overridden e.g. in the depolarization product class. |
binietoglou@43 | 170 | |
binietoglou@43 | 171 | Returns |
binietoglou@43 | 172 | ------- |
binietoglou@43 | 173 | |
binietoglou@43 | 174 | channel_variables: dict |
binietoglou@43 | 175 | A dictionary with channel variable specifications. |
binietoglou@43 | 176 | """ |
binietoglou@43 | 177 | channel_variables = \ |
ulalume3@71 | 178 | {'Background_Low': (('channels',), 'd'), |
binietoglou@43 | 179 | 'Background_High': (('channels',), 'd'), |
binietoglou@43 | 180 | 'LR_Input': (('channels',), 'i'), |
binietoglou@43 | 181 | 'DAQ_Range': (('channels',), 'd'), |
binietoglou@43 | 182 | 'Pol_Calib_Range_Min': (('channels',), 'd'), |
binietoglou@43 | 183 | 'Pol_Calib_Range_Max': (('channels',), 'd'), |
binietoglou@43 | 184 | } |
binietoglou@43 | 185 | return channel_variables |
binietoglou@43 | 186 | |
binietoglou@38 | 187 | class UnequalMeasurementLengthError(RuntimeError): |
binietoglou@38 | 188 | """ Raised when the plus and minus files have different length. |
binietoglou@38 | 189 | """ |
binietoglou@38 | 190 | pass |
binietoglou@38 | 191 | |
binietoglou@38 | 192 | class WrongNumberOfTimescalesError(RuntimeError): |
binietoglou@38 | 193 | """ Raised when timescales are not two. |
binietoglou@38 | 194 | """ |
binietoglou@38 | 195 | pass |
i@164 | 196 | |
i@164 | 197 | |
i@164 | 198 | class DarkLicelCalibrationMeasurement(LicelCalibrationMeasurement): |
i@164 | 199 | |
i@164 | 200 | def __init__(self, dark_files=None, use_id_as_name=False, licel_timezone='UTC'): |
i@164 | 201 | """Class to handle dark files for depolarization calibration measurements according to the SCC. |
i@164 | 202 | |
i@164 | 203 | It assumes that a single sent of dark measurements will be use for both plus and minus 45 channels. |
i@164 | 204 | |
i@164 | 205 | Parameters |
i@164 | 206 | ---------- |
i@164 | 207 | dark_files : list of str |
i@164 | 208 | List of paths for the dark measurement files. |
i@164 | 209 | use_id_as_name : bool |
i@164 | 210 | Defines if channels names are descriptive or transient digitizer IDs. |
i@164 | 211 | licel_timezone : str |
i@164 | 212 | String describing the timezone according to the tz database. |
i@164 | 213 | """ |
i@164 | 214 | # Setup the empty class |
i@164 | 215 | super(DarkLicelCalibrationMeasurement, self).__init__(dark_files, dark_files, |
i@164 | 216 | use_id_as_name=use_id_as_name, |
i@164 | 217 | licel_timezone=licel_timezone) |
i@164 | 218 | |
i@164 | 219 | def correct_timescales(self): |
i@164 | 220 | """ For dark measuremetns, no need to correct timescales. """ |
i@164 | 221 | pass |