atmospheric_lidar/licel_depol.py

Thu, 19 Nov 2020 10:23:32 +0000

author
Ioannis Binietoglou <ulalume3@gmail.com>
date
Thu, 19 Nov 2020 10:23:32 +0000
changeset 208
686c1525ce36
parent 198
f1eee7daf593
permissions
-rw-r--r--

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

mercurial